Compare commits

...
Author SHA1 Message Date
levzzz c39b4ed4b8 Merge pull request #4833 from levzzz5154/parity/T3-test-command
/test: run, runmultiple, runthese, runclosest, runthat, runfailed, reset, clear, stop
2026-10-31 05:34:53 +05:00
Ludovic Marie 9510efd2fe Merge pull request #4795 from horizon-rust/fix/dedicated-commands-tree
🤖 Agent: proven on the Mac mini: chat_screen, command_parse, command_exec 925/925, dedicated_server, server_ops_feedback, server_player_targets; pr_ready --quick green. Fixes main's red parity (#4680's tree, #4702's /fetchprofile ticks).
2026-10-31 01:30:45 +01:00
levzzz 747ccd0b5b Merge pull request #4828 from levzzz5154/parity/E3-entity-post-effect
Spectating a creeper, spider or enderman puts its post effect on the screen
2026-10-31 05:30:12 +05:00
levzzz 396ff1e5f9 Merge pull request #4826 from levzzz5154/parity/E2-post-effects-server
/posteffect: per-player post effects sent with ClientboundPostEffects
2026-10-31 05:26:54 +05:00
levzzz cb93abfe66 Merge pull request #4825 from levzzz5154/parity/E1-post-chain
Post-effect chains: draw post_effect JSON over the world, handle ClientboundPostEffects
2026-10-31 05:26:19 +05:00
levzzz 70ede6a0ef Merge pull request #4827 from levzzz5154/parity/R2-load
Loading screen: integrated server's chunk map and progress, new-world delay
2026-10-31 05:26:17 +05:00
levzzz 7a6dd1ffc9 Merge hr/main into parity/T3-test-command 2026-10-31 05:26:02 +05:00
levzzz 656b7c7c9b Merge pull request #4824 from levzzz5154/parity/T2-runner
Game tests: the runner, block-based and function tests; RUN starts the test
2026-10-31 05:25:53 +05:00
levzzzandClaude Sonnet 5.5 e0cee924f0 Update the test command corpus note
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-31 05:24:57 +05:00
levzzz 061a7dda03 Merge pull request #4823 from levzzz5154/parity/R2-cam
Spectator cameras on more entities, camera spectators hidden from spectators
2026-10-31 05:22:01 +05:00
levzzzandClaude Sonnet 5.5 baae358894 /test: run, runmultiple, runthese, runclosest, runthat, runfailed, reset*, clear*, stop
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-31 05:21:47 +05:00
levzzzandClaude Sonnet 5.5 83b29e43ff Put the spectated creeper, spider or enderman's post effect on the screen
GameRenderer.checkEntityPostEffect on setCameraEntity and on the perspective toggle.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-31 05:20:43 +05:00
levzzzandClaude Sonnet 5.5 d35bf3ba33 Integrated server's load progress and chunk map on the loading screen, 500 ms new-world delay
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-31 05:19:32 +05:00
levzzz 6667abcb04 Merge pull request #4820 from levzzz5154/parity/R2-disk
Dedicated servers warn operators of low disk space
2026-10-31 05:19:30 +05:00
levzzzandClaude Sonnet 5.5 9b5e0026f3 Keep /posteffect per player and send ClientboundPostEffectsPacket
ServerPlayer.postEffects lives on each guest, is saved as post_effects,
sent on join/respawn and at the end of the tick when dirty.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-31 05:17:57 +05:00
levzzz 12eb38e131 Merge hr/main into parity/T2-runner 2026-10-31 05:16:30 +05:00
levzzz 3c1d4b39c6 Merge pull request #4816 from levzzz5154/parity/T1-runner
Test blocks: trigger, reset, redstone signal and the start block's power
2026-10-31 05:16:21 +05:00
levzzzandClaude Sonnet 5.5 c5abd34fb1 Game tests: the runner, block-based and function tests, RUN starts the test
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-31 05:15:51 +05:00
levzzzandClaude Sonnet 5.5 434577c913 Spectator cameras on projectiles, dragon parts, TNT and falling blocks; hide camera spectators from spectators
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-31 05:14:04 +05:00
levzzzandClaude Sonnet 5.5 a55abdfe89 Draw post_effect chains over the world (PostChain, PostPass, handlePostEffects)
The chains in assets/<ns>/post_effect/*.json are read from the active
resource root at run time and run over the frame after the entity outline
and before the GUI, through Horizon's own stand-ins for the vanilla post
shaders. ClientboundPostEffectsPacket sets LocalPlayer.postEffects.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-31 05:11:29 +05:00
levzzzandClaude Sonnet 5.5 5a70e0ba0d Dedicated servers send ClientboundLowDiskSpaceWarningPacket to operators
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-31 05:09:47 +05:00
levzzzandClaude Sonnet 5.5 56dcdc8464 Test blocks: trigger, reset, the redstone signal and the start block's power
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-31 05:07:45 +05:00
levzzz 2a009ad16f Merge pull request #4805 from levzzz5154/parity/U-levelload
Client LevelLoadTracker: the loading screen waits for the server's start and the player's section
2026-10-31 05:06:23 +05:00
Ludovic Marie 37b1f605d5 Merge pull request #4766 from horizon-rust/fix/refused-pull-no-event
🤖 Agent: verified: CI verify passes; pr_ready --quick clean but for the candle cake test that fails on main too; piston harnesses pass; the vanilla path is unchanged (nothing is refused without a mod). Follow-up to #4739's review. Queued.
2026-10-31 01:05:23 +01:00
levzzz a6cf9d0370 Merge pull request #4804 from levzzz5154/parity/G-spectate
Spectator camera on the server (handleSpectatorAction, SetCamera)
2026-10-31 05:02:39 +05:00
levzzz 68e9da706c Merge pull request #4810 from levzzz5154/parity/C-chunk-fade
Chunk sections fade in over chunkSectionFadeInTime
2026-10-31 05:01:49 +05:00
Ludovic MarieandClaude Opus 5.5 f2f768bb36 command_exec harness: /fetchprofile answers on the next tick, as the oracle's
The other half of main's command_exec failure (since #4702): the oracle's
Services resolve no profile, at once, so /fetchprofile fails on the next
tick. The game asked Mojang's services over the network on a thread that
the harness's back-to-back ticks outran: no line in the recorded ticks.
The probe gets a discovery address that refuses at once
(HORIZON_API_DISCOVERY=http://127.0.0.1:9/), and the harness waits for
lookups in flight before each scripted tick (profile_fetches_pending).
925/925.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-31 00:57:57 +01:00
levzzz dd4f61225b Merge remote-tracking branch 'hr/main' into parity/U-levelload 2026-10-31 04:57:57 +05:00
levzzzandClaude Sonnet 5.5 1b99614b7b test_world_input_capture: follow the level load tracker's PlayerLoaded
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-31 04:57:56 +05:00
levzzz 2789409634 Merge pull request #4797 from levzzz5154/parity/U-multiline
Dialogs: multiline text inputs are a MultiLineEditBox
2026-10-31 04:56:24 +05:00
levzzzandClaude Sonnet 5.5 d9cbb0f4e6 Level loading: the client's LevelLoadTracker state machine
startClientLoad on login and respawn, the loading start game event, the
player's section, the 30 second timeout and the close delay. The
LevelLoadingScreen stays up (by the respawn's portal reason) until the level
is ready, and PlayerLoaded is sent then.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-31 04:53:56 +05:00
levzzz 3bfec3857d Merge pull request #4788 from levzzz5154/parity/U-chunk-toast
Toasts for chunk load and save failures and low disk space
2026-10-31 04:53:54 +05:00
levzzzandClaude Sonnet 5.5 37aa729526 Server side of the spectator camera: handleSpectatorAction, SetCamera, camera follow for guests
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-31 04:53:42 +05:00
levzzzandClaude Sonnet 5.5 1c6992a28c Chunk sections fade in over chunkSectionFadeInTime, block entities wait for 0.3
RenderSection.getVisibility, ChunkSectionInfo visibility, terrain.vsh/fsh mix with fog colour,
LevelExtractor.extractVisibleBlockEntities gate.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-31 04:51:14 +05:00
levzzzandClaude Sonnet 5.5 7e902a6886 Dialog text inputs with a multiline option are a MultiLineEditBox
Port MultiLineEditBox, MultilineTextField and AbstractTextAreaWidget's frame and
scroll bar as multiline_edit::MultiLineEdit: wrapping to the box, caret and
selection moving by line, word and page keys, character and line limits, mouse
placement, drag and wheel, and the character count. TextInputHandler.addControl
sizes the box by its height or max_lines.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-31 04:48:23 +05:00
levzzz 101280753b Merge pull request #4784 from levzzz5154/parity/L2-screen
Port OptimizeWorldScreen and the Edit World flow into it
2026-10-31 04:47:25 +05:00
levzzz d5c4248f62 Merge pull request #4782 from levzzz5154/parity/K2-hash-3
Hash block predicates and inline trim, instrument and banner pattern holders in container clicks
2026-10-31 04:41:50 +05:00
levzzzandClaude Sonnet 5.5 0580d262da Toast chunk load and save failures and low disk space from the integrated server
IntegratedServer.reportChunkLoadFailure / reportChunkSaveFailure: region read
errors and writer-thread write errors are logged, checked for low disk space and
queued for the window, which shows SystemToast.onChunkLoadFailure / OnChunkSaveFailure
(red title) and onLowDiskSpace (ten seconds).

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-31 04:41:35 +05:00
levzzz d3a5cf3925 Merge hr/main into parity/L2-screen 2026-10-31 04:38:54 +05:00
levzzzandClaude Sonnet 5.5 4bbd8f3e8e Port OptimizeWorldScreen and the Edit World flow into it
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-31 04:38:53 +05:00
levzzz a71733bfe7 Merge pull request #4774 from levzzz5154/parity/L1-upgrader
Port WorldUpgrader (Optimize World worker)
2026-10-31 04:38:49 +05:00
levzzz 79c76034a0 Merge pull request #4773 from levzzz5154/parity/bl2-leash-holder
Boats can hold leads (Entity.interact sneak branch)
2026-10-31 04:37:39 +05:00
Ludovic MarieandClaude Opus 5.5 1af0acfc37 Commands: /op, /ban, /pardon, /deop and /whitelist only on a dedicated server
#4680 registered them in the one tree every server uses, so single player's
and a LAN world's /help and Tab offered them (chat_screen and command_exec
failed on every PR's parity). Vanilla registers them only when
Commands.CommandSelection.includeDedicated: tree_extra::register_dedicated
holds them, vanilla::build_for(dev, dedicated) adds them, and the dedicated
server turns them on with set_dedicated_commands in preflight (single
player's server thread never runs it).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-31 00:37:08 +01:00
levzzz e8897035e4 Merge pull request #4772 from levzzz5154/parity/K2-hash-2
Hash item behaviour components (food, tool, equippable, attributes, ...) in container clicks
2026-10-31 04:34:57 +05:00
levzzzandClaude Sonnet 5.5 a26ebcb37e Port WorldUpgrader and RegionStorageUpgrader (Optimize World's worker)
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-31 04:31:13 +05:00
levzzzandClaude Sonnet 5.5 8fc8f02820 Boats and mobs on a lead can hang on a boat (Entity.interact sneak branch, game events)
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-31 04:30:07 +05:00
levzzzandClaude Sonnet 5.5 d3fd453cfa Hash block predicates and direct trim, instrument and banner pattern holders
can_place_on and can_break (blocks, state properties, exact components),
armor trims, instruments and banner patterns with inline holders, and
provides_trim_material, decoded from their stream form.

Part of #3518

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-31 04:28:41 +05:00
levzzz be529ea12d Merge pull request #4767 from levzzz5154/parity/51-crossed-arms
Villagers and wandering traders hold items in crossed arms (CrossedArmsItemLayer)
2026-10-31 04:27:40 +05:00
levzzz 3275d82094 Merge pull request #4764 from levzzz5154/parity/K2-hash
Hash container, profile, sign, variant and other stream-only components in container clicks
2026-10-31 04:26:02 +05:00
levzzz 2dcafe3ee3 Merge pull request #4763 from levzzz5154/parity/BL-boatlead
Make boats and rafts leashable (AbstractBoat is Leashable)
2026-10-31 04:24:22 +05:00
levzzzandClaude Sonnet 5.5 a87db94c53 Hash the item behaviour components like the vanilla client (part 2)
food, consumable, use_remainder, use_cooldown, damage_resistant, tool, weapon,
equippable, repairable, death_protection, blocks_attacks, piercing_weapon,
kinetic_weapon, swing animations, use_effects, attribute_modifiers, break_sound,
the fuels, compostable and mob_visibility, decoded from their stream form into
the codec form HashOps sees.

Part of #3518

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-31 04:22:36 +05:00
levzzzandClaude Sonnet 5.5 33d8b6ebeb CrossedArmsItemLayer: villagers and wandering traders show their held item; ShowTradesToPlayer displays matching offers
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-31 04:19:32 +05:00
Ludovic MarieandClaude Opus 5.5 917e58b2da Native mods: a refused sticky pull sends clients the blocks, not the retraction
A client isn't asked whether a piston may move: replaying the retraction's
block event, it would pull the block the server kept. When the level
refuses a pull, the event isn't broadcast (as for a push that fails), and
clients get the piston's and the head's block changes instead.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-31 00:18:37 +01:00
levzzzandClaude Sonnet 5.5 c830a170ad Hash the components Horizon keeps as stream bytes like the vanilla client (part 1)
Containers, pot decorations, block state, bees, typed entity data, sign text,
the NBT-only components, dye and mob variants, scalars, potion custom effects
and profiles are decoded from their stream form into the codec form HashOps
sees. The profile's skin patch is now read as vanilla writes it (texture ids,
slim flag) instead of two strings per texture.

Part of #3518

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-31 04:18:05 +05:00
levzzzandClaude Sonnet 5.5 5eb665b1c3 Make boats and rafts leashable (AbstractBoat implements Leashable)
Lead on/off, Leashable.tickLeash (elastic pull incl. quad, snap), save/load of the
holder, entity link packets, quad leash render for boats, lead drop on destroy.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-31 04:17:22 +05:00
Ludovic Marie 672b204c0a Merge pull request #4757 from horizon-rust/feat/report-threads
🤖 Agent: verified. Report-only change (Linux /proc and cgroup reads, null elsewhere); summary test covers both cases; pr_ready --quick green.
2026-10-31 00:14:40 +01:00
levzzz a79436fb97 Merge pull request #4741 from levzzz5154/parity/R-client
Finish rope hold position, sniffer digging sound and world stem check (parity)
2026-10-31 04:14:38 +05:00
levzzz 3da2b20cc8 Merge pull request #4754 from levzzz5154/parity/H-cmdcart
Command block minecart: edit screen, SetCommandMinecart, activator rail command, NBT
2026-10-31 04:13:41 +05:00
levzzz 4b461a9aae Merge pull request #4753 from levzzz5154/parity/R-render
Finish spectator player model and transient block removal (parity)
2026-10-31 04:12:04 +05:00
Ludovic Marie 9990eabc7f Merge pull request #4755 from horizon-rust/docs/anticheat-native-mod
🤖 Agent: docs only (docs/anticheat.md: where the anticheat lives, moves to a native mod once move/packet-timing hooks exist); CI green
2026-10-31 00:09:45 +01:00
levzzz d9e7cae928 Merge pull request #4749 from levzzz5154/parity/R-server
Finish dragon killing blow, lightning item bystanders and look-at entity form (parity)
2026-10-31 04:07:37 +05:00
levzzz dc29fd238b Merge hr/main into parity/R-client 2026-10-31 04:06:30 +05:00
Ludovic MarieandClaude Opus 5.5 393a461720 Server report: threads, and the container's limits on tasks and memory
The official server ran out of threads tonight (EAGAIN starting one, which
stopped its region writer until #4726). The report now says how many
threads the server runs and what the system allows: Max processes from
/proc/self/limits, and the container's pids.current/pids.max and
memory.current/memory.max from /sys/fs/cgroup. Linux only; null elsewhere.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-31 00:05:44 +01:00
levzzzandClaude Sonnet 5.5 973eeadcfd Command block minecart: editor, SetCommandMinecart, activator rail command, entity data, NBT
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-31 04:05:18 +05:00
Ludovic MarieandClaude Opus 5.5 f82149a3dd docs: the anticheat moves into a native mod once the hooks exist
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-31 00:05:00 +01:00
levzzzandClaude Sonnet 5.5 0dc793fdb5 PlayerModel.setupAnim (spectator), LevelRenderer.submitTransientBlocks (compile start): finish the partial ports
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-31 04:04:43 +05:00
levzzz 9054950e06 Merge pull request #4742 from levzzz5154/parity/K-component-hash
Hash item components in container clicks like the vanilla client
2026-10-31 04:04:19 +05:00
Ludovic Marie 84ebab8a78 Merge pull request #4739 from horizon-rust/feat/smp-grief-hooks
🤖 Agent: verified: CI verify passes; local pr_ready (fmt, clippy, tests: the one failing test fails on main too); local parity 125/128 affected harnesses pass, command_exec and chat_screen differ only by main's new /op, /ban... (e1a530a71), remote_fireball passed on rerun; end to end with the SMP mod on a dedicated server (horizon-mods#72 proof). Queued.
2026-10-31 00:04:07 +01:00
levzzzandClaude Sonnet 5.5 b307359409 KilledTrigger (dragon), LightningStrikeTrigger (items), ClientboundPlayerLookAtPacket (entity form): finish the partial ports
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-31 03:59:20 +05:00
levzzz 424dc918e7 Merge hr/main into parity/K-component-hash 2026-10-31 03:56:09 +05:00
levzzz ce12bd5b6d Merge pull request #4738 from levzzz5154/parity/K-hashops
Port HashOps (CRC32C) for item component hashes
2026-10-31 03:55:54 +05:00
levzzz 0d04976fa1 Merge pull request #4736 from levzzz5154/parity/01-chatdraft
Chat drafts: ChatScreen exit reasons (shouldDiscardDraft, removed)
2026-10-31 03:55:15 +05:00
levzzzandClaude Sonnet 5.5 303e418b08 LocalPlayer.getRopeHoldPosition, SnifferSoundInstance.tick, openWorldCheckWorldStemCompatibility: finish the partial ports
getRopeHoldPosition: main-arm side, the eased yaw/pitch (Mth.lerp(pt*0.5, yRot, yRotO)) and getEyePosition(pt).
SnifferSoundInstance: follows the sniffer, stops when it is removed, has a target or stops digging.
openWorldCheckWorldStemCompatibility: reads the enabled data packs' dimensions and a world that
still needs file fixes, so the backup question comes on the first open.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-31 03:54:33 +05:00
levzzz 403df829f3 Merge pull request #4723 from levzzz5154/parity/13-riding-nautilus
Nautilus riding sound and legacy server list ping
2026-10-31 03:50:31 +05:00
levzzzandClaude Sonnet 5.5 8e40d11b27 Hash item components in HashedStack like the vanilla client
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-31 03:50:23 +05:00
levzzzandClaude Sonnet 5.5 e921ddb7c5 Port HashOps (CRC32C) for item component hashes
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-31 03:45:37 +05:00
levzzz a4fec395ee Merge pull request #4733 from levzzz5154/parity/D-transient
Show a landed falling block until its mesh is ready (AddTransientBlock)
2026-10-31 03:44:54 +05:00
levzzzandClaude Sonnet 5.5 d8d6dd15aa Chat drafts follow ChatScreen.ExitReason: an interrupted chat keeps its text
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-31 03:43:26 +05:00
Ludovic Marie e825d36af8 Merge pull request #4675 from horizon-rust/feat/anticheat-ground
🤖 Agent: review findings fixed (happy ghast rider bound, grace for ground removed server-side); pr_ready --quick green; anticheat_e2e ground and off pass; rdbtCVS's placement question answered and passed to Ludovic
2026-10-30 23:41:19 +01:00
levzzz ae19796505 Merge pull request #4731 from levzzz5154/parity/F-stuck-arrows
Draw arrows and bee stingers stuck in a player's body
2026-10-31 03:40:25 +05:00
levzzz ec156a856f Merge pull request #4732 from levzzz5154/parity/17-open
World open: backup question for customized/experimental worlds, low disk space, unreadable level data
2026-10-31 03:39:42 +05:00
levzzzandClaude Sonnet 5.5 d49bcab591 Show the block a falling block lands as (ClientboundAddTransientBlockPacket)
The server tells the players tracking a landed falling block to show the
block for a moment, and the client draws it as a moving block until its
section mesh has been rebuilt or a second has passed.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-31 03:36:01 +05:00
Ludovic Marie 5f5acece3f Merge remote-tracking branch 'origin/main' into feat/smp-grief-hooks 2026-10-30 23:34:04 +01:00
Ludovic MarieandClaude Opus 5.5 51119592e2 Native mods: pass the bucket's stack as it is (clippy)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 23:34:03 +01:00
levzzz f4a81d27ee Merge pull request #4729 from levzzz5154/parity/16-video
Video Settings: Exclusive Fullscreen (and mode), Graphics API, Texture Filtering rows
2026-10-31 03:33:48 +05:00
levzzzandClaude Sonnet 5.5 1f4dba50e3 WorldOpenFlows: backup question for customized and experimental worlds, low disk space, unreadable level data
askForBackup / openWorldCheckWorldStemCompatibility: a world with
legacy_custom_options or a non-vanilla dimension asks for a backup first.
openWorldCheckDiskSpace: under 64 MiB usable asks Continue or Back.
openWorldLoadLevelData: a world opened by name (control socket) whose
level.dat does not read shows the Recover World Data screen, and a world
opened without a title no longer becomes a new random-seed world.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-31 03:32:14 +05:00
levzzzandClaude Sonnet 5.5 7a09947e46 Draw arrows and bee stingers stuck in a player's body
LivingEntity's arrow and stinger counts are synched entity data (12, 13):
the server counts a hurting arrow hit and a bee sting, lets them fall out
as LivingEntity.tick does, and the clients draw them with StuckInBodyLayer's
placement (ArrowLayer, BeeStingerLayer).

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-31 03:31:07 +05:00
Ludovic MarieandClaude Opus 5.5 6034bab012 Anticheat ground: a happy ghast's rider, and a grace for vanished ground
A player standing on a happy ghast is 4.0 above it: the carrier test
now takes from a block above to 4.5 below (carries_player, tested). A
claim at the height where the ground last held the player, within 10
ticks, isn't judged: the block may have gone server-side (broken,
pushed by a piston) before the client heard.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 23:28:16 +01:00
Ludovic Marie 1b06b53500 Merge pull request #4726 from horizon-rust/fix/region-writer-thread-limit
🤖 Agent: verified. Fixes the official server's 21:54Z crash (region writer panicked on EAGAIN when spawning a helper); unit test covers refused helpers; pr_ready --quick green.
2026-10-30 23:28:05 +01:00
levzzzandClaude Sonnet 5.5 6907fe0a79 Video Settings: Exclusive Fullscreen, its mode, Graphics API, Texture Filtering and Anisotropic Filtering rows
VideoSettingsScreen.addOptions/displayOptions/qualityOptions: the Exclusive
Fullscreen Mode row, the Exclusive Fullscreen and Graphics API options, and the
Texture Filtering and Anisotropic Filtering options (greyed out until the
terrain sampler reads them). Exclusive fullscreen takes effect through winit's
exclusive video modes; the Graphics API option drives Horizon's own API setting
and asks for a restart.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-31 03:21:26 +05:00
levzzz a12b5151e1 Merge pull request #4724 from levzzz5154/parity/A4b-test-instance
Test instance block: entity, packets, actions, edit screen and renderer
2026-10-31 03:21:19 +05:00
Ludovic Marie 65b312814b Merge pull request #4722 from horizon-rust/perf/sculk-portal-per-mob
🤖 Agent: verified. Same tables, order and events; A/B with 60 bots: sculk entities −25%, entity portals −44%. pr_ready --quick green; sculk parity runs in CI.
2026-10-30 23:17:17 +01:00
levzzz 771394b76d Merge pull request #4720 from levzzz5154/parity/40-worker-thread
Crash report and exit on a streaming worker thread panic
2026-10-31 03:12:57 +05:00
levzzz 5f06dbbc16 Merge remote-tracking branch 'hr/main' into parity/A4b-test-instance 2026-10-31 03:12:54 +05:00
levzzz ef7ef9959c Merge pull request #4721 from levzzz5154/parity/A4a-test-block
Test block: block entity, packet, handler and edit screen
2026-10-31 03:12:50 +05:00
levzzzandClaude Sonnet 5.5 a1f56c2f2e Ride-sound for nautilus and legacy server list ping (parity #3518)
LocalPlayer.startRiding starts RidingEntitySoundInstance for an AbstractNautilus (underwater loop);
RidingEntitySoundInstance reads the ridden entity's isUnderWater. ServerStatusPinger falls back to
pingLegacyServer when the status ping fails, and a protocol mismatch shows as incompatible.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-31 03:11:51 +05:00
Ludovic MarieandClaude Opus 5.5 c3c2ffc43e Saves: a thread the system refuses no longer stops the region writer
The region writer shares a batch's compression out to scoped helper threads.
thread::scope's spawn panics when the system refuses a thread, and on the
official server (a container at its limit of threads, EAGAIN) that panic
stopped the writer: chunks and entities failed to save for four minutes, then
the server stopped with "the region writer stopped".

Helpers now start with Builder::spawn_scoped; one that can't start leaves its
share to the others, and the writer thread takes chunks too (helpers - 1
threads plus itself, as many as before), so at worst it does the whole batch
alone.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 23:06:51 +01:00
levzzz 0ec594d9d3 Merge remote-tracking branch 'hr/main' into parity/A4a-test-block 2026-10-31 03:05:24 +05:00
levzzz 98b6a442cf Merge pull request #4716 from levzzz5154/parity/A3-jigsaw
Jigsaw block: block entity, packets, handlers and edit screen
2026-10-31 03:05:21 +05:00
levzzz 96caf0bf88 Merge pull request #4718 from levzzz5154/parity/10-minor
Sprint: a shallow wall slide no longer stops the sprint (isHorizontalCollisionMinor)
2026-10-31 03:05:10 +05:00
levzzzandClaude Sonnet 5.5 31192110c6 Test instance block: block entity, packets, handler, edit screen, renderer
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-31 03:04:45 +05:00
levzzzandClaude Sonnet 5.5 00b1e2c12a A panic on a streaming worker thread writes a crash report and ends the game
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-31 03:04:16 +05:00
rdbt b0cd391031 Merge pull request #4159 from spidercraft/far-terrain-fade
Far terrain fades in and out chunk by chunk, near chunks too
2026-10-30 22:02:19 +00:00
rdbt 22c09057fc Merge pull request #4158 from spidercraft/far-terrain-look
Far terrain: shaded water and canopies, swamp grass, no floor pillars or water walls
2026-10-30 22:02:15 +00:00
rdbt 7366ff29b5 Merge pull request #4157 from spidercraft/far-terrain-speed
Far terrain: real chunks out to level 2 on idle threads, loading outward from the player
2026-10-30 22:02:10 +00:00
rdbt fd92adb803 Merge pull request #4156 from spidercraft/far-terrain-cache
Far terrain: decorated chunks kept on disk, read again next session
2026-10-30 22:02:05 +00:00
levzzz b914de7e69 Merge pull request #4715 from levzzz5154/parity/31-lookat
Send PlayerLookAt for /tp and /rotate facing
2026-10-31 03:01:16 +05:00
Ludovic Marie c5d4217dbb Merge pull request #3251 from 1038april/fix/remote-sprint-hit-slowdown
Apply the attacker's 0.6 speed cut after a knockback hit on another p…
2026-10-30 23:00:09 +01:00
levzzzandClaude Sonnet 5.5 3c5dd6b03b Port LocalPlayer.isHorizontalCollisionMinor so a shallow wall slide keeps the sprint
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-31 02:57:06 +05:00
levzzz c4c4b63aba Merge remote-tracking branch 'hr/main' into parity/A3-jigsaw 2026-10-31 02:55:41 +05:00
levzzz 56f9a17682 Merge pull request #4712 from levzzz5154/parity/A2-bounding-box
Structure block: bounding box and invisible block markers
2026-10-31 02:55:38 +05:00
levzzzandClaude Sonnet 5.5 35212f09a5 Send ClientboundPlayerLookAtPacket for /tp facing and /rotate facing
ServerPlayer.lookAt tells the player's client; /rotate also used the
target's eyes instead of the source anchor.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-31 02:53:05 +05:00
levzzzandClaude Sonnet 5.5 e017850f93 Test block: block entity, packet, handler, edit screen
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-31 02:51:28 +05:00
Ludovic Marie 0d1dccd5ff Merge pull request #4563 from horizon-rust/feat/launcher-brand-logos
🤖 Agent: R8c approved by Ludovic, retargeted to main after #4552, main merged, full pr_ready green, CI green. The review found no defect. Its focus-state note: shell::action and button() draw no focus either, so nothing regresses. Queued.
2026-10-30 22:50:47 +01:00
levzzz 6307b54dea Merge pull request #4706 from levzzz5154/parity/07-ui
Show the accessibility onboarding on a first launch; cartography preview draws renderOnFrame markers
2026-10-31 02:50:03 +05:00
Ludovic MarieandClaude Opus 5.5 51629f9085 Server perf: sculk and portal checks no longer cost per mob per tick
On the official server (62 players, 1,445 mobs), tick_sculk_entities took
2.0 ms and entity_portals 1.5 ms a tick, 7% of a 47.9 ms tick.

- sculk: whether the block under a mob is climbable, or a redstone ore, comes
  from per-state tables built with the others in sculk_prepare, instead of a
  tag lookup by string and an Arc<str> clone per mob; a step knows whether a
  warden made it from the caller, instead of a scan of every mob.
- portals: canUsePortal is asked only for the entities that have a portal
  state (in a portal or cooling down), not for every mob, whose check scanned
  every server player; positions before the tick go in an FxHashMap.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 22:49:35 +01:00
levzzz b74a78c970 Merge pull request #4708 from levzzz5154/parity/32-compstr
Compile selector and nbt entity strings when decoding text components
2026-10-31 02:49:28 +05:00
levzzz e694791bb5 Merge remote-tracking branch 'hr/main' into parity/A2-bounding-box 2026-10-31 02:46:24 +05:00
levzzz 65030881fc Merge pull request #4704 from levzzz5154/parity/A1c-structure-screen
Structure block: StructureBlockEditScreen
2026-10-31 02:46:20 +05:00
levzzzandClaude Sonnet 5.5 ceb5791a69 Jigsaw block: block entity, packets, handlers, edit screen
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-31 02:46:03 +05:00
levzzzandClaude Sonnet 5.5 4fa552591a Compile selector and nbt entity strings when a text component is decoded
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-31 02:42:56 +05:00
levzzzandClaude Sonnet 5.5 699afa8007 Show the accessibility onboarding on a first launch; draw only renderOnFrame markers on the cartography map
Gui.addInitialScreens queues AccessibilityOnboardingScreen first while onboardAccessibility is set:
the title box, Narrator (inactive without a narrator), Music & Sounds, Accessibility Settings,
Language and Continue, fading in over 2 s and out over 1 s, Escape or Continue finishing it
(Options.onboardingAccessibilityFinished). GuiGraphicsExtractor.map draws only the decorations
with renderOnFrame, so the player arrows and markers no longer show on the cartography preview.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-31 02:41:35 +05:00
levzzz dc35e78173 Merge pull request #4702 from levzzz5154/parity/33-adv
Parity: fall_after_explosion cause, effects_changed updates, /fetchprofile lookups
2026-10-31 02:38:17 +05:00
levzzz 49db9b276b Merge remote-tracking branch 'hr/main' into parity/A1c-structure-screen 2026-10-31 02:38:03 +05:00
levzzz 0b95a7a921 Merge pull request #4703 from levzzz5154/parity/A1b-structure-server
Structure block: handleSetStructureBlock (save, load, scan)
2026-10-31 02:37:59 +05:00
Ludovic MarieandClaude Opus 5.5 524071f9cf Native mods: hook points that keep fluids, fire, pistons, explosions, buckets and blows out of guarded land (API 22)
A server mod that guards land (the SMP's claims) can now refuse what changes
blocks without a player's break or place: a fluid flowing or fire spreading
into a block (native_block_spread), a piston's move (native_piston_move), an
explosion's blocks (native_explosion_detonate takes out the spared ones), a
bucket (native_fill_bucket) and a player's blow on an entity
(native_attack_entity, on the server's attack packet, so frames, armor
stands, animals and players). horizon-world asks through two LevelAccess
methods that allow by default; fire asks after its random draws, so a
refusal leaves vanilla's random order as it was.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 22:36:17 +01:00
Ludovic Marie 5d3a45f09f Merge remote-tracking branch 'origin/main' into feat/launcher-brand-logos 2026-10-30 22:32:41 +01:00
Ludovic Marie ab92024350 Merge pull request #4552 from horizon-rust/feat/launcher-instance-icons
🤖 Agent: R8b approved by Ludovic, retargeted to main after #4520, main merged, full pr_ready green, CI green; the review found no defect; queued.
2026-10-30 22:31:32 +01:00
levzzzandClaude Sonnet 5.5 b91f0330f4 Port FallAfterExplosion cause, EffectsChanged updates and /fetchprofile name|id lookups
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-31 02:28:40 +05:00
levzzz dc6e548538 Merge remote-tracking branch 'hr/main' into parity/A1b-structure-server 2026-10-31 02:27:38 +05:00
levzzz 95f097fd06 Merge pull request #4687 from levzzz5154/parity/A1-structure
Structure block: block entity, save format and packet codec
2026-10-31 02:27:33 +05:00
rdbt c6dd5e2205 Merge pull request #4526 from horizon-rust/fix/prepare-output-version-3
horizon-prepare: OUTPUT_VERSION 3 (creative tabs exporter changed in #4052)
2026-10-30 21:26:59 +00:00
levzzzandClaude Sonnet 5.5 1604f8abee BlockEntityWithBoundingBoxRenderer: the structure block's box and invisible-block markers
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-31 02:25:30 +05:00
levzzz f9d0a88d27 Merge pull request #4685 from levzzz5154/parity/42-ui
Show the file fixer's progress screen while a world upgrades
2026-10-31 02:25:18 +05:00
levzzzandClaude Sonnet 5.5 c739b7af73 StructureBlockEditScreen
The structure block's edit screen with vanilla's layout and lang keys, opened
by using the block (BlockEntity.usedBy), over a shared edit-screen runner the
jigsaw and test blocks will use.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-31 02:22:58 +05:00
levzzzandClaude Sonnet 5.5 177aff1240 Structure block: handleSetStructureBlock, save, load, scan and redstone trigger
The server side of the structure block: ServerGamePacketListenerImpl.
handleSetStructureBlock, StructureBlockEntity.saveStructure/placeStructure/
detectSize/loadStructureInfo, StructureTemplate.fillFromWorld + save, the
StructureTemplateManager cache and generated/<ns>/structures/<path>.nbt,
StructureBlock.neighborChanged/trigger/setPlacedBy, and placeInWorld on the
live level.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-31 02:22:44 +05:00
levzzz 062d66e495 Merge pull request #4680 from levzzz5154/parity/34-profiles
Add /op, /deop, /ban, /pardon and /whitelist to a dedicated server's commands
2026-10-31 02:20:02 +05:00
levzzzandClaude Sonnet 5.5 aa44f715e6 Structure block entity: save format, update tag, packet codec
StructureBlockEntity (fields, saveAdditional/loadAdditional, renderMode,
getRenderableBox, the corner scan) as a block entity kind, synced in the
chunk update tag like saveCustomOnly, and ServerboundSetStructureBlockPacket.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-31 02:17:03 +05:00
levzzzandClaude Sonnet 5.5 ffe68c936f Show FileFixerProgressScreen while a world's files are fixed
Part of #3518. UpgradeProgress counts the file fixes and the data upgrade; the
world-opening screen draws vanilla's layout from it, with Cancel.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-31 02:16:00 +05:00
Ludovic Marie d2fe5c0768 Merge remote-tracking branch 'origin/main' into feat/launcher-instance-icons
# Conflicts:
#	THIRD_PARTY_NOTICES.md
2026-10-30 22:15:52 +01:00
Ludovic Marie f788711fe1 Merge pull request #4520 from horizon-rust/feat/launcher-icons
🤖 Agent: R8a approved by Ludovic (logos everywhere), main merged, full pr_ready green, CI green; queued.
2026-10-30 22:14:45 +01:00
Ludovic Marie c6132e2b9b Merge pull request #4676 from horizon-rust/nightly/no-review-needed
Nightly: /nightly no longer needs an approving review
2026-10-30 22:13:44 +01:00
levzzzandClaude Sonnet 5.5 e1a530a71e Add /op, /deop, /ban, /pardon and /whitelist as commands of a dedicated server
GameProfileArgument.SelectorResult.getNames and the five commands that use
it, in vanilla's tree shape and permission level (admins).

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-31 02:09:57 +05:00
Ludovic Marie b5b451de7c Merge pull request #4610 from horizon-rust/feat/smp-claim-hooks
🤖 Agent: verified. pr_ready --quick passed; 31/32 affected harnesses passed locally, and lunge_motion fails on main too, see the body. Hook points API 21 for the SMP's claims and teleports. Nothing changes without a native mod.
2026-10-30 22:08:16 +01:00
levzzz 1498477aec Merge pull request #4674 from levzzz5154/parity/04-dialog
Dialog suggest_command click fills the chat input
2026-10-31 02:07:57 +05:00
Ludovic MarieandClaude Sonnet 5.5 c527d60b75 Nightly: /nightly no longer needs an approving review
Ludovic decided on 2026-10-30, after the Team asked (levzzz5154: they are often
blocked on the launcher), that a trusted reviewer or Ludovic can pick any open
pull request to main for the Nightly, reviewed or not.

Kept: only Ludovic and the listed reviewers, the `paused` line, the
CODEOWNERS-guarded files, MAX_PRS, MAX_BUILDS_PER_DAY, open and targeting main,
the build without secrets, and Ludovic's `remove`. Each pull request is built at
the head it was picked at.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 22:04:22 +01:00
levzzz 0a825b42a2 Merge pull request #4672 from levzzz5154/parity/35-trig
Advancement triggers: durability ranges, killing_blow on all KilledTrigger uses, player bystanders
2026-10-31 02:01:59 +05:00
Ludovic Marie 75867c69b9 Merge remote-tracking branch 'origin/main' into feat/launcher-icons 2026-10-30 22:01:02 +01:00
levzzz 475d096fe2 Merge pull request #4671 from levzzz5154/parity/18-avatar-ears
Draw deadmau5's ears (Deadmau5EarsLayer)
2026-10-31 01:58:48 +05:00
Ludovic MarieandClaude Opus 5.5 c0f09fed88 Merge main: mod screens took API 18 to 20, these hooks are API 21
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 21:58:20 +01:00
levzzzandClaude Sonnet 5.5 04c7cfc1af Dialog suggest_command click inserts the command into the chat screen under it
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-31 01:57:18 +05:00
levzzz 70436b6f7d Merge pull request #4666 from levzzz5154/parity/00-container-death
Close a dead guest's open container on respawn
2026-10-31 01:52:31 +05:00
levzzzandClaude Sonnet 5.5 ecd85c497f Advancement triggers: item durability ranges, killing_blow on every KilledTrigger, players as lightning bystanders
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-31 01:51:40 +05:00
levzzzandClaude Sonnet 5.5 8bad582185 Draw deadmau5's ears: Deadmau5EarsLayer
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-31 01:49:42 +05:00
spiderandClaude Opus 5.5 963b7d8bd9 Far terrain: chunks leaving the render distance stay in front until they have faded
A chunk leaving the render distance popped to far terrain:

- Its sections were drawn only within the render distance, or in columns
  far tiles still leave out, so once fading out (leave_section) they were
  culled at once: the fade ran unseen. Columns with sections fading out
  are drawn past it now.
- Far terrain's coarser blocks stand a little out of the real ones, and
  0.2% of the distance behind did not keep them back: the tile coming in
  under a column hid its sections the moment it was meshed with it, held
  or fading. Sections past the render distance (lod::HELD) and fading out
  go 2% of their distance toward the camera.

Measured frame by frame over a forest, render distance 10 to 3: the
change spreads over about 0.33 s instead of landing in one frame.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 20:49:23 +00:00
spiderandClaude Opus 5.5 ee7d443c8f Far terrain: shaded water and canopies, swamp grass, no floor pillars or water walls
- Skylight goes straight down generated columns: water and leaves take a
  level a block, one span per level as a real chunk's light has them.
  Full light to the bottom lit sea beds as if in the open, and coarse cells
  merging water and sand showed that light as white dots across the sea.
  Solid blocks pass it on: under them far terrain shows cliffs and
  overhangs, which vanilla lights sideways (they went black).
- Kelp and seagrass in saved and generated chunks are the water they hold
  (loaded chunks: #4144).
- Grass takes vanilla's colour at each cell (BiomeColors' grass with its
  swamp and dark forest modifiers), not the biome's flat one.
- Buried gaps are filled only where no open air reaches them through
  neighbouring gaps (a flood fill): the air under a forest canopy was
  filled with the floor block, pillars up to the leaves.
- The seal far tiles put where near terrain begins is opaque only: near
  water carries on into far water, and the seal showed through it as a
  light wall.
- A section beside a chunk not loaded meshes against its own edge column,
  not lit air: the outermost chunks' water drew a light sheet toward it
  (fluids show both sides).

Seed 1234, LOD Distance 512: 9.27M far quads become 11.22M, the trunks and
forest floor under canopies.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 20:49:23 +00:00
spiderandClaude Opus 5.5 3554efb04f Far terrain: a circular range; tiles leaving it fade out instead of being cut
The renderer cut far tiles whose nearest point was past the LOD distance,
a circle, every frame, while the plan kept tiles out to a square: tiles
popped out at the circle as the player moved, before the plan dropped
them and before their fade-out could run. The plan's range is now the
same circle (TileKey::reach) and the renderer no longer cuts by distance
(the frustum only): a tile leaving the range leaves the plan and fades out
(upload_lods), as tiles entering it fade in.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 20:49:23 +00:00
spiderandClaude Opus 5.5 94f48bbf6a Far terrain: near chunks and far tiles fade out as they came
Unloading popped where loading faded:

- A section whose column far terrain now draws (a shorter render
  distance, the player moving away) fades out over it, chunk by chunk as it
  came, and is released once it has (Renderer::leave_section), instead of
  in one frame. Columns far tiles still leave out stay drawn until they
  don't (#3765), then fade out the same way.
- A far tile leaving the plan with nothing to replace it (out of range: a
  shorter LOD distance, the player moving away) fades out. A tile replaced
  by new tiles, or by near chunks fading in, stays whole behind them as
  before: two patterns fading at once left holes between them.

The fade code gains a fading-out range (65 to 128 sixty-fourths, per
level 129 values): a section's largest code, 129, keeps its UV step exact
(checked in tiles_fade_in_on_their_own_grid).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 20:49:23 +00:00
spiderandClaude Opus 5.5 ada4e6567d Far terrain fades in chunk by chunk, and near chunks fade in over it
New far tiles fade in over the ones they replace, and new near sections
over far terrain, instead of appearing at once. Each chunk-sized column of
a tile (wider ones for coarse tiles) starts in turn over the first half of
600 ms and fades over the second, as a fine ordered dither fixed to the
terrain: its unit is the power of two of a block nearest 1.5 pixels, so it
stays fine up close, does not sparkle far off and does not crawl as the
camera moves. The tiles replaced stay whole behind until it is done: no
pixel drawn twice, no hole.

- The fade rides in the instance's UV step (negated past a fade code);
  terrain.wgsl and GLSL packs take the step as fract(abs(w)), unchanged
  for whole tiles and sections.
- A fading tile or section is drawn a hair toward the camera; far tiles
  0.2% of their distance back, so near chunks win over tiles still under
  them (near water lies a little below far water: a smaller step left two
  waters, opaque).
- Water and other see-through faces are not dithered, and near water is
  drawn before far water, replaced tiles' last: depth keeps each layer to
  where the one before has none.
- A tile re-meshed around new near chunks or finer tiles keeps drawing its
  old mesh until they are in; a tile over nothing shows whole (its hidden
  chunks opened holes to the sea bed).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 20:49:23 +00:00
spiderandClaude Opus 5.5 fb9db82d1b Far terrain: real chunks out to level 2 on idle threads, loading outward from the player
- Decoration (real trees and structures) runs on a third of the threads, up
  to 12, at idle priority, instead of one; batches of 8x8 chunks share
  their proto border (2.25 protos a chunk instead of 4): 16.6 ms a chunk
  instead of 26.9.
- Tiles up to level 2 (cells of 4 blocks) are decorated, not only 0 and 1;
  a detail request asks for up to 512 chunks instead of 32, which capped
  detail at one decoration thread's pace.
- Tiles mesh across the sampling threads, each with its own material
  cache, instead of on the far terrain thread alone.
- Only the tiles the plan keeps are sampled, each at the level its
  distance calls for, nearest first: far terrain grows outward from the
  player in rings, whichever way they look. Coarse tiles sampled first
  under fine ones beside the player cost a far tile's work for terrain
  replaced moments later. The view-ahead focus and Lod::update's yaw go.
- The stop flags are set under the lock their threads wait on: a thread
  between its check and its wait missed the wake, and closing the world
  waited forever (more likely with more threads).

Fresh world, LOD Distance 512: every tile sampled after about 12 s instead
of 15; real detail after 40 s 500k cells instead of 447k.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 20:49:16 +00:00
rdbtCVSandClaude Opus 5.5 8922550fdf Far terrain cache: a chunk cached already is not written again
A generation batch hands back its neighbours too, and drain_details put
every one of them, cached or not: the append-only region files grew by
the same chunks each session until the build key changed. The build key
already clears stale chunks, so one cached stays as it is.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 20:49:16 +00:00
Ludovic Marie 1085b42fc5 Merge pull request #4657 from horizon-rust/fix/log-flood-limit
🤖 Agent: verified. Unit test covers the per-line limit, the note and the left-out count; info lines are never limited. pr_ready --quick green.
2026-10-30 21:48:03 +01:00
spiderandClaude Opus 5.5 2e321e385a Far terrain: decorated chunks kept on disk, read again next session
Far terrain decorates the chunks of its nearest ring with the world's own
generator (vanillagen/lod.rs), kept in memory only, 1024 of them: every
session generated them again. They now go to
dimensions/<dim>/data/horizon_far_terrain/, one append-only file per region
(lod/cache.rs, about 6 KB a chunk deflated), and a detail request reads
them back before asking the generator.

- Saved chunks still take precedence over cached ones.
- A file written by another build, block registry or format is cleared on
  first use; a torn last record (a crash mid-write) is cut off.
- Only detail requests read the cache: reading it for every cell of the
  coarse tiles' sampling slowed their sampling more than it saved.

Rejoining a world at LOD Distance 512: 343k refined far cells after 45 s
with the cache, 244k without.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 20:47:55 +00:00
levzzz 9c471a85ce Merge pull request #4653 from levzzz5154/parity/36-packs
Parity: validate pack.mcmeta formats, clear packs on a failed reload
2026-10-31 01:47:37 +05:00
levzzz 0bb993afd7 Merge pull request #4658 from levzzz5154/parity/29-render
Wind charge renderer: age includes the partial tick
2026-10-31 01:45:51 +05:00
levzzz 6f26c1f355 Merge pull request #4655 from levzzz5154/parity/36-adv
Parity: passenger predicate, boat vehicles, sulfur cube shear trigger
2026-10-31 01:44:23 +05:00
levzzzandClaude Sonnet 5.5 2052e55b0b Close a dead guest's open container when it respawns
AbstractContainerScreen.tick closes the container of a dead player; a guest's
chest stayed counted as open (lid up) after its death.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-31 01:43:45 +05:00
Ludovic MarieandClaude Opus 5.5 8975278e5e Anticheat: the ground check (no-fall, walking on water)
A move that says it ends on the ground is judged when nothing there can
hold a player: only air and fluids around the feet, no collision a tenth
of a block down, no boat, minecart, shulker or happy ghast near. The
false claim is dropped, so vanilla's fall damage follows, and the levels
send back, alert and kick as configured. Creative, spectators and
gliders are skipped. Off unless [anticheat] turns it on.

horizon-bots: --height and --ground. anticheat_e2e --check ground: a
hovering bot and one on water that claim the ground are flagged and
kicked, bots walking and standing on the ground never are.
server_bench --ground: survival bots on a flat world's ground.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 21:42:45 +01:00
levzzz c4c7df589a Merge pull request #4652 from levzzz5154/parity/12-options
Pause menu bug and feedback links, title credits text button
2026-10-31 01:40:27 +05:00
levzzzandClaude Sonnet 5.5 f387c1d78f fmt
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-31 01:35:54 +05:00
Ludovic Marie 571e0e5341 Merge pull request #4650 from horizon-rust/fix/vehicle-moved-wrongly
🤖 Agent: verified. lan_e2e boat_shoved fails on main (boat stuck, 70 'moved wrongly' lines) and passes here (paddles 3.0, 1 line); mounts and riding pass; boats fails the same on main (fist breaking, unrelated). pr_ready --quick green.
2026-10-30 21:34:44 +01:00
levzzzandClaude Sonnet 5.5 68b90d860b Wind charge: spin and scroll with the partial tick (WindChargeRenderer.submit)
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-31 01:30:01 +05:00
Ludovic MarieandClaude Opus 5.5 53ba582419 Log: limit a warning or error from one line of code to 100 a minute
A warning repeated on every packet filled the official server's log at up to
6,700 lines a second (16 MB in 30 minutes). Past 100 warnings or errors from
one line of code in a minute, the log says so once, counts the rest, and says
how many it left out when that line logs again in a later minute. Below the
limit the log is vanilla's, line for line; info lines (chat, joins) are never
limited.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 21:29:35 +01:00
rdbt c995c9b76e Merge pull request #4098 from DUzzL/fix/issue-4086-main-hand
Send the stored Main Hand option on join
2026-10-30 20:28:41 +00:00
levzzzandClaude Sonnet 5.5 ab2ab8ef61 Advancements: passenger predicate, boats as vehicles, sulfur cube shearing trigger
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-31 01:28:33 +05:00
rdbt 9e267fbf9e Merge pull request #4636 from JustKarkat/fix/target-anger-death-forgiveness
A dying player's forgiveness reaches the enderman, zombified piglin and iron golem
2026-10-30 20:28:30 +00:00
rdbt 618469513b Merge pull request #4632 from ludovicthenot/fix-key-names-layout
Key names follow the keyboard layout: an AZERTY player's Q key shows A
2026-10-30 20:28:24 +00:00
rdbt e387764263 Merge pull request #4626 from AurelienBelloir/parity/block-entity-updates
Send beacon, end gateway and creaking heart update tags
2026-10-30 20:28:19 +00:00
rdbt 0e6e9ab098 Merge pull request #4624 from AurelienBelloir/parity/team-packets-to-guests
Send a team leave before the join that moves a player
2026-10-30 20:28:14 +00:00
rdbt 6f3757c8e4 Merge pull request #4621 from WooCaash/fix/4567-pillager-ominous-banner
Give raid captains the ominous banner stack for clients
2026-10-30 20:28:09 +00:00
rdbt 49878e9aee Merge pull request #4617 from WooCaash/fix/4428-title-screen-toasts
Draw ToastManager toasts over the title screen
2026-10-30 20:28:03 +00:00
rdbt dd63a5cc5f Merge pull request #4603 from xFedeT/fix/piglin-doors
Piglins open the doors on their path (InteractWithDoor)
2026-10-30 20:27:58 +00:00
rdbt 8e83c3eb23 Merge pull request #4596 from xFedeT/fix/path-recompute
Mobs re-path when a block changes near their route (PathNavigation.shouldRecomputePath)
2026-10-30 20:27:52 +00:00
rdbt 48f539e7d3 Merge pull request #4595 from doodlyyousuf/fix/sound-packet-seed
Give each server sound packet its own seed
2026-10-30 20:27:46 +00:00
rdbt 6db9492f5f Merge pull request #4587 from phexuss/fix/cartography-map-markers
Draw map markers on the cartography table's preview
2026-10-30 20:27:41 +00:00
rdbt 3ad2bda85f Merge pull request #4585 from xFedeT/fix/frog-preferred-jump
Port map: the frog and goat long jump rows point at frog.rs (ported in #3946)
2026-10-30 20:27:34 +00:00
rdbt 2817362973 Merge pull request #4582 from phexuss/fix/enchanting-book
Draw the enchanting table's book in its screen
2026-10-30 20:27:27 +00:00
rdbt 4acfc70975 Merge pull request #4578 from phexuss/fix/happy-ghast-riding-sound
Play the happy ghast's riding sound while the player rides one
2026-10-30 20:27:22 +00:00
levzzzandClaude Sonnet 5.5 d214aa61d7 Validate pack.mcmeta formats like PackFormat.IntermediaryFormat; clear packs on a failed reload
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-31 01:27:18 +05:00
rdbt 797d6512ea Merge pull request #4568 from xFedeT/fix/armadillo-follow-adult
Baby armadillos follow the nearest visible adult (AdultSensor, BabyFollowAdult 5..16)
2026-10-30 20:27:16 +00:00
rdbt 3c66c314b9 Merge pull request #4566 from CocofireHD/fix/pillager-crossbow
A pillager shows the arrow in its loaded crossbow
2026-10-30 20:27:11 +00:00
rdbt 337b0e67fb Merge pull request #4564 from doodlyyousuf/fix/item-slime-piston-bounce
Bounce dropped items on slime, including piston-moved slime
2026-10-30 20:27:05 +00:00
rdbt 12a7bf8b29 Merge pull request #4559 from xFedeT/fix/frog-breeding
Frogs breed: slimeball feeding, pregnancy, and frogspawn laid on water
2026-10-30 20:27:00 +00:00
rdbt 3a906e8647 Merge pull request #4544 from AurelienBelloir/test/windows-world-preset-dirs
Name the world preset test's folders without a colon
2026-10-30 20:26:54 +00:00
rdbt d2b451af13 Merge pull request #4540 from DmytroChup/fix-4431
Use workspace base64 and sha1 crates in skins.rs
2026-10-30 20:26:48 +00:00
rdbt 5560aadd24 Merge pull request #4523 from xFedeT/fix/kid-tag-run-off
mob_ai: baby villagers play tag, checked tick for tick against vanilla
2026-10-30 20:26:42 +00:00
levzzzandClaude Sonnet 5.5 795c8e96a0 Pause menu bug/feedback links and the title's credits text button
PauseScreen.createPauseMenu: the Report Bugs and Give Feedback icon buttons open
CommonLinks through a trusted ConfirmLinkScreen. PlainTextButton on the title's
bottom text opens Credits & Attribution.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-31 01:26:37 +05:00
rdbt f257d62aa3 Merge pull request #4518 from xFedeT/fix/copper-golem-doors
Copper golems open the doors on their path (InteractWithDoor)
2026-10-30 20:26:37 +00:00
rdbt 4bd0bd7dc1 Merge pull request #4516 from arismoko/vanilla-server-block-writes
Route vanilla server block writes through level updates
2026-10-30 20:26:32 +00:00
rdbt b0d000b4b4 Merge pull request #4501 from xFedeT/fix/iron-golem-village-parity
Iron golems draw the sprint particle's randoms; village goal parity scenarios
2026-10-30 20:26:26 +00:00
rdbt 860a3ec2b2 Merge pull request #4497 from xFedeT/fix/fish-school-parity
Fish schools recruit in vanilla's order, with mob_ai parity scenarios (FollowFlockLeaderGoal)
2026-10-30 20:26:21 +00:00
rdbt 0c65b58a65 Merge pull request #4492 from xFedeT/fix/dolphin-follow-ridden
Dolphins swim after the boat or nautilus a player steers (FollowPlayerRiddenEntityGoal)
2026-10-30 20:26:16 +00:00
rdbt 5ab1a8f543 Merge pull request #4468 from Bloodulon/pr/display-passengers
Client: seat displays and hitboxes on whatever carries them
2026-10-30 20:26:11 +00:00
rdbt 0e282c022c Merge pull request #4467 from Bloodulon/pr/interaction-entities
Client: draw click boxes and hit-test server interaction entities
2026-10-30 20:26:06 +00:00
rdbt dcddbbe841 Merge pull request #4457 from DenisHumen/fix/player-attribute-save
Save and restore the player's attribute bases
2026-10-30 20:26:00 +00:00
rdbt 03f7a69993 Merge pull request #4367 from elythepreciouscat/feat/quit-shortcuts
Controls: implement Quit Shortcuts toggle and window-close blocking
2026-10-30 20:25:55 +00:00
rdbt 2d4e6ba0be Merge pull request #4339 from MSedgeMC/fix/3962-cooking-recipe-book
Encode cooking recipes into the recipe book
2026-10-30 20:25:50 +00:00
rdbt cab2754fe1 Merge pull request #4332 from WhyFovius/fix/player-hurt-sound
Combat: the hurt player hears its own hurt sound (single player and multiplayer)
2026-10-30 20:25:44 +00:00
rdbt dfb9e41079 Merge pull request #4307 from MSedgeMC/fix/4129-nametag-pose-height
Hang nametags from the pose box, not the sneak flag
2026-10-30 20:25:38 +00:00
rdbt 1374959d6f Merge pull request #4304 from MSedgeMC/fix/eye-of-ender-lift
Lift the thrown eye of ender by the item ground transform
2026-10-30 20:25:33 +00:00
rdbt 0f1a100391 Merge pull request #4283 from DUzzL/fix/issue-4269-creative-tab-tooltip
Pad the creative tab tooltip like the item tooltip
2026-10-30 20:25:27 +00:00
rdbt ac6f38c3d6 Merge pull request #4277 from DUzzL/fix/issue-4266-candle-on-cake
Place a candle on an uneaten cake
2026-10-30 20:25:22 +00:00
rdbt 5d47a8557c Merge pull request #4270 from DenisHumen/fix/lectern-scroll
Keep the mouse wheel off the hotbar while a book or lectern is open
2026-10-30 20:25:16 +00:00
rdbt 48960bffd6 Merge pull request #4263 from DUzzL/fix/issue-4251-lectern-take-book
Close the lectern book screen after taking the book
2026-10-30 20:25:10 +00:00
rdbt 5370ae92cf Merge pull request #4258 from DUzzL/fix/issue-4248-render-icons
Clip HUD item icons as polygons at the scissor edge
2026-10-30 20:25:05 +00:00
rdbt 77e8224441 Merge pull request #4249 from DenisHumen/fix/return-to-sender
Return to Sender: a fireball struck back credits the player's kill
2026-10-30 20:24:59 +00:00
rdbt c003ada028 Merge pull request #4246 from DenisHumen/fix/falling-block-offset
Falling blocks keep their random model offset, so pointed dripstone doesn't jump when it falls
2026-10-30 20:24:53 +00:00
rdbt 4daf4be5c0 Merge pull request #4238 from DUzzL/fix/issue-4234-worldspawn-after-levels
Stop the levels E2E test leaving an Overworld spawn point
2026-10-30 20:24:47 +00:00
rdbt b3f160b674 Merge pull request #4216 from DenisHumen/fix/shield-server-stop
Lower the shield when a server stops the player's use
2026-10-30 20:24:42 +00:00
rdbt 13451e7207 Merge pull request #4207 from GIGABAIT93/fix/port-publish-utf8
port_publish: read gh's and Kira's output as UTF-8
2026-10-30 20:24:37 +00:00
rdbt 00c456d313 Merge pull request #4196 from kacpherek/fix/gameplay-mp-combat
Riders never claim to be on the ground, so Grim stops flagging boats
2026-10-30 20:24:31 +00:00
rdbt 6c8e7e3e59 Merge pull request #4195 from kacpherek/fix/discord-carts-boats
A player who boards a boat while gliding leaves the elytra pose
2026-10-30 20:24:26 +00:00
rdbt a71dfda331 Merge pull request #4194 from kacpherek/fix/discord-dupes
Creative inventory: send the edit with the click, so holding Q over a stack can't duplicate it
2026-10-30 20:24:21 +00:00
rdbt 0d1681d649 Merge pull request #4193 from DUzzL/fix/issue-4190-crystal-beam-reset
Clear a respawned end crystal's beam for LAN guests
2026-10-30 20:24:15 +00:00
rdbt 318ed97be4 Merge pull request #4172 from DUzzL/fix/issue-4168-pack-download-timeout
Time out stalled resource pack downloads
2026-10-30 20:24:10 +00:00
Ludovic MarieandClaude Opus 5.5 fde63c0597 Server: reset a ridden vehicle's last good position every tick
ServerGamePacketListenerImpl.tick sets vehicleFirstGood/LastGood to where the
vehicle is at every tick. Horizon set it only on boarding and after an
accepted move, so once the server moved the vehicle itself (/tp, a piston,
a push), the rider's moves were checked against a stale position: each one
failed, was sent back, and the client's answer failed again. The boat stayed
stuck and the official server logged up to 6,700 "moved wrongly" lines a
second.

lan_e2e boat_shoved: a piston shoves, then /tp moves, the boat the host and
the guest each ride; they paddle on, with at most a few "moved wrongly" lines.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 21:24:05 +01:00
rdbt 1803841469 Merge pull request #4146 from DUzzL/fix/issue-4145-gateway-exact-teleport-key
Use the ExactTeleport key in the levels E2E end-gateway setup
2026-10-30 20:24:04 +00:00
rdbt 660babec5b Merge pull request #4141 from DUzzL/fix/issue-4134-slot-gap-hover
Hit-test the one-pixel border around inventory slots
2026-10-30 20:23:59 +00:00
rdbt 3613a6eb0d Merge pull request #4138 from DUzzL/fix/issue-4131-crystal-random-rotation
Randomise the spin phase of end crystals placed by the server
2026-10-30 20:23:53 +00:00
rdbt 49544daa24 Merge pull request #4132 from DUzzL/fix/issue-4118-attribute-main-hand
Apply held-item attribute modifiers to the main hand
2026-10-30 20:23:47 +00:00
rdbt 44a07ac4bf Merge pull request #4125 from Benflex1/fix/3515-trade-sound
Play the merchant's yes sound on a shift-clicked trade
2026-10-30 20:23:42 +00:00
rdbt 1beefecb54 Merge pull request #4108 from DUzzL/fix/issue-4096-remote-default-skin
Show the default skin when a remote player's skin sprite is missing
2026-10-30 20:23:34 +00:00
rdbt a2fa7b322d Merge pull request #4101 from DUzzL/fix/issue-4090-lingering-cloud-color
Keep the potion colour on lingering clouds
2026-10-30 20:23:28 +00:00
levzzz e654389d2a Merge pull request #4646 from levzzz5154/parity/26-render
Draw a happy ghast's quad leads and ropes, bob pufferfish
2026-10-31 01:23:22 +05:00
rdbt 0ce6c9fe94 Merge pull request #4093 from arismoko/fix/server-lists-offline-test
Keep stopped server list test offline
2026-10-30 20:23:19 +00:00
rdbt d57bcbca83 Merge pull request #4081 from xFedeT/fix/inventory-preview-lighting
Inventory preview: the player is lit as ENTITY_IN_UI, not by a guess
2026-10-30 20:23:13 +00:00
rdbt 521a01282b Merge pull request #4075 from xFedeT/fix/ctrl-click-right-click
Right Click Emulation: Control + left click is a right click on macOS
2026-10-30 20:23:07 +00:00
rdbt aa1a9b9379 Merge pull request #4012 from elythepreciouscat/fix/armor-rendering
Render: multi-layer equipment definitions and overlays
2026-10-30 20:23:02 +00:00
rdbt 176cfef84f Merge pull request #4007 from DUzzL/fix/issue-4000-invisible-armor
Invisible players keep their armour, elytra and held items
2026-10-30 20:22:56 +00:00
rdbt 9d59efaf3f Merge pull request #3927 from DenisHumen/fix/launcher-trash-com
Launcher: moving a world to the trash no longer closes it on Windows
2026-10-30 20:22:51 +00:00
rdbt d9cbe12a4f Merge pull request #3917 from quintyo/fix/hand-swap-scale
Fix item model hand swap animation scale
2026-10-30 20:22:45 +00:00
rdbt 2b2851a0dc Merge pull request #3869 from spidercraft/fix/friends-code-ends-with-world
Play with Friends: leaving the world ends its invite code
2026-10-30 20:22:39 +00:00
rdbt 1adb5cf92d Merge pull request #3841 from DUzzL/fix/inventory-server-transfer
Reset the client menu when a server replaces the player
2026-10-30 20:22:34 +00:00
rdbt 4742c9b3f5 Merge pull request #3839 from GIGABAIT93/fix/offscreen-pointer
--offscreen: hide the window and leave the system pointer alone
2026-10-30 20:22:28 +00:00
rdbt 1f82ef2116 Merge pull request #3811 from DenisHumen/fix/launcher-menus-close-outside
Launcher: the version and account menus close on a click outside them
2026-10-30 20:22:23 +00:00
rdbt b9e348c2c0 Merge pull request #3669 from martipops/fix/stick-on-servers
Controller movement on servers: the stick moves as the keys it sets do
2026-10-30 20:22:17 +00:00
levzzz f4da935e0f Merge pull request #4289 from Sylviromi/native-mod-screen-widgets
Native mods: buttons, tooltips and item icons on mod screens
2026-10-31 01:19:39 +05:00
levzzzandClaude Sonnet 5.5 066eb31ad5 Draw a happy ghast's quad leads and ropes, bob pufferfish, scroll breeze wind by partial tick
PufferfishRenderer.setupRotations, RopesLayer.submit, LeashFeatureRenderer/EntityRenderer quad leash, RenderTypes.breezeWind.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-31 01:10:38 +05:00
levzzz 073afd484e Merge pull request #4639 from levzzz5154/parity/37-deathmsg
Reload sends tags and recipes and reloads advancements; code of conduct task
2026-10-31 01:08:21 +05:00
levzzzandClaude Sonnet 5.5 2bc03baa6b Merge main; buttons, tooltips and item icons on mod screens are API 20
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-31 01:04:57 +05:00
levzzz 3a5ac7ffed Merge pull request #4106 from Sylviromi/native-mod-screens
Native mods: mod-drawn screens for mod blocks (own GUI textures, layout, data)
2026-10-31 01:03:37 +05:00
levzzz 0a7ad01ed6 Merge pull request #4635 from levzzz5154/parity/23-misc
Parity: view-blocking overlay from isViewBlocking and the near plane
2026-10-31 01:00:57 +05:00
levzzzandClaude Sonnet 5.5 be0445848d Reload: send tags, recipes and the recipe book to players and reload their advancements; offer the code of conduct
PlayerList.reloadResources and ServerConfigurationPacketListenerImpl.addOptionalTasks.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-31 01:00:13 +05:00
JustKarkatandZCode f5c79065e9 A dying player's forgiveness reaches the target-anger mobs
The forgiveness dispatch's default arm is the tame gate, which needs
extra.angry_at — a field only the wolf's anger writes. The enderman and
the zombified piglin hold their persistent anger as their target (the
stare goal, the hurt-by path) and the iron golem keeps a timer and its
target, so none of the three were ever forgiven when the player they
were angry at died, while vanilla's NeutralMob.playerDied ends the anger
of all three through stopBeingAngry (Enderman and ZombifiedPiglin are
NeutralMob implementers in 26.3, checked with javap).

Their arms clear the target the dying player held, the last hurt by
with it, and for the golem the timer; anger at another mob stays, and
the tame, bee and polar-bear paths are untouched.

Follow-up to #3507, closed for main carrying the area and gate fixes:
this is the piece main still misses, on a fresh small branch as asked.

Co-Authored-By: ZCode <[email protected]>
2026-10-30 14:57:09 -05:00
levzzzandClaude Sonnet 5.5 a14cac7f7c Merge main; mod screens are API 19; the server menu skips the slots a mod screen hides
A slot the mod's screen hides was still in the server's menu, so a shift-click
could move items into a slot the player cannot see. The server asks the same
native_screen when it opens the menu and the menu's clicks and quick moves skip
those slots. Also documents that data slots are 16-bit and start empty.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-31 00:56:49 +05:00
levzzzandClaude Sonnet 5.5 08918a8a7f Show the view-blocking block overlay by BlockState.isViewBlocking against the near plane
Frustum.getNearPlaneBounds, then stairs and slabs by outline, snow at 8 layers, others by isSuffocating.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-31 00:54:36 +05:00
levzzz 34118b285f Merge pull request #4622 from levzzz5154/parity/38-srvpkts
Server players: the flying kick and the build limit message told twice
2026-10-31 00:54:18 +05:00
levzzz b680d71bdc Merge pull request #4097 from Sylviromi/native-mod-neighbours
Native mods: what a machine's tick reaches (neighbouring mod block entities, its block's state)
2026-10-31 00:53:30 +05:00
Ludovic MarieandClaude Opus 5.5 1eedd39c76 lunge_motion: the jump's positions follow the server's ticks, not 50 ms sleeps
LocatorBarClient's arc slept 50 ms between positions, which drifts from the
server's ticks: on a busy runner the stab of `arc 8` came 8, 9 or 10 server
ticks after the jump, on vanilla as on Horizon, so the falling and sprinting
motions differed run to run (#4526's parity run failed on it). Each position
now waits until the game time moves past the latest `time query gametime`
reply, so the stab of `arc N` lands N ticks after the jump on both servers.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 20:49:29 +01:00
Neferr 74351c8057 Key names follow the keyboard layout: an AZERTY player's Q key shows A
Keys are kept by position (SDL scancodes), as 26.3 keeps them, but named by what the US layout writes. Each key press now records what the player's layout writes on that key (winit's key_without_modifiers), and display_name shows it, as SDL_GetKeyFromScancode names keys with SDL's default keycode options: letter and symbol keys only, Latin letters only on a letter key, digits kept on the number row. Fixes #4592.
2026-10-30 20:46:36 +01:00
levzzz 8486a32926 Merge pull request #4620 from levzzz5154/parity/21-antenna
Copper golem antenna: apply the item's block_state component
2026-10-31 00:46:17 +05:00
levzzzandClaude Sonnet 5.5 31300e6846 Merge hr/main into parity/38-srvpkts
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-31 00:45:58 +05:00
Ludovic MarieandClaude Opus 5.5 cf6e5c2474 Merge main: mod inventories and menus took API 16 and 17, these hooks are API 18
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 20:45:35 +01:00
levzzzandClaude Sonnet 5.5 deec9041ac Merge main; neighbour and block-state helpers are API 18
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-31 00:44:25 +05:00
levzzz c822f6eb82 Merge pull request #4072 from Sylviromi/native-mod-menus
Native mods: menus for mod containers (vanilla slot grids and furnaces)
2026-10-31 00:43:16 +05:00
Łukasz KopańskiandCursor e1314bbcc9 Give raid captains the ominous banner stack for clients.
Fixes #4567: wear_banner only set the white_banner id, so SetEquipment
sent a plain banner and clients without PatrolLeader drew white cloth.
Keep Raid.getOminousBannerInstance on armor[0] and treat that stack as
captain for CustomHeadLayer.

Co-Authored-By: Cursor <[email protected]>
2026-10-30 20:42:23 +01:00
levzzzandClaude Sonnet 5.5 8d82e986f9 Server players: the floating kick, and the build limit told twice at the top
ServerGamePacketListenerImpl.noBlocksAround, getMaximumFlyingTicks and
tickPlayer's clientIsFloating / aboveGroundTickCount check, and
handleUseItemOn's second build-limit message for a failed placement on top
of the world.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-31 00:42:12 +05:00
levzzzandClaude Sonnet 5.5 d51a2eb268 Copper golem antenna block takes the item's block_state component (CopperGolemRenderer.extractRenderState)
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-31 00:39:26 +05:00
levzzz b9f756ebad Merge pull request #4612 from levzzz5154/parity/20-render
Cat: lie on top of a sleeping player like vanilla
2026-10-31 00:36:20 +05:00
Ludovic Marie cf38f00b54 Merge pull request #3724 from aangeraakt/claude/kind-dijkstra-aaxkhw
🤖 Agent: pr-triage-1 (Ludovic's AI agent, Claude), not Ludovic himself. Main moved and conflicted in `NetConfig`: its new `horizon_clients_only` field now sits beside `owner_profile` (faf7d52f2). Rechecked on main: `parity.py --changed` 110/110, `pr_ready --quick` pass, lan_e2e `tab`, `self` and `chest` pass. Queued again.
2026-10-30 20:36:09 +01:00
levzzzandClaude Sonnet 5.5 76ab943b40 Merge main; menus for mod containers are API 17
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-31 00:35:35 +05:00
levzzz 6f846cf6a5 Merge pull request #4611 from levzzz5154/parity/39-build-limit-shoulder
Server players: build limit overlay and shoulder parrot ambient sound
2026-10-31 00:35:18 +05:00
Łukasz KopańskiandCursor d0fbd3f638 Draw ToastManager toasts over the title screen.
Fixes #4428: the title early-return skipped advancement, tutorial and
pack toasts, so a resource-reload failure never appeared there.

Co-Authored-By: Cursor <[email protected]>
2026-10-30 20:33:44 +01:00
levzzz a14c9886ce Merge pull request #4055 from Sylviromi/native-mod-inventories
Native mods: item slots for mod block entities (hoppers, comparators, drops)
2026-10-31 00:33:32 +05:00
Ludovic Marie 07641bb0b2 Merge pull request #4607 from horizon-rust/feat/anticheat-framework
🤖 Agent: pr_ready --quick green on af5528ef2; dedicated_server, server_entities, server_player_damage pass; anticheat_e2e: a 1.5x timer bot is flagged, set back, alerted and kicked, 6 legit bots never flagged, nothing runs when [anticheat] is absent; 60-bot A/B: off = main within noise
2026-10-30 20:32:35 +01:00
Ludovic MarieandClaude Opus 5.5 bf29bd9166 Merge main: block entities took API 15, these hooks are API 16
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 20:30:00 +01:00
levzzzandClaude Sonnet 5.5 820913dc4c Cat: a lying cat on top of a sleeping player sits 0.15 further along (CatRenderer.setupRotations)
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-31 00:27:53 +05:00
Ludovic MarieandClaude Opus 5.5 e021395210 Native mods: native_action_bar, which reaches the acting player in their hook
native_tell reaches nobody inside a player's swap (the host is out of
self.lan there): a claim's refusal is told on the action bar instead,
through the acting guest's queued packets.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 20:26:46 +01:00
levzzzandClaude Sonnet 5.5 e311967b29 Server players: build limit overlay, shoulder parrot ambient sound
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-31 00:26:45 +05:00
levzzzandClaude Sonnet 5.5 5edf728ee3 Merge main; item slots for mod block entities are API 16; a block entity that breaks its own block drops its contents
The tick takes the block entity out of the world while the mod runs. If the
mod removed or replaced its block meanwhile, the saved items were dropped with
the taken value. They now spill like Containers.dropContents does on removal.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-31 00:23:21 +05:00
levzzz 6e8e088c25 Merge pull request #3996 from Sylviromi/native-mod-block-entities
Native mods: block entities for mod blocks (create, tick, save)
2026-10-31 00:21:02 +05:00
Ludovic Marie c27092a567 Merge remote-tracking branch 'origin/main' into HEAD 2026-10-30 20:17:29 +01:00
levzzz 17b1f8fd19 Merge pull request #4598 from levzzz5154/registry/form-links
Mod registry form: list a project by its repository, no updates through the form
2026-10-31 00:16:49 +05:00
Federico TaglianiandClaude Sonnet 5.5 6b3aacf619 Piglins open the doors on their path (InteractWithDoor)
Refs #3521

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 20:15:46 +01:00
levzzzandClaude Sonnet 5.5 f2d9c04794 Merge main; block entity hooks are API 15 (main is at 14)
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-31 00:11:56 +05:00
Ludovic Marie 19ca1c8b33 Merge pull request #3762 from spidercraft/fsr-optical-flow
🤖 Agent: pr-triage-1 (Ludovic's AI agent, Claude), not Ludovic himself. Gate for this part (1/3), on 785796786 merged with main, Mac mini M4 Pro (Metal). **License**, checked against CONTRIBUTING's new rule on other projects' code (#4482): - FidelityFX SDK v1.1.4 is commit c6efa6bf7f20. At that commit, `LICENSE.txt` and the header of every file this port follows are AMD's MIT text, copyright line included: all ten `sdk/include/FidelityFX/gpu/opticalflow/*.h`, `sdk/src/components/opticalflow/ffx_opticalflow.cpp` and `gpu/spd/ffx_spd.h`. - `optical_flow.rs` follows `ffx_opticalflow.cpp` (its passes, their order and their dispatch sizes). So I added the same notice at its top and named that file in `THIRD_PARTY_NOTICES.md`, with a link to the SDK at the commit (785796786). Nothing else changed. #3763 and #3764 contain the old head and need this merged in. **Proof:** - `pr_ready --quick` passes. `parity.py --changed` has nothing to run: the selector maps these files only to lan_integration, which runs when named or nightly. - `shader_validates` and `finds_a_shift_and_a_scene_change` pass on Metal. **Cost per frame:** - Off: zero. `OpticalFlow` is constructed nowhere until part 3. - On: measured locally with a throwaway test (not pushed) that dispatches it on two alternating frames, 100 frames after a warm-up: | size | GPU, median (p90) | CPU encode (debug build) | |---|---|---| | 1280×720 | 2.33 ms (2.96) | 0.27 ms | | 1920×1080 | 3.30 ms (3.76) | 0.29 ms | | 2560×1440 | 4.68 ms (5.53) | 0.31 ms | | 3840×2160 | 8.48 ms (9.35) | 0.32 ms | Part 3 should keep this in mind: on an M4 Pro the flow alone costs a fifth of a 60 fps frame at 1080p, before the interpolation, so frame generation only gains where the real frame takes much longer than that. Keep it off by default, as planned. - Smaller notes, none blocking: - `dispatch` creates the `prepare_luma` bind group every frame; it could be cached per colour view. - The scene-change "last workgroup" handoff relies on relaxed WGSL atomics being visible across workgroups, which holds on current drivers. Queued for the merger (`skip-release`: players see no change).
2026-10-30 20:10:55 +01:00
levzzzandClaude Sonnet 5.5 b781de899d Form service: list a project by its repository, take no updates for it
A submission with repo (and no file) opens a pull request adding an entry that
links the public GitHub repository; its versions are the repository's releases.
An update through the form of a project that has a repo is refused with
'publish a GitHub release in <repo>'. Projects without a repo keep the file path.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-31 00:08:06 +05:00
levzzz af8f2fb7c2 Merge pull request #4593 from levzzz5154/registry/entry-repo-releases
Registry: an entry is a repository link and a page; versions come from its releases
2026-10-31 00:08:02 +05:00
levzzz 04687c99c6 Merge pull request #4589 from levzzz5154/registry/links-and-releases
Launcher: accept a GitHub release asset as a registry file
2026-10-31 00:06:45 +05:00
Ludovic Marie b90b7d8aac Merge pull request #3743 from UMCEKO/mods/equipment-every-namespace
🤖 Agent: pr-triage-1 (Ludovic's AI agent, Claude), not Ludovic himself. I merged main into this branch (97f2c57e5, 6350e5219). On main, an equipment kind now holds every layer (leather's overlay is layer 1). So the namespace resolution now runs for each layer's texture, and `content_mods_equipment_loads_under_its_namespace` expects a list of layers. `hud.rs` keeps your namespaced sprite names with main's decoded image. Proof, on main with #3741 and #3742: - `parity.py --changed` 110/110; - `pr_ready --quick` pass, including both new tests. - In the game: pr-triage-3's pack test at your heads (comment on #3740) showed amethyst, copper and chorundum armour drawing their own textures. - A root without content mods: `content_namespaces::namespaces` is empty, so no extra folder is walked, and every vanilla texture still takes the `strip_prefix(textures)` branch. Same sprites as before. Queued after #3742.
2026-10-30 20:06:28 +01:00
Ludovic Marie 03331b3d52 Merge pull request #3742 from UMCEKO/mods/port-item-stats
🤖 Agent: pr-triage-1 (Ludovic's AI agent, Claude), not Ludovic himself. I merged main into this branch through #3741's carry (no conflicts of its own). It's proven with #3741 and #3743 on main: `parity.py --changed` 110/110 and `pr_ready --quick` pass. Queued after #3741.
2026-10-30 20:06:23 +01:00
Ludovic Marie bf4e738618 Merge pull request #3741 from UMCEKO/mods/harness-item-components
🤖 Agent: pr-triage-1 (Ludovic's AI agent, Claude), not Ludovic himself. I merged main into this branch (fd41f0e36; `mod_port_probe.rs` now uses main's `PackOverlay` loaders). I also made the one change pr-triage-3 asked for in its pack test on #3740 (a502eccc9): the attribute normaliser drops 1.21.1's `player.` prefix too, so spears' `entity_interaction_range` matches the oracle. Proven together with #3742 and #3743 on main: `parity.py --changed` 110/110 and `pr_ready --quick` pass. Queued for the merger; #3742 and #3743 follow.
2026-10-30 20:04:07 +01:00
Ludovic MarieandClaude Opus 5.5 1de9f9fc69 Native mods: right click on a block, the acting player, server commands (API 15)
native_right_click_block hears a player's right click on a block before
the block's own use and the item's, and may cancel it; native_acting_player
says who acts inside a player's hook; native_run_command runs a command as
the server's console (teleports across levels); server players carry
their health. What a server mod needs for land claims and teleports.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 20:03:12 +01:00
Federico TaglianiandClaude Sonnet 5.5 2b85acfeaf Mobs re-path when a block changes near their route (PathNavigation.shouldRecomputePath)
Refs #3521

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 20:03:07 +01:00
Muhammad AsifandOhMyAgent e74907f551 Give each server sound packet its own seed
Hub::sound used the server tick as ClientboundSoundPacket.seed, so two
sounds in the same tick picked the same client variant. Level.playSound
draws soundSeedGenerator.nextLong per call.

Closes #4576.

Co-Authored-By: OhMyAgent (grok-4.6) <[email protected]>
2026-10-31 00:00:27 +05:00
levzzzandClaude Sonnet 5.5 85e7517b76 Registry: an entry is a link and a page; its versions are the repository's releases
An entry names its GitHub repository (repo). The page fields may come from
horizon-registry.toml in that repository (the entry's own win), and the versions
the registry's CI read from the repository's releases are kept in
versions/<id>.json and listed beside the entry's own [[versions]], which become
optional. cargo horizon-mod registry release|page run a submission's checks on a
release and print the version/page for that cache. The index schema is unchanged.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 23:59:13 +05:00
Ludovic MarieandClaude Opus 5.5 faf7d52f20 Merge main into claude/kind-dijkstra-aaxkhw
Conflict: main's horizon_clients_only beside owner_profile in NetConfig.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 19:57:26 +01:00
Ludovic Marie cb2a7fb1b6 Merge pull request #4561 from horizon-rust/docs/smp-gambling
🤖 Agent: docs only (docs/smp.md), checked at 4cbfa78: the gambling rules match the orchestrator's decision of 2026-10-30 (on for the official server only, off in the mod by default; in-game currency only, never bought with real money or cashed out, no paid random items), and the coinflip lines match horizon-mods #58 as merged; pr_ready and CI green.
2026-10-30 19:56:36 +01:00
Ludovic MarieandClaude Opus 5.5 7857967863 Optical flow: AMD's notice on the Rust side too, and the SDK commit
optical_flow.rs follows ffx_opticalflow.cpp (passes, order, dispatch
sizes), so it keeps the SDK's copyright line and MIT notice like the WGSL
port; THIRD_PARTY_NOTICES names that file and links the SDK at the v1.1.4
commit, as CONTRIBUTING's rule on other projects' code asks.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 19:56:02 +01:00
levzzzandClaude Sonnet 5.5 4fabf2025c Launcher: install registry files from a GitHub release asset
The registry's index will list the release assets of a mod's own repository
(https://github.com/<owner>/<repo>/releases/download/<tag>/<name>), so an author
ships a version without a registry pull request. The launcher accepted only the
registry's own releases; it now also accepts that exact address shape. The
index's SHA-256 still pins the bytes.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 23:55:22 +05:00
levzzz 3c580814c8 Merge pull request #4483 from ludovicthenot/native-block-hooks
Native mods: hook points for breaking and placing a block (API 6)
2026-10-30 23:52:42 +05:00
Ludovic MarieandClaude Opus 5.5 876d0003d5 Anticheat framework, off by default, with a timer check
A new crate, horizon-anticheat, holds server.toml's [anticheat] settings, each player's violation
levels (they decay every second) and the checks' rules, with no game state. The dedicated server's
hub runs the checks where it handles packets and carries out a flag: send the player back, alert the
console, the log file and online operators, then kick; never a ban. Each check has its own toggle and
thresholds. Without [anticheat] (the default, and the generated server.toml only has a commented
example) the hub holds no anticheat and the server does what vanilla's does.

The first check is timer: vanilla's client ends each tick with ServerboundClientTickEndPacket at the
server's tick rate, so a client further ahead of the wall clock than 300 ms is flagged, with a capped
balance for lag spikes and slow server ticks.

horizon-bots gets --timer F (a sped-up client), tools/anticheat_e2e.py runs a real dedicated server
with legit and cheating bots, and server_bench.py --anticheat measures the cost.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 19:49:49 +01:00
levzzz af5528ef25 Merge pull request #4580 from levzzz5154/launcher/mods-from-github-ui
Mods page: Add from GitHub, origin line and Check for updates
2026-10-30 23:44:33 +05:00
levzzz f4c1010c64 Merge pull request #4489 from ludovicthenot/fix-zoom-keybind-label
Options > Mods: a test that a native mod's key shows and keeps the key pressed
2026-10-30 23:42:43 +05:00
Claude 72f66bb71c Draw map markers on the cartography table's preview
MapRenderer.render in a screen draws the map's decorations over its image,
all of them since the map isn't in an item frame.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Rx5M6NS763D2WVnb2HmcTH
2026-10-30 18:42:28 +00:00
levzzzandClaude Sonnet 5.5 f969e62a56 Merge main: block hooks are API 14
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 23:42:16 +05:00
levzzz edbeffbcf3 Merge pull request #4302 from ludovicthenot/native-use-item-hook
Native mods: hook points for using an item (API 5)
2026-10-30 23:40:44 +05:00
levzzzandClaude Sonnet 5.5 8a2e5cb55f Mods page: Add from GitHub, origin line and Check for updates
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 23:36:23 +05:00
levzzz 1c7eb12800 Merge pull request #4573 from levzzz5154/launcher/mods-from-github
Launcher: install a native mod from a GitHub link (CLI)
2026-10-30 23:36:17 +05:00
levzzz 02c48952b8 Merge pull request #4575 from levzzz5154/registry/owner-updates
Registry form: sign an owner's update so horizon-mods can merge it without a reviewer
2026-10-30 23:36:13 +05:00
levzzz 0ddaf69abc Merge remote-tracking branch 'hr/main' into fix/pr-4489 2026-10-30 23:30:09 +05:00
levzzzandClaude Sonnet 5.5 0e060bb2ac Merge main: use-item hooks are API 13
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 23:29:33 +05:00
levzzzandClaude Sonnet 5.5 754440dd55 Registry form: sign an owner's update so horizon-mods can merge it without a reviewer
An owner's new version with nothing else of the entry changed gets an HMAC
trailer (REGISTRY_UPDATE_KEY) in the pull request body and the label
owner-update. Off when the secret is unset.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 23:28:00 +05:00
levzzz cd79f2e326 Merge pull request #4284 from ludovicthenot/native-tooltip-hook
Native mods: a hook point for item tooltips (API 4)
2026-10-30 23:27:32 +05:00
Claude aced527885 Draw the enchanting table's book in its screen
EnchantmentScreen.tickBook/extractBook: the book opens while an offer has a
cost and turns to a new page when the enchanted item changes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Rx5M6NS763D2WVnb2HmcTH
2026-10-30 18:26:59 +00:00
Claude 648531c6da Play the happy ghast's riding sound while the player rides one
LocalPlayer.startRiding starts a RidingEntitySoundInstance for a happy
ghast (entity.happy_ghast.riding, volume 5 * clampedLerp(speed, 0, 1)),
as it does the two inside sounds for a minecart; Horizon only had the
minecart's. Port the instance: it stops when the player gets off or the
ghast is silenced, and is silent under water and below 0.01 speed.

Part of #3518.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Rx5M6NS763D2WVnb2HmcTH
2026-10-30 18:26:58 +00:00
levzzzandClaude Sonnet 5.5 b4ad459449 Launcher: install a native mod from a GitHub link (mods add-github, update-github)
Resolves a repo, release, tag, ref or commit link through GitHub's public API to an
exact release .hnmod or commit, checks the bundle as the registry does, asks for
--trust-native-code, and keeps the origin beside the file.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 23:26:38 +05:00
DUzzL c08c051a92 Document the menu-transfer check 2026-10-30 19:24:04 +01:00
Ludovic MarieandClaude Opus 5.5 3b284f173f Merge mods/port-item-stats into mods/equipment-every-namespace
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 19:24:00 +01:00
Ludovic MarieandClaude Opus 5.5 18a900a360 Merge mods/harness-item-components into mods/port-item-stats
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 19:24:00 +01:00
Ludovic MarieandClaude Opus 5.5 a502eccc92 Mod port harness: drop 1.21.1's player. attribute prefix too
1.21.2 renamed player.entity_interaction_range, player.block_interaction_range
and the other player.* attributes along with generic.*, so a spear's
interaction range never matched the oracle's.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 19:24:00 +01:00
Federico TaglianiandClaude Sonnet 5.5 8dff50318a Port map: the frog and goat long jump rows point at frog.rs (ported in #3946)
Refs #3521

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 19:22:06 +01:00
Ludovic Marie a3726f4bf8 Merge pull request #4547 from horizon-rust/feat/horizon-clients-only
🤖 Agent: verified on the Mac mini at 7a4f7d5: tools/server_horizon_only_e2e.py on a real dedicated server: off by default (a brandless client joins); with horizon-clients-only=true, no brand and "vanilla" are turned away with "This server is for the Horizon client: playhorizon.dev" and a console line, "horizon" joins. pr_ready --quick green (1067 tests); dedicated_server, server_entities, server_player_damage, server_files harnesses pass.
2026-10-30 19:22:05 +01:00
levzzz d17163d417 Merge pull request #4401 from levzzz5154/launcher/native-pin-mismatch
Launcher: build native mods made for different Horizon versions, after "Build anyway"
2026-10-30 23:19:17 +05:00
Federico TaglianiandClaude Sonnet 5.5 c8c02381b5 Baby armadillos follow the nearest visible adult (AdultSensor, BabyFollowAdult 5..16)
Refs #3521

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 19:18:27 +01:00
Ludovic Marie a83ac6ac1c Merge pull request #4553 from horizon-rust/feat/server-perf-report
🤖 Agent: requeued: main merged in (main had dropped the two native_lifecycle source-shape tests, so main's file is taken), docs/api/server-reference.md regenerated for the new command; horizon-server tests 53/53, report and dedicated tests pass; end to end verified earlier (init/start/report 10/stop).
2026-10-30 19:16:55 +01:00
levzzz 701168f1e5 Merge pull request #4560 from levzzz5154/docs/no-wasm-docs
Docs: remove the old WebAssembly mod system
2026-10-30 23:16:14 +05:00
levzzzandClaude Sonnet 5.5 15a18c3a5e Merge main: tooltip hook is API 12
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 23:14:14 +05:00
CocofireHDandHermes Agent c1a5052c43 A pillager shows the arrow in its loaded crossbow
The crossbow model picks its charged form from the stack's
`charged_projectiles` component. Nothing set it for a mob that loads its own
crossbow, and the mob renderer and the equipment packet kept only the item
name, so a mob's components never reached the model at all. Set and clear the
component with the pillager's charge state, draw the stack the mob actually
holds, and keep the stack the equipment packet sends.

Fixes #4521.

Co-Authored-By: Hermes Agent (deepseek-v4.1-flash) <[email protected]>
2026-10-30 19:13:35 +01:00
levzzz 7af000900b Merge pull request #4390 from arismoko/native-vanilla-particles
Native mods: send vanilla particles from server code
2026-10-30 23:12:34 +05:00
Muhammad AsifandOhMyAgent fd855f8815 Bounce dropped items on slime, including piston-moved slime
ItemEntity.tick zeroed Y on any vertical collision, so items never
got slime restitution. A moving piston carrying slime has no
bounceRestitution of its own. Landing now bounces at 0.8 (non-living
Entity.restituteMovementAfterCollisions), and a piston slime launch
clears on_ground so ItemEntity.tick does not skip movement.

Vanilla: ItemEntity.tick, Entity.move, PistonMovingBlockEntity.causeBounce.

Co-Authored-By: OhMyAgent (grok-4.6) <[email protected]>
2026-10-30 23:11:12 +05:00
AurelienBelloirandClaude Opus 5.5 c1db3bad48 Leave ExactTeleport out of an end gateway's update tag when unset
TheEndGatewayBlockEntity.saveAdditional writes Age and exit_portal, then
puts ExactTeleport (as true) only when exactTeleport is set. getUpdateTag
is saveCustomOnly, so vanilla's chunk packets and BlockEntityData never
carry ExactTeleport = false. entity_tag wrote the key unconditionally;
it now only writes it when the flag is set, as save.rs already does.
The test checks both cases round-trip.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 19:10:37 +01:00
levzzzandClaude Sonnet 5.5 f7ce36c8e1 Merge main into native-pin-mismatch
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 23:09:14 +05:00
Ludovic MarieandClaude Opus 5.5 4cbfa78bee docs/smp.md: the gambling rules, on for the official server only
In-game currency only, never bought with real money, never cashed out, no
paid random items; off in the mod by default, on for the official server.
How a coinflip's stakes are held, as the mod does it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 19:08:30 +01:00
levzzz f6154f9ebb Merge pull request #3877 from arismoko/content-block-shapes
Content mods: set per-state outline and collision shapes
2026-10-30 23:07:31 +05:00
levzzzandClaude Sonnet 5.5 f5eb5a18f3 Docs: remove the old WebAssembly mod system from the docs and roadmap
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 23:07:28 +05:00
Federico TaglianiandClaude Sonnet 5.5 a9b932e4c2 Frogs breed: slimeball feeding, pregnancy, and frogspawn laid on water
Refs #3521

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 19:07:17 +01:00
Ludovic MarieandClaude Opus 5.5 71e6b60d96 docs/api/server-reference.md: the report command
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 19:05:57 +01:00
AurelienBelloirandClaude Opus 5.5 0c9ac0cfae Send beacon, end gateway and creaking heart update tags
BeaconBlockEntity, TheEndGatewayBlockEntity and CreakingHeartBlockEntity
override getUpdatePacket (ClientboundBlockEntityDataPacket.create) with
getUpdateTag = saveCustomOnly, so vanilla puts their data in the chunk
packet (BlockEntityInfo.create) and in the BlockEntityData that
ChunkHolder.broadcastBlockEntityIfNeeded sends for a block the level
reports changed (sendBlockUpdated, /data merge block). The LAN host's
entity_tag had no tag for them, so guests got none: a gateway's Age (its
spawning beam) and a beacon's effects never reached them.

entity_tag now gives the three their saveAdditional fields: primary and
secondary effect and Levels; Age, exit_portal and ExactTeleport; the
creaking UUID. The rest of the row from #3520 was already in place: the
host's block log is its sendBlockUpdated, and sign edits, sign dyes, ink
and wax, shelves, campfires, vaults, spawners, trial spawners, decorated
pots and /data already push to it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 19:05:45 +01:00
Ludovic Marie f533c3af7d Merge pull request #4531 from horizon-rust/fix/server-reach
🤖 Agent: verified on the Mac mini at d39eb5d: tools/server_reach_e2e.py on a real dedicated server refuses attacks from 7 and 50 blocks (survival) and 9 (creative), and accepts 3 and 6 (survival) and 7 (creative), as vanilla's handleAttack does (main lets all of them hit). pr_ready --quick green (1065 tests), 34 of 35 affected harnesses pass; lunge_motion's vanilla transcript changes between runs (STAB path, untouched).
2026-10-30 19:05:38 +01:00
Ludovic Marie 25e03754e2 Merge main into feat/server-perf-report
# Conflicts:
#	crates/horizon-client/src/game/native_lifecycle.rs
2026-10-30 19:03:27 +01:00
Ludovic MarieandClaude Opus 5.5 e51783d5bd Launcher: Sign in with Microsoft and loader logos (redesign R8c)
Every button that signs in is now Microsoft's "Sign in with Microsoft",
drawn to Microsoft's published branding (41 pixels, the four-square
symbol from its official SVG, dark #2F2F2F / light white with a #8C8C8C
border, semibold 15 pixels): the Overview's account row, preparing
without the game, the sign-in sheet and Settings. The New instance
dialog's loader cards show Fabric's and Quilt's logos.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 19:01:11 +01:00
levzzzandClaude Sonnet 5.5 00c9b04674 Native particles: API 11
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 23:00:46 +05:00
levzzz 20703366f9 Merge remote-tracking branch 'hr/main' into fix/pr-4390 2026-10-30 22:59:06 +05:00
AurelienBelloirandClaude Opus 5.5 9b47769757 Send a team leave before the join that moves a player
ServerScoreboard.addPlayerToTeam sends the old team's leave
(createPlayerPacket REMOVE) before the new team's join, and a client's
Scoreboard.removePlayerFromTeam throws IllegalStateException when the
player already sits in another team. The LAN host's scoreboard diff sent
teams in name order with joins before leaves, so moving a player to a
team sorting earlier (red to blue) made a vanilla guest drop the packet
with an error and Horizon's remote client take the player out of the
new team. All leaves now go before any add or join.

The remote client's handleSetPlayerTeamPacket leave now only takes a
player out of the named team, as removePlayerFromTeam(name, team) does.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 18:55:55 +01:00
levzzzandClaude Sonnet 5.5 d7cb3537ea Merge main into content-block-shapes: keep shapes and default_state
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 22:55:44 +05:00
levzzz 59e71f00ef Merge pull request #3846 from arismoko/content-default-states
Content mods: allow a block default state override
2026-10-30 22:50:58 +05:00
levzzz fcf76afc05 Merge pull request #4485 from arismoko/native-lifecycle-behavior-cleanup
Native mods: drop source-text lifecycle tests
2026-10-30 22:50:42 +05:00
levzzz b12a499a9a Merge pull request #4412 from levzzz5154/native/server-tick-at-start
Native mods: server tick Start before any level; one tick-clock step per server tick
2026-10-30 22:50:26 +05:00
Ludovic MarieandClaude Opus 5.5 59526900a0 native_lifecycle test: find the server tick in the report's match
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 18:47:29 +01:00
Ludovic MarieandClaude Opus 5.5 b6e7d3e7c0 Launcher: instance icons, pictures and loader marks (redesign R8b)
An instance's icon, in the sidebar, Home's hero and its dialog, is now its
own picture first: one the player chose (Choose image…, scaled to 128
pixels and kept as icon.png in its game folder) or its modpack's icon,
written when the Mods page installs a pack. Else the block or colour
chosen in the dialog; else its mark: the loader's logo (Quilt's and
NeoForge's shipped under CC0 and CC BY 4.0, Fabric's and Forge's fetched
from their own sites), the grass block for plain Minecraft, Horizon's for
Horizon. The letter tile is the last resort.

The dialog's icon tiles sit in rows of eleven: a wrapping row was
measured as one line and the plate ended above the buttons.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 18:46:24 +01:00
arismoko 4783b8d552 Native mods: send vanilla particles from server code 2026-10-30 12:40:06 -05:00
Ludovic Marie ab6851d5a8 Merge pull request #4494 from horizon-rust/feat/smp-design
🤖 Agent: docs only (docs/smp.md + an index line), checked at 6f6650a: it matches Ludovic's scope of 2026-10-30 (the SMP is a native mod only the official server builds; players install nothing) and the hook points of #4514, #4519 and #4534 as they are; pr_ready green.
2026-10-30 18:37:47 +01:00
Ludovic MarieandClaude Opus 5.5 6350e5219d Equipment test: a kind holds every layer, as on main
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 18:35:44 +01:00
Ludovic Marie a03ce15c4e Merge pull request #4534 from horizon-rust/feat/smp-server-menus
🤖 Agent: verified at e23b4b7d8 (stacked on #4519): pr_ready green (1072 horizon-client tests, clippy); dedicated_server and server_entities pass; the SMP mod's /trade on a real dedicated server with two real clients clicking the server's chest menu: closing, the cancel button, death (keep_inventory on) and leaving moved nothing; a full inventory was refused with nothing moved; a confirmed trade swapped 10 diamonds for 5 emeralds and 35 coins, as counted on both clients.
2026-10-30 18:35:39 +01:00
Ludovic Marie d60c8d4e0f Merge pull request #4519 from horizon-rust/feat/smp-item-helpers
🤖 Agent: verified at 396f45655 (stacked on #4514): pr_ready green (1069 horizon-client tests, clippy); dedicated_server and server_entities pass on #4534's head, which holds this one; with the SMP mod's market, a real dedicated server and two real clients ran /sell wheat, /sell hand and /buy torch 5, and the buyer's own client counted 0 diamond, 0 wheat and 5 torch; a creative player was refused.
2026-10-30 18:35:34 +01:00
Ludovic Marie 152a0db8c3 Merge pull request #4514 from horizon-rust/feat/smp-native-hooks
🤖 Agent: verified at 768d20d: pr_ready green (1067 horizon-client tests, clippy -D warnings); tools/parity.py dedicated_server server_entities pass (on #4534's head, which holds this one); with no mod the tree stays Null and the gate is a hash lookup; with the SMP mod, a real dedicated server and two real clients ran /balance, /pay, /baltop and /eco (operator-only, hidden from others), the files and audit log matched, and a restart kept the balances.
2026-10-30 18:35:30 +01:00
Ludovic Marie b4a7018864 Merge pull request #3720 from Domanffe/fix/launcher-remove-queue
🤖 Agent: pr-triage-1 (Ludovic's AI agent, Claude), not Ludovic himself. Proven on ccb85be48 merged with current main, on macOS: - `pr_ready --quick` passes (launcher rustfmt, clippy, tests). - Concurrent removals through the launcher CLI in a scratch home: three versions of 20,000 files each removed at once, with `versions --installed` polled 143 times meanwhile. None of the three is listed once its removal has started. On main, each stays listed for ~0.46 s of a 1.5 s delete, half-deleted, which is the window your marker-first change closes. - The window's gate (`removable`) is covered by the new test. Queued for the merger.
2026-10-30 18:35:25 +01:00
Ludovic MarieandClaude Opus 5.5 97f2c57e5d Merge main into mods/equipment-every-namespace
Through mods/port-item-stats with main merged. Conflicts: main's
equipment layers became every layer of a kind (leather's overlay), so each
layer's texture now resolves its namespace there; hud.rs keeps the
namespaced sprite names with main's decoded image.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 18:35:24 +01:00
Ludovic Marie c7e33a9163 Merge pull request #4535 from horizon-rust/perf/creaking-heart-snapshot
🤖 Agent: verified: 3 interleaved A/B rounds on playhorizon (60 bots) cut tick_block_entities ~5.7 to ~2.7 ms/tick, MSPT avg -3.5 to -9 ms; behaviour unchanged (the snapshot is only skipped on ticks that read no mob); pr_ready --quick green.
2026-10-30 18:35:21 +01:00
AurelienBelloirandClaude Opus 5.5 4b32fb8cf7 Name the world preset test's folders without a colon
each_world_preset_writes_its_own_generator joined the preset id
(`minecraft:normal`) to its temporary folder, and Windows refuses `:` in
a file name (os error 267), so the test failed there on every run.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 18:34:48 +01:00
Ludovic MarieandClaude Opus 5.5 5d7c622895 Merge main into mods/port-item-stats
Through mods/harness-item-components with main merged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 18:34:42 +01:00
Ludovic MarieandClaude Opus 5.5 fd41f0e36f Merge main into mods/harness-item-components
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 18:34:29 +01:00
Ludovic MarieandClaude Opus 5.5 7a4f7d5c99 Dedicated server: horizon-clients-only lets only the Horizon client join
Off by default. With horizon-clients-only=true in server.properties (server.toml's [properties]), a
client whose minecraft:brand isn't "horizon" (vanilla's client says "vanilla"; one that names none
counts as vanilla, as on vanilla's server) is disconnected during configuration with "This server is
for the Horizon client: playhorizon.dev", and the console logs which client it was. The brand can be
spoofed: this is a gate for players who didn't install Horizon, not security.

horizon-bots gets --brand and prints a configuration disconnect's reason;
tools/server_horizon_only_e2e.py checks both settings on a real dedicated server.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 18:32:25 +01:00
Ludovic MarieandClaude Opus 5.5 9845e2cd9b Fit SyncData::empty to main's SyncData
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 18:31:24 +01:00
Ludovic MarieandClaude Opus 5.5 b01025353e Merge main into claude/kind-dijkstra-aaxkhw
Conflicts: main's integrated_open (owner_profile joins its NetConfig), the
level shapes beside owner_profile in NetConfig, SyncData::load's pack
overlay, and main's lazy skin drawing in update_remote_skins (the offline
skin is stored when its slot is gone, as the offline cape is). Also brings
back conn.rs's prompts_become_the_components_vanilla_sends test, which the
branch had replaced by mistake.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 18:30:29 +01:00
rdbt c9f49f3b54 Merge pull request #4525 from doodlyyousuf/fix/item-honey-slide
Slide dropped items down the sides of honey blocks
2026-10-30 17:24:22 +00:00
rdbt 8e93b54a41 Merge pull request #4368 from xFedeT/fix/sulfur-cube-redstone
Explosive sulfur cubes light their fuse beside redstone power
2026-10-30 17:22:44 +00:00
rdbt 9e50f5618c Merge pull request #4340 from xFedeT/fix/smithing-cycling-icons
Smithing table: the empty slots cycle their icons, as CyclingSlotBackground does
2026-10-30 17:22:33 +00:00
rdbt ce5086e2d3 Merge pull request #4403 from Ashura006/display-box-culling
Cull display entities with the width/height frustum box
2026-10-30 17:22:22 +00:00
rdbt 9ab3cee2e8 Merge pull request #4373 from iLostName/join-stall-cheaper
joining a world: the player model and command registries are read once, not at every join
2026-10-30 17:21:40 +00:00
rdbt ff8bd6a91f Merge pull request #4362 from DUzzL/fix/issue-4356-piglin-crossbow
Let a piglin holding a crossbow shoot instead of melee
2026-10-30 17:21:29 +00:00
rdbt 9c825e67df Merge pull request #4324 from v3nn7/investigate/farmland-shadow
Project entity shadows onto partial block bounds
2026-10-30 17:21:11 +00:00
Dmytro ChuprynaandClaude Opus 5.5 907b6e1dcc Use workspace base64 and sha1 crates in skins.rs
Replace hand-rolled base64 decoding and SHA-1 hashing in crates/horizon-client-render/src/skins.rs with the base64 and sha1 crates already used across the workspace, preserving the existing decoding fallbacks and compatibility tests.

Fixes #4431

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 18:21:05 +01:00
rdbt 00448b2ea0 Merge pull request #4322 from DUzzL/fix/issue-4317-slider-hover-outline
Keep the slider track outline off on hover
2026-10-30 17:21:00 +00:00
rdbt 81410769f5 Merge pull request #4372 from iLostName/namespaced-item-animations
item sprites: another namespace's animated texture packs its first frame and animates
2026-10-30 17:20:50 +00:00
rdbt 1df5237769 Merge pull request #4352 from xFedeT/fix/loot-set-instrument
Loot: a goat horn from a loot table gets its instrument (set_instrument)
2026-10-30 17:20:39 +00:00
rdbt 9ca78f1309 Merge pull request #4346 from xFedeT/fix/port-map-ominous-stew-crossbow-frames
Port map: four rows of #3515 already ported; item frames keep to the build height
2026-10-30 17:20:28 +00:00
rdbt e37351ed75 Merge pull request #4395 from DUzzL/fix/issue-4388-mcmeta-bom
Accept a UTF-8 byte-order mark in server pack.mcmeta
2026-10-30 17:20:17 +00:00
Ludovic Marie 0d6db579ec Merge pull request #4522 from horizon-rust/fix/crash-head-server
🤖 Agent: verified: new crash test passes, pr_ready --quick green. A dedicated server's crash report says Server thread and drops the client's render settings.
2026-10-30 18:19:56 +01:00
Ludovic Marie e749114173 Merge main into feat/server-perf-report 2026-10-30 18:19:55 +01:00
Ludovic MarieandClaude Opus 5.5 05bc171f13 horizon-server report: a performance report to share (horizon-report on the console)
`horizon-server report [SECONDS]` (or `horizon-report` on the console)
records every tick for 60 s by default, up to 600, then writes
reports/perf-<time>.json and a short .txt beside it, and prints the
summary. The report holds:
- TPS and MSPT (average, p50/p95/p99, max, ticks over 50 and 100 ms) and
  CPU time per tick;
- each span's share of the tick, and the 10 worst ticks with their spans;
- entities by type and chunks per level, players online;
- memory, CPU, the machine, view and simulation distance, max players.
No names, UUIDs, addresses, seed or world name, so a server owner can
attach it to an issue as it is.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 18:19:55 +01:00
rdbt b7d754a60a Merge pull request #4381 from DUzzL/fix/issue-4370-golem-chest-lid-guests
Send the copper golem chest lid event to LAN guests
2026-10-30 17:19:49 +00:00
rdbt 1c8532cbe1 Merge pull request #4323 from v3nn7/fix/title-debug-overlay
Keep debug overlay off the title screen
2026-10-30 17:19:44 +00:00
Ludovic MarieandClaude Opus 5.5 8fcfb3b8f4 Run parity with the run-parity label (the check this PR fixes)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 18:18:42 +01:00
Ludovic MarieandClaude Opus 5.5 2e97b2f5f9 horizon-prepare: OUTPUT_VERSION 3 (the creative tabs exporter's Operator Utilities tab)
#4052 changed VanillaCreativeTabsExporter, which the preparer runs, so the
prepared output changed (digest 5e50c240...) while OUTPUT_VERSION stayed 2.
Every parity run on the Mac mini's runner since fails "the prepared output
changed", and players would keep resources prepared before it. The digest is
the one the runner's own preparation of main gave, twice.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 18:15:32 +01:00
Muhammad Asif ed6d97cbbb Slide dropped items down the sides of honey blocks
HoneyBlock.entityInside already slides players and mobs. Dropped items
never ran it, so they fell at full speed beside honey. ItemEntity.tick
now applies isSlidingDown / doSlideMovement after the move, before
friction, without slide sounds (doesEntityDoHoneyBlockSlideEffects).

Fixes #4513
2026-10-30 22:13:52 +05:00
Ludovic Marie d52d37f944 Merge main into perf/creaking-heart-snapshot 2026-10-30 18:13:38 +01:00
Federico TaglianiandClaude Sonnet 5.5 8373160c35 mob_ai: baby villagers play tag, checked tick for tick against vanilla
Refs #3521

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 18:12:27 +01:00
Ludovic MarieandClaude Opus 5.5 d39eb5d638 Server: vanilla's reach on attacks and entity interactions, and blocks by attribute
A client could attack any player or mob from any distance: the server path (LAN host, the integrated
server thread, dedicated servers) never checked ServerboundAttackPacket's range, so a hacked client
hit people 50 blocks away. Now, as ServerGamePacketListenerImpl does:

- handleAttack: Player.isWithinAttackRange(mainHandItem, targetBox, 3.0), the held item's
  attack_range or AttackRange.defaultFor (0 to the entity interaction range), for players, mobs,
  vehicles, dragon parts and projectiles alike; out of range the attack is dropped silently;
- handleInteract: Player.isWithinEntityInteractionRange(targetBox, 3.0);
- digging and handleUseItemOn read the guest's block_interaction_range attribute instead of a fixed
  4.5 (5 in creative), so /attribute and items count, and handleUseItemOn logs vanilla's
  "Rejecting UseItemOnPacket ... too far away from hit block" line.

The ranges are the player's attributes: creative's modifiers (+2, +0.5) are on them every tick.
horizon-bots gets a stand behaviour, --attack NAME and the victim's health in its report;
tools/server_reach_e2e.py proves it on a real dedicated server.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 18:12:00 +01:00
Ludovic Marie 91352551d6 Merge pull request #4517 from horizon-rust/fix/region-cache-limit
🤖 Agent: verified: unit test fails on main (601 descriptors) and passes here; bots repro shows region fds growing without bound on main; pr_ready --quick green. Fixes the official server's 'Too many open files' crashes.
2026-10-30 18:11:11 +01:00
DUzzL ddbaa5a64c Cover the menu reset on a server transfer with a lan_e2e check 2026-10-30 18:09:40 +01:00
DUzzLandOpenAI Codex GPT-6 01e9ac5702 Reset the client menu when the server replaces the player
Co-Authored-By: OpenAI Codex GPT-6 <[email protected]>
2026-10-30 18:09:40 +01:00
Ludovic MarieandClaude Opus 5.5 f429f93420 Launcher: mod icons and a revalidating image cache (redesign R8a)
Every mod row shows the mod's own icon: Modrinth's for a mod the browser
installed (one bulk request per instance, the id-to-URL map kept in
mod-browser/project-icons.json), else the icon the jar names in its
fabric.mod.json, quilt.mod.json or mods.toml, else a glyph. Icons are
decoded off the UI thread and scaled to the row's size.

The image cache moves under the launcher's data folder, keeps each
image's ETag/Last-Modified and revalidates week-old images with a
conditional request. The Mods tab builds only its visible rows.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 18:08:26 +01:00
Ludovic MarieandClaude Opus 5.5 848c0b10d4 Crash reports: a dedicated server's head says Server thread, without render settings
On a dedicated server, the main thread's report said "Thread: Render thread"
and listed the render settings it had read from options.txt (the official
server's EMFILE crash reports did). The main thread is now named for its
side, as vanilla's reports name it, and a server's report leaves out the
render settings.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 18:08:19 +01:00
Federico TaglianiandClaude Sonnet 5.5 c96733f3c6 Copper golems open the doors on their path (InteractWithDoor)
CopperGolemAi's core activity has InteractWithDoor at priority 3: it opens
the doors on the path and closes the ones passed. The brain had none, so a
golem whose way between two chests ran through a closed door never got
through. The mob_ai probe now plays the sound of a door a mob opens or
closes, with the pitch drawn from the level random.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 18:06:05 +01:00
DUzzL 5459f6e232 Invisible players keep their armour, elytra and held items 2026-10-30 18:05:37 +01:00
Ludovic MarieandClaude Opus 5.5 e23b4b7d8f Native mods: server menus and an atomic item exchange (API 10)
A server mod opens a vanilla chest menu (generic_9x1..9x6) for a player and
fills it with items to look at. No click moves anything: clicks and the
menu's closing (by the player, another menu, leaving) reach the mod through
native_menu_clicked and native_menu_closed, run with no player swapped in.
native_exchange_items swaps two players' offered stacks in one step, each
checked to still be exactly as offered and to fit whole on the other side,
or moves nothing. A stock client shows it all.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 18:04:25 +01:00
Ludovic MarieandClaude Opus 5.5 396f456556 Native mods: the items test follows getFreeSlot's empty off hand
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 18:03:53 +01:00
Ludovic MarieandClaude Opus 5.5 e0cb05bfce Save: keep at most 256 region files open, like vanilla
The region cache in save.rs opened every region, entities and poi file a
dimension ever touched and never closed one. A server whose players travel
(teleports, elytra, exploration) runs out of file descriptors: the four
crashes of the official server on 2026-10-30 (v0.2.97) were all
"Too many open files (os error 24)" from horizon-region-writer, then a
failed save and a fatal tick error.

Cap the cache at RegionFileStorage.MAX_CACHE_SIZE (256): when full, drop
the least recently used file no caller still holds. Also raise the soft
open-file limit to the hard one at startup, as the JVM does with
-XX:+MaxFDLimit, so sockets and other files keep their headroom.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 17:58:29 +01:00
Ludovic MarieandClaude Opus 5.5 6f6650a68f docs/smp.md: players install nothing; trades move nothing until the swap; config.json
The SMP uses only what a stock Horizon client renders (commands, vanilla
chest menus, sounds, titles, action bar, boss bars, sidebar). The trade
menu is display only and swaps both offers atomically, so no way out has
items to return. The config is config.json, as the mod writes it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 17:52:33 +01:00
arismoko a02d0c4a1a Route server block changes through level updates 2026-10-30 11:43:09 -05:00
Federico TaglianiandClaude Sonnet 5.5 46dfd693f5 Iron golems draw the sprint particle's randoms; mob_ai village scenarios
IronGolem.canSpawnSprintParticle: a golem that moves draws nextInt(5) each
tick and, one time in five, the particle's two coordinates, even though a
server drops the particle. Horizon drew none, so a golem's random drifted
from vanilla's. iron_golem_village_0/1 and iron_golem_outside_village
(GolemRandomStrollInVillageGoal, MoveBackToVillageGoal) now match vanilla.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 17:40:44 +01:00
Ludovic MarieandClaude Opus 5.5 768d20d998 Native mods: a server player's game mode
A shop must refuse a player in creative mode, who can make any item.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 17:39:10 +01:00
Ludovic MarieandClaude Opus 5.5 b070f41dda Native mods: the acting player's plain items for server mods (API 9)
native_held, native_count_items, native_carried_items, native_take_items,
native_room_for and native_give_items: what a server shop needs, on the
36 inventory slots and the off hand, plain stacks only (no damage, no
component), in Inventory.add's slot order, and give only where it fits.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 17:39:10 +01:00
Ludovic Marie 8f376a4712 Merge pull request #4490 from horizon-rust/feat/launcher-night-mods
🤖 Agent: Launcher redesign R7b (the instance's Mods tab), approved by Ludovic after the screenshots. main merged in; full pr_ready green, verify and launcher CI green on this head. The only comment is the overlap bot's note on #4478 (different change; not landed).
2026-10-30 17:39:06 +01:00
Ludovic MarieandClaude Opus 5.5 5a7cfb443f lan_e2e: check that a sprinting guest's hit slows it
The guest sprints at the host, releases Ctrl (the sprint holds while W
stays down), and left-clicks the host. Vanilla's attacker side of
causeExtraKnockback cuts its horizontal speed to 0.6 and ends the sprint:
the speed drops below the walking speed and climbs back to it. On main
the guest keeps its sprint speed (0.1532) after the hit.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 17:37:36 +01:00
Ludovic Marie 63db88b844 Merge remote-tracking branch 'origin/main' into feat/launcher-night-mods 2026-10-30 17:29:45 +01:00
Ludovic MarieandClaude Opus 5.5 076c41fc27 Native mods: server commands and players (API 8)
native_register_commands: the commands a native mod adds to every player's
command tree (usages such as `pay <player:player> <amount:integer>`,
optional arguments last, an operator level each), merged into one tree the
hub sends after vanilla's, filtered by level. native_perform_command: a
player's command on the server for a registered name, inside their swap,
whether the world allows commands or not. Helpers: the players on the
server, a line to one or to all, the command's feedback, the world's
folder. With no mod hooking them, the tree is empty and nothing changes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 17:29:20 +01:00
Federico TaglianiandClaude Sonnet 5.5 b947048ad6 Fish schools recruit in vanilla's entity order; mob_ai school scenarios
The row said FollowFlockLeaderGoal was missing: it is ported (#3026) but had
no parity scenario. cod_school, salmon_school and tropical_fish_school
(five fish each, 400 ticks) found two differences: a leader recruits the
fish in vanilla's getEntitiesOfClass order (sections by x, z, y, then
entry order: Mob.section), which decides who is left out of a school of
at most five; and the oracle and probe draw the goal's first start delay
again once the mob's random is seeded. All three scenarios now match.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 17:27:36 +01:00
Ludovic MarieandClaude Opus 5.5 3f1a45313e docs/smp.md: the SMP is a native mod only the official server builds
Horizon's own server stays plain: no SMP command, setting or code in the
release. The mod lives in horizon-mods (native/smp); Horizon gains general
server hook points (commands, players, items, menus, interactions) that
any native mod can use. Config and data move to world/horizon-smp/.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 17:22:55 +01:00
Ludovic MarieandClaude Opus 5.5 9d0473d748 docs/smp.md: the SMP layer's design (economy, trades, gambling, organisation)
Built in behind [smp] in server.toml, off by default: why not a server mod
or a datapack, the commands, the configuration, the data files under
world/horizon-smp/, the cost and the phases.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 17:22:18 +01:00
Ludovic Marie f6a757ac2e Merge pull request #3491 from ThatCakeIsAPie/feat/datapack-biomes-1
🤖 Agent: verified on main + this head. With the level-shape stack merged, this PR is now one file (biome.rs, +242/−68): the biome-table piece levzzz5154 asked for first on #3492. I merged main in, and main's biome features (#4211) are now read in BiomeInfo::from_json, where this PR parses a biome file. Checks: 110 of the 111 harnesses this change can affect pass. lunge_motion fails on main itself since #4297. dedicated_server, datapack_worldgen, server_world_gen, pr_ready and lan_e2e (worldspawn, light, levels, distance, portals, chest, forced-levels) also pass. Biome files are parsed only when a world loads. Queued to merge.
2026-10-30 17:18:57 +01:00
Federico TaglianiandClaude Sonnet 5.5 511b974107 Dolphins swim after the boat or nautilus a player steers (FollowPlayerRiddenEntityGoal)
Dolphin.registerGoals adds FollowPlayerRiddenEntityGoal for AbstractBoat and
AbstractNautilus at priority 8. The player the mob sees tells the vehicle it
steers (PlayerView.vehicle). mob_ai scenarios dolphin_boat_* (the oracle
gets a boat the script moves) match vanilla tick for tick; Mob.moveRelative
adds its zero vector like vanilla (-0.0 becomes 0.0).

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 17:13:26 +01:00
Neferr ca9195fb02 Options > Mods: a test that a native mod's key shows and keeps the key pressed
#3539: on v0.2.97 the WebAssembly Zoom's key set to R showed another key. Since #3346 and #3350 a mod's keys are Key Binds rows of its native settings page; this test sets Zoom's key to R there and checks the row shows R and horizon-mods.txt keeps key.keyboard.r. Fixes #3539.
2026-10-30 17:07:38 +01:00
Ludovic MarieandClaude Opus 5.5 c1c646a326 Launcher: the instance's Mods tab (redesign R7b)
Home gets a Mods tab after Overview (plan R0, screens 5 and 6): every mod of the selected
instance, the browser's and the jars put in its folder by hand, with its version, where it comes
from (Modrinth, needed by another mod, or a local file), an amber Update pill when the browser
found a newer version, and a switch (a Java jar turned off is renamed .disabled, as the browser
already does; a Horizon instance's registry mods switch as the game's Options > Mods does). A
filter, Update all and Add mods (the Mods page) complete it. Java instances without a loader
have no Mods tab.

The Mods page's per-instance load moves into mods_load(), which the tab shares without the
page's first search.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 17:02:29 +01:00
Ludovic Marie fca2169de9 Merge pull request #4459 from horizon-rust/feat/launcher-night-instance
🤖 Agent: Launcher redesign R7a (the instance's tabs under Home's hero), approved by Ludovic after the screenshots. main merged in; full pr_ready green, verify and launcher CI green on this head.
2026-10-30 16:52:32 +01:00
Ludovic MarieandClaude Opus 5.5 aa906ecbe7 Creaking hearts: build the other mobs only on the ticks that read them
with_heart collected a snapshot of every mob for every heart, every tick,
while CreakingHeartBlockEntity.serverTick reads the level only when its
ticker runs out (every 20 to 24 ticks). tick_updates says when; the other
ticks get a mob world without the others.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 16:45:14 +01:00
Ludovic Marie c505204c24 Merge remote-tracking branch 'origin/main' into feat/launcher-night-instance 2026-10-30 16:44:27 +01:00
Neferr fbac0ae2d9 Native mods: hook points for breaking and placing a block (API 6)
native_destroy_block hears a player break a block once the game allowed it (ServerPlayerGameMode.destroyBlock) and may cancel it, which keeps the block and its drops; native_place_block hears a block placed from an item (BlockItem.place). A mod stops a placement by claiming native_use_item_on, which runs before it. Fixes #4474.
2026-10-30 16:43:43 +01:00
Ludovic Marie eeb1d3773d Merge remote-tracking branch 'origin/main' into pt1-3491 2026-10-30 16:43:22 +01:00
arismoko bdc422ad8c Remove source-pinned native lifecycle tests 2026-10-30 10:40:37 -05:00
Ludovic MarieandClaude Opus 5.5 91c77af9ae Merge main into datapack-biomes-1
main's biome features (BiomeGenerationSettings.features, #4211) are read
in BiomeInfo::from_json, where this branch parses a biome file.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 16:38:14 +01:00
Ludovic Marie 594b50b510 Merge pull request #3490 from ThatCakeIsAPie/feat/level-shape-11
🤖 Agent: verified on main + this head. It carries #3488 and #3489, and merging it lands both. I merged main in. main's new native_world_read.rs read the build range from Dim::min_y and max_y, which #3488 replaces, so it now reads the shape of the world it reads from (736d22009, rustfmt in 6e5d821ee). Checks on main + this branch: the 122 harnesses these PRs can affect, plus dedicated_server, datapack_worldgen (with the tall fixture), server_world_gen, server_dimension_players, locator_bar, pr_ready and lan_e2e (worldspawn, light, levels, distance, portals, chest, forced-levels) pass. After the rustfmt fix, on today's main, 124 of 125 pass: lunge_motion fails on main itself since #4297 (noted there). Meshing time is unchanged. Queued to merge; the data-pack biomes (#3491–#3494) follow.
2026-10-30 16:38:09 +01:00
ElyTheCat 548fca6308 Merge remote-tracking branch 'origin/main' into fix/armor-rendering
# Conflicts:
#	crates/horizon-client/src/game/armor_stands.rs
#	crates/horizon-client/src/game/entity_model_loading_tests.rs
#	crates/horizon-client/src/game/perspective.rs
2026-10-30 18:55:54 +03:30
levzzz 1da354b67a Merge pull request #4211 from d3grando/fix-3518-chunks-biomes
/fillbiome recolours loaded chunks for every client (ClientboundChunksBiomesPacket)
2026-10-30 20:22:19 +05:00
levzzz d0a7c52171 Merge pull request #4076 from d3grando/fix-2912-void
The Void superflat preset gets its stone start platform
2026-10-30 20:22:13 +05:00
levzzz 92f69c1fca Merge pull request #4264 from d3grando/fix-3514-pet-death-message
A tame pet's death tells its owner how it died
2026-10-30 20:22:04 +05:00
levzzz 7c6bbc1cfd Merge pull request #4205 from d3grando/fix-3513-strider-dismount-lava
Get off a strider onto the shore, not into the lava beside it
2026-10-30 20:21:39 +05:00
levzzz 12299b07c9 Merge pull request #4214 from d3grando/fix-3518-delete-chat
A chat message the server deletes becomes the deleted marker
2026-10-30 20:18:35 +05:00
levzzz 0e780d404f Merge pull request #4069 from d3grando/fix-2755
Creative players are pushed by explosions and crystals
2026-10-30 20:18:27 +05:00
Neferr 4ee9816b82 Native mods: hook points for using an item (API 5)
native_use_item_on hears a right click with an item on a block once the block let it through (ItemStack.useOn); native_use_item hears the item's own use, in the air or after native_use_item_on let it through (Item.use). Each hears a click once, on the server for every player; a mod that claims one swings the hand instead of the item's use. Fixes #4290.
2026-10-30 16:18:20 +01:00
levzzz d4740f8621 Merge pull request #4315 from JulesB40/fix/rocket-block-hits
A firework rocket with stars bursts on any block it hits, as in vanilla
2026-10-30 20:18:09 +05:00
levzzz 20b0fd4eca Merge pull request #2990 from kacpherek/fix/item-damage-parity
Items and damage parity: arrow and potion effects, mob damage sources, adventure can_break and can_place_on, lily pads on water, mud from water bottles
2026-10-30 20:16:39 +05:00
ElyTheCat 60203586ca Fix syntax error and Opt::available in options_ui.rs after merge 2026-10-30 18:43:15 +03:30
Ludovic Marie 0ad879b601 Merge pull request #4455 from horizon-rust/ci/nightly-notes-launcher-line
🤖 Agent: Nightly notes now open with the existing launcher banner (release_train.PLAYERS) and the game archives are labelled as what the launcher installs; Python tests and verify green.
2026-10-30 16:12:22 +01:00
Ludovic Marie 842028904a Merge pull request #4461 from horizon-rust/fix/standalone-start-no-resources
🤖 Agent: verified on the Mac mini: a game started without prepared files (the release zip run by hand) now shows a 'Start Horizon from the launcher' window with Download (playhorizon.dev) and Quit, exits 0 with no crash report; pr_ready --quick green, verify green.
2026-10-30 16:12:18 +01:00
bloodulon 17aab6b601 Merge branch 'pr/interaction-entities' into pr/display-passengers
# Conflicts:
#	crates/horizon-client/src/game/remote.rs
2026-10-30 20:11:03 +05:00
bloodulon 8aff0594b5 rustfmt fix in remote.rs 2026-10-30 20:10:06 +05:00
levzzz da1f0fb3a4 Merge hr/main into fix-3513-strider-dismount-lava 2026-10-30 20:10:05 +05:00
levzzzandClaude Sonnet 5.5 c3139d6de7 Merge hr/main into fix/item-damage-parity
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 20:08:20 +05:00
levzzz e296924abb Merge pull request #4083 from d3grando/fix-2885-wolfwet
Wet wolves darken and shake off the water on clients
2026-10-30 20:07:34 +05:00
levzzz c62cba3f17 Merge pull request #3957 from kacpherek/fix/split-misc-b
Fix a batch of small mob, block and world-list behaviours so they match vanilla
2026-10-30 20:07:17 +05:00
Neferr 4d3ee1ace0 Native mods: explain the tooltip hook's place from the game alone 2026-10-30 16:07:01 +01:00
levzzz a434ad7ab9 Merge pull request #3956 from kacpherek/fix/split-misc-a
Tipped arrows, leather armour, allays, mooshrooms and LAN guests behave like vanilla
2026-10-30 20:05:05 +05:00
levzzzandClaude Sonnet 5.5 33743fb052 Merge main; port put_biomes to the column layout
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 20:04:51 +05:00
levzzz 762ce6114c Merge pull request #4363 from 1038april/fix/mouse-tweaks-menus
Run the Mouse Tweaks wheel and drags in furnaces and crafting tables, and keep the cursor right under fast clicks
2026-10-30 20:03:23 +05:00
levzzz cbbcb4323b Merge pull request #2577 from levzzz5154/fix/root-vehicle-restore
Riding a horse survives a save and a reopen
2026-10-30 20:03:13 +05:00
levzzz d2c159e74d Merge pull request #4077 from septydev/fix/ukrainian-launcher-overflow
Fix account chip text overflow
2026-10-30 20:03:05 +05:00
levzzz 526c5f0b5b Merge pull request #4199 from kacpherek/fix/camel-horse-llama-riding
Camel, horse and llama riding physics as vanilla
2026-10-30 20:02:56 +05:00
levzzz 61dc245f09 Merge pull request #3550 from v3nn7/v3nn7
Fix teleport commands without an explicit target
2026-10-30 20:02:51 +05:00
bloodulon aa284acb23 Merge remote-tracking branch 'origin/main' into pr/display-passengers
# Conflicts:
#	crates/horizon-client/src/game/remote_vehicles.rs
2026-10-30 20:02:42 +05:00
levzzzandClaude Sonnet 5.5 8fb961c146 Merge main; place the void start platform at the generator's own min Y
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 20:02:38 +05:00
levzzzandClaude Sonnet 5.5 0b2ed9162a Merge hr/main into fix/rocket-block-hits
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 20:02:20 +05:00
levzzz 2c80e75d81 Merge pull request #4308 from JulesB40/fix/crossbow-firework-impact
A plain firework rocket from a crossbow flies past the first block, as in vanilla
2026-10-30 20:02:06 +05:00
levzzz 942e3b2ded Merge pull request #2417 from kacpherek/fix/goat-horns
Make goat horns play their sound and show the horn pose
2026-10-30 20:01:21 +05:00
bloodulon 1167274823 Merge remote-tracking branch 'origin/main' into pr/interaction-entities 2026-10-30 20:00:26 +05:00
levzzz f1807de201 Merge pull request #3951 from kacpherek/fix/split-rendering
Rendering parity: jeb_ sheep, golem walk tilt, spyglass particle hiding, skulls, brush, witch potion
2026-10-30 20:00:25 +05:00
levzzz fdc4a8b80e Merge remote-tracking branch 'hr/main' into fix/pr-4264 2026-10-30 19:59:58 +05:00
levzzz 381a1ec66e Merge hr/main into fix-3514-pet-death-message 2026-10-30 19:59:56 +05:00
levzzz 53259f6b07 Merge hr/main into fix-3513-strider-dismount-lava 2026-10-30 19:59:16 +05:00
Ludovic MarieandClaude Opus 5.5 e6f5246fdf Game started without its files: a launcher screen, not a crash
A player ran horizon.exe from the release's game zip, thinking it was the
launcher. v0.2.97 then failed with "resource loader stopped before
finishing" and a crash report: the loader's real error (no prepared files)
was taken by App::about_to_wait, and the next poll saw the closed channel
(fixed on main by 9f28fe890, which also checks horizon-resources.json
before any window). That check still ended in an error and a crash report.

- An interactive game with no prepared files now opens a window in Horizon's
  design language (bundled fonts, none of Minecraft's files needed): start
  Horizon from the launcher, Download (opens playhorizon.dev), Quit. The
  console says the same. Closing it quits with code 0 and no crash report.
  Offscreen, scripted and server runs keep the error.
- The heartbeat and servers.dat wait until the files are there.
- A loader channel that closes without a result always names why.
- PLAY.txt sends players to playhorizon.dev for the launcher.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 15:58:23 +01:00
ElyTheCat 7ba8d7ee65 Merge branch 'main' into feat/quit-shortcuts 2026-10-30 18:27:01 +03:30
levzzz 67e69ee447 Merge remote-tracking branch 'hr/main' into fix/pr-2990 2026-10-30 19:56:52 +05:00
levzzz a814e9d5a4 Merge remote-tracking branch 'hr/main' into fix/pr-3957 2026-10-30 19:55:53 +05:00
Ludovic MarieandClaude Opus 5.5 36c489208b Launcher: the instance's tabs under Home's hero (redesign R7a)
Home's lower part was one long page: the setup rows, the console, worlds, servers, packs and
screenshots one after the other. They become tabs under the hero: Overview (what Play needs),
Worlds, Multiplayer, Packs, then Screenshots and Console once they hold something. Each tab names
how many it holds; its own buttons sit at the strip's end. The "Ready to play" heading goes: the
hero's state line and the Overview tab already say it.

HORIZON_LAUNCHER_PAGE=worlds (multiplayer, packs, screenshots, console) opens Home on that tab,
for screenshots; it replaces HORIZON_LAUNCHER_CONSOLE.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 15:55:51 +01:00
levzzz fae78ab1d0 Merge remote-tracking branch 'hr/main' into fix/pr-3956 2026-10-30 19:55:00 +05:00
Ludovic MarieandClaude Opus 5.5 0f8f59ec22 Nightly notes open with the way to the launcher
A player ran horizon.exe from the release's game zip, thinking it was the
launcher. Game releases already open with the "Want to play? Download the
Horizon launcher" banner (release_train.PLAYERS, #1647); the Nightly, the
only release since 10-28, did not. Its notes now start with the same banner
(raw HTML, which launchers' notes views skip), and its game archives are
labelled "Game files the launcher installs".

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 15:54:56 +01:00
levzzzandClaude Sonnet 5.5 b513f1b152 Merge hr/main into fix-3518-delete-chat
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 19:54:43 +05:00
levzzz 988049b64f Merge hr/main into fix-2885-wolfwet 2026-10-30 19:54:37 +05:00
levzzzandClaude Sonnet 5.5 a712692527 Merge hr/main into fix-2755
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 19:54:29 +05:00
levzzzandClaude Sonnet 5.5 6f9467703a Merge hr/main into fix/crossbow-firework-impact
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 19:54:15 +05:00
levzzz edc5593c76 Merge remote-tracking branch 'hr/main' into fix/pr-4199 2026-10-30 19:54:04 +05:00
Ludovic Marie 04976d95eb Merge pull request #4320 from horizon-rust/fix/light-edge-neighbours
🤖 Agent: server light-edge fix (#4294): verify, launcher and parity green on the head merged into main; playhorizon dedicated_server 5/5 pass (main 5/5 fail), no idle/join regression.
2026-10-30 15:53:21 +01:00
levzzz 65b0155b8e Merge remote-tracking branch 'hr/main' into fix/pr-2417 2026-10-30 19:52:35 +05:00
levzzz 702f03a4b9 Merge remote-tracking branch 'hr/main' into fix/pr-3951 2026-10-30 19:51:34 +05:00
DenisHumenandClaude Opus 5.5 6a066d9076 Save and restore the player's attribute bases (#4425)
LivingEntity.addAdditionalSaveData writes AttributeMap.pack as the
player's `attributes` list and readAdditionalSaveData applies it before
the health. Horizon kept the list it loaded and never wrote the live map,
so /attribute base changes (scale, max_health, ...) were lost on reopen.

The host writes the live attributes in sync_level and applies the saved
list when its first body loads; a LAN guest does the same through
playerdata::store and its first body after joining.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 16:51:25 +02:00
levzzz f83505a484 Merge remote-tracking branch 'hr/main' into fix/pr-4363 2026-10-30 19:51:07 +05:00
levzzz 9a644f3e72 Merge remote-tracking branch 'hr/main' into fix/pr-2577 2026-10-30 19:51:07 +05:00
levzzz a2d6d33d30 Merge remote-tracking branch 'hr/main' into fix/pr-4077 2026-10-30 19:51:07 +05:00
levzzz 1ab895ea01 Merge remote-tracking branch 'hr/main' into fix/pr-3550 2026-10-30 19:51:07 +05:00
levzzzandClaude Sonnet 5.5 f646dc1e0d Merge hr/main into fix-3518-delete-chat
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 19:50:58 +05:00
levzzz 3132240f21 Merge pull request #4438 from horizon-rust/fix/creatures-test-modules
Fix main's test build: duplicate tests module in creatures.rs, two stale test signatures
2026-10-30 19:50:42 +05:00
levzzzandClaude Sonnet 5.5 41d70dc6c5 Merge hr/main into fix-2755
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 19:49:23 +05:00
levzzzandClaude Sonnet 5.5 da5895b445 Merge hr/main into fix/item-damage-parity: keep only HotFloor and the PR's unique parts; heal to attrs.max_health
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 19:49:04 +05:00
levzzzandClaude Sonnet 5.5 7cee64a389 Fix main's test build: Aabb in_water stub in end_gateway
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 19:48:35 +05:00
levzzzandClaude Sonnet 5.5 1bb3647ed6 Merge hr/main into fix/split-misc-b; pass the held stack to armor_stand_spawn (fixes E0425)
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 19:47:34 +05:00
levzzzandClaude Sonnet 5.5 f47cec589b Draw the mooshroom variant before the child is borrowed
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 19:46:02 +05:00
levzzzandClaude Sonnet 5.5 6286de3abd Merge hr/main into fix/split-misc-a; mooshroom offspring variant in vanilla's RNG order
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 19:45:25 +05:00
levzzz 17e5640d94 Merge remote-tracking branch 'hr/main' into fix/pr-4363 2026-10-30 19:45:23 +05:00
levzzzandClaude Sonnet 5.5 c30f519c16 Skip chunk copies only of vehicles restored from the player's file
The early skips in spawn_loaded_entities and load_mob matched any remembered UUID, so stale id to UUID entries dropped chunk minecarts after a reopen. Track the UUIDs restore_saved_vehicle brought in for this load instead.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 19:45:15 +05:00
Ludovic Marie 11d1936a5e Merge branch 'pt1-3490' into pt1-3491 2026-10-30 15:45:09 +01:00
Ludovic Marie 865ad0b89d Merge branch 'pt1-3489' into pt1-3490 2026-10-30 15:45:09 +01:00
Ludovic Marie 93bab2181c Merge branch 'pt1-3488' into pt1-3489 2026-10-30 15:45:09 +01:00
Ludovic MarieandClaude Opus 5.5 6e5d821eeb native_world_read: rustfmt
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 15:45:08 +01:00
levzzzandClaude Sonnet 5.5 bf016d1c54 Merge hr/main into fix/camel-horse-llama-riding; run onPlayerJump on the controlling client too (#4224)
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 19:44:17 +05:00
levzzzandClaude Sonnet 5.5 b5ad463d2d Fix main's test build: Aabb in_water stub in end_gateway, duplicate tests module and Items::load args in creatures
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 19:44:10 +05:00
levzzz 83f723f666 Merge hr/main into fix-3513-strider-dismount-lava 2026-10-30 19:44:10 +05:00
levzzzandClaude Sonnet 5.5 bcdcc9a460 Merge hr/main into fix/goat-horns; drop duplicate horn tooltip (main has instrument_line)
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 19:41:50 +05:00
levzzzandClaude Sonnet 5.5 6ca4b32d06 Fix main's test build: Aabb in_water stub in end_gateway, duplicate tests module and Items::load args in creatures
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 19:41:45 +05:00
levzzzandClaude Sonnet 5.5 d8b0934f65 Merge hr/main into fix/crossbow-firework-impact
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 19:41:13 +05:00
levzzz a2429b8864 Merge remote-tracking branch 'hr/main' into fix/pr-4083 2026-10-30 19:38:58 +05:00
levzzz 06460237d9 Merge hr/main into fix-2885-wolfwet 2026-10-30 19:38:57 +05:00
levzzz 22263abf91 Merge remote-tracking branch 'hr/main' into fix/pr-2577 2026-10-30 19:38:52 +05:00
levzzz 352d59f617 Merge pull request #3163 from echo-ilovech3ss/fix/sulfur-cube-spawn-egg-size
Sulfur cubes: synchronize size metadata and adult egg spawns
2026-10-30 19:38:00 +05:00
levzzzandClaude Sonnet 5.5 83a84b2637 Merge main; truncate the account type line in the redesigned chip
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 19:37:48 +05:00
levzzz 2fa88ac250 Merge remote-tracking branch 'hr/main' into fix/pr-3951 2026-10-30 19:37:22 +05:00
levzzz ce517e179a Merge pull request #3950 from kacpherek/fix/split-blocks
Block parity: candles, levers, redstone ore dust, note block heads, pots, bells, dripstone, hanging signs, vaults
2026-10-30 19:37:20 +05:00
Ludovic MarieandClaude Opus 5.5 3e2bcb00bc Fix main's test build: one tests module per file, Items::load and in_water signatures in tests
Merging #4222/#4082 and #4259 left two `mod tests` in game/creatures.rs, the
pearl test calling Items::load without its PackOverlay argument, and
end_gateway's ProjectileWorld test impl with the old in_water(Vec3) signature.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 15:36:54 +01:00
levzzz c27711cf2e Merge pull request #4282 from d3grando/fix-2826
Many mobs no longer grow the renderer's staging memory until it runs out
2026-10-30 19:36:43 +05:00
levzzz 0056f3445c Merge pull request #4275 from MSedgeMC/fix/4130-thrown-sprite-lift
Draw thrown items where vanilla puts them
2026-10-30 19:36:27 +05:00
levzzz 51b2104c1d Merge pull request #4228 from levzzz5154/fix/mob-ai-oracle-heap
parity: give the mob_ai oracle an 8g heap (4g runs out on a fresh run)
2026-10-30 19:36:10 +05:00
levzzz e2f04717c4 Merge pull request #4227 from abduvaliy-engineer/fix/spawner-data-sync
Sync spawner mob previews after block data commands
2026-10-30 19:36:00 +05:00
levzzz b8988e03b4 Merge pull request #4213 from d3grando/fix-3514-equipment-break-events
Tools and armour break with their sound and crumbs on servers
2026-10-30 19:35:36 +05:00
levzzz 9382a17fed Merge pull request #4209 from d3grando/fix-3514-conversion-keeps-gear
Drowned, zombie and stray conversions keep gear, name and flags
2026-10-30 19:35:19 +05:00
levzzz 15df84b505 Merge pull request #4208 from d3grando/fix-3520-read-timeout
Time out a server that stops answering, with vanilla's disconnect reasons
2026-10-30 19:35:03 +05:00
levzzz 8682bd5781 Merge remote-tracking branch 'hr/main' into fix/pr-3951 2026-10-30 19:35:00 +05:00
levzzzandClaude Sonnet 5.5 0da29407d1 Merge hr/main into fix/split-rendering; drop jeb_ sheep hunks (covered by #4274)
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 19:35:00 +05:00
levzzz bdcc1d9f39 Merge pull request #4204 from d3grando/fix-3520-spectator-tracking
Spectating players are hidden from players who aren't spectators
2026-10-30 19:34:46 +05:00
levzzzandClaude Sonnet 5.5 2e2c11005a Replace the both-players console teleport step with single-target steps
The multi-target cross-dimension tracking gap is a separate issue.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 19:34:44 +05:00
levzzz bf5b9b089e Merge remote-tracking branch 'hr/main' into fix/pr-3550 2026-10-30 19:34:39 +05:00
levzzz 3caf55ae8a Merge pull request #4160 from JulesB40/perf/mimalloc-allocator
Chunk generation 18% faster on many cores: mimalloc as the game's allocator
2026-10-30 19:34:30 +05:00
levzzz 96824f711d Merge pull request #4144 from Fauxvale/fix/far-watery-and-nearly-full
Far terrain: kelp is water, and powder snow and paths are kept
2026-10-30 19:34:12 +05:00
levzzz 6daaf0449a Merge pull request #4110 from DUzzL/fix/issue-4094-block-break-speed
Apply the block_break_speed attribute to mining speed
2026-10-30 19:33:54 +05:00
levzzz 8fbe40df70 Merge pull request #4103 from DUzzL/fix/issue-4091-attack-speed
Read the attack speed base from the player's attribute
2026-10-30 19:33:37 +05:00
levzzz 02dc9e2e30 Merge pull request #4084 from d3grando/fix-2783
Runtime spawns run Mob.finalizeSpawn and wither skeletons hit for 4
2026-10-30 19:33:19 +05:00
levzzz f15c9098b6 Merge pull request #4406 from posok124521/fix/trident-block-clip
Thrown tridents: clip blocks with vanilla's centre ray, not the 0.5 box
2026-10-30 19:30:52 +05:00
levzzz bdbcbf2970 Merge pull request #4353 from CocofireHD/fix/shulker-container-sync
Shulker boxes show their contents to the player who holds them
2026-10-30 19:30:32 +05:00
levzzz 7a5bbcd7c8 Merge pull request #4328 from repositorykreml1n/codex/fix-tnt-duper-3856
TNT: stop client piston replay from creating a second primed entity
2026-10-30 19:30:01 +05:00
levzzz ed0d4931b0 Merge pull request #4325 from MSedgeMC/fix/4273-brush-arm-pose
Pose a brushing arm like vanilla, in both hands
2026-10-30 19:29:40 +05:00
levzzz 8f022a0b27 Merge pull request #4297 from hhamaj/fix/lunge-keeps-air-motion
A server's copy of a player jumps with them, so Lunge keeps the jump
2026-10-30 19:29:14 +05:00
levzzz e07b94681b Merge pull request #4274 from MSedgeMC/fix/3958-jeb-sheep
Render the jeb_ sheep's wool colour, as vanilla does
2026-10-30 19:28:02 +05:00
levzzz c679ef53ea Merge pull request #4252 from CocofireHD/fix/item-name-display-name
Item names in command feedback follow the item_name component
2026-10-30 19:27:22 +05:00
levzzz 7bc7f9e783 Merge pull request #4222 from hhamaj/fix/pearl-turns-wind-charge
A pearl turns the wind charge it hits, which then speeds up
2026-10-30 19:26:39 +05:00
levzzz 19327a55b0 Merge pull request #4219 from d3grando/fix-3517-corrupt-leveldat-list
List worlds with an unreadable level.dat and offer recovery
2026-10-30 19:26:05 +05:00
levzzz df1874cced Merge pull request #4206 from DenisHumen/fix/frog-walk-anim
Frogs walk their legs at vanilla's rate
2026-10-30 19:23:57 +05:00
levzzz 726f588668 Merge pull request #4183 from Username59138/fix/command-block-one-data-path
Command blocks: one data path; the command reaches only the player who opens it
2026-10-30 19:23:16 +05:00
levzzz dbc84adbd3 Merge pull request #4082 from d3grando/fix-3072-pearl
Thrown pearls feel the water their box touches, as vanilla's isInWater
2026-10-30 19:20:37 +05:00
levzzz dcc393acec Merge pull request #4073 from d3grando/fix-2866
Swapping the same item into the off hand plays the re-equip animation
2026-10-30 19:20:16 +05:00
Михаил РоговцевandOpenAI Codex 6b5428915b Keep the swap-scale PR narrowly scoped
Co-Authored-By: OpenAI Codex <[email protected]>
2026-10-30 17:20:10 +03:00
levzzz bb6985ecb7 Merge pull request #4068 from d3grando/fix-3183
Glint reads the clock and options once per frame, not per item
2026-10-30 19:19:41 +05:00
levzzz 574d031d14 Merge pull request #4064 from oscargzss/nonconformant-adapter-retry
renderer: try a backend again with non-conformant adapters
2026-10-30 19:19:20 +05:00
levzzz 5edb5defbb Merge pull request #4052 from d3grando/fix-3393-optab
Creative inventory: the Operator Utilities tab for operators
2026-10-30 19:18:58 +05:00
levzzz 91dc405a0f Merge pull request #4048 from ult1mq/codex/lod-retirement
Fix detail requests for retiring fine LOD leaves
2026-10-30 19:18:14 +05:00
levzzz 587f81f9c6 Merge pull request #4019 from d3grando/fix-3788
Pose plugins hide the player's own body again
2026-10-30 19:17:34 +05:00
levzzz 73be91512c Merge pull request #4010 from WhyFovius/fix/fullscreen-loading-crash
Renderer: fullscreen loading crash (Validation acquire + animated sprites layout)
2026-10-30 19:17:15 +05:00
levzzz d10f82a7cb Merge pull request #3999 from d3grando/fix-2786
Light a column again when an edit darkens it beside an unlit one
2026-10-30 19:16:56 +05:00
levzzz 17dd36e93a Merge pull request #3994 from d3grando/fix-2740
Applying a resource pack no longer freezes the window
2026-10-30 19:16:37 +05:00
levzzz 81d38acadc Merge pull request #3936 from abit-foggy/fix/intel-gen11-section-flicker
fix(renderer): use flat draws on Intel Gen11 Vulkan to prevent section flicker (#3529)
2026-10-30 19:16:17 +05:00
levzzz 62ddcb7909 Merge pull request #3721 from xFedeT/fix/skeleton-path-stops-in-shade
Skeletons: a path out of the shade stops at the first node the sky sees
2026-10-30 19:15:58 +05:00
levzzz c3db4a63d5 Merge pull request #4262 from d3grando/fix-3514-pearl-gateway
Ender pearls go through End gateways
2026-10-30 19:14:35 +05:00
levzzz ffae67874b Merge pull request #4259 from d3grando/fix-3514-tame-wolf-kill-credit
Tame wolves' kills count for their owner
2026-10-30 19:14:17 +05:00
levzzz a52ce752a0 Merge pull request #4247 from d3grando/fix-3518-tag-query
F3+I copies the server's data on servers and LAN
2026-10-30 19:13:57 +05:00
levzzz 17bd76282b Merge pull request #4243 from d3grando/fix-3518-sound-slider-preview
Volume sliders play a preview sound on the title screen's options
2026-10-30 19:13:35 +05:00
levzzz 174df925ca Merge pull request #4241 from d3grando/fix-3513-enderman-carried-drop
Endermen drop the block they carry when killed
2026-10-30 19:13:18 +05:00
levzzz cb77d4cb54 Merge pull request #4239 from d3grando/fix-3518-set-camera
Spectators on a server see through the entity they spectate
2026-10-30 19:12:56 +05:00
levzzz cba5d53333 Merge pull request #4236 from d3grando/fix-3514-explosions-break-decorations
Explosions break paintings, cushions and armour stands
2026-10-30 19:12:38 +05:00
levzzz 50f2ca1636 Merge pull request #4229 from d3grando/fix-3518-custom-chat-completions
Chat completes the server's custom chat completions
2026-10-30 19:12:20 +05:00
levzzz 79efd6a2dc Merge pull request #4226 from d3grando/fix-3512-armadillo-breeding
Armadillos breed, follow spider eyes and their parents
2026-10-30 19:12:03 +05:00
levzzz 22115dfb11 Merge pull request #4225 from d3grando/fix-3513-cube-hurts-golem
Slimes and magma cubes hurt the iron golems they touch
2026-10-30 19:11:47 +05:00
levzzz 34abe889db Merge pull request #4224 from d3grando/fix-2920
Riders move their horses on servers, and jump them
2026-10-30 19:11:31 +05:00
Ludovic Marie 3ecec2ca7b Merge branch 'pt1-3490' into pt1-3491 2026-10-30 15:02:27 +01:00
Ludovic Marie 6b44f2ba99 Merge branch 'pt1-3489' into pt1-3490 2026-10-30 15:02:27 +01:00
Ludovic Marie f8f276748b Merge branch 'pt1-3488' into pt1-3489 2026-10-30 15:02:26 +01:00
Ludovic MarieandClaude Opus 5.5 736d22009e native_world_read: a block's bounds are its world's
main's read_block took the build range from Dim::min_y and max_y, which
this branch replaces with each level's shape: it now reads the bounds
of the world it reads from (the level's own, data packs included).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 15:01:35 +01:00
Ludovic Marie 32bb54e90c Merge remote-tracking branch 'origin/main' into pt1-3488 2026-10-30 14:58:25 +01:00
Ludovic Marie 22ad79ba01 Merge pull request #4410 from horizon-rust/fix/mod-submit-errors
🤖 Agent: Merged by Ludovic's AI agent (Claude). The registry now answers refused submissions after the upload (drained), sends 413 as JSON, and returns 504 'GitHub is slow' on timeouts. Proof: pr_ready --quick green, and the crate's own tests (mock GitHub, 413, timeout, a TCP drain test that fails without the fix). Railway redeploys horizon-mod-registry from main through its watchPatterns.
2026-10-30 14:52:08 +01:00
Ludovic Marie 235e3e97c6 Merge pull request #4409 from horizon-rust/chore/telemetry-drop-hub-suggestions
🤖 Agent: telemetry server only (its own workspace): cargo fmt, clippy -D warnings clean and cargo test all pass there; pr_ready --quick passes for the rest (Python tests, game workspace). Removes the hub's suggestion endpoints, now without a caller (horizon-site#21, kira#85). skip-release.
2026-10-30 14:52:04 +01:00
levzzzandClaude Opus 5.5 94a93ab7aa Native mods: server tick Start fires before any level; the tick clock ticks once per server tick
`ServerTickStart` now fires at the top of `tick_server`, before the first level's streaming
and world update, where NeoForge fires `ServerTickEvent.Pre`. Before, it fired inside the first
level's `tick`.

The tick rate manager now ticks once per server tick, before the levels, as
`MinecraftServer.tickServer` does; each level reuses its answer, like `ServerLevel.tick` reading
`runsNormally`. Before, every extra dimension called `world_runs` again, so a `/tick step` was
spent on the first level and the others stayed frozen.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 18:49:47 +05:00
Ludovic Marie a827346dd5 Merge pull request #4399 from horizon-rust/feat/launcher-night-new-instance
🤖 Agent: Launcher redesign R6 (New instance with Horizon, Vanilla and Modded), approved by Ludovic. Full pr_ready green, verify and launcher CI green on this head. Contains #4382, #4391 and #4392; lands after them.
2026-10-30 14:48:44 +01:00
Ludovic Marie b853fd4599 Merge pull request #4392 from horizon-rust/feat/launcher-night-sign-in
🤖 Agent: Launcher redesign R5 (sign-in sheet), approved by Ludovic. Full pr_ready green, verify and launcher CI green on this head. Contains #4382 and #4391; lands after them.
2026-10-30 14:48:40 +01:00
Ludovic Marie f10edd4f40 Merge pull request #4391 from horizon-rust/feat/launcher-night-steps
🤖 Agent: Launcher redesign R4b (Play's steps while it gets ready), approved by Ludovic. Full pr_ready green, verify and launcher CI green on this head. Contains #4382; lands after it.
2026-10-30 14:48:37 +01:00
Ludovic Marie 52ff5ebdc6 Merge pull request #4382 from horizon-rust/feat/launcher-night-home
🤖 Agent: Launcher redesign R4 (Home's hero), approved by Ludovic after the screenshots. Full pr_ready green, verify and launcher CI green on this head. First of the stack #4382 → #4391 → #4392 → #4399.
2026-10-30 14:48:33 +01:00
Ludovic MarieandClaude Opus 5.5 4e9ddb17d9 Telemetry server: drop the hub's suggestion endpoints
The website's suggest form is gone (horizon-site#21) and Kira no longer relays it
(kira#85): roadmap suggestions come only from #horizon-ideas posts. This removes
POST /hub/suggest, GET /hub/suggestions, the hub_suggestions_per_day and hub_token
settings (HORIZON_TELEMETRY_HUB_SUGGESTIONS_PER_DAY, HORIZON_TELEMETRY_HUB_TOKEN),
and their rows in the server's README and roadmap/README.md. The rate-limit test now
uses bug reports; the keep-days expiry is tested on the store.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 14:44:09 +01:00
chi4aandDeepSeek 4.1 Flash 726d9e86ef Thrown tridents: clip blocks with vanilla's centre ray, not the 0.5 box
The server tick tested blocks with an AABB sweep of the trident's 0.5 x 0.5 box
and a per-axis moved != velocity test, so a trident passing up to 0.25 blocks to
the side of a block stuck in mid-air beside it. Use AbstractArrow.tick's
zero-width level().clip(...) and AbstractArrow.onHitBlock's 0.05 pull-back
along Math.signum, in ground and shakeTime, on the server and on the client's
predicted copy.

Fixes #4365

Co-Authored-By: DeepSeek 4.1 Flash (DeepSeek Harness) <[email protected]>
2026-10-30 15:41:24 +02:00
Ludovic MarieandClaude Opus 5.5 b086d4298b Mod registry: answer refused submissions after the upload, JSON 413, name a slow GitHub
A submission refused before its body is read (signed out, CSRF, the hourly limit) or partway through (a file over
64 MiB) answered and closed the connection while the browser was still sending the file. The browser then reports a
network error, and the form says "The service didn't answer" instead of the reason. The service now reads the rest
of the upload (up to its limit) before it answers.

A file over the limit gets 413 with a JSON {error}, as every other refusal, instead of a 400.

GitHub calls get 30 s each and the file's upload 5 minutes (it had one 60 s timeout for everything, a 64 MiB upload
included). A call that times out answers 504 "GitHub is slow right now: try again in a few minutes" instead of the
generic 502.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 14:35:03 +01:00
levzzz d2186eb9aa Merge pull request #3981 from arismoko/native-block-write-observation
Expose native server block change observations
2026-10-30 18:30:39 +05:00
levzzz 4ca814d896 Merge pull request #3941 from arismoko/native-neighbour-observation
Expose native neighbour-change observations
2026-10-30 18:30:30 +05:00
levzzz 41672b1221 Merge pull request #3923 from arismoko/native-server-block-read
Expose read-only native server block queries
2026-10-30 18:30:20 +05:00
levzzz 44471a42a8 Merge pull request #3878 from arismoko/native-lifecycle-server-ticks
Expose native server tick hooks
2026-10-30 18:30:11 +05:00
Ashura006andCursor Agent 8a806d23e0 Cull display entities with the width/height frustum box
Matches Display.updateCulling: width or height 0 keeps distance-only
noCulling; otherwise EntityRenderer.shouldRender uses the interpolated
AABB inflated by 0.5 for text, block and item displays.

Co-Authored-By: Cursor Agent <[email protected]>
2026-10-30 16:30:01 +03:00
Ludovic Marie e262d68fcb Merge pull request #3551 from GIGABAIT93/feat/launch-api-client
🤖 Agent: Merged by Ludovic's AI agent (Claude) after review against #3545 and a proof on macOS: full pr_ready on main + the whole Launch API stack (parity 111/112, 0 failed), at this head pr_ready --quick, parity on the changed files, lan_e2e chest and eat on both paths, and a real end to end (prepare into a scratch --output, then horizon-ctl launch with --game-dir/--home/--quickPlaySingleplayer in isolated homes). Thanks GIGABAIT93 for the Launch API and the quick answers to the review.
2026-10-30 14:26:59 +01:00
levzzzandClaude Opus 5.5 85dbd835d6 Launcher: build native mods made for different Horizon versions, after "Build anyway"
Mods pinning different Horizon commits used to be refused. They often still apply and
compile, so the build now picks the newest pinned commit (by commit time, from GitHub),
lists each mod made for another one, and builds only once the player chooses "Build
anyway" (CLI: native-build --allow-version-mismatch). Those mods are repinned on the
build's own copy, like hook-point mods. A failed apply or compile then says the mods were
made for other versions.

horizon-mixins pins that differ resolve to the newest of them and the launcher's own,
never one older than a mod was made with.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 18:25:41 +05:00
Ludovic Marie 941c4498a2 Merge branch 'feat/launcher-night-sign-in' into feat/launcher-night-new-instance
# Conflicts:
#	docs/design-system.md
#	launcher/lang/de.json
#	launcher/lang/es.json
#	launcher/lang/fr.json
#	launcher/lang/pl.json
#	launcher/lang/ru.json
#	launcher/lang/uk.json
#	launcher/lang/zh.json
#	launcher/src/main.rs
2026-10-30 14:24:26 +01:00
Ludovic Marie 16ae712381 Merge branch 'feat/launcher-night-steps' into feat/launcher-night-sign-in
# Conflicts:
#	docs/design-system.md
#	launcher/lang/de.json
#	launcher/lang/es.json
#	launcher/lang/fr.json
#	launcher/lang/pl.json
#	launcher/lang/ru.json
#	launcher/lang/uk.json
#	launcher/lang/zh.json
#	launcher/src/main.rs
2026-10-30 14:23:54 +01:00
Ludovic Marie 69937e5495 Merge branch 'feat/launcher-night-home' into feat/launcher-night-steps 2026-10-30 14:23:16 +01:00
Ludovic Marie 486e3001d7 Merge remote-tracking branch 'origin/main' into feat/launcher-night-home 2026-10-30 14:23:09 +01:00
Ludovic Marie edeecb4971 Merge branch 'feat/launcher-night-home' into feat/launcher-night-new-instance 2026-10-30 14:09:30 +01:00
Ludovic Marie 5e2d9e758b Merge branch 'feat/launcher-night-home' into feat/launcher-night-sign-in 2026-10-30 14:09:30 +01:00
Ludovic Marie f26519bb53 Merge branch 'feat/launcher-night-home' into feat/launcher-night-steps 2026-10-30 14:09:30 +01:00
Ludovic MarieandClaude Opus 5.5 c7cc6e1256 Launcher: Home says Ready to play only when Play would start the game
Not while the version needs preparing, prepares, waits for an account that
owns the game, or runs (review: it showed beside Needs preparing and over
the steps at 34%).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 14:09:10 +01:00
DUzzL fd21d6ee82 Cover no-BOM and partial-BOM pack.mcmeta (#4388) 2026-10-30 14:08:19 +01:00
Ludovic MarieandClaude Opus 5.5 e2479b2272 Launcher: New instance with Horizon, Vanilla and Modded kinds (R6)
The New instance dialog gets the night look (an opaque plate cut in steps,
night buttons) and tabs for the kind. Vanilla and Modded make a Java
instance: the version comes from Mojang's public list (search, Snapshots,
Old; the newest release chosen), Modded picks Fabric or Quilt at its
newest stable version (asked of the loader's service on Create, so an
unsupported version says so in the dialog). The name follows the choice
("Fabric 1.21.4") until the player types one. Horizon instances are made as
before. The game itself still comes with the first Play, for an account
that owns it. HORIZON_LAUNCHER_PAGE=new-instance opens it for screenshots.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 14:03:46 +01:00
Ludovic Marie d5f484c89f Merge pull request #4387 from horizon-rust/fix/windows-credential-manager-full
🤖 Agent: merging after CI. Launcher-only fix: Windows Credential Manager error 8 (store full) now says what is wrong and how to make room, and a save that stops partway keeps the old sign-in instead of a half-written one. Unit-tested with a fake store that fills up; pr_ready --quick green.
2026-10-30 14:00:04 +01:00
DUzzL c274dad88d Read pack.mcmeta with a UTF-8 byte order mark (#4388) 2026-10-30 13:59:36 +01:00
Ludovic MarieandClaude Opus 5.5 66f4bfbaae Launcher: say when Windows Credential Manager is full, and keep the old sign-in when a save stops partway
Windows players saw "Platform secure storage failure: Windows error code 8" when signing in
(#4313, #4366). CredWriteW returns ERROR_NOT_ENOUGH_MEMORY (8) once the user's Credential
Manager reaches its undocumented total size, for an item of any size; the Xbox app and the
Minecraft Launcher are known to fill it with XblGrts/Xbl| items. The launcher now says the store
is full and how to make room, instead of the bare code.

A secret is kept in several items; a write that failed after the first piece left new pieces in
front of old ones, which read back as no secret (signed out at the next start). The old secret
now goes back when a write stops partway.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 13:52:19 +01:00
Ludovic Marie e4472a7423 Merge pull request #4232 from horizon-rust/perf/load-time-faces
🤖 Agent: verify and both parity jobs are green at c8ee23f2b (merged-head rerun passed at 12:35Z); approved by rdbtCVS. Queued for the merger.
2026-10-30 13:50:33 +01:00
Ludovic MarieandClaude Opus 5.5 0cbb5bae35 Launcher: the sign-in sheet in the night look (R5)
The owner gate's screen becomes a sheet: an opaque plate cut in steps over
the dimmed launcher. "Sign in to play", why (the account that owns Java
Edition, like other launchers; no Minecraft install needed, Horizon
downloads it from Mojang for that account), the code in mono on a raised
box with Copy code, Open link as the main action, Not now, and "Waiting for
Microsoft… the window goes on by itself." The link reads as typed
(microsoft.com/link). The gate itself is unchanged: when it shows, what it
asks, and what Play does without an owner.
HORIZON_LAUNCHER_PAGE=sign-in shows the sheet with a made-up code for
screenshots; nothing asks Microsoft.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 13:43:34 +01:00
Ludovic MarieandClaude Opus 5.5 c81de0f908 Launcher: translate the steps' strings, drop the old preparing line
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 13:36:20 +01:00
simonandClaude Opus 5.5 69ec4ccdd6 Keep the cursor and the items right under fast menu clicks
Rapid clicks, as the Mouse Tweaks wheel sends, could leave a stack in the
cursor, and in creative mode add real items. The server's inventory sync echoed
slots a click in another menu had settled, onto clicks predicted since, and it
never compared the click's state id (handleContainerClick does, and answers a
stale click with the whole menu). The crafting table answered every click with
the whole menu, which undid predictions. The client took state ids from windows
other than the open one, and in creative sent its predictions as inventory edits.

The wheel's clicks assumed the first of them worked: on a slot just emptied, as
when a furnace burns its fuel, the right click that followed picked up half a
stack. It now places with a one-slot drag, which does nothing with an empty hand.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 22:35:54 +10:00
simonandClaude Opus 5.5 217fb7af0d Run the Mouse Tweaks wheel and drags in furnaces and crafting tables
The gate only allowed the inventory and chest-like menus, so nothing moved
in furnaces, crafting tables, hoppers and dispensers. The wheel now takes its
target from the menu engine's quickMoveStack, so fuel, inputs and result slots
follow vanilla's routing, and every action is still ordinary slot clicks.

Fixes #4312

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 22:35:54 +10:00
Ludovic MarieandClaude Opus 5.5 5a42da4fe1 Launcher: test the time left and the steps' order
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 13:32:17 +01:00
Ludovic MarieandClaude Opus 5.5 2b78aa1735 Launcher: Play's steps while it gets ready, in the night look (R4b)
While a version prepares, Play reads "Getting ready…" with the download's
percentage, Cancel beside it while a download from Mojang runs. Under them:
the bar, what is happening ("Downloading Minecraft 26.3 from Mojang · 1,754
of 5,160 files") with the time left from the download's pace, and the
steps: game files, Java, sounds, prepare, and play when Play was pressed.
backend::PrepareStep is set by the preparation as it moves on. The progress
bar takes the night look (6 px, plate track, accent fill) everywhere.
HORIZON_LAUNCHER_PAGE=getting-ready shows the state for screenshots.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 13:32:17 +01:00
Ludovic MarieandClaude Opus 5.5 95825f818c Launcher: translate Folder and Version, drop the strings Home no longer has
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 13:32:07 +01:00
Ludovic Marie 5c34936e1b Merge pull request #4242 from horizon-rust/perf/dedicated-no-block-models
🤖 Agent: verify and both parity jobs are green at 3a11501e2 (the rerun passed dedicated_server and world_spawn#3); approved by rdbtCVS. Queued for the merger.
2026-10-30 13:31:55 +01:00
GIGABAIT93andClaude Opus 5.5 267e7063d5 Launch API: promise play only for an owner, name the option in errors (#3545)
- `--name` leaves OPTIONS: the game still takes it, but the contract doesn't
  promise offline play. The guide's first step is now the owner rule, and a
  session the game can't read is stated as a fact, not an option.
- A bad value names its option (`Horizon: --width: invalid digit ...`), since
  a launcher shows that line to the player.
- Session errors say "the session", true for --session-stdin too.
- --quickPlaySingleplayer takes one folder name of saves/, as vanilla's level
  id; a path is refused. --world still takes a path for harnesses.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 14:26:24 +02:00
DUzzL 8a196eeb77 Copper golems: send the chest lid block event to guests too 2026-10-30 13:20:50 +01:00
Ludovic MarieandClaude Opus 5.5 b32541cb88 Map proto sections through the column layout
Main's level-shape work replaced vanillagen's fixed MIN_Y and SECTIONS
with ColumnLayout, so the branch merged with main no longer built.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 13:17:29 +01:00
Ludovic Marie 1df4c50f81 Merge pull request #4032 from horizon-rust/feat/launcher-night-shell
🤖 Agent: R3 night frame (sidebar, top bar, account chip). Ludovic approved the style on 2026-10-30; main merged in, pr_ready --quick green (launcher clippy, 278 launcher tests), CI green on fe89da2a2. Merging.
2026-10-30 13:17:16 +01:00
Ludovic Marie 7fb48c7154 Merge remote-tracking branch 'origin/main' into fix/light-edge-neighbours 2026-10-30 13:15:37 +01:00
Ludovic MarieandClaude Opus 5.5 ca7e8bed1a Launch API: rustfmt the exit code test
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 13:11:41 +01:00
Ludovic MarieandClaude Opus 5.5 416678ba17 Launch API: test the exit codes on the real program (#3545)
tests/launch_api.rs runs horizon-client: --launch-api prints JSON and opens no
log; a refused command line (unknown option, bad value, missing value) exits 2
without a crash report; an error exits 1 with the report's path as the last
stderr line, under a relative --home, and creates a relative --game-dir
without writing saves or options.txt where the game was started. Paths have
spaces, as launchers' instance folders do.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 13:11:16 +01:00
Ludovic MarieandClaude Opus 5.5 bbc9c46c17 Launcher: Home's hero in the night look (R4)
The selected instance's name in the display cut, a caption naming what it
plays and whether it is ready, when it was played, and the update line.
Play, the version picker, Mods and Folder in one row as night buttons
(shell::action: main, raised, quiet). A plate on the right holds a
release's notes line (not a Nightly's build line) and Join a friend, which
leaves the rows below. "Host a server" goes: Servers is in the top bar.
The picker names a Nightly as "Nightly · ba6344a" instead of its tag.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 13:10:19 +01:00
Federico TaglianiandClaude Sonnet 5.5 1e14e33704 Explosive sulfur cubes light their fuse beside redstone power
SulfurCube.primeWhenOnPoweredPosition: each tick an explosive cube that is
alive and not primed checks the best neighbour signal at its block and
primes (SignalGetter.getBestOwnOrNeighbourSignal; no block has an own signal).
lan_e2e check sulfur_cube_power.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 13:00:05 +01:00
ElyTheCatandAntigravity 59a1f51d34 Controls: implement Quit Shortcuts toggle and window-close blocking
In vanilla Minecraft 26.3, the Quit Shortcuts option (options.quitShortcuts,
default true) controls whether keyboard shortcuts (Alt+F4 on Windows/Linux,
Cmd+Q on macOS) can close the game window. When disabled, close requests
initiated via quit shortcuts are ignored to prevent accidental quits, while
window close buttons continue to work.

Horizon already loaded and serialized quitShortcuts in options.txt and
had the cycle button wired in options_ui, but the setting was excluded from
Opt::available and the close handler never checked InputQuirks.isQuitShortcutDown.

- Add is_quit_shortcut_down in horizon-client-input::input checking
  platform modifiers and physical keys (Command+Q on macOS, Alt+F4 elsewhere).
- Check is_quit_shortcut_down on WindowEvent::CloseRequested in game.rs
  and startup_menu.rs when quit_shortcuts is disabled.
- Enable Opt::QuitShortcuts in Opt::available so the toggle is clickable in
  Controls with its vanilla tooltip.
- Add unit tests in horizon-client-input, horizon-client-render, and horizon-client.

Fixes #4336.
Part of #3518.

Co-Authored-By: Antigravity <[email protected]>
2026-10-30 15:29:45 +03:30
iLostName 7ee787f8de item sprites: another namespace's animated texture packs its first frame and animates
A server pack's animated item texture (lobby:item/..., frames stacked in one PNG) was packed whole into the GUI atlas: the path check kept the namespace, so models drawn from it (item display NPCs) sampled every frame at once, in stripes. It now cuts the mcmeta's first frame and animates like vanilla's own.
2026-10-30 12:44:42 +01:00
iLostName 737d74f592 joining a world: the player model and command registries are read once, not at every join
Each join parsed entity-models.json twice for the player's model, walked every block state for /setblock's completions and parsed the 1.6 MB command registries export again on the render thread: about 160 ms of one frozen frame per join. None of them changes while the game runs, so each is read once per resources root or block registry (the player model already as the world resources load), and the join itself now takes about 20 ms.
2026-10-30 12:44:34 +01:00
Ludovic Marie fe89da2a2c Merge remote-tracking branch 'origin/main' into feat/launcher-night-shell 2026-10-30 12:41:22 +01:00
DUzzL 1a37932efd Make crossbow piglins shoot instead of meleeing
register_piglin ran before the spawn weapon was equipped, so the crossbow goal was never added and the melee goal took over. Always register the crossbow goal (it checks the held item) and keep the melee goal off while a crossbow is held, as MeleeAttack does for usable projectile weapons. Fixes #4356.
2026-10-30 12:39:15 +01:00
Ludovic Marie 0a341cc353 Merge pull request #3487 from ThatCakeIsAPie/feat/level-shape-8
🤖 Agent: verified on main + this head. I merged main in. goals.rs keeps main's restricted check and max_h, now with the world's bounds. nether.rs keeps main's ghast fly-to target. main's new occupied_pois_in_range reads the world's bounds too. One harness failed: use_item_on. GameLevel's build range now reads the world's shape, but the dispense fixture only set the dimension, so a bed exploding in a Nether or End case broke the block at y -1, below the level. I added one line so the fixture sets the shape from the case's dimension, as enter_dimension does (f0651e754). Checks: 107 harnesses, dedicated_server, pr_ready and lan_e2e (worldspawn, light, levels, distance, portals, chest, forced-levels) passed on the head before that fix. On this head, the 32 harnesses that depend on the fixture (use_item_on among them) and pr_ready pass. Queued to merge; #3488 follows.
2026-10-30 12:33:13 +01:00
Federico TaglianiandClaude Sonnet 5.5 5866611176 Port map: SetInstrumentFunction.run is ported
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 12:19:59 +01:00
CocofireHDandHermes Agent da4db61ba9 Shulker boxes show their contents to the player who holds them
`ItemContainerContents` travels as the `minecraft:container` component, and the
client already decodes it into the tooltip's contents lines. The encoder that
hands a stack from the integrated or LAN server to a player never wrote it, so a
box that held items listed none: only "Shulker Box" in the tooltip, however full
it was (#2979).

Write the component as `ItemContainerContents.STREAM_CODEC` does: the slot
count, then one optional template per slot, each inner stack through the same
encoder so its own name, count and components survive.

Co-Authored-By: Hermes Agent <[email protected]>
2026-10-30 12:16:58 +01:00
Federico TaglianiandClaude Sonnet 5.5 fe8c64382d Loot: set_instrument puts the picked instrument on the item (a goat horn's tune)
SetInstrumentFunction drew the random option and dropped it, so a goat horn
from a loot table (pillager outpost chests) had no instrument.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 12:16:37 +01:00
Ludovic Marie 71571ed2cc Merge branch 'pt1-3490' into pt1-3491 2026-10-30 12:14:41 +01:00
Ludovic Marie 9d23d4e542 Merge branch 'pt1-3489' into pt1-3490 2026-10-30 12:14:40 +01:00
Ludovic Marie 2c39ef7cbf Merge branch 'pt1-3488' into pt1-3489 2026-10-30 12:14:40 +01:00
Ludovic Marie 21ecb7f80b Merge branch 'pt1-3487' into pt1-3488 2026-10-30 12:14:40 +01:00
Federico TaglianiandClaude Sonnet 5.5 5f7338178c Port map: four rows of #3515 that are already ported; an item frame keeps to the build height
OminousBottleAmplifier.addToTooltip, SuspiciousStewEffects.addToTooltip and
CrossbowItem.createProjectile (the crossbow hit sound) are in the game; a
frame may go on a vertical face (ItemFrameItem.mayPlace) and only needs the
build height, which place_item_frame now checks.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 12:02:31 +01:00
Ludovic MarieandClaude Opus 5.5 f0651e7540 Dispense fixture: the world's shape follows the case's dimension
GameLevel's build range now reads the world's shape, so a case in the
Nether or the End must set it as enter_dimension does: a bed exploding
there broke the block at y -1, below the level (use_item_on).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 12:00:25 +01:00
Federico TaglianiandClaude Sonnet 5.5 1c27a0c66d clippy: is_multiple_of in CyclingSlot
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 11:58:10 +01:00
MSedgeMC 6a03966205 Encode cooking recipes into the recipe book
AbstractCookingRecipe.display() gives smelting, blasting, smoking and
campfire one and the same FurnaceRecipeDisplay: the ingredient, any fuel,
the result, the station, the cooking time and the experience. Only the
station and the book category differ, and the category comes from the
recipe, not from the client.

book_entries encoded shaped and shapeless crafting only and returned an
empty list for everything else, so the furnace, blast furnace and smoker
books were empty for every player whose window is a client: single player
on its server thread, LAN guests and dedicated players alike.

One shared arm now writes that display, and the category switches follow
the jar: smelting food/blocks/other, blasting blocks and one misc tab,
smoking, and campfire (which no book tab lists, as in vanilla).
2026-10-30 16:26:42 +05:30
Federico TaglianiandClaude Sonnet 5.5 cfcd6c9112 Smithing table: the empty slots cycle their icons, as CyclingSlotBackground does
The template slot cycles the armour trim and netherite upgrade template
icons; with a template in the table the base slot cycles what it takes (the
11 tools and armour of the netherite upgrade, the four armour pieces of a
trim) and the additions slot the ingot or the seven trim materials, one icon
every 30 ticks with a 4 tick fade. It showed one fixed netherite icon.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 11:53:06 +01:00
DUzzL c172ab7dae Drop unverified vanilla claim from the slider getSprite ledger line 2026-10-30 11:51:52 +01:00
SylviromiandClaude Opus 5.5 d3d714e9eb Native mods: native_data, a data file of the resources, content mods' included (API 9)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 13:45:33 +03:00
DUzzL cf5290e835 Outline the slider track while dragged 2026-10-30 11:44:54 +01:00
unknownandClaude b3f866c5a6 Combat: the hurt player hears the own hurt sound again
The server thread plays every guest's MotionEvent::HurtSound into its
own silent Audio, so single player and multiplayer heard nothing (#4149);
the client's ClientboundDamageEventPacket handler never played it either,
unlike vanilla's LivingEntity.handleDamageEvent.

Vanilla splits the sound in two: the hurt player's own client plays it
from handleDamageEvent (Player.getHurtSound: the damage type's effects,
volume 1.0, a pitch that walks 1.0 ± 0.2 on the level random), while the
server's Entity.playSound reaches every other player around but that one.

The DamageEvent handler now resolves the packet's damage type through the
synchronized registry and plays hurt_sound(effects) at the player, and
tick_guests sends the server's sound to the hub for everyone but the hurt
player (Some(conn), the same split the death sound and the honey slide
already use).

Fixes #4149

Co-Authored-By: Claude <[email protected]>
2026-10-30 13:42:57 +03:00
Ludovic Marie a1dc00d466 Merge branch 'pt1-3490' into pt1-3491 2026-10-30 11:39:01 +01:00
Ludovic Marie fb51619741 Merge branch 'pt1-3489' into pt1-3490 2026-10-30 11:39:00 +01:00
Ludovic Marie 2c74f84b97 Merge branch 'pt1-3488' into pt1-3489 2026-10-30 11:39:00 +01:00
Ludovic Marie cc1e38ef22 Merge branch 'pt1-3487' into pt1-3488 2026-10-30 11:39:00 +01:00
Ludovic Marie 47a3b39fa9 Merge pull request #3486 from ThatCakeIsAPie/feat/level-shape-7
🤖 Agent: verified on main + this head. I merged main in (through the carried #3485). The crate split had moved chunk.rs, lod.rs and minimap.rs to horizon-client-render, so this PR's changes to them are reapplied there. Checks: 121 of the 122 harnesses this change can affect pass. dedicated_server differed once on a single light section in one column, the intermittent mismatch already seen on #2608, and passed on rerun. pr_ready and lan_e2e (worldspawn, light, levels, distance, portals, chest, forced-levels) pass. chunk_load_bench gives the same meshes as main, and meshing time is the same (median 42.4 vs 42.5 µs per section over 6 pairs). The random-tick cache allocates its Vec once per cached chunk, not per tick. Queued to merge; #3487 follows.
2026-10-30 11:38:20 +01:00
Ludovic MarieandClaude Opus 5.5 2867be0d53 Merge main into level-shape-8
goals.rs keeps main's restricted check and max_h, with the world's
bounds; nether.rs keeps main's ghast fly-to target (no heightmap mirror).
main's occupied_pois_in_range reads the world's bounds too.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 11:37:14 +01:00
MSedgeMC 8a378f441c Pose a brushing arm like vanilla, in both hands
Vanilla's AvatarRenderer builds the arm pose per hand and gives a brush
that is in use the BRUSH pose, whose HumanoidModel case drops the arm
twice as far as an ordinary item: xRot * 0.5 - pi/5 rather than - pi/10,
with yRot zeroed. Horizon had no Brush pose, so a brushing player stood
there with an ordinary item arm while the dust flashed.

The pose is selected from the literal item id with the same using-item
predicate the bow, trident and crossbow arms already use, and the off-hand
ladder gets the matching arm, since brushing works from the off hand too
and vanilla poses that hand the same way. Brush is one-handed and does not
affect the off hand, so shields, bows and tridents keep their paths.

The first-person sweep is not here: PR #3951 already implements it.
2026-10-30 15:51:23 +05:30
v3nn7andCodex 7090cd2d38 Project entity shadows onto partial block bounds
Co-Authored-By: Codex <[email protected]>
2026-10-30 11:20:04 +01:00
Ludovic Marie 9a0e718599 Merge pull request #3656 from jaszkus/fix/waterlogged-bucket
🤖 Agent: Merged by Ludovic's AI agent (Claude) after its proof run: buckets place and collect water in waterlogged blocks, as vanilla does.
2026-10-30 11:17:03 +01:00
v3nn7andCodex 66cc52fd9d Keep debug overlay off the title screen
Co-Authored-By: Codex <[email protected]>
2026-10-30 11:12:38 +01:00
DUzzL adc6e8626e Update port map note for the slider track outline 2026-10-30 11:11:18 +01:00
DUzzL 3bf35d81c8 Draw the slider track outline only for focus, not hover 2026-10-30 11:06:39 +01:00
simonandClaude Opus 5.5 61066655fe Mirror the attacking client's Player.attack, not the server's, and add its harness
The no-damage guard and the knockback check read the held item, effects and
enchantments. On a client none of them count: items and effects only change
attributes on a server, ATTACK_DAMAGE and ATTACK_KNOCKBACK aren't synced, only
ServerPlayer overrides getEnchantedDamage, and getKnockback only adds
enchantments on a ServerLevel. So an empty-hand sprint hit under Weakness does
slow a vanilla client, and a Knockback sword alone doesn't.

test_remote_attack_parity runs the real Player.attack on a LocalPlayer shell
against a RemotePlayer and a zombie and compares the attack strength, velocity
and sprint flag: 51,840 cases identical on 26.3.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 19:55:21 +10:00
JulesB40andClaude Opus 5.5 191fc3302d lan_e2e rocket-ceilings: a star rocket must rise before it bursts
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 10:40:50 +01:00
JulesB40andClaude Opus 5.5 4e86e200a5 Fireworks: every free rocket with explosions goes off on a block
FireworkRocketEntity.tick checks each rocket's move against blocks
(getHitResultOnMoveVector, COLLIDER shapes), not only a rocket shot at an
angle, and onHitBlock explodes it when it has explosions. A rocket launched
from the ground stayed stuck under a ceiling until its lifetime; it now
bursts there. The block hit comes from the ray rocket_struck already casts,
so an angled rocket uses it too instead of its box's collision.
lan_e2e's rocket-ceilings check covers it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 10:38:06 +01:00
Ludovic MarieandClaude Opus 5.5 d24853c0a0 Send a column once its neighbours reach INITIALIZE_LIGHT (#4294)
A server sent a column as soon as it was lit, and its light layers
counted only the neighbours already loaded. Vanilla's ChunkStatus.LIGHT
needs every neighbour at INITIALIZE_LIGHT, and its light storage counts
their sections. So a column loaded with its saved light next to a proto
chunk still being made went out without the top light section that
proto's blocks add. dedicated_server failed 5 of 5 on playhorizon since
an idle dedicated server keeps no spawn area (#4095).

A column now waits until each neighbour is loaded or a decorated proto
chunk, and a proto neighbour's non-air sections (its TerrainChunk
counts, read when sent) go into the light layers. Nothing is kept per
column.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 10:35:54 +01:00
SylviromiandClaude Opus 5.5 3df5f5d5c4 Native mods: native_block, the block at a position (API 9)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 12:34:16 +03:00
jaszkusandjaszkus's AI agent 3da531255a Fix the bucket test after the barrier case
The barrier section leaves a barrier at the test position, so the following 'Emptying water into air makes a source' step ran against a non-air block and failed for a non-creative player. Restore air there; bucket_empty is unchanged.

Co-Authored-By: jaszkus's AI agent <[email protected]>
2026-10-30 10:27:41 +01:00
MSedgeMC c0f16e65e2 Hang nametags from the pose box, not the sneak flag
Vanilla anchors a name tag at the entity's NAME_TAG attachment,
(0, bbHeight, 0) of the box of the current pose, and the submitter lifts it
0.5 more. The sneak flag only decides isDiscrete (the see-through pass).

Horizon took the height from the flag for players, so a player swimming or
gliding with shift held (pose box 0.6, shared crouch bit set) got a tag at
2.0 instead of 1.1, and riding with the flag gave 2.0 against vanilla's
2.3. The height now comes from m.height(), which reads the pose box.

That branch was only needed because the local player's model never filled
extra.avatar_pose (guests and server players get it from DATA_POSE), so
tick_player_model now stores the pose it already computes. Crouching and
standing heights are unchanged at 2.0 and 2.3.
2026-10-30 14:52:26 +05:30
Ludovic Marie e84a47c069 Merge pull request #3485 from ThatCakeIsAPie/feat/level-shape-6
🤖 Agent: verified on main + this head. I merged main in (through the carried #3484); save.rs keeps main's mob imports. Checks: the 52 harnesses this change can affect, dedicated_server, server_entities, server_dimension_players, pr_ready and lan_e2e (worldspawn, light, levels, distance, portals, chest, forced-levels: chunk packets to a LAN guest in each level) pass. The changes are per chunk, not per block. Queued to merge; #3486 follows.
2026-10-30 10:21:17 +01:00
JulesB40andClaude Opus 5.5 835733c7fc lan_e2e rocket-walls: measure each rocket's life in client ticks
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 10:19:41 +01:00
JulesB40andClaude Opus 5.5 5d4016ca61 lan_e2e rocket-walls: count the rocket every tick in one ctl process
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 10:19:41 +01:00
JulesB40andClaude Opus 5.5 48055aa98f Fireworks: a plain rocket shot at an angle flies past the first block
FireworkRocketEntity.onHitBlock explodes only a rocket with explosions
(hasExplosion). A plain rocket out of a crossbow or a dispenser went off
with a poof at the first block it touched; it now flies on to its lifetime,
as in vanilla. Rockets with explosions still burst on impact. lan_e2e's
rocket-walls check covers both.

Fixes #4281

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 10:19:41 +01:00
MSedgeMC 57eac62cd2 Lift the thrown eye of ender by the item ground transform
Vanilla draws eye of ender with ThrownItemRenderer(context, 1.0F, true),
which only scales and billboards; the visible lift comes from the item
model. item/ender_eye parents item/generated with no display override, so
its ground transform's translation [0, 2, 0] applies: 2/16 of a block,
outside the 0.5 scale, along the camera's up axis.

Horizon's hand-rolled billboard centred on the entity, so the eye sat 0.125
blocks low. The quad maths moves into eye_quad with a named GROUND_LIFT and
tests that pin both the offset and its axis (a tilted camera must move the
quad along its own up, not world Y).
2026-10-30 14:47:34 +05:30
ElyTheCatandAntigravity 18f79e7683 Merge fork/fix/armor-rendering into fix/armor-rendering
Co-Authored-By: Antigravity <[email protected]>
2026-10-30 12:32:50 +03:30
Ludovic Marie ba6344a59c Merge pull request #4217 from horizon-rust/fix/keepalive-timeout
🤖 Agent: verify and parity green at c3601b74f (the merged-head parity rerun passed); approved by rdbtCVS. Queued for the merger.
2026-10-30 09:49:11 +01:00
Ludovic Marie 483d832a31 Merge pull request #3484 from ThatCakeIsAPie/feat/level-shape-5
🤖 Agent: verified on main + this head. I merged main in (through the carried #3483). save.rs keeps main's mob imports. LevelShape::for_dir, which the carried #3481 had moved beside the type for the crate split, is gone, as this PR intends: SaveDir::new takes the level's shape. Checks: the 108 harnesses this change can affect, dedicated_server, pr_ready and lan_e2e (worldspawn, light, levels, distance, portals, chest, forced-levels) pass. Queued to merge; #3485 follows.
2026-10-30 09:43:13 +01:00
hhamajandClaude Opus 5.5 9465fb9cb7 A server's copy of a player jumps with them, so Lunge keeps the jump
handlePlayerPositionChange calls jumpFromGround when a player on the
ground moves up without being on the ground. Horizon only charged the
jump's food exhaustion, so the server's copy fell from rest, and the
velocity a jab's impulse sends the player replaced their jump with that
fall: Lunge in the air pushed them down.

jump_from_ground already charges the exhaustion and counts the jump
statistic, which a server's players never got.

Fixes #4278

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 11:41:22 +03:00
SylviromiandClaude Opus 5.5 0c9468753d Native mods: buttons, tooltips and item icons on mod screens
native_screen_click turns a press on a mod screen into a menu button id,
sent as ContainerButtonClick; native_menu_button runs it on the server for
the mod block entity behind the menu. native_screen_tooltip shows lines
under the mouse. NativeScreen gets its width, the inventory's left column
and own_labels, so a mod can lay out a window that isn't vanilla's. Helpers:
native_item (an item's icon, any size), native_cursor, native_screen_title,
native_block_property, native_insert_item (into a container, as a hopper).
API 9.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 11:26:11 +03:00
ElyTheCatandAntigravity 0887fd0c69 Merge branch 'origin/main' into fix/armor-rendering
Drop CustomHeadLayer changes now merged in main (#3605), keeping
multi-layer equipment parsing and rendering for leather overlay.

Co-Authored-By: Antigravity <[email protected]>
2026-10-30 11:52:05 +03:30
ludovicthenot 01c6de15a3 Native mods: a hook point for item tooltips (API 4)
native_tooltip hears every item tooltip the game shows, with the stack and its lines, which a mod can add to, change or take out of. Fixes #4087.
2026-10-30 09:19:37 +01:00
DUzzL ca04a6de62 Draw creative tab and trash tooltips with the item tooltip box 2026-10-30 09:12:31 +01:00
Ludovic Marie 45982faa5c Merge pull request #4245 from horizon-rust/perf/one-loot-data
🤖 Agent: all checks green including parity; approved by rdbtCVS; no comments. Queued for the merger.
2026-10-30 09:11:26 +01:00
MSedgeMC c361096ab0 Draw thrown items where vanilla puts them
Vanilla's thrown snowball, egg and ender pearl are drawn by
ThrownItemRenderer, which only scales and billboards the item: the visible
lift comes from item/generated's ground transform, translation [0, 2, 0]
of 16 units, i.e. 0.125 blocks scaled by the renderer's scale (1.0 for the
throwables, 0.75 and 3.0 for the fireballs). Horizon's hand-rolled
billboard centred the sprite on the entity, so every thrown item drew
0.125 blocks low.

The quad now takes that lift, gated to the kinds vanilla draws with
ThrownItemRenderer: snowball, egg, ender pearl and both fireballs. Llama
spit shares this quad here, but vanilla draws it from
LlamaSpitRenderer with its own translate(0, 0.15, 0) along world up, so it
is deliberately not lifted.

Spawn position, eye height, rotation and velocity already matched vanilla
(the spawn differs by 1.5e-9 blocks), so this is a rendering offset only.
The test pins exact vertex bounds: 99.875..100.375 at scale 1.0,
99.625..101.125 at 3.0, llama spit at no lift, and the rise following the
billboard's own up axis rather than world up.

Not covered here: eye of ender has the same 0.125 offset at its own call
site (game/end.rs eye_geometry) and is left as a follow-up. XP bottles
already take the item-model path, which applies the same ground transform,
so the report's XP-bottle half is not explained by this change.
2026-10-30 13:38:18 +05:30
MSedgeMC fb8e62bc7e Render the jeb_ sheep's wool colour, as vanilla does
SheepRenderer gives a sheep named exactly "jeb_" the lerped dye colour
(ColorLerper.Type.SHEEP: 16 colours, 25 ticks each, white as 0xFFE6E6E6)
and keeps its undercoat. Horizon read no name at all, so the egg did
nothing.

The name test matches MobRenderer.checkMagicName (exact, custom name), the
lerp is Mth.lerpInt's floor per channel, and the age threaded in is the
render state's ageInTicks (tick count plus the partial tick), so the hue
moves per frame instead of stepping at 20 Hz.

The colour vectors came out of the 26.3 jar and were re-derived
independently before the tests were written; jeb_wool_lerps_through_the_
sixteen_dye_colours pins 0xE6E6E6, 0xD0A37D, 0xBA6015, 0x953A8D, 0xDDDDDD
and the 400-tick wrap, and the layer test covers the dyed, baby and
sheared cases.

Known gap, not introduced here: a sheared jeb_ (or dyed) sheep still loses
its undercoat. Vanilla's SheepWoolUndercoatLayer has no sheared guard
while Horizon's outer arm does, which is pre-existing and left for its own
issue.
2026-10-30 13:38:04 +05:30
DUzzL dab1861c21 Let a candle be placed on an uneaten cake (CakeBlock.useItemOn)
Fixes #4266
2026-10-30 09:06:38 +01:00
Ludovic Marie 5d80bf5b89 Merge pull request #3483 from ThatCakeIsAPie/feat/level-shape-4
🤖 Agent: verified on main + this head. I merged main in (through the carried #3482). The crate split had moved gfx.rs, gfx/resident.rs, mesher.rs, streaming.rs and world.rs to horizon-client-render, so this PR's changes to them are reapplied there. World::set keeps main's narrower remesh marking (only the sections a write can reach), now with the chunk's own min_y and section count. Checks: the 108 harnesses this change can affect, dedicated_server, pr_ready and lan_e2e (worldspawn, light, levels, distance, portals, chest, forced-levels) pass. chunk_load_bench gives the same meshes as main, and meshing time is within the machine's noise. Queued to merge; #3484 follows.
2026-10-30 09:04:16 +01:00
Ludovic MarieandClaude Opus 5.5 fb8321177b Pass the world to nav_move_to_path in the callers main added
Main's piglin WalkAway goal and MoveThroughVillageGoal still called the
two-argument form.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 09:01:29 +01:00
Ludovic Marie 3bff52e242 Merge remote-tracking branch 'origin/main' into fix-3721 2026-10-30 08:59:22 +01:00
DenisHumenandClaude Opus 5.5 c7cfd91f14 Keep the mouse wheel off the hotbar while a book or lectern is open
MouseHandler.onScroll gives the wheel to the open screen and turns the
hotbar only when there is none. Books, lecterns, signs, server dialogs
and the link and pack prompts are drawn over gameplay while the screen
stays Playing, so the wheel still changed the selected slot under them.

Fixes #4253

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 09:58:55 +02:00
DUzzL 8e10fc9924 Lectern: close the book screen after Take Book 2026-10-30 08:50:52 +01:00
DUzzL bf103fc383 Keep GUI blend ranges aligned when clip_since rewrites vertices 2026-10-30 08:50:52 +01:00
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
DUzzL f69bf3deb5 Clip GUI item triangles at the scissor instead of dropping them by centre
Advancement icons at the top and bottom of the tree lost one of their two
triangles when the scissor cut through them. HudBatch::clip_since now clips
non-rectangular geometry as polygons, interpolating UV, glint UV and depth.

Refs #4248
2026-10-30 08:42:28 +01: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
DenisHumenandClaude Opus 5.5 393d90d870 Return to Sender: a fireball struck back credits the player's kill
The large and small fireball hits on a mob didn't record the player who
owned the fireball (LivingEntity.hurtServer's setLastHurtByPlayer), as an
arrow's hit does. On the default single player the host plays as a guest
on its server thread, so a ghast killed by its own fireball, turned back,
was credited to nobody: no Return to Sender, Monster Hunter or kill
statistic.

tools/test_return_to_sender_parity.py strikes a still fireball into a
ghast with no AI on both single-player paths and expects the advancement.

Fixes #4212

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 09:34:07 +02:00
CocofireHDandHermes Agent 03c8dd0d98 Item names in command feedback follow the item_name component
ItemStack.getDisplayName appends getHoverName, and getItemName prefers the
`minecraft:item_name` component over the item's own name (Item.getName), so
/give and the other item commands name a renamed stack after that component.
stack_display_name only read `custom_name`, so a datapack- or server-renamed
item was reported under its plain item name ("[Diamond]" for a diamond whose
item_name is "ItemName"). Read the component first, as vanilla does.

Co-Authored-By: Hermes Agent (deepseek-v4.1-flash) <[email protected]>
2026-10-30 08:33:54 +01: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
Ludovic Marie bb64067197 Merge pull request #4200 from horizon-rust/fix/suggestions-one-per-tick
🤖 Agent: parity rerun green (the earlier dedicated_server light-layers failure was a load flake); approved by rdbtCVS; no blocking comments. Queued for the merger.
2026-10-30 08:29:23 +01:00
SylviromiandClaude Opus 5.5 7255ecd851 Native mods: mod screens are API 8
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 10:27:06 +03:00
Sylviromi a1e033003a Merge branch 'native-mod-neighbours' into native-mod-screens 2026-10-30 10:27:06 +03:00
SylviromiandClaude Opus 5.5 a3aa60d7bc Native mods: neighbour helpers are API 7
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 10:27:05 +03:00
SylviromiandClaude Opus 5.5 0e573b8e25 Merge native-mod-inventories; menus are API 6
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 10:26:57 +03:00
Sylviromi b4181cf43a Merge branch 'native-mod-menus' into native-mod-neighbours 2026-10-30 10:26:57 +03:00
SylviromiandClaude Opus 5.5 14d7f036c3 Native mods: item slots are API 5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 10:26:49 +03:00
SylviromiandClaude Opus 5.5 c89bf6241d Native mods: a mod screen takes data only for its own window
ContainerSetData for another window (an old or replaced one) no longer
reaches the open mod screen.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 10:26:36 +03:00
Ludovic Marie cfab69e790 Merge pull request #4233 from horizon-rust/fix/claim-agent-prefix
🤖 Agent: /claim accepts agents' '🤖 Agent:' prefix; 480 Python tests pass.
2026-10-30 08:25:35 +01:00
DenisHumenandClaude Opus 5.5 0271c62825 Falling blocks keep their random model offset (pointed dripstone)
FallingBlockRenderer draws the block through
ModelBlockRenderer.tesselateBlock, which moves it by
BlockState.getOffset(BlockPos.containing(x, maxY, z)). Horizon drew
falling blocks centred, so a pointed dripstone (OffsetType.XZ) jumped
sideways when it broke off and fell.

Fixes #4102

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 09:25:16 +02:00
Sylviromi 0bebf89220 Merge branch 'native-mod-neighbours' into native-mod-screens 2026-10-30 10:24:48 +03:00
SylviromiandClaude Opus 5.5 3c2a49c4ef Native mods: neighbour changes notify after the change; properties update neighbours
native_with_mod_block_entity runs the mod's change first, then marks the
chunk changed and, when items moved, lets comparators read the container
again (native_mod_block_entity_at notified before). native_set_block_property
goes through Level.setBlock with UPDATE_ALL, so neighbours and observers hear
of it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 10:24:44 +03:00
Ludovic MarieandClaude Opus 5.5 2c526aa767 Load the loot data once when a world opens with the packs world generation used
World generation loads the loot data with no packs while the world is
prepared; opening the world then forgot it and loaded a second copy, which
the generator's copy kept alive beside it (8 MB for the whole run). Now
opening a world keeps the loaded copy when its root and packs are the same.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 08:23:55 +01: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
Sylviromi 1eeda33ae5 Merge branch 'native-mod-menus' into native-mod-neighbours 2026-10-30 10:21:47 +03:00
SylviromiandClaude Opus 5.5 aba4ee3d56 Native mods: a mod menu shows only its container's slots
A menu reads, writes and sends only the declared slot count; items saved
past it (a container that shrank) stay saved and untouched.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 10:21:46 +03:00
DUzzL 803bff885b lan_e2e: level_respawn_points leaves no Overworld spawnpoint (#4234) 2026-10-30 08:20:27 +01:00
Sylviromi 51b39ee118 Merge branch 'native-mod-inventories' into native-mod-menus
# Conflicts:
#	crates/horizon-client-render/src/chunk/block_entity.rs
#	crates/horizon-client-render/src/containers.rs
#	crates/horizon-client/src/game/native_hooks.rs
2026-10-30 10:19:41 +03:00
Ludovic MarieandClaude Opus 5.5 3a11501e23 Dedicated server: bake no block models, which only the renderer draws
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 08:19:39 +01: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
SylviromiandClaude Opus 5.5 4459fe3989 Native mods: a mod's own Items stays its own
Items become slots only when they are an item list saved exactly so; any
other Items stays in the mod's compound. A block without a container gets
its Items back in its compound before its tick, and breaking it drops
nothing.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 10:19:08 +03:00
Ludovic Marie d129fb7cfe Merge pull request #3482 from ThatCakeIsAPie/feat/level-shape-3
🤖 Agent: verified on main + this head. I merged main in (through the carried #3481); it merged cleanly. Checks: the 108 harnesses this change can affect, dedicated_server, pr_ready and lan_e2e (light, levels, distance, portals, chest, forced-levels) pass. worldspawn fails only when it runs after levels, on main from before this stack as well: the harness gives the guest its own spawnpoint (#4234), so that isn't this PR. The light flood fill reads the level's column instead of constants: chunk_load_bench main vs this branch gives the same best time for generating and lighting (28.95 ms per column) and the same meshes. Queued to merge; #3483 follows.
2026-10-30 08:19:07 +01:00
Ludovic MarieandClaude Opus 5.5 92e956be80 /claim: accept the "🤖 Agent:" prefix agents put on every comment
Agents start each comment with "🤖 Agent:" (CLAUDE.md), so their
"🤖 Agent: /claim" was never seen: the workflow only started on comments
beginning with /claim, and parse() read the first line as is. rdbt
noticed agents' claims missing (2026-10-30).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 08:16:24 +01:00
Sylviromi 027aef4b3b Merge branch 'native-mod-block-entities' into native-mod-inventories
# Conflicts:
#	crates/horizon-client-render/src/containers.rs
#	crates/horizon-client/src/game/native_hooks.rs
2026-10-30 10:15:56 +03:00
SylviromiandClaude Opus 5.5 2de673c4f8 Merge main; block entity hooks are API 4 (#3706 took 3)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 10:15:13 +03: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
levzzzandClaude Opus 5.5 186540eb84 parity: give the mob_ai oracle an 8g heap
VanillaMobAiOracle ran out of its 4g heap (OutOfMemoryError: Java heap
space, 34 times in one run) within about 3 minutes of a fresh run on main,
with nothing else running. Runs that reused cached oracle results kept
passing, which hid it until the scenarios changed. With 8g the fresh run
passes in 2.9 minutes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 12:09:41 +05:00
abduvaliy-hbaiandOpenAI Codex 4b14acc1f6 Sync block entity data edits to connected clients
Mark block entity NBT changes dirty and queue an update even when the block state is unchanged, so spawner mob previews refresh after data commands. Add an isolated screenshot regression for zombie/pig/zombie transitions.

Co-Authored-By: OpenAI Codex <[email protected]>
2026-10-30 12:07:20 +05: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
Ludovic MarieandClaude Opus 5.5 c8ee23f2b8 Keep no face shapes from the load-time passes over the collision and support tables
analyse_shape and the cached support flags computed every shape's six faces
through face_shape, which caches them on the shape for good: about 39 MB that
nothing reads again. They now use face_shape_transient; runtime queries still
cache.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 08:04:59 +01: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
bloodulonandQoder 912a9febc7 Seat displays and hitboxes on whatever carries them
ModelEngine mounts a model's item displays (its bones) and interaction
hitboxes (its click boxes) on a pivot, usually an area effect cloud, and
moves only the pivot. #3776 seats text displays on mobs and the local
player through the passenger attachment; an item display riding a pivot
display or an area effect cloud, and a hitbox riding a pivot, had no
seat at all and stayed where the server first spawned them.

Every display and interaction box now asks its vehicle where to sit:
mobs through their passenger attachment, boats and minecarts from their
seat, armor stands from their box, another display or interaction from
its position, and any other tracked entity (the area effect cloud) from
its feet. Riders of riders settle over three passes, as mobs already did.

Co-Authored-By: Qoder <[email protected]>
2026-10-30 12:02:24 +05:00
levzzz b1dfde280b Merge pull request #3948 from kacpherek/fix/split-spear-ai-golem
Mobs use spears; iron golems keep to their village
2026-10-30 12:00:15 +05: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
bloodulonandQoder 558a9a13e9 Draw the click boxes of server interaction entities
Servers (Denizen, Nexo, ModelEngine) spawn minecraft:interaction entities:
invisible boxes with a width, height and response flag that a script
answers. The client knew nothing of them, so clicks aimed at the box hit
the block behind or nothing, and the map's NPC models could not be
clicked.

A spawned interaction becomes a RemoteInteraction (SetEntityData ids 8,
9 and 10), pickable for use and attack from its feet up its height, and
its teleports replace its position at once (Entity's no-op
InterpolationHandler). The click ends at the box: SUCCESS when it
responds, and the arm swings; CONSUME otherwise. Either way the Interact
packet carries the hand, nothing behind the box is used, and pick-empty
leaves no entity to pick.

Co-Authored-By: Qoder <[email protected]>
2026-10-30 11:55:42 +05:00
levzzzandClaude Opus 5.5 1bd934d3fb Merge main into fix/split-spear-ai-golem
lan.rs: keep both the spear hit event (2) and main's fox eating event (45).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 11:51:46 +05:00
hhamajandClaude Opus 5.5 2d41c9d432 A pearl turns the wind charge it hits, which then speeds up
Projectile.onHit calls onRedirectProjectile on a redirectable target:
AIM_DEFLECT along the thrower's look, the thrower its owner, acceleration
power 0.1. #3599 made the pearl teleport but left the charge as it was.
A wind charge now runs applyInertia (power 0, inertia 1 in and out of
water), so the power it gets from the turn takes effect.

The harness needs a still charge the pearl can't miss, so /summon and
saved worlds now load both wind charges, and selectors see them.

Fixes #3299

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 09:51:13 +03:00
levzzzandClaude Opus 5.5 03498ad1c7 Merge main into fix/split-spear-ai-golem
piglin.rs: keep main's piglin/hoglin brain memories and add this branch's spear memories to them; the fight activity keeps the spear behaviours before the melee one and main's NotRiding repellent goal; piglin_pose_holding takes both main's passenger flag and the spear.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 11:50:00 +05:00
levzzz fbd7288a1d Merge pull request #3949 from kacpherek/fix/split-mob-sounds-behaviour
Mob sounds and behaviour: fox, zoglin, turtle, iron golem, sheep, cat, nautilus, pig and more as vanilla
2026-10-30 11:48:46 +05:00
levzzz 6966a69d36 Merge pull request #3946 from kacpherek/fix/split-agelock-goat-allay
Goats long jump between ledges, golden dandelions lock a baby's growth, allays remember a note block
2026-10-30 11:48:36 +05:00
Ludovic Marie e4fb44e067 Merge pull request #4185 from horizon-rust/fix/guest-inbox-cap
🤖 Agent: all checks green on 5a017c900 (inbox capped in packets and bytes; A/B in the description). Queued.
2026-10-30 07:45:06 +01:00
levzzzandClaude Opus 5.5 0c441c9009 Merge main into fix/split-mob-sounds-behaviour
ai.rs: main's piglin_ambient/hoglin_ambient now pick the angry and retreat sounds from the brain's activity; keep those and add this branch's zoglin angry sound.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 11:41:48 +05:00
Ludovic Marie c48d6773c5 Merge pull request #3481 from ThatCakeIsAPie/feat/level-shape-2
🤖 Agent: verified on main + this head. I merged main into the branch: the crate split had moved chunk.rs to horizon-client-render, so the LevelShape helpers this PR uses from save.rs are pub now and LevelShape::for_dir sits beside the type (an impl in horizon-client on a type of horizon-client-render no longer compiles). Checks: the 108 harnesses this change can affect, dedicated_server, pr_ready and lan_e2e (light, levels, distance, worldspawn, portals, chest, forced-levels) pass. chunk_load_bench main vs this branch: same meshes (checksum), mesh time unchanged (42.0–44.2 vs 42.7–43.2 µs per section). Queued to merge; the rest of the level-shape stack follows in order.
2026-10-30 07:41:33 +01:00
levzzz 2160cd016a Merge pull request #3791 from xFedeT/fix/flyers-scan
Ghast: target position, move control timing, look angle and shot sounds as vanilla
2026-10-30 11:39:28 +05:00
levzzz a99ef86dc6 Merge pull request #3942 from kacpherek/fix/split-piglin-hoglin
Piglins and hoglins retreat, retaliate, grunt and ride like vanilla
2026-10-30 11:39:12 +05: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
DenisHumenandClaude Opus 5.5 6f42cbb13f Lower the shield when a server stops the player's use
On a server, the client ignored DATA_LIVING_ENTITY_FLAGS for its own
player. When the server stopped the use (an axe hit disabling the raised
shield: Player.disableShield → stopUsingItem), the shield stayed up on
screen while hits landed. LocalPlayer.onSyncedDataUpdated stops a running
use when the flag clears and starts one in the flagged hand when it sets.

Fixes #4115

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 08:35:32 +02:00
Ludovic Marie 37f449f91f Merge branch 'pt1-3490' into pt1-3491 2026-10-30 07:33:51 +01: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
Ludovic MarieandClaude Opus 5.5 c3601b74f3 Server: the game thread is the keep-alive timeout's only judge; kick, then close a second later
The writer thread's own check raced it and logged "Disconnected" instead of
vanilla's "Timed out".

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 07:30:07 +01:00
Ludovic Marie 824953813a Merge branch 'pt1-3489' into pt1-3490 2026-10-30 07:29:40 +01:00
Ludovic Marie 04cbf91533 Merge branch 'pt1-3488' into pt1-3489 2026-10-30 07:29:25 +01:00
Ludovic MarieandClaude Opus 5.5 3b584979a8 Merge main (through the carried #3487) into level-shape-9
Keeps both sides: main's world_root and LAN online mode beside the level
shapes, and main's content-identity check before the registry packets,
which now carry the levels' dimension types.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 07:27:48 +01: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
GIGABAIT93andClaude Opus 5.5 6ffe36494e port_publish: read gh's and Kira's output as UTF-8
gh() and kira() ran subprocess with text=True and no encoding, so on
Windows Python decoded their UTF-8 output with the code page: an issue
body's → came back as →, and merged() wrote it back on the next update.
Follows d3grando's review on #3587.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 08:24:38 +02:00
Ludovic MarieandClaude Opus 5.5 265bb903d7 Merge main (through the carried #3486) into level-shape-8
The crate split moved chunk.rs, collisions.rs and world.rs to
horizon-client-render: this commit's changes are reapplied there.
render_tick.rs keeps main's heightmap reads for rain splashes, with the
world's bottom in place of MIN_Y; main's section_mesh_pending (moving
pistons) keys sections from the level's column as well.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 07:24:36 +01: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
Ludovic Marie 68f036059e Merge pull request #4181 from horizon-rust/tools/bots-abusive-client
🤖 Agent: all checks green on 6edf1ff46 (horizon-bots only, skip-release). Queued.
2026-10-30 07:22:09 +01:00
Ludovic MarieandClaude Opus 5.5 0b73217d3d Merge main (through the carried #3485) into level-shape-7
The crate split moved chunk.rs, lod.rs and minimap.rs to
horizon-client-render: this commit's changes are reapplied there.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 07:22:02 +01: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
Ludovic MarieandClaude Opus 5.5 99998b9f3c Merge main (through the carried #3484) into level-shape-6
Keeps main's mobs imports in save.rs.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 07:20:00 +01:00
Ludovic MarieandClaude Opus 5.5 c2db12a1a0 Merge main (through the carried #3483) into level-shape-5
Keeps main's mobs imports in save.rs. LevelShape::for_dir, which the
carried #3481 moved to chunk.rs for the crate split, goes as this
commit removes it: SaveDir::new takes the level's shape instead.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 07:19:04 +01: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
DenisHumenandClaude Opus 5.5 dcd86510f5 Frogs walk their legs at vanilla's rate
Frog.updateWalkAnimation overrides the base: the walk target is
min(distance * 25, 1), and 0 while the jump animation is started.
Horizon used the generic min(distance * 4, 1), so a frog's slow steps
barely moved its legs.

Fixes #2848

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 08:18:47 +02: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
repositorykreml1nandCodex b87f65b1b6 TNT: cover client replay and preserved server duplication
Add four regression tests for issue #3856: client neighbor updates and placement leave TNT alone, server priming is not limited per tick, and a piston duper retains its TNT block while creating exactly one server entity and no client entity.

Verified against nightly-2026-10-29-4baa571 with prepared Java 26.3 resources.

Co-Authored-By: Codex <[email protected]>
2026-10-30 09:17:05 +03:00
Ludovic MarieandClaude Opus 5.5 0c149c7d1c Merge main (through the carried #3482) into level-shape-4
The crate split moved gfx.rs, gfx/resident.rs, mesher.rs, streaming.rs and
world.rs to horizon-client-render: this commit's changes are reapplied there.
World::set keeps main's narrower remesh marking (only the sections a write
can reach) with the chunk's own min_y and section count.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 07:15:27 +01:00
repositorykreml1nandCodex df1a26d2bd TNT: prime powered blocks only on the server
Match vanilla TntBlock.prime: client-side piston replay must not create a second primed TNT or remove its block. Server-side TNT duplication remains unchanged.

Refs horizon-rust/horizon#3856.

Co-Authored-By: Codex <[email protected]>
2026-10-30 09:14:38 +03:00
arismoko 05275d7682 Expose native server block change observations 2026-10-30 01:13:03 -05:00
arismoko 907b5729b1 Expose native neighbour-change observations 2026-10-30 01:13:03 -05:00
arismoko c2670e7bd2 Expose read-only native server block queries 2026-10-30 01:13:03 -05:00
arismoko 2a6ae514f8 Expose native server tick hooks 2026-10-30 01:13:03 -05:00
Ludovic MarieandClaude Opus 5.5 5fb90b440c Server: time out a client that stops answering keep-alives, even while its writer is blocked
The writer thread sent and checked the keep-alives; a client that stopped
reading blocked it in a write, so it was never timed out and held its slot.
The game thread now checks the keep-alive clock once a second and closes the
socket, which ends the blocked write.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 07:11:58 +01: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
Ludovic Marie bd9b325872 Merge pull request #3674 from d3grando/fix-3129
🤖 Agent: Ludovic's AI agent (Claude), proof. With your check fix (one loaf each, bites read once they settle), `lan_e2e` `eating` and `eat` pass 2 runs out of 2 on the server thread and 2 out of 2 `--in-frame`. That was in a union with #3706 and #3551 merged into main at a6aa6a071: 111 of 112 affected harnesses passed, none failed, and clippy and the tests passed. Queued.
2026-10-30 07:07:53 +01:00
Ludovic Marie b51637616b Merge pull request #3706 from arismoko/upstream/native-lifecycle
🤖 Agent: Ludovic's AI agent (Claude), proof. Thanks for taking up the review points (the context now carries only authority and epoch, and the crash paths are documented). This head was in a union with #3674 and #3551 merged into main at a6aa6a071. `pr_ready`: 111 of 112 affected harnesses passed, none failed; the 112th is the LAN suite, which runs by name only. clippy passed, and 1541 tests passed. `lan_e2e` (chest, eat, eating) passes twice on the server thread and twice `--in-frame`. Your container proof covers the dedicated runs and identical saves. Queued.
2026-10-30 07:04:11 +01:00
Ludovic MarieandClaude Opus 5.5 5a017c9006 lan_bot: queue test packets with their size
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 07:02:33 +01:00
Kacpherek c1c7cdb3c1 Clear a passenger's ground state every tick
Player.tick calls setOnGround(false) for any passenger, so the look packets a rider sends (ServerboundMovePlayerPacket.Rot) never carry onGround. Our passenger tick skipped the player's own motion step, so the ground flag from before boarding stayed set and every packet claimed the rider stood on something while the boat bobbed, which Grim flags as NoFall and Simulation.

Fixes #3793
2026-10-30 07:00:49 +01:00
Ludovic Marie c18e893143 Merge branch 'pt1-3481' into pt1-3482 2026-10-30 06:46:22 +01:00
Kacpherek 8880df58c1 End a glide when a player boards a boat
A player who boarded a boat while gliding kept the FallFlying pose: passenger_pose_tick cleared swimming but not fall_flying, so update_player_pose still picked FallFlying and fall_fly_ticks kept counting. Vanilla LivingEntity.updateFallFlying (in aiStep, run by rideTick's tick) clears the glide flag once canGlide fails for a passenger (canGlide needs !isPassenger()), and LivingEntity.tick resets fallFlyTicks when not gliding (LivingEntity.java).

Fixes #3093
2026-10-30 06:39:28 +01:00
Alexander KrylovandHermes Agent ccb85be48e Launcher: a version stops counting as installed when its removal starts
backend::remove dropped the .installed marker whenever remove_dir_all reached it,
so a second removal finishing first re-read installed and put the half-deleted
version back in the list that selection_after_removal and Play read.

Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
2026-10-30 08:38:06 +03:00
Ludovic MarieandClaude Opus 5.5 b9f8ea2f27 Server: answer one command suggestion request a tick, the latest (ServerCommandSuggestionsProvider)
A client asking 100 times a tick took the server to 16 TPS: every request was parsed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 06:33:55 +01:00
Ludovic MarieandClaude Opus 5.5 536c3b90fd LevelShape helpers are pub, and for_dir sits beside the type
Since the crate split, chunk.rs is in horizon-client-render while save.rs
(horizon-client) reads min_section, section_count, has_section and
for_dir, so they are pub and for_dir moves out of save.rs's impl block.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 06:33:02 +01:00
DUzzL 6b0d88bf88 Send a cleared end crystal beam to LAN guests (#4190)
A crystal whose beam target went back to none was left out of the entity state with no listed default, so guests kept the respawn beam.
2026-10-30 06:32:09 +01:00
Kacpherek 0917285a33 Merge remote-tracking branch 'origin/main' into fix/split-misc-a 2026-10-30 06:32:04 +01:00
Kacpherek b886586ca6 Merge remote-tracking branch 'origin/main' into fix/split-misc-a
# Conflicts:
#	crates/horizon-client/src/entity_nbt.rs
#	crates/horizon-client/src/game.rs
#	crates/horizon-client/src/game/advancements.rs
#	crates/horizon-client/src/game/allays.rs
#	crates/horizon-client/src/game/armor_stands.rs
#	crates/horizon-client/src/game/lan_actions.rs
#	crates/horizon-client/src/game/remote_projectiles.rs
#	crates/horizon-client/src/mobs/leash.rs
2026-10-30 06:32:04 +01:00
Ludovic MarieandClaude Opus 5.5 4abef77bba Server: cap a player's waiting packets in bytes too (16 MB)
400 full books a tick passed the packet count while holding hundreds of MB.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 06:31:48 +01:00
Kacpherek 2061f797ba Share stand_if_possible between the rear goal and the rider's jump 2026-10-30 06:31:44 +01:00
Kacpherek 3f690a811c Walk a camel's legs only while it stands and does not dash
A camel's legs used the default walk: they kept swinging while it sat or dashed and eased in at 0.4. Camel.updateWalkAnimation aims at 6 times the distance moved (at most 1) only in the standing pose with no dash animation running, otherwise at zero, and eases at 0.2. The remote client's own walk update uses it for camels too.
2026-10-30 06:31:39 +01:00
Kacpherek 381e2e9a84 Stand a sitting camel up when its lead pulls taut
A leashed sitting camel stayed down however hard the lead pulled. Camel.onElasticLeashPull stands it up when it is sitting, not changing pose, and the standing box fits where it is (canCamelChangePose, wouldNotSuffocateAtTargetPose). AbstractHorse.onElasticLeashPull, which it calls first, also ends a horse's grazing.
2026-10-30 06:31:39 +01:00
Kacpherek f4b7a899b8 Let a horse slide on the rider's keys after a jump while it rears
A horse that landed from a small hop while still rearing ignored the keys until the rear ended, and a donkey or mule never reared on a jump. AbstractHorse.onPlayerJump sets allowStandSliding and calls standIfPossible (every horse that can rear), getRiddenInput only zeroes the input while the horse is standing on the ground without a jump pending and without allowStandSliding, and tick clears it once the horse stands no more.
2026-10-30 06:31:39 +01:00
Kacpherek dff7cc1dda Slide a rider back and up while its horse rears
A horse, donkey, mule or undead horse that reared kept its rider at the fixed seat. AbstractHorse.getPassengerAttachmentPoint adds (0, 0.15, -0.7) times the previous tick's stand animation (standAnimO) to the seat, turned with the horse. The player and a mob rider both slide.
2026-10-30 06:31:39 +01:00
Kacpherek fa27fb4088 Seat camel riders by the camel's sit and stand animation
The rider's seat on a camel was a fixed height for sitting and standing, built from the old full-height box, and a mob rider sat at the top of the box when the camel sat. Camel.getPassengerAttachmentPoint takes the body anchor from the current pose box and lerps it through the sit and stand transitions (getBodyAnchorAnimationYOffset), at the camel's age scale. The player and a mob rider both use it now.
2026-10-30 06:31:39 +01:00
Kacpherek d59d8e7147 Shrink a sitting camel's hitbox to its sitting size
A camel that sat down kept its standing box, so it blocked and was hit as if it still stood. Camel.getDefaultDimensions gives Pose.SITTING an adult box 1.43 lower with eyes at 0.845, and a baby box 0.425 tall with eyes at 0.41. Mob::awake_dims now follows the camel's synced pose, on the server and on a client.
2026-10-30 06:31:39 +01:00
Kacpherek b2ba13377b Hurt a llama and its riders only for falls of six blocks
A llama landed with the generic fall sound and hurt on any damaging fall. Llama.causeFallDamage makes no fall sound, and hurts the llama and hands the fall to its riders only when the fall is six blocks or more.
2026-10-30 06:31:39 +01:00
Kacpherek 3379afe807 Pass a mount's fall damage on to its riders
A rider took no damage when the mob under it landed hard, and a mob on a mount's back never felt it either. Entity.propagateFallToPassengers hands the fall to every passenger (LivingEntity.causeFallDamage first, AbstractHorse.causeFallDamage after its own hurt and only when hurt), and each passenger runs its own causeFallDamage with its own safe distance. A mount now raises a fall event the game turns into Player.causeFallDamage for the local player and server players, and the mob causeFallDamage for a mob rider.
2026-10-30 06:31:30 +01:00
Ludovic Marie bc3383d1bf Merge remote-tracking branch 'origin/main' into pt1-3481 2026-10-30 06:31:05 +01:00
Ludovic Marie b76ae35f5b Merge pull request #3480 from ThatCakeIsAPie/feat/level-shape-1
🤖 Agent: pr-triage-1 (Ludovic's AI agent, Claude), not Ludovic himself. I merged main into this branch (eb3f03d7f, a merge commit, fast-forward). The only conflict was `lib.rs`'s re-export list: `section_mask` now sits beside main's `potent_sulfur` and `split_layout`. The full proof the orchestrator asked for, on this head merged into today's main: - the 121 vanilla harnesses this branch can reach pass, and `dedicated_server` too; - pr_ready passes; - lan_e2e passes on the server-thread host: light, levels, distance, portals, chest, forced-levels and worldspawn. In-frame is deprecated now. worldspawn failed once on timing, then passed 2/2. Speed: the masks only grow from 4 to 32 bytes, in per-section bookkeeping (dirty sections, the light log, LOD detail columns). There is no allocation and no per-block work, so frame and tick costs stay the same. Queued for merge. #3481 comes next, once this one is in.
2026-10-30 06:30:54 +01:00
Kacpherek f83be65d79 Send creative inventory edits with the click, not at tick end
A creative click on a server changed the local slot but sent it at the end of the tick, while a thrown stack went out at once. A slot update the server sent in between (the pickup of an item a held Q had thrown) replaced the unsent change and was marked as synced, so the throw was never taken off the stack and the count climbed. CreativeModeInventoryScreen.slotClicked ends in broadcastChanges (CreativeInventoryListener.slotChanged -> handleCreativeModeItemAdd), so the changed slots follow the click immediately; Horizon now sends them right behind it.

Fixes #3090
2026-10-30 06:30:25 +01:00
Kacpherek 1a4ee0e7ee Release the probe lock when a world is checked for being open elsewhere
Checking a world on the title screen takes the session lock for an instant; hand it back explicitly, as SessionLock does, so a quit world is never listed as locked. Drop the stale note that powder snow climbing is not ported for rabbits.
2026-10-30 06:30:14 +01:00
Kacpherek 1e5aaec352 Skip the note block head sound on a client level 2026-10-30 06:29:39 +01:00
Kacpherek 360582703d Merge origin/main into fix/split-rendering 2026-10-30 06:29:18 +01:00
Kacpherek cb0be4adbe Merge remote-tracking branch 'origin/main' into fix/split-misc-b
# Conflicts:
#	crates/horizon-client/src/game/remote_mob_data.rs
#	crates/horizon-client/src/game/remote_projectiles.rs
2026-10-30 06:28:40 +01:00
Kacpherek 43777b9d16 Stub the redstone ore dust in the remote mining fixture 2026-10-30 06:28:23 +01:00
Kacpherek ef917292ed Merge remote-tracking branch 'origin/main' into fix/split-blocks
# Conflicts:
#	crates/horizon-client/src/game/remote.rs
2026-10-30 06:28:07 +01:00
Ludovic Marie c6bf75b3b1 Merge pull request #3740 from UMCEKO/mods/oracle-item-components
🤖 Agent: Ludovic's AI agent (Claude), proof. The real NeoForge pack test above covers this PR's exporter. On a 3-mod pack it wasn't developed against, the exported item facts matched 52 of 52 values read with vanilla's own commands on the same server, without the exporter. CI verify is green. Queued, first in the stack's order.
2026-10-30 06:21:42 +01:00
Ludovic Marie 08e7c00806 Merge remote-tracking branch 'origin/main' into upd-3721
# Conflicts:
#	tools/test_mob_ai_parity.py
2026-10-30 06:14:43 +01:00
Ludovic Marie a6aa6a071a Merge pull request #4142 from horizon-rust/perf/idle-generator-scratch
🤖 Agent: verified by server-tick-1. A chunk worker idle for 5 s frees its generator's density scratch and lookup caches (caches only, results unchanged), and the last idle worker returns freed memory to the OS once; −71 MB idle RSS on Linux. pr_ready green.
2026-10-30 06:10:05 +01:00
levzzzandClaude Opus 5.5 c9a87963ef Merge main into fix/split-agelock-goat-allay
frog.rs: keep the goat jump cooldown and main's vanilla memory id long_jump_cooling_down.
remote_mob_data.rs: keep the tadpole AGE_LOCKED line beside main's mooshroom comment.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 10:07:25 +05:00
Ludovic MarieandClaude Opus 5.5 6edf1ff467 horizon-bots: costly floods (use, click, suggest, book, sign, recipe) and --no-read
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 05:58:24 +01:00
levzzz 33f417fb32 Merge pull request #4051 from d3grando/fix-3199
A camel and the husk on its back no longer push each other
2026-10-30 09:55:26 +05:00
Ludovic Marie ee883db4b9 Merge pull request #4117 from horizon-rust/perf/shared-advancement-tree
🤖 Agent: verified by server-tick-1, after merging main. ServerAdvancements and the game's criteria share one advancement tree (Tree::shared, Weak-held) when the world's packs bring no advancement files; with pack advancements each loads its own, as main does. −9 MB idle RSS on Linux. pr_ready green.
2026-10-30 05:55:06 +01:00
My NameandClaude Opus 5.5 7e6da998fa README: the creative toolbar keys are wired
The known issues still said C/X aren't wired to the hotbar loader; they are
since 85f34393f (game/creative_hotbar.rs), and #3693 added the Saved Hotbars tab.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 07:52:38 +03:00
levzzz f11bbea3b1 Merge pull request #3881 from TJKDev1/fix/own-head-pitch-follows-mouse
Third person: the player's head pitch follows the mouse between ticks
2026-10-30 09:48:58 +05:00
My NameandClaude Opus 5.5 dbce6420c6 Command blocks: one data path; the command reaches only the player who opens it
#3582 and #3748 fixed the same bug (#3614) twice. The server now sends the
command only through `lan_open_command_block` (ServerPlayer.openCommandBlock),
no longer in every chunk and block update (CommandBlockEntity keeps the empty
getUpdateTag), and the open edit screen refreshes its fields in one place
(CommandBlockEditScreen.updateGui). docs/rust-parity.md: the toolbar keys and
the Saved Hotbars tab are wired since #3693.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 07:44:42 +03:00
levzzzandClaude Opus 5.5 bb0bf9e1c8 Pass PackOverlay::NONE to Items::load in the same-vehicle push test
Main added a PackOverlay argument to Items::load after this branch was written.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 09:44:29 +05:00
levzzz a6a734da85 Merge commit 'refs/pull/4051/head' of https://github.com/horizon-rust/horizon into HEAD 2026-10-30 09:39:36 +05:00
levzzzandClaude Opus 5.5 3b82d4770f Merge main into fix/own-head-pitch-follows-mouse
Keeps both look_at_rotation (main) and position_packet_pitch (this branch) in remote.rs.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 09:39:17 +05:00
Ludovic MarieandClaude Opus 5.5 fa50f136fe Server: disconnect a player whose packets pile up past a second of its budget
The per-player inbox had no limit: a client sending 10 000 packets a tick
grew the server by about 1 GB a minute (#4179).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 05:38:59 +01:00
Ludovic MarieandClaude Opus 5.5 eb3f03d7f3 Merge main into this branch (split_layout and potent_sulfur re-exports)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 05:38:07 +01:00
Ludovic Marie 061751a023 Merge pull request #4095 from horizon-rust/carry/fusseltv-idle-spawn-area
🤖 Agent: pr-triage-1 (Ludovic's AI agent, Claude), not Ludovic himself. Proof of this head merged into today's main (101d3b0be, which now has #3300, #2698 and #3802): - the 108 vanilla harnesses this branch can reach pass, `dispenser` and `dedicated_server` included; - pr_ready passes; - lan_e2e passes: worldspawn, forced-levels, distance, spawned, levels, light, mobs. Queued for merge.
2026-10-30 05:33:12 +01:00
Ludovic Marie 5851a80ce0 Merge pull request #3735 from xFedeT/fix/zombified-piglin-turtle-egg-goal
🤖 Agent: Ludovic's AI agent (Claude), triage. #3712 landed with a mob_ai case of its own, so I merged main into your branch again (no rebase). The only conflict was the case list in `tools/test_mob_ai_parity.py`, and both sides are kept. `parity.py mob_ai` passes on main with this and #3721/#3735 merged. Requeued.
2026-10-30 05:24:41 +01:00
Ludovic Marie bf6c0b8a24 Merge pull request #4078 from horizon-rust/perf/shared-face-visibility
🤖 Agent: the parity rerun is green (dedicated_server's ender pearl step passed; the 03:10 failure was load). Requeued, same head.
2026-10-30 05:19:39 +01:00
Ludovic Marie 05f5c2197b Merge pull request #4092 from horizon-rust/perf/shared-feature-block-tags
🤖 Agent: verified by server-tick-1. Worldgen binds the feature block tags once (GeneratorData::feature_block_bindings) and shares them through Arcs; the maps' readers are unchanged, so decoration gives the same result. −11.6 MB allocated on the dedicated server. pr_ready green; parity 152/153, the failing dispenser case comes from main (golem PRs #4022/#3959), not from this change.
2026-10-30 05:17:46 +01:00
Ludovic MarieandClaude Opus 5.5 3ffa45a5cf horizon-bots: flood-chunks climbs to cruising height first
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 05:17:03 +01:00
Ludovic Marie 05114a3673 Merge remote-tracking branch 'origin/main' into upd-3735
# Conflicts:
#	tools/test_mob_ai_parity.py
2026-10-30 05:14:03 +01:00
Ludovic Marie 9ee6b293d4 Merge remote-tracking branch 'origin/main' into upd-3721
# Conflicts:
#	tools/test_mob_ai_parity.py
2026-10-30 05:14:02 +01:00
Ludovic MarieandClaude Opus 5.5 d322e068e2 horizon-bots: flood-chunks, one legal move a tick that never waits for chunks
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 04:54:54 +01:00
Ludovic Marie df583d34af Merge pull request #4056 from DUzzL/fix/issue-4037-armadillo-campfire
🤖 Agent: merging DUzzL's campfire contact damage for mobs (tested in game on both single-player paths: armadillos, cows and sheep burn on lit campfires, Infested spawns silverfish). Thanks!
2026-10-30 04:49:27 +01:00
Ludovic Marie 737657443d Merge pull request #3712 from xFedeT/fix/villager-grows-up-brain
🤖 Agent: Ludovic's AI agent (Claude), triage. #3729 landed with mob_ai cases of its own. I merged main into your branch (no rebase). The only conflict was where the new cases go in `tools/test_mob_ai_parity.py`, and both sets are kept. The Rust merged clean, so the proof above still holds. Requeued at the merged head.
2026-10-30 04:44:09 +01:00
Ludovic Marie 101d3b0bec Merge pull request #3773 from xFedeT/fix/cat-goals
🤖 Agent: Ludovic's AI agent (Claude), proof. This PR was in a union of five mob PRs (#3712, #3721, #3729, #3735, #3773) merged into current main. `parity.py --changed origin/main`: 55 of 55 affected harnesses passed, `mob_ai` included. `horizon-client` lib tests: 726 passed. clippy `-D warnings` is clean. The only conflicts were the new cases in `tools/test_mob_ai_parity.py`, as resolved on your branch. Queued.
2026-10-30 04:32:06 +01:00
Ludovic Marie 0359138d94 Merge pull request #2698 from horizon-rust/cg20/fusseltv-worldgen-mobs
🤖 Agent: pr-triage-1 (Ludovic's AI agent, Claude), not Ludovic himself. Since sirrayi's review, I merged main into this branch twice; the last merge is 655b331b1. In the merges: - `VanillaTerrain` keeps main's world presets (2607d06f9). This branch only adds `region_random` and `spawn_generated`, and its tests call `for_dimension` without the preset argument. - Main's goat and frog code (`goat_init_memories`, the goat ram cooldown, the frog's long jump) used main's memory names, while this branch saves vanilla's ids. Both now use vanilla's: `long_jump_cooling_down` and `ram_cooldown_ticks`. - main's mooshroom kind test sits beside #2544's baby villager test. Tested on today's main: - With #3802, #4095 and #3300 together: 123 of the 125 harnesses this branch can reach pass, along with pr_ready and lan_e2e (mobs, spawned, worldspawn, drops). The 2 failures don't come from this branch: `dispenser` passes on main plus this branch alone, and `command_exec` failed because the vanilla server was under load. - On this head alone: `dispenser`, `finalize_spawn`, `natural_spawn` and `mob_ai` pass, and the 48 harnesses that cover goat.rs, frog.rs and finalize.rs pass 48/48. Queued for merge. When it merges, GitHub marks @FusselTV's #2544 and #2586 as merged.
2026-10-30 04:32:02 +01:00
Ludovic Marie b5ec91342e Merge remote-tracking branch 'origin/main' into upd-3735
# Conflicts:
#	tools/test_mob_ai_parity.py
2026-10-30 04:32:00 +01:00
Ludovic Marie f1eedfcb40 Merge remote-tracking branch 'origin/main' into upd-3721
# Conflicts:
#	tools/test_mob_ai_parity.py
2026-10-30 04:31:59 +01:00
Ludovic Marie 6883b74274 Merge remote-tracking branch 'origin/main' into upd-3712
# Conflicts:
#	tools/test_mob_ai_parity.py
2026-10-30 04:31:58 +01:00
Ludovic MarieandClaude Opus 5.5 d9e5234fa3 horizon-bots: abusive client behaviours (flood-move, flood-acks, flood-packets) to test a server's limits
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 04:27:58 +01: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
DUzzL effec4887d Time out stalled server resource pack downloads
The download queue's HTTP agent had no connect or read timeout, so a server
that accepted the connection and went silent blocked the single queue thread
and the pack screen forever. Fail the request after 30s instead.

Fixes #4168
2026-10-30 04:20:36 +01:00
Ludovic MarieandClaude Opus 5.5 0ec337f17e Merge main into this branch (world data packs' advancements: a tree is shared only without pack advancements)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 04:17:22 +01:00
Ludovic Marie a93ffc2f36 Merge pull request #3847 from horizon-rust/perf/spawner-idle
🤖 Agent: verified by server-tick-1: a spawner asks for an alive player in range before the spawn level is built, as BaseSpawner.serverTick returns first thing without one (no draw, no change); natural_level 1.6 % → 0.5 % of the tick thread at 100 bots; verify green, clippy -p horizon-client clean (main's log_display and axolotl test failures are main's); merging once CI parity passes.
2026-10-30 04:16:53 +01:00
Ludovic Marie 6e0605ad33 Merge pull request #4161 from horizon-rust/fix/lan-e2e-gateway-exact
🤖 Agent: queuing the lan_e2e levels test fix (#4145): the End gateway's ExactTeleport key and closing the guest's credits; levels passes on main's binary, pr_ready green.
2026-10-30 03:54:35 +01:00
Ludovic Marie 3bd99f79ae Merge pull request #3300 from masa-masa-0314/fix/datapack-recipe-loot-tags
🤖 Agent: pr-triage-1 (Ludovic's AI agent, Claude), not Ludovic himself. @masa-masa-0314, this PR is on the maintainer's priority list, so instead of asking you again I merged main into your branch myself. Your commits are not rewritten; it took three main merges today, the last one at ada669266. In the conflicts: - main's new code (cushions, crafted-recipe triggers, creative slots, item use) now reads the world's tables through your `self.items()`, `world_items!`, `world_stations!` and `world_recipes!`; - main's new tests pass `PackOverlay::NONE`; - `game/mod_render.rs` stays deleted, as main removed the wasm mod UI. Tested together with #3802, #4095 and #2698 on today's main: - 123 of the 125 vanilla harnesses this branch can reach pass. `dispenser` fails because of another PR in that set; it passes on main plus this PR alone. `command_exec`'s failure was the vanilla server under load; on this head it matches 902/902. - pr_ready passes. - lan_e2e passes (crafting, recipes, drops, mobs, spawned, cushions). Queued for merge. Thank you for the data-pack work!
2026-10-30 03:54:30 +01:00
Ludovic MarieandClaude Opus 5.5 655b331b1e Merge main into this branch (goat memory init, mooshroom kinds test)
Main's goat_init_memories, goat ram and frog jump code keep their brain
memories under main's names; this branch saves them under vanilla's ids.
Both now use vanilla's: long_jump_cooling_down and ram_cooldown_ticks.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 03:53:15 +01:00
levzzz a46ce6898b Merge pull request #3729 from xFedeT/fix/constructor-path-maluses
Mobs: the path maluses vanilla's constructors set (fire, lava, powder snow, cocoa, trapdoors)
2026-10-30 07:50:58 +05:00
Ludovic Marie 8a7cd0deaf Merge pull request #4089 from horizon-rust/fix/command-exec-oracle-settle
🤖 Agent: fixes main's red command_exec (oracle timeout under load); passed fresh locally on current main.
2026-10-30 03:49:50 +01:00
Ludovic Marie 6269b7a21e Merge pull request #4136 from horizon-rust/fix/portal-overlay-cooldown
🤖 Agent: queuing the portal trip fixes (Discord: Nether portal overlay stuck after a dimension change; End portal leaving the player in the air): the overlay fades in the arrival portal, and the player stays on the loading screen until the exit is found. Tested in game on both paths; lan_e2e portals passes. pr_ready is green except dispenser, main's regression from #3959 fixed by #4120.
2026-10-30 03:46:07 +01:00
Ludovic Marie c5410785c0 Merge pull request #3748 from bunchy7s/fix/command-block-screen
🤖 Agent: Ludovic's AI agent (Claude), proof. This PR was in a union of 16 needs-proof PRs merged into main at 1dde830ba. Parity: 195 of 196 affected harnesses passed, none failed. clippy and rustfmt passed. `lan_e2e --check commands` passes on the server thread and `--in-frame`. The command block's edit screen refreshing from the server's data is covered there. Queued.
2026-10-30 03:42:24 +01:00
Ludovic MarieandClaude Opus 5.5 ada669266f Merge main into this branch (creative display slots)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 03:30:02 +01:00
JulesB40andClaude Opus 5.5 85ff723581 Allocator: say which heaps mi_collect frees
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 03:21:13 +01:00
JulesB40andClaude Opus 5.5 5b695d5a26 Allocator: give free pages back at once
With mimalloc's default 1 s purge delay the server held 26% and the client
37% more memory than with the system heap. Purging at once keeps the server
within 3% and the client within 9%, and generation keeps most of its speed-up
(+18% chunks a second on 24 workers, +3% on one).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 03:21:13 +01:00
JulesB40andClaude Opus 5.5 800e7cef33 Game: mimalloc as the global allocator
World generation, meshing and the tick allocate and free small blocks on
every worker at once, and the system heap serialised them: on Windows
RtlAllocateHeap/RtlFreeHeap were 16% of chunkgen_bench's CPU. mimalloc keeps
a heap per thread. release_free_memory also gives mimalloc's free pages back.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 03:21:13 +01:00
Ludovic Marie b9c54ecd50 Merge pull request #4027 from horizon-rust/perf/dedicated-no-sprite-pixels
🤖 Agent: verified by server-tick-1. Stacked on #4009: a dedicated server keeps the block models and sprite layers but no texture pixels or animation frames, which only the renderer reads; −28 MB idle RSS on Linux. dedicated_server passes locally; merge after #4009.
2026-10-30 03:12:58 +01:00
Ludovic MarieandClaude Opus 5.5 24bea8540d Make lan_e2e levels' guest_end step match vanilla's End exit
The End gateway's exact flag is `ExactTeleport` in TheEndGatewayBlockEntity's
NBT; `exact_teleport` was ignored, so the gateway searched for a landing and
left the guest away from the spawn platform. The guest's first exit from the
End then rolls the credits (ServerPlayer.showEndCredits) and only closing
them takes it home, as the portals step already does for the host.

Fixes #4145

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 03:03:16 +01:00
DUzzL 73632dae7e lan_e2e: use vanilla ExactTeleport key for the End gateway in guest_end
Fixes #4145
2026-10-30 02:59:29 +01: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
DUzzL 8ec6e3d9f4 Update port ledger note for slot_at hover margin 2026-10-30 02:53:03 +01:00
DUzzL a97afdc93f Hover the nearest slot when the pointer is in the gap between slots
Vanilla's AbstractContainerScreen.isHovering grows each 16x16 slot by one
pixel on every side; slot_at used the bare 16x16 box, so the 2px gaps
between slots selected nothing. Fixes #4134.
2026-10-30 02:46:03 +01:00
Ludovic MarieandClaude Opus 5.5 f03c1771af Drop the old level's pending teleport when a portal trip starts
Its late acceptance set the player's position back to the old level's
coordinates, so a trip that started right after a teleport waited forever for
chunks around the exit.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 02:45:25 +01:00
levzzz 25f9cd3a99 Merge pull request #3875 from xFedeT/fix/boat-paddle-input-latch
Boats: steer with the input latched on the previous tick
2026-10-30 06:44:08 +05:00
Ludovic MarieandClaude Opus 5.5 f257391453 Chunk workers: give freed memory back once every worker is idle
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 02:42:50 +01:00
Ludovic MarieandClaude Opus 5.5 e497995b44 Chunk workers: free the generator's scratch buffers after 5 s with nothing to do
Each streaming worker keeps its generator's density scratch buffers and
lookup caches between jobs, megabytes per worker, for as long as the world
is open: about 51 MB on the Mac mini's dedicated server with nobody on. A
worker that has waited 5 s with an empty queue now frees them (and the
mesher's face cache); the next job makes them again. Results do not change.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 02:42:50 +01: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
Ludovic MarieandClaude Opus 5.5 2c91574bbb Ignore a portal traveller's old moves until it lands
Without the teleport change_level used to send at once, moves the client had
sent from the old level were taken as positions in the new one, and the level
loaded its chunks there instead of around the exit.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 02:37:29 +01:00
DUzzL 557c4c5cf9 Start remote end crystal spin from a random phase
A crystal spawned from an add-entity packet kept time 0, so every crystal
placed in multiplayer or LAN looked identically rotated. Vanilla's
EndCrystal constructor draws time from nextInt(100000).
2026-10-30 02:36:05 +01:00
levzzzandClaude Opus 5.5 f0ac52bf7f Merge main into fix/boat-paddle-input-latch (resolve tools/lan_e2e.py)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 06:35:03 +05:00
levzzz 1f3ccb0fc3 Merge pull request #3747 from xFedeT/fix/break-effects-for-popped-blocks
Break effects: a block that pops off when its support is mined plays its sound and particles for the miner
2026-10-30 06:30:23 +05:00
Ludovic Marie fcbb0d627a Merge pull request #4120 from horizon-rust/fix/golem-base-any-head
🤖 Agent: fixes main's red dispenser (golem base check from #3959); pr_ready green, 33 harnesses pass.
2026-10-30 02:28:55 +01:00
levzzz 30666b33b6 Merge pull request #3655 from jaszkus/feat/debug-stick
Add the Debug Stick
2026-10-30 06:24:29 +05:00
levzzz ada67586d0 Merge pull request #3642 from iLostName/off-hand-waits-for-main-hand
off hand: filling a composter or using bone meal ends the click before the off-hand torch
2026-10-30 06:24:12 +05:00
levzzzandClaude Opus 5.5 6ca9531468 Merge main into fix/boat-paddle-input-latch (resolve tools/lan_e2e.py)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 06:22:44 +05:00
levzzzandClaude Opus 5.5 972719f088 Merge main into fix/break-effects-for-popped-blocks (resolve tools/lan_e2e.py)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 06:22:23 +05:00
DUzzL 29363ee3e9 Apply the held item's main-hand modifiers to the player's attributes
/attribute reported the base attack damage, attack speed and reach with a
weapon in hand because apply_equipment skipped the main-hand slot. Apply
them and refresh when the held item changes. Fixes #4118.
2026-10-30 02:19:33 +01:00
levzzz b548c70594 Merge pull request #4018 from xFedeT/fix/wind-burst-smash
Mace smash on another player: Wind Burst launches the attacker, the fall stops
2026-10-30 06:18:40 +05:00
Ludovic Marie 4fef8a8682 Merge pull request #3921 from horizon-rust/feat/parity-headline
🤖 Agent: merging the parity headline (#3859 step 5): verify green, gap tests pass, the site built against it shows the paragraph without markers.
2026-10-30 02:18:15 +01:00
Benflex 7cb03beeed Play the merchant's yes sound on a shift-clicked trade
MerchantMenu.playTradeSound plays the trader's notify-trade sound
(villager or wandering trader), but the game dropped the queued
TradeSound event. Play it at the trade's origin with volume and pitch 1.

Part of #3515.
2026-10-30 02:17:10 +01:00
levzzzandClaude Sonnet 5.5 a80678b890 Merge main into fix/split-mob-sounds-behaviour (resolve game/creatures.rs, game/potions.rs, mobs/leash.rs)
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 06:16:25 +05:00
levzzz be8a82a40b Merge pull request #3955 from kacpherek/fix/split-animals
Animals behave like vanilla: turtles, goats, foxes, bees, axolotls, camels and horses
2026-10-30 06:13:52 +05:00
levzzz e54e8003e5 Merge pull request #3756 from horizon-rust/fix/server-brand-horizon
Server brand: Horizon's server names itself "horizon"
2026-10-30 06:13:35 +05:00
levzzzandClaude Sonnet 5.5 bc31cef0a7 Merge main into feat/debug-stick (no conflicts)
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 06:08:52 +05:00
levzzzandClaude Sonnet 5.5 2b2a8bedd4 Merge main into off-hand-waits-for-main-hand (no conflicts)
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 06:08:11 +05:00
levzzzandClaude Sonnet 5.5 6db70a1ae4 Merge main into fix/boat-paddle-input-latch (resolve tools/lan_e2e.py)
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 06:07:50 +05:00
levzzzandClaude Sonnet 5.5 0d5cd6a591 Merge main into fix/break-effects-for-popped-blocks (resolve tools/lan_e2e.py)
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 06:07:30 +05:00
levzzzandClaude Sonnet 5.5 6d67bf43bb Merge main into fix/wind-burst-smash (resolve tools/lan_e2e.py)
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 06:07:11 +05:00
levzzz 6d59158973 Merge pull request #3643 from iLostName/crossbow-near-tnt-minecart
minecart: right-clicking a tnt minecart uses the held item, so a crossbow shoots at it
2026-10-30 06:06:30 +05:00
levzzz 647050d7aa Merge pull request #3953 from kacpherek/fix/split-screens
Menus, tooltips, world opening and chat now behave like vanilla
2026-10-30 06:05:42 +05:00
levzzz 1c333aa336 Merge pull request #3944 from kacpherek/fix/split-sulfur-glowsquid-water
Sulfur geysers and potent sulfur, glow squid dark ticks, and splash water bottles that douse fire
2026-10-30 06:05:24 +05:00
levzzz 41d772380a Merge pull request #3952 from kacpherek/fix/split-shelves-signs
Signs run the click commands in their text, as vanilla
2026-10-30 06:05:08 +05:00
levzzz df00ca8693 Merge pull request #3954 from kacpherek/fix/split-server
Fix server and LAN-host behaviour: spam kicks, straw beds, shulkers, ravagers, item frames, advancements and tab list
2026-10-30 06:04:51 +05:00
levzzz 589164c071 Merge pull request #3693 from Username59138/feat/creative-saved-hotbars
Creative inventory: the Saved Hotbars tab, as vanilla shows it
2026-10-30 06:04:34 +05:00
levzzz ed196ffd66 Merge pull request #4015 from d3grando/fix-3393-fallingblock
Falling sand and gravel ride bubble columns
2026-10-30 06:01:50 +05:00
levzzz 00480831ce Merge pull request #3995 from xFedeT/fix/fishing-hook-smooth
Fishing hook on a server's client moves between the tracker's updates, as FishingHook.tick does
2026-10-30 05:57:43 +05:00
levzzz 2fa595676e Merge pull request #3645 from iLostName/pillar-place-sound
block placement: a click on a block is handled before the move behind it, so pillar blocks are heard
2026-10-30 05:49:32 +05:00
FauxandClaude Opus 5.5 edefd8ef0c Far terrain: kelp is water, and powder snow and paths are kept
A chunk's summary skipped blocks that neither draw solid nor collide as a
full block. Kelp and seagrass reaching the surface left holes in the far
sea, and patches of powder snow left holes in mountains (paths, farmland
and stairs dips).

- Blocks holding water that aren't kept (kelp, seagrass, bubble columns,
  waterlogged ones) are drawn as water, so the water is one span.
- Powder snow, dirt paths, farmland, honey blocks and stairs are kept.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012gFJzABp1uePB4gm32PuaC
2026-10-29 17:48:18 -07:00
Sylviromi 7aaa68865f Merge branch 'native-mod-neighbours' into native-mod-screens 2026-10-30 03:38:55 +03:00
SylviromiandClaude Opus 5.5 723a2a44cb Native mods: set a block's property from a tick (a machine's lit)
native_set_block_property (a helper, API 3): as a furnace's tick sets
its lit, so a machine shows when it works.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 03:38:55 +03:00
levzzz d3338640bc Merge pull request #4002 from DUzzL/fix/issue-3997-guest-arrow-pickup
Arrows can be picked up by LAN guests and the single-player window (#3997)
2026-10-30 05:38:17 +05:00
Aria Figueredo ea10a17842 Expose native world lifecycle hooks 2026-10-29 19:36:15 -05:00
Ludovic Marie fcb14be442 Merge pull request #4100 from horizon-rust/fix/no-official-server-in-harness-runs
🤖 Agent: Ludovic's AI agent (Claude), pr-triage-3. Harness and horizon-ctl runs no longer add the official server to servers.dat in the working directory (the repo root) or send the players ping; before/after checked with a fixture run in a scratch folder. Queued.
2026-10-30 01:32:33 +01:00
levzzz f1c0ab982a Merge pull request #4040 from phexuss/fix/remote-look-at
Turn the player when a server sends PlayerLookAt
2026-10-30 05:32:12 +05:00
DUzzL 91bd434281 Apply block_break_speed attribute to mining speed
Read the player's minecraft:block_break_speed attribute and multiply the destroy speed by it in both the server mining tick and the client's destroy-progress prediction, matching Player.getDestroySpeed.

Fixes #4094.
2026-10-30 01:32:11 +01:00
levzzz 173d00dadb Merge pull request #3668 from martipops/fix/damage-other-players
/damage hurts players other than the one running it
2026-10-30 05:31:56 +05:00
levzzz ea7e58787d Merge pull request #3675 from d3grando/fix-3236
Mooshrooms keep their colour: save Type, sync DATA_TYPE at 18
2026-10-30 05:31:39 +05:00
levzzzandClaude Sonnet 5.5 7ce7388582 Merge main into fix/split-animals (resolve crates/horizon-client/src/mobs/living.rs)
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 05:30:56 +05:00
DUzzL d2e26c32d4 Players with a missing skin sprite draw the default skin, not nothing
A remote player or mannequin whose skin location had no atlas sprite
(a stored slot without its upload, an evicted skin) drew no body. The
model now falls back to the default skin texture, and a skin counts as
stored only when its sprite is in the atlas, so it is stored again.

Refs #4096
2026-10-30 01:30:27 +01:00
levzzzandClaude Sonnet 5.5 6fea98f668 Merge main into fix/split-spear-ai-golem (resolve crates/horizon-client/src/mobs/goals.rs)
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 05:29:40 +05:00
levzzzandClaude Sonnet 5.5 3ee203767a Merge main into fix/own-head-pitch-follows-mouse (resolve crates/horizon-client/src/game/perspective.rs)
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 05:28:23 +05:00
levzzz a013e649c9 Merge pull request #4014 from DUzzL/fix/issue-4003-guest-trial-omen
Trial/Bad Omen of server players make trial spawners ominous
2026-10-30 05:28:08 +05:00
levzzzandClaude Sonnet 5.5 b78e407c12 Merge main into fix/split-agelock-goat-allay (resolve crates/horizon-client/src/mobs/ai.rs)
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 05:26:51 +05:00
levzzzandClaude Sonnet 5.5 9858aec68b Merge main into fix/split-piglin-hoglin (resolve crates/horizon-client/src/game/creatures.rs, mobs.rs)
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 05:25:06 +05:00
SylviromiandClaude Opus 5.5 7df9ed0545 Native mods: screens for mod blocks, drawn with the mod's GUI textures
A content mod's textures/gui files are GUI sprites under its namespace
(mymod:gui/...). On the client, native_screen lays out a mod block's menu
screen (height, slot positions, the inventory's row) and
native_screen_draw draws it, over a plain panel with the slots' frames,
from the menu's data slots, which the client now keeps for it; the
helpers native_sprite and native_sprite_size draw any part of a sprite.
The server's menu is unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 03:25:06 +03:00
levzzzandClaude Sonnet 5.5 7852c700f1 Merge main into fix-3199 (resolve crates/horizon-client/src/mobs/ai.rs, camel.rs)
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 05:23:23 +05:00
Ludovic MarieandClaude Opus 5.5 14fad0786e Game: harness and horizon-ctl runs don't add the official server or ping
The official server's servers.dat entry and its marker go into the game
folder, which is the working directory, and the "N playing now" ping
starts at the same place. The guard skipped servers, benchmarks,
offscreen and frame-limited runs, but not a harness's fixture
(HORIZON_DISPENSE_FIXTURE, HORIZON_SCULK_FIXTURE, HORIZON_COMMAND_SCRIPT,
HORIZON_NET_REPORT) or a run horizon-ctl drives. Harnesses start those
in the source checkout, so a parity run left servers.dat and
horizon_official_server.txt in the repo root and counted itself as a
player. The guard now reuses synchronous_startup, which already names
every one of these runs.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 01:23:18 +01:00
levzzz 0dfa513424 Merge pull request #4043 from DUzzL/fix/issue-4031-fall-damage-sound
Fall damage plays its sound in multiplayer and for LAN guests
2026-10-30 05:22:21 +05:00
Federico TaglianiandClaude Sonnet 5.5 735cdab711 Merge origin/main into fix/skeleton-path-stops-in-shade
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 01:21:32 +01:00
DUzzL ce91a02d84 Use the player's attack_speed base for the attack cooldown (#4091) 2026-10-30 01:20:23 +01:00
levzzzandClaude Sonnet 5.5 51c10dd31d Merge main into fix/villager-grows-up-brain (resolve tools/test_mob_ai_parity.py)
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 05:20:20 +05:00
levzzzandClaude Sonnet 5.5 0d9399ead9 Merge main into fix/constructor-path-maluses (resolve tools/test_mob_ai_parity.py)
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 05:20:03 +05:00
Ludovic MarieandClaude Opus 5.5 923f425f19 Advancements: load the tree once for the server's packets and the game's criteria
ServerAdvancements (the advancement packets a server sends) and the game
that runs the criteria each read and parsed every advancement file into a
tree of their own: 7 MB each, the same twice. Tree::shared keeps one per
data folder while anyone holds it, and both use it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 01:20:01 +01:00
levzzzandClaude Sonnet 5.5 2be6cda974 Merge main into fix/zombified-piglin-turtle-egg-goal (resolve tools/test_mob_ai_parity.py)
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 05:19:36 +05:00
levzzzandClaude Sonnet 5.5 9ad9d087ef Merge main into fix/boat-paddle-input-latch (resolve tools/lan_e2e.py)
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 05:18:24 +05:00
levzzz 79593d80b6 Merge pull request #3659 from d3grando/fix-3091
Let parrots fly: flying navigation and move control, flapping and gliding
2026-10-30 05:18:18 +05:00
levzzzandClaude Sonnet 5.5 58853fdf50 Merge main into fix/wind-burst-smash (resolve tools/lan_e2e.py)
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 05:18:06 +05:00
levzzzandClaude Sonnet 5.5 834819ddf4 Merge main into fix/fishing-hook-smooth (resolve tools/lan_e2e.py)
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 05:17:45 +05:00
levzzzandClaude Sonnet 5.5 7835010339 Merge main into fix/break-effects-for-popped-blocks (resolve tools/lan_e2e.py)
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 05:17:22 +05:00
levzzz 590542c5ee Merge pull request #4004 from d3grando/fix-3006
Both mouse buttons at once replace the block in creative
2026-10-30 05:16:53 +05:00
DUzzL 2c803ae875 fix(client): send lingering potion cloud colour to clients
The area_effect_cloud entity states are built while the LAN hub is out of
`self.lan` (tick_lan takes it), so encoding the potion cloud's particle
through `network_particle` always returned None and DATA_PARTICLE was
never sent. Clients then fell back to AreaEffectCloud.DEFAULT_PARTICLE, the
white entity effect, for every potion. Encode it through the hub passed to
lan_projectile_states instead, as the dragon breath particle already is.

Closes #4090
2026-10-30 01:15:30 +01:00
Federico TaglianiandClaude Sonnet 5.5 8f7c959679 Navigation: trimPath runs in moveTo, before the node count check
PathNavigation.moveTo trims the path first and refuses it when the trim
leaves no node. The trim was deferred to the next nav_tick, so move_to
answered true for a tick and goals started on a path that was already empty.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 01:14:41 +01:00
Ludovic Marie 4cb754474d Merge pull request #3732 from xFedeT/fix/wither-skeleton-parity
🤖 Agent: Ludovic's AI agent (Claude), triage. Wither skeleton: its piglin target goal comes first among the priority 3 goals (vanilla's registerGoals order). levzzz5154's main merge touched only the mob_ai case list; the Rust change is the same as on the CI-parity-green head, and verify is green. Requeued.
2026-10-30 01:13:47 +01:00
Ludovic Marie 0855cc910f Merge pull request #3850 from Fauxvale/fix/far-open-water-light
🤖 Agent: Ludovic's AI agent (Claude), triage. Far terrain: no dark band in the sea where chunks were loaded: checked against vanilla, and proven in union pt3-w3 (29 PRs, Mac mini): pr_ready full, parity 109/110 affected passed (lan_integration runs nightly). Queued.
2026-10-30 01:13:42 +01:00
Ludovic Marie 793337b0ed Merge pull request #3892 from matyden/telemetry/scrub-user-named-user
🤖 Agent: Ludovic's AI agent (Claude), triage. Telemetry: a user named "user" keeps the <user> label whole: checked against vanilla, and proven in union pt3-w3 (29 PRs, Mac mini): pr_ready full, parity 109/110 affected passed (lan_integration runs nightly). Queued.
2026-10-30 01:13:38 +01:00
Ludovic Marie a603c95eba Merge pull request #3802 from ThatCakeIsAPie/fix/datapack-block-state-providers
🤖 Agent: Ludovic's AI agent (Claude), triage. Data packs: a pack's block state providers join the state_providers bundle: checked against vanilla, and proven in union pt3-w3 (29 PRs, Mac mini): pr_ready full, parity 109/110 affected passed (lan_integration runs nightly). Queued.
2026-10-30 01:13:34 +01:00
Ludovic Marie 564b8a4b9e Merge pull request #3599 from doodlyyousuf/fix/pearl-hits-wind-charge
🤖 Agent: Ludovic's AI agent (Claude), triage. Let thrown pearls hit wind charges: checked against vanilla, and proven in union pt3-w3 (29 PRs, Mac mini): pr_ready full, parity 109/110 affected passed (lan_integration runs nightly). Queued.
2026-10-30 01:13:30 +01:00
Ludovic MarieandClaude Opus 5.5 a16097dd96 Merge main into this branch (cushion placement checks, lan_bot sync test)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 01:11:09 +01:00
DUzzL baa05a30ec Send the stored Main Hand option in ClientInformation
The Skin Customization Main Hand setting was saved to options.txt and
applied to the local hand, but the ClientInformation packet sent at join
always hardcoded main_hand = 1 (right). The server therefore never
learned the choice, so mirrored and server-side arm visuals stayed on the
right.

Session setup now receives the ClientInformation built from the settings
(Settings::client_information), the same value already used for the
resend-on-change path. Adds round-trip and mapping tests (LEFT = 0,
RIGHT = 1).
2026-10-30 01:10:05 +01:00
arismoko 94e167b796 Keep stopped server list test offline 2026-10-29 19:09:58 -05:00
Ludovic MarieandClaude Opus 5.5 4c802c1342 Merge main into this branch
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 01:08:59 +01:00
Ludovic Marie 304a3ca76e Merge pull request #3371 from horizon-rust/draft/remove-wasm-mods-docs
🤖 Agent: WebAssembly removal 4/4 (docs), on #3353, merged up to date with main (roadmap: main's, with the native-mod wording reapplied; docs-only on top of the union proven in proven.txt). Merge after #3353; horizon-site#6 follows.
2026-10-30 01:08:50 +01:00
Federico TaglianiandClaude Sonnet 5.5 4ef3c1f54a Merge origin/main into fix/skeleton-path-stops-in-shade
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 01:08:27 +01:00
levzzz d163f38b6a Merge pull request #4005 from d3grando/fix-3117
Cushions swing the arm when placed and let a seated player change seats
2026-10-30 05:08:20 +05:00
levzzz e2fa940581 Merge pull request #3607 from degepie/fix/issue-3024-worn-armor-glint
Armour: worn pieces and the elytra show the enchantment glint (#3024)
2026-10-30 05:06:49 +05:00
Ludovic MarieandClaude Opus 5.5 a32ab2c5e4 Merge main into this branch (world presets, worldgen data packs, noise clamp)
Main now builds a world's preset itself (2607d06f9): VanillaTerrain keeps
main's for_world/for_world_with signatures, this branch adds region_random
and spawn_generated beside them, and its tests call for_dimension without
the preset argument.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 01:06:35 +01:00
Ludovic Marie 03b464ec0e Merge pull request #3902 from DUzzL/fix/issue-3882-defer-dataversion-stamp
🤖 Agent: Ludovic's AI agent (Claude), triage. Stamp DataVersion only after the world data is upgraded: checked against vanilla, and proven in union pt3-w3 (29 PRs, Mac mini): pr_ready full, parity 109/110 affected passed (lan_integration runs nightly). Queued.
2026-10-30 01:06:35 +01:00
Ludovic Marie 186095dd37 Merge pull request #3908 from DUzzL/fix/issue-3883-camel-pose-tick
🤖 Agent: Ludovic's AI agent (Claude), triage. Fix camel freeze after spawn and LastPoseTick saved as a sign: checked against vanilla, and proven in union pt3-w3 (29 PRs, Mac mini): pr_ready full, parity 109/110 affected passed (lan_integration runs nightly). Queued.
2026-10-30 01:06:30 +01:00
levzzz a4f0482bd9 Merge pull request #3792 from GIGABAIT93/fix/npc-skins
Player entities keep the skin they found: NPCs show theirs
2026-10-30 05:06:24 +05:00
Ludovic MarieandClaude Opus 5.5 2ba6caa289 Golems: a pumpkin's base check takes any block at the head
canSpawnGolem (a dispenser's carved pumpkin) and the setBlock that places a
head ask before the head is there, with vanilla's base patterns (" ", "#",
"#") and ("~ ~", "###", "~#~"), whose head cell takes any block. #3959's
find_pattern required the head there too, so a dispenser no longer built a
snow or iron golem: dispenser parity failed on main (carved_pumpkin, 1 of
6000 cases).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 01:06:05 +01:00
Federico Tagliani 5f5d0434cb Merge remote-tracking branch 'origin/main' into fix/cat-goals 2026-10-30 01:05:14 +01:00
Federico TaglianiandClaude Sonnet 5.5 11159a1ed5 Merge fork/fix/cat-goals (maintainer's merge of main)
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 01:05:13 +01:00
levzzz bf8d58facd Merge pull request #4045 from d3grando/fix-3684-hitbox
A small player's server box follows minecraft:scale
2026-10-30 05:04:26 +05:00
Ludovic MarieandClaude Opus 5.5 a8e3cebc5f Merge main into this branch (wasm mod UI removed, item use slows sprint, crafted-recipe triggers, world presets)
Main's new code reads the world's tables (self.items(), world_stations!,
world_recipes!); main's new tests pass PackOverlay::NONE.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 01:03:05 +01:00
Ludovic MarieandClaude Opus 5.5 30fcf2c30c command_exec oracle: wait for entities by the clock, not 400 ticks
Vanilla loads entity sections on other threads, so on a loaded runner
400 ticks (~2 s) ran out before the chunks around the player were
entity-ticking: the oracle threw "entities around the player did not
load" and main's parity has failed on command_exec since 2026-10-29 22:05.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 01:01:05 +01:00
levzzz 9d72c1ddcd Merge pull request #3716 from xFedeT/fix/merchant-closes-out-of-range
Villagers: the trade screen closes when the trader is gone or out of reach
2026-10-30 05:00:59 +05:00
levzzz 62caef810a Merge pull request #3934 from DUzzL/fix/issue-3912-turtle-ambient
Turtle: silent in water, in the air and as a baby
2026-10-30 05:00:41 +05:00
Federico TaglianiandClaude Sonnet 5.5 1c16f98098 Merge origin/main into fix/cat-goals: the sprint now comes from main
main's feline_server_ai_step sets the cat's sprint and Mob::movement_speed
adds the +30% for it, so the cat arm of custom_server_ai_step, the sprint dust
draws and the speed_bonus arm (a second +30%) are dropped; the avoidance goal
order and the CatAvoidEntityGoal probe name stay.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 01:00:39 +01:00
levzzz 69d2086e7d Merge pull request #3623 from degepie/fix/issue-3026-fish-schools
Fish form schools: port FollowFlockLeaderGoal (#3026)
2026-10-30 05:00:16 +05:00
levzzz 0829657bcc Merge pull request #3868 from spidercraft/feat/lan-online-mode
LAN and friends worlds authenticate guests when the host is signed in: their skins and capes show
2026-10-30 04:59:38 +05:00
Ludovic MarieandClaude Opus 5.5 88e79c1119 Keep a portal traveller on the loading screen until its exit is found
Vanilla finds a portal's exit (the End platform, the Nether portal, the spawn
column after the End) before ServerPlayer.teleport sends the Respawn packet.
Horizon finds it once the destination has loaded, and meanwhile showed the
client at the approximate point: in the air above the End's void or at the
raw world spawn. The client now gets its position, LEVEL_CHUNKS_LOAD_START
and the portal sound only when the trip lands, so it stays on its level
loading screen as in vanilla.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 00:58:15 +01:00
Ludovic Marie 1dde830baf Merge pull request #3353 from horizon-rust/draft/remove-wasm-mods-launcher
🤖 Agent: WebAssembly removal 3/4 (launcher: a WebAssembly mod needs a native port), on #3351; proven with #3351 and #3371 on union/wasm-removal (proven.txt). Launcher clippy and its 275 tests pass. Merge after #3351.
2026-10-30 00:56:58 +01:00
Ludovic Marie 588ca0cd22 Merge pull request #3152 from senjinthedragon/hdr-screenshot
🤖 Agent: pr-triage-1 (Ludovic's AI agent, Claude), not Ludovic himself. I merged main into your branch (a fast-forward merge commit, 0e4f8e279; none of your commits rewritten). Since this PR was opened, main changed the HDR parameters (black level, UI brightness, tiers: 20 floats) and screenshots (`ScreenshotRequest`, world icons). In the merge: - `Hdr::params` fills main's layout for both the swapchain and your HDR10 capture; - only full-frame screenshots get the `-hdr.png` copy, not world icons; - the docs sentence moved into main's HDR paragraph. Tested on this head merged into main: - The harnesses it can reach pass (59/60). The one failure, `command_exec`'s `/data get` `custom_name`, comes from main. - fmt, clippy and the render tests pass. - On macOS (HDR10 output), a screenshot wrote both PNGs: the HDR one is 16-bit RGB with cICP 9/16/0/1 and cLLI 1000/203. Queued for merge.
2026-10-30 00:56:54 +01:00
Ludovic Marie 4488cda84f Merge pull request #3808 from doodlyyousuf/fix/release-notes-canonical-repo
🤖 Agent: Ludovic's AI agent (Claude), triage. Tooling only: release notes always link horizon-rust/horizon, even when a fork's CI builds them. verify is green. Queued.
2026-10-30 00:56:51 +01:00
Ludovic Marie 2a8c60b6df Merge pull request #3351 from horizon-rust/draft/remove-wasm-mods-crates
🤖 Agent: WebAssembly removal 2/4 (crates: runtime, SDK, official WebAssembly mods), on #3350, merged up to date with main c7065027b. Workspace compiles (--all-targets --locked). Merge after #3350.
2026-10-30 00:56:47 +01:00
levzzz 4693954aa1 Merge pull request #4016 from d3grando/fix-3393-eggs
Thrown eggs and snowballs turn with the camera, no flip looking straight up
2026-10-30 04:56:16 +05:00
Ludovic MarieandClaude Opus 5.5 2fd01bd8bd Merge main (0208c75ef) into the docs layer
The roadmap: main's (Now at most 8 items) with this layer's native-mod
wording reapplied; the WebAssembly runtime is removed, not being removed.
docs/README.md: main's data pack upgrade line kept.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 00:55:10 +01:00
levzzzandClaude Sonnet 5.5 ba5585da84 Merge main into fix/npc-skins (resolve crates/horizon-client/src/game/remote.rs)
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 04:53:10 +05:00
Ludovic Marie d409f691dc Merge remote-tracking branch 'origin/main' into upd-3642
# Conflicts:
#	crates/horizon-client/src/game/remote.rs
2026-10-30 00:52:43 +01: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
Ludovic Marie 0208c75ef4 Merge pull request #3979 from DUzzL/fix/issue-3975-owners-doc
🤖 Agent: Ludovic's AI agent (Claude), triage. Docs only: a server.properties → server.toml page for owners, linked from docs/README. verify is green. Queued.
2026-10-30 00:51:53 +01:00
levzzzandClaude Sonnet 5.5 cc683c75f0 Merge main into fix/issue-3024-worn-armor-glint (resolve crates/horizon-client/src/game/perspective.rs)
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 04:51:24 +05:00
SylviromiandClaude Opus 5.5 dc925bf620 Native mods: reach a neighbouring mod block entity from a tick
native_mod_block_entity_at (a helper, API 3): the mod block entity at a
position, to read or change from another's tick, as a generator charging
the machine beside it. As setChanged would, its chunk is saved next time
and comparators read it again; a tick that moved its own items tells
comparators too.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 02:50:39 +03:00
Ludovic Marie 57e49b068e Merge remote-tracking branch 'origin/main' into upd-3655
# Conflicts:
#	tests/test_remote_mining.py
2026-10-30 00:50:35 +01: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
levzzzandClaude Sonnet 5.5 490a33aea9 Merge main into fix/issue-3912-turtle-ambient (resolve crates/horizon-client/src/mobs/ai.rs)
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 04:48:40 +05:00
Ludovic MarieandClaude Opus 5.5 7cce0bf15b Keep the portal cooldown on a server's client so the overlay fades in the arrival portal
handleRespawn gives the new LocalPlayer the server's portalCooldown, and the
client's setAsInsidePortal only restarts it while the player stands in the
arrival portal. Horizon sent 0 and ignored it, so the purple overlay built up
again and stayed on screen after a dimension change.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 00:48:17 +01:00
Ludovic MarieandClaude Opus 5.5 a536b00c30 Worldgen: bind the feature block tags once, shared by decoration, structures and live features
Decoration, structure placement and live features each built their own
FeatureBlockBindings, binding every block and every block tag to its state
set: 5.5 MB each, the same three times. GeneratorData now binds them once
(feature_block_bindings) and the bindings share the maps through Arcs. Each
decorated region also cloned the whole tag map; it now shares it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 00:48:07 +01:00
Sylviromi b916a49fbf Merge branch 'native-mod-inventories' into native-mod-menus 2026-10-30 02:47:20 +03:00
Sylviromi 99791ea225 Merge branch 'native-mod-block-entities' into native-mod-inventories 2026-10-30 02:47:20 +03:00
SylviromiandClaude Opus 5.5 807901034b Native mods: a mod block entity's tick marks its chunk changed
Review follow-up: as a ticking block entity's setChanged does, so a save
after an autosave still writes what the tick changed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 02:47:12 +03:00
SylviromiandClaude Opus 5.5 3cb4177026 Native mods: furnace menus for mod containers, with their data slots
A mod container may open a furnace's menu (furnace, blast furnace,
smoker): its block entity's data slots, which the mod sets in its tick and
nothing saves, draw the flame and the arrow as a furnace's dataAccess
does. The tick hook now takes the block entity whole (id, nbt, items,
data), so fields added later don't change its signature.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 02:46:50 +03:00
Alexander KrylovandHermes Agent 24c323960e Launcher: keep removable beside its own doc comment
The new fn sat between selection_after_removal's doc comment and that
function, so the comment documented the wrong one.

Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
2026-10-30 02:46:21 +03:00
levzzzandClaude Sonnet 5.5 4289f22f00 Merge main into fix/merchant-closes-out-of-range (resolve tools/lan_e2e.py)
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 04:46:19 +05:00
Federico TaglianiandClaude Sonnet 5.5 e4b5b0f07f Inventory preview: the player is lit as Lighting.Entry.ENTITY_IN_UI
The shading was a rough guess (front 1.0, top 0.4); it is now
minecraft_mix_light with the two inventory lights, ambient 0.4 and power 0.6.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 00:45:21 +01:00
Ludovic Marie 71527fa938 Merge remote-tracking branch 'origin/main' into upd-4016
# Conflicts:
#	crates/horizon-client/src/game/creatures.rs
2026-10-30 00:42:49 +01:00
levzzzandClaude Sonnet 5.5 e4d8570fbc Merge main into fix/villager-grows-up-brain (resolve tools/test_mob_ai_parity.py)
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 04:42:41 +05:00
levzzzandClaude Sonnet 5.5 4e3a161ac6 Merge main into fix/constructor-path-maluses (resolve tools/test_mob_ai_parity.py)
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 04:41:45 +05:00
levzzzandClaude Sonnet 5.5 6cd8cc4987 Merge main into fix/wither-skeleton-parity (resolve tools/test_mob_ai_parity.py)
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 04:41:08 +05:00
levzzzandClaude Sonnet 5.5 30da32b83c Merge main into fix/zombified-piglin-turtle-egg-goal (resolve tools/test_mob_ai_parity.py)
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 04:40:41 +05: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
levzzzandClaude Sonnet 5.5 173e278206 Merge main into fix/flyers-scan (resolve tools/test_mob_ai_parity.py)
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 04:40:15 +05: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
levzzz d6adbe1cb5 Merge pull request #4057 from degepie/fix/issue-3422-shield-slowdown
A raised shield keeps a sprint from starting, as in vanilla (#3422)
2026-10-30 04:37:03 +05:00
levzzz bc93f7da59 Merge pull request #4063 from arismoko/fix/command-loot-name-parity
Preserve item modifier custom name components
2026-10-30 04:36:46 +05:00
levzzz 8b843008eb Merge pull request #3776 from GIGABAIT93/fix/text-display-passengers
Text displays ride their vehicles: an NPC's name sits over its head
2026-10-30 04:36:36 +05:00
levzzz 8bd015284b Merge pull request #3605 from deloskiytbackup/fix/pumpkin-head-rendering
Render: draw carved pumpkins and mob heads on player and humanoid heads
2026-10-30 04:36:20 +05:00
levzzz b97e677cbc Merge pull request #3998 from d3grando/fix-3829
Players and heads keep their skins on busy servers
2026-10-30 04:36:04 +05:00
levzzz 117fbcd1aa Merge pull request #3922 from GIGABAIT93/feat/remote-player-scale
Other players and mannequins take their minecraft:scale
2026-10-30 04:35:35 +05:00
levzzz 5bafa93fa0 Merge pull request #4020 from DenisHumen/fix/sleep-camera
Sleeping: lie 0.125 above the bed's top, as vanilla does
2026-10-30 04:35:18 +05:00
levzzz ae4660dfdf Merge pull request #4021 from phexuss/fix/station-scrollbar-drag
Drag the stonecutter and villager trade scrollers
2026-10-30 04:35:02 +05:00
levzzz 141b1e0cf5 Merge pull request #4026 from JustKarkat/test/text-display-shadow-parity
Add shadow_color coverage to the text display render parity test
2026-10-30 04:34:45 +05:00
levzzz bf4d46df7e Merge pull request #3594 from d3grando/fix-3503
Paintings: the item's variant is placed and named in its tooltip
2026-10-30 04:34:28 +05:00
levzzz 4bf451bd2d Merge pull request #3611 from degepie/fix/issue-3027-adventure-ghost-blocks
Adventure: don't predict breaking a block the player may not break (#3027)
2026-10-30 04:33:59 +05:00
Ludovic MarieandClaude Opus 5.5 31ad980443 Merge main: main's new tab test builds a Tab with hotbar false
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 00:33:49 +01:00
levzzz 4c8178f4fd Merge pull request #3620 from iLostName/pearl-through-end-crystal
ender pearl: one that hits an end crystal still takes its thrower there
2026-10-30 04:33:43 +05:00
levzzz f38db18183 Merge pull request #3624 from deloskiytbackup/fix/tool-break-sound
Audio: play break sound and track stats when tools break while mining
2026-10-30 04:33:26 +05:00
levzzz 919c6803c5 Merge pull request #3647 from iLostName/elder-curse-skips-cursed
elder guardian: a player already cursed isn't cursed again every minute
2026-10-30 04:33:10 +05:00
levzzz abce5d7864 Merge pull request #4044 from DenisHumen/fix/armor-stand-swing
Armor stands: placing one swings the hand, as vanilla does
2026-10-30 04:32:54 +05:00
levzzz 54ac508d90 Merge pull request #3686 from k3yh/fix/spawn-egg-entity-data
An op's spawn egg sets its entity data on a spawner
2026-10-30 04:32:37 +05:00
levzzz 2f6babf098 Merge pull request #3900 from DUzzL/fix/issue-3885-function-tag-dedupe
Fix O(n^2) function-tag resolution and tag cycles
2026-10-30 04:32:20 +05:00
levzzz df051f7089 Merge pull request #3906 from DUzzL/fix/issue-3889-explosion-drop-merge
Explosion drops merge by item, components and max stack size
2026-10-30 04:32:04 +05:00
DUzzL 084394a849 Keep campfire damage from making mobs panic 2026-10-30 00:32:02 +01:00
levzzz 706fc7aa13 Merge pull request #3907 from DUzzL/fix/issue-3884-recipe-count-range
Reject recipe result counts outside 1..=99 (Fixes #3884)
2026-10-30 04:31:55 +05:00
levzzz db0a98d645 Merge pull request #4017 from DUzzL/fix/issue-4013-blocked-fireball-no-ignite
Keep a shield-blocked small fireball from igniting its target
2026-10-30 04:31:26 +05:00
levzzz b8eb63db68 Merge pull request #4054 from DUzzL/fix/issue-4038-enderman-arrow-teleport
Arrows fly on when an enderman teleports away from them
2026-10-30 04:31:10 +05:00
Ludovic Marie e8d5059b6a Merge remote-tracking branch 'origin/main' into upd-3693 2026-10-30 00:31:02 +01:00
levzzz ce18dafd4e Merge pull request #3928 from xFedeT/fix/riptide-with-elytra
Riptide in rain works on a server's client: isInRain from its own chunks and weather
2026-10-30 04:30:26 +05:00
levzzz 971af532f9 Merge pull request #3991 from d3grando/fix-3393-darkness
Darkness dims the screen on servers and in single player
2026-10-30 04:30:10 +05:00
levzzz bdf9cb8eb0 Merge pull request #4029 from d3grando/fix-3810
No rain splashes or rain sound under a high roof
2026-10-30 04:29:42 +05:00
levzzz 1b90753fab Merge pull request #4033 from d3grando/fix-2780-wolfbeg
Wolves tilt their head at food they want
2026-10-30 04:29:26 +05:00
levzzz b890329854 Merge pull request #4035 from d3grando/fix-2893
Ender Dragon wings push the player only when their hit lands
2026-10-30 04:29:09 +05:00
levzzz e618347e38 Merge pull request #4036 from d3grando/fix-2780-babypet
Food grows a tamed baby cat or wolf instead of making it sit
2026-10-30 04:28:53 +05: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
levzzz dbf445db2d Merge pull request #4053 from d3grando/fix-3616
Your body keeps up with the camera at Levitation 255
2026-10-30 04:28:37 +05:00
levzzz bf702cfe22 Merge pull request #3853 from DUzzL/fix/armor-stand-elytra-3843
Draw a worn elytra on armour stands
2026-10-30 04:28:20 +05:00
levzzz 03805b8eeb Merge pull request #3965 from GIGABAIT93/fix/shadow-scale
Entity shadows follow the entity's scale and age
2026-10-30 04:28:03 +05:00
levzzz c081525853 Merge pull request #3959 from DenisHumen/fix/golem-pattern-orientations
Golems: a snow or iron golem builds in any orientation, as vanilla does
2026-10-30 04:27:46 +05:00
Federico Tagliani df19d46a5a Merge remote-tracking branch 'origin/main' into fix/ctrl-click-right-click 2026-10-30 00:27:40 +01:00
levzzz 6a6acac27c Merge pull request #4022 from d3grando/fix-3098
Withers and golems build in any orientation, like vanilla
2026-10-30 04:27:30 +05:00
levzzz c5e9265bf0 Merge pull request #3797 from Bloodulon/pr/packs-overlay
Packs: read deflate entries to their end; skip a bad entry
2026-10-30 04:27:14 +05:00
levzzz 421834b82e Merge pull request #3799 from DenisHumen/fix/bucket-eye-block
Buckets and bottles pick up the water the eyes are in
2026-10-30 04:26:58 +05:00
levzzz a98cf88458 Merge pull request #3865 from elythepreciouscat/fix/server-disconnect-saving-screen
Disconnect: do not show saving world screen on external server
2026-10-30 04:26:41 +05:00
levzzz 79aca92478 Merge pull request #3867 from bxnjxmin27/fix/3689-screenshot-while-chat
Screenshots: the key works with chat, a sign or a book open
2026-10-30 04:26:25 +05:00
levzzz 48d29b20f1 Merge pull request #3873 from DUzzL/fix/issue-3863-breed-advancement
Fix breeding advancement and statistic on the server thread
2026-10-30 04:26:09 +05:00
levzzz 0229716f22 Merge pull request #3895 from ccclosed/fix/mob-effect-attribute-modifiers
Mobs: apply Weakness, Slowness and Health Boost attribute modifiers
2026-10-30 04:25:52 +05:00
levzzz a3809490f6 Merge pull request #3898 from Bloodulon/pr/item-definitions
Client: load item model definitions from nested folders
2026-10-30 04:25:36 +05:00
levzzz 8e49a414c2 Merge pull request #3857 from DUzzL/fix/issue-3854-lever-piston
Fix lever dropping when placed on a piston's back
2026-10-30 04:24:46 +05:00
levzzz 33e56c7a56 Merge pull request #3851 from DUzzL/fix/axolotl-nbt-glowing-test-3840
Fix axolotl NBT test: query glowing effect by namespaced id
2026-10-30 04:24:18 +05: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
Federico TaglianiandClaude Sonnet 5.5 d9d0bd3289 Right Click Emulation: Control + left click is a right click on macOS
The option was in Options > Controls and saved, but nothing read it.
MacosUtil.setCtrlClickEmulatesRightClick makes the click a right click, and
its release too.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 00:22:16 +01:00
SylviromiandClaude Opus 5.5 def1dbef33 Native mods: a mod container opens a vanilla slot menu
ModContainer.menu names a plain slot grid (hopper, generic_3x3,
generic_9x1..9x6) with as many slots; using the block opens it over the
block entity's slots, through the menu code vanilla's containers use, titled
with the block's name (Block.getDescriptionId).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 02:21:47 +03:00
Ludovic MarieandClaude Opus 5.5 23c5335910 Block entity rendering: reuse the meshes' face visibility instead of loading it again
BlockEntityRenderer loaded occlusion.nbt and the render definitions a second
time and built its own FaceVisibility, whose shapes then computed their faces
again: 13.6 MB of face shapes, twice. It now takes a clone of the mesh
tables' visibility, which shares the shapes and their cached faces.
Probes and tests that load the renderer alone still load their own.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 00:18:41 +01:00
Ludovic MarieandClaude Opus 5.5 37a1e8859f Merge main: RemoteTextDisplay::new takes what the display shows
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 00:18:16 +01:00
Ludovic MarieandClaude Opus 5.5 ae0512e299 Merge main: the composter's useItemOn result is an InteractionResult
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 00:17:37 +01:00
septydev c449d797bd Fix account chip text overflow 2026-10-30 01:17:01 +02:00
Ludovic Marie 6d666c30e2 Merge remote-tracking branch 'origin/main' into upd-3776 2026-10-30 00:16:56 +01:00
Ludovic Marie 8c3d85eb58 Merge remote-tracking branch 'origin/main' into upd-3642 2026-10-30 00:16:56 +01:00
oscargzss 8bbfc5cbf0 renderer: try a backend again with non-conformant adapters
wgpu hides every adapter whose driver reports Vulkan conformance version 0
unless `ALLOW_UNDERLYING_NONCOMPLIANT_ADAPTER` is set, and Mesa reports
0.0.0.0 for hasvk, which drives Ivy Bridge and Haswell. Such a GPU is told
it has no driver while Vulkan runs, and the only escape was the player's
environment variable.

`open_device` now asks a backend that offered no adapter at all a second
time with those adapters shown, and says in the log which adapter it took.
Every machine where a backend already gives an adapter is untouched.

Fixes #4059
2026-10-29 17:15:13 -06:00
Ludovic Marie 4bd1313677 Merge pull request #4049 from horizon-rust/fix/launcher-java-home-version
🤖 Agent: A Java instance's Home no longer shows Horizon's version picker or nightly updates; its chip names its own version and loader. Checked in the real launcher (scratch homes, Fabric Java instance and Horizon instance), launcher fmt/clippy/278 tests pass on this head.
2026-10-30 00:14:55 +01:00
Ludovic Marie 6eeeb454f5 Merge pull request #3805 from DenisHumen/fix/rea-vanilla-windows
🤖 Agent: Ludovic's AI agent (Claude), triage. Tooling only: `npx` is resolved by path (`npx.cmd` on Windows), with a clear error when it's missing. verify is green. Queued.
2026-10-30 00:14:52 +01:00
Ludovic Marie 4592a7e799 Merge remote-tracking branch 'origin/main' into upd-3655
# Conflicts:
#	crates/horizon-client/src/game/remote.rs
2026-10-30 00:14:42 +01:00
Ludovic Marie 5dc59b0f6a Merge remote-tracking branch 'origin/main' into upd-3748
# Conflicts:
#	crates/horizon-client/src/game/command_blocks.rs
2026-10-30 00:11:18 +01:00
DUzzL e93427276f Hurt mobs standing in a lit campfire (#4037)
CampfireBlock.entityInside hurts any living entity in a lit campfire for 1 (2 for soul campfires) with the campfire damage type, which is fire, makes mobs panic and does not knock back. Mobs ignored campfires entirely.
2026-10-30 00:11:15 +01:00
arismoko 8eb2ecfa76 Preserve loot name components in item commands 2026-10-29 18:10:27 -05:00
Ludovic Marie c3071c7b61 Merge pull request #3575 from d3grando/fix-3153
🤖 Agent: pr-triage-1 (Ludovic's AI agent, Claude), not Ludovic himself. Thanks for the fix: with the wither held in place the scenario now matches vanilla. On this head (f5de793b3) merged into current main, the harnesses this change can reach pass: `command_exec`'s wither spawn (4/4), wither breaks (9/9) and ender dragon (5/5) groups match vanilla. Its 2 remaining mismatches are `/data get` printing a `custom_name` as a component; they come from main and this PR doesn't touch them. fmt, clippy and the horizon-client tests pass too. Queued for merge.
2026-10-30 00:09:06 +01:00
DUzzL 80cb00a494 Arrows fly on when an enderman teleports away from them 2026-10-30 00:08:47 +01: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
Ludovic MarieandClaude Opus 5.5 fcea81151c Launcher: a Java instance's Home shows its own game, not Horizon's versions
Home drew Horizon's version picker ("No version" or a nightly) beside a Java instance's Play,
and offered Horizon's next nightly in its hero. A Java instance now shows neither: its chip
says its Minecraft version and loader ("Minecraft 1.21.1 · Fabric 0.16.9").

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 00:05:05 +01: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
Сидоренко Егор АлекчеевичandCodex db38f50b12 Fix detail requests for retiring fine LOD leaves
Seed the existing bounded request path with planned retiring leaves still missing saved detail, so full-detail geometry can retire after its replacement is ready.

Fixes #4039

Co-Authored-By: Codex (GPT-6) <[email protected]>
2026-10-30 02:01:27 +03:00
DUzzL 8a773bca20 Play the fall damage sound for multiplayer clients and LAN guests 2026-10-30 00:00:06 +01:00
SylviromiandClaude Opus 5.5 f57fe836c4 Native mods: item slots for mod block entities (hoppers, comparators, drops)
A mod block's block entity gets item slots when its mod says the block has
a container (native_block_entity_container: the slot count, the slots each
face reaches, and which slots take input or give output, as a
WorldlyContainer). Hoppers, droppers and comparators use them through the
container code vanilla's blocks use, breaking the block drops them
(Containers.dropContents), they save as Items, and the tick hook gets them.

Fixes #4034.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 01:59:56 +03:00
Ludovic Marie 224b99b724 Merge pull request #3930 from DUzzL/fix/issue-3916-dead-code
🤖 Agent: Ludovic's AI agent (Claude), triage. Removes `ctov.rs`'s modular_compat code, which nothing calls any more (lithostitched owns that processor), with no change in behaviour. verify is green. Queued.
2026-10-29 23:59:44 +01: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
Ludovic Marie e8ecd77cc8 Merge pull request #3879 from matyden/tools/port-publish-utf8
🤖 Agent: Ludovic's AI agent (Claude), triage. Tooling only: the issue body is written as UTF-8 whatever the code page. verify is green. Queued.
2026-10-29 23:57:20 +01:00
Ludovic Marie 97b6e16684 Merge pull request #3696 from xFedeT/fix/chicken-water-malus
🤖 Agent: Ludovic's AI agent (Claude), triage. Main gained another mob_ai case beside yours, so I merged main into your branch (no rebase). The only conflict was the new cases' place in `tools/test_mob_ai_parity.py`, and both are kept. Requeued at the merged head.
2026-10-29 23:57:16 +01:00
DenisHumenandClaude Opus 5.5 fdc7593180 Armor stands: placing one swings the hand, as vanilla does
ArmorStandItem.useOn returns SUCCESS once the stand fits (FAIL on a bottom
face or when something is in its box), and SUCCESS swings the hand. The
local placement now swings, and a server's client runs the same check in
startPrediction, before UseItemOn goes out, and swings when it holds.

armor_stand_spawn holds the checks both share.

Fixes #4024

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 00:57:02 +02: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
Ludovic MarieandClaude Opus 5.5 0e4f8e2799 Merge main into hdr-screenshot (HDR tiers, black level, UI brightness, world icons)
Hdr::params now fills main's 20-float layout for both the swapchain and the
HDR10 capture; the capture keeps its own pipeline and parameters. Screenshots
use main's ScreenshotRequest and copy_frame; only full-frame requests (not
world icons) get the -hdr.png copy.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 23:52:47 +01:00
Ludovic Marie 4464c5d64a Merge remote-tracking branch 'origin/main' into upd-3735
# Conflicts:
#	tools/test_mob_ai_parity.py
2026-10-29 23:52:44 +01:00
Ludovic Marie b0c2f375bb Merge remote-tracking branch 'origin/main' into upd-3729
# Conflicts:
#	tools/test_mob_ai_parity.py
2026-10-29 23:52:42 +01:00
Ludovic Marie 183e772727 Merge remote-tracking branch 'origin/main' into upd-3721
# Conflicts:
#	tools/test_mob_ai_parity.py
2026-10-29 23:52:39 +01:00
Ludovic Marie c41e95bf53 Merge remote-tracking branch 'origin/main' into upd-3712
# Conflicts:
#	tools/test_mob_ai_parity.py
2026-10-29 23:52:37 +01:00
Ludovic Marie 3887ceefec Merge origin/main into feat/launcher-night-shell 2026-10-29 23:48:53 +01:00
Ludovic MarieandClaude Opus 5.5 0d322f4fca Launcher: hover layers only while hovered, plates painted in one canvas
A row or tab that isn't hovered no longer builds its (invisible) hover plate, and plate::cut
paints its rectangles in one canvas instead of a div each: the sidebar and the top bar redraw
on every sky frame, so they cost a few elements instead of hundreds.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 23:48:04 +01:00
degepieandClaude Sonnet 5.5 b1dcbdc0eb A raised shield (or any item in use) stops a sprint from starting
LocalPlayer.canStartSprinting refuses while isSlowDueToUsingItem (an item in
use whose use_effects do not set can_sprint), and LocalPlayer.aiStep zeroes
the double-tap window while it holds. The sprint phase had the condition but
the player never fed it, so the sprint key or a double-tap of forward could
start a sprint with a shield up, which vanilla does not allow (measured: 0.055
blocks per tick against 0.043 for the shielded walk). Items now carry
use_effects.can_sprint (spears set it) and the window passes the state to the
sprint phase.

Fixes #3422

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
2026-10-30 03:46:46 +05: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
Ludovic Marie 3c8a73f86f Merge remote-tracking branch 'origin/main' into upd-3773
# Conflicts:
#	tools/test_mob_ai_parity.py
2026-10-29 23:46:15 +01:00
Ludovic Marie f58660a875 Merge remote-tracking branch 'origin/main' into upd-3732
# Conflicts:
#	tools/test_mob_ai_parity.py
2026-10-29 23:46:12 +01:00
Ludovic Marie 49aa9dd00b Merge remote-tracking branch 'origin/main' into upd-3696
# Conflicts:
#	tools/test_mob_ai_parity.py
2026-10-29 23:46:09 +01: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
Claude 396ba2e5e7 Turn the player when a server sends PlayerLookAt
ClientboundPlayerLookAtPacket was decoded as an unknown packet, so
`/teleport ... facing` and `/rotate ... facing` on a server never turned
the camera. Decode it and port ClientPacketListener.handleLookAt:
Entity.lookAt from the player's feet or eyes towards the packet's entity
(its feet or eyes) when the client tracks it, else towards the point.

Part of #3518.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Rx5M6NS763D2WVnb2HmcTH
2026-10-29 22:42:00 +00:00
Ludovic MarieandClaude Opus 5.5 3d68cb4d71 Merge main: CustomHeadLayer beside main's player pumpkin (#3584)
head_item_geometry now draws the local player's and other players' head
items; main's player_pumpkin_geometry stays only for LAN guests drawn on
the in-frame host, whose models carry no worn stacks.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 23:41:28 +01:00
Ludovic Marie bd665f28ad Merge pull request #3782 from xFedeT/fix/armor-stand-bubble-column
🤖 Agent: Ludovic's AI agent (Claude), triage. `travelInWater` (the 0.8 slowdown, then `getFluidFallingAdjustedMovement` with gravity/16) and the bubble column push for armour stands match vanilla. The per-tick water scan only covers the stand's own box. CI parity is green at this head. Queued.
2026-10-29 23:40:26 +01:00
JustKarkat 2982f6eb9a Add shadow_color coverage to the text display render parity test
Fixes #3531's render side landed in #3341; this adds the shadow cases its
branch carried for tools/test_text_display_render_parity.py so the
packed ARGB ints, the [r, g, b, a] lists and NO_SHADOW=0 are checked
against the vanilla TextDisplayRenderer oracle.
2026-10-29 17:40:25 -05:00
Ludovic Marie 499a08ba81 Merge pull request #3690 from xFedeT/fix/fish-random-swimming-parity
🤖 Agent: Ludovic's AI agent (Claude), triage. `WaterAnimal` and `AgeableWaterCreature` set the WATER malus to 0, and FishMoveControl's yaw uses `Mth.atan2` in degrees. CI parity (mob_ai with new fish cases) is green at this head. Queued.
2026-10-29 23:40:22 +01:00
ElyTheCat 0239b3c97f Merge branch 'main' into fix/armor-rendering 2026-10-30 02:09:12 +03:30
Ludovic MarieandClaude Opus 5.5 10e715be15 Dedicated server: no block sprite pixels, which only the renderer reads
A dedicated server loads the block models and the sprites' layers, as before,
but no longer lays out the texture array's pixels or the animations' frames:
nothing draws them. The integrated server shares the window's resources and
is unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 23:36:59 +01:00
Ludovic Marie 7f54b8abb4 Merge pull request #2705 from senjinthedragon/hdr-tiers
🤖 Agent: pr-triage-1: the parity run was cancelled (not failed); I reran it on this same head.
2026-10-29 23:36:18 +01:00
Ludovic Marie 573140cc5a Merge pull request #3639 from xFedeT/fix/wither-immune-to-wither-friends
🤖 Agent: Ludovic's AI agent (Claude), triage. `WitherBoss.hurtServer` ignores damage whose source entity is in `#wither_friends` (the undead), for explosions and wither skulls alike. CI parity is green at this head. Queued.
2026-10-29 23:35:00 +01:00
Ludovic Marie bdd6275570 Merge pull request #3722 from vvnikita74/fix/no-day-sleep-thunder-flag
🤖 Agent: Ludovic's AI agent (Claude), triage. Sleeping now follows the sky darkening (`skyDarken >= 4`, `isDarkOutside`) instead of a fixed time window or the thunder flag alone, so a thunderstorm at noon allows it and rain alone doesn't. CI parity is green at this head. Queued.
2026-10-29 23:34:55 +01:00
Ludovic Marie 588c0ea77a Merge pull request #3779 from leifiyoo/fix/powered-minecart-speed
🤖 Agent: Ludovic's AI agent (Claude), triage. `MinecartFurnace.push` is a Vec3, and `calculateNewPushAlong` (`projectedOn(...).normalize().scale(push.length())`) keeps its 3D length; saving drops y as `addAdditionalSaveData` does. CI parity is green at this head. Queued.
2026-10-29 23:34:52 +01:00
Ludovic Marie 3f08e59ad2 Merge pull request #3767 from leifiyoo/fix/template-block-entity
🤖 Agent: Ludovic's AI agent (Claude), triage. Matches `StructureTemplate.placeInWorld`, which sets a barrier wherever saved data goes, so the old block entity always goes and the template's data always loads. CI parity is green at this head. Queued.
2026-10-29 23:34:48 +01:00
Ludovic Marie 518ecfb511 Merge pull request #3750 from leifiyoo/fix/chunk-block-entity-replace
🤖 Agent: Ludovic's AI agent (Claude), triage. Read the whole diff: a loaded chunk's block entity replacing one at the same position keeps its ticker slot only if the new state ticks, as `LevelChunk.setBlockEntity` + `updateBlockEntityTicker` do. CI parity is green at this head. Queued.
2026-10-29 23:34:45 +01: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
levzzz 6daad36de2 Merge pull request #3931 from arismoko/content-identity-handshake
Reject mismatched content block registries
2026-10-30 03:31:18 +05:00
Ludovic Marie 91635b6852 Merge pull request #3637 from horizon-rust/feat/launcher-night-sky
🤖 Agent: Redesign R2 (the animated night sky behind every page), stacked on #3636 which merged; approved by Ludovic 2026-10-29 (8 px/s clouds accepted); launcher fmt/clippy/tests and sky.rs tests pass on this head, CI green.
2026-10-29 23:30:36 +01: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
Ludovic Marie 0e15c274da Merge pull request #3636 from horizon-rust/feat/launcher-night-specimen
🤖 Agent: Redesign R1b (the night look's specimen page, pixel-cut plates in plate.rs), stacked on #3635 which merged; approved by Ludovic 2026-10-29; launcher fmt/clippy/tests and plate.rs tests pass on this head, CI green.
2026-10-29 23:28:48 +01:00
SylviromiandClaude Opus 5.5 27c3c0be31 Native mods: the block entity hooks in the hook catalog
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 01:28:25 +03:00
Ludovic MarieandClaude Opus 5.5 3b6972a40a Slowness: the -0.15F modifier widened from a float, as vanilla's MobEffects
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 23:27:40 +01:00
Sylviromi 52399eb28a Merge main into native-mod-block-entities: the hook catalog 2026-10-30 01:27:37 +03:00
Ludovic Marie 829c4021a3 Merge pull request #3635 from horizon-rust/feat/launcher-night-tokens
🤖 Agent: Redesign R1 (night tokens, Bricolage with Geist's Cyrillic, pixel-cut corners), approved by Ludovic on 2026-10-29; launcher fmt/clippy/278 tests and horizon-theme tests pass on this head, CI green.
2026-10-29 23:26:38 +01:00
DUzzL 7a12e4444a Keep a shield-blocked small fireball from igniting its target 2026-10-29 23:25:57 +01:00
DenisHumenandClaude Opus 5.5 c3e9ec54a9 Sleeping: lie 0.125 above the bed's top, as vanilla does
LivingEntity.setPosToBed puts a sleeper at the bed's x/z centre and at
pos.y + getSleepHeight + 0.125, getSleepHeight being the top of the bed's
shape (0.5625). Horizon left out the 0.125, so the player and sleeping
villagers lay 0.125 lower, and the first-person camera (eye 0.2 + 0.15)
ended up inside the player's own head.

Fixes #4006

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 00:25:36 +02: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
Ludovic MarieandClaude Opus 5.5 71127f982f Launcher: the night frame, a sidebar plate over the sky and a top bar of tabs (redesign R3)
shell.rs draws the frame's parts: the sidebar's translucent plate from the window's top to its
bottom (wordmark, instances, servers, links, version), rows and tabs on pixel-cut plates when
selected and eased in at half opacity when hovered, the account chip with the signed-in
account's face. The top bar sits over the pages only, without a plate. Java instances say
their version and loader in the sidebar and get no preparing state. hover_layer's easing is
now hover_fade, for layers of any shape; blocks::side_item is gone.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 23:23:20 +01:00
Ludovic MarieandClaude Opus 5.5 a72876f0c0 dedicated_server harness: Horizon's server brand is "horizon"
The one intended difference from vanilla's server (MinecraftServer.getServerModName):
the harness checks Horizon sends brand=horizon, then compares the rest as before.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 23:22:02 +01:00
Federico Tagliani 39314175e9 Merge remote-tracking branch 'origin/main' into fix/wind-burst-smash 2026-10-29 23:21:58 +01:00
Federico TaglianiandClaude Sonnet 5.5 88b9342ffc Mace smash on another player: the attacker's side of hurtEnemy and the Wind Burst launch
Player.itemAttackInteraction ran only for a mob victim, so smashing a
player never stopped the fall, never fired the Wind Burst explosion on the
attacker and never wore the weapon.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-29 23:21:57 +01:00
DUzzL 21a033cea0 Trial Omen and Bad Omen of a server's players make trial spawners ominous 2026-10-29 23:21:28 +01:00
ElyTheCatandAntigravity f3a3c1291b Render: custom head items and multi-layer equipment
In vanilla Minecraft (26.3), items equipped in the head slot without an
armour asset (such as carved pumpkins, player heads, and mob skulls) are
rendered via CustomHeadLayer with ItemDisplayContext.HEAD transform
(down 0.25, rotated 180 degrees, scaled 0.625; skulls scaled 1.1875) for
both the local player, remote players/guests, and humanoid mobs.

Additionally, equipment assets can specify multiple layers under their
layer kinds (e.g. equipment/leather.json has layer 0 leather and layer 1
leather_overlay). Horizon previously only parsed the first layer index 0,
causing overlays like leather_overlay to be dropped.

- ArmorStandAssets: load all humanoid and wings layers in equipment JSON
  definitions into a list of layers.
- In armor_layers and armor_stand_geometry: render all humanoid layers
  for equipped items in order, including non-dyeable overlays.
- custom_head_geometry: implement CustomHeadLayer for worn non-armour
  head items and skulls, drawing worn skulls with their special model
  (including player profile skin texture) and items (e.g. carved pumpkin)
  via the item rendering pipeline.
- Hook custom_head_geometry into player model rendering, inventory preview,
  remote players/mobs, and LAN guests.
- Update port ledger mappings for CustomHeadLayer and EquipmentLayerRenderer.

Co-Authored-By: Antigravity <[email protected]>
2026-10-30 01:50:07 +03:30
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
unknownandClaude 1761eb0842 Renderer: a Validation acquire recovers, animated sprites layout stays inside the error scopes
Opening a world fullscreen could crash ~8s into the loading screen (#3866).
Two faults stacked up:

- draw_frame answered CurrentSurfaceTexture::Validation with Err on every
  frame; the loading screen runs ControlFlow::Poll, so the warn ran ~16k
  times a second (130k log lines in the report) while nothing recovered.
- SpriteAnimator::new asked the animated sprites pipeline for its bind
  group layout after the error scopes in pipeline() had been popped. A
  device lost during that window makes create_compute_pipeline return a
  silent invalid placeholder (device-lost errors bypass the scopes), so
  the scopes saw nothing, and the out-of-scope get_bind_group_layout then
  hit wgpu's uncaptured-error path and panicked. pipeline() now returns
  the layout queried while the scopes are open, where a validation error
  is caught and the CPU path takes over, as it already does for drivers
  without working compute shaders.

Validation on acquire now gets the swapchain a fresh configuration like
Outdated does, then reattaches the window like Lost does once that has
not helped, and a frame is skipped (Ok(false)) instead of failed; the
counter resets on the next acquired frame.

Fixes #3866

Co-Authored-By: Claude <[email protected]>
2026-10-30 01:17:39 +03:00
Ludovic Marie 1884052eda Merge pull request #3471 from d3grando/bench-windows-results
🤖 Agent: pr-triage-1: read the new head. It only merges main and replaces the RD 8 and RD 32 reports with reruns on an idle machine, so the write-up no longer leans on runs with builds alongside. tests/test_perf_bench.py and the public tree check pass. skip-release stays. Thank you!
2026-10-29 23:17:02 +01:00
Claude 0801c954b5 Drag the stonecutter and villager trade scrollers
The scrollers on the stonecutter and merchant screens only moved with the
mouse wheel: pressing and dragging them did nothing. Port
StonecutterScreen.mouseClicked/mouseDragged and
MerchantScreen.mouseClicked/mouseDragged, and keep the stonecutter's
scrollOffs as a float like vanilla so the wheel and the scroller share it.

Part of #3518.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Rx5M6NS763D2WVnb2HmcTH
2026-10-29 22:14:39 +00: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
SylviromiandClaude Opus 5.5 da98be3276 Native mods: a save leaves out a mod block entity mid-tick
Review of #3996: while native_block_entity_tick runs, the block entity
at its position is an empty-id placeholder; a save then skips it rather
than writing `id: ""`. The hook's doc says so, and that clients (the
window's own in single player too) don't get mod block entities yet.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 01:14:23 +03:00
Ludovic Marie 2204e39515 Merge pull request #2987 from kacpherek/fix/entity-physics-lan-parity
🤖 Agent: proven by pr-triage-3 (Ludovic's AI agent) on union/pt3-w2 db5591ac7: parity --changed all passed, clippy, tests, lan_e2e 11 checks on both paths; tipped/spectral arrows and text/block/item displays drawn alike by host and LAN guest, save and reload identical, the ominous item spawner seen by a LAN guest. Mojang rules, supply chain, per-tick cost read.
2026-10-29 23:13:04 +01: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
DUzzL ad9f9a66f6 LAN guests and single-player windows pick up arrows they touch (#3997) 2026-10-29 23:11:26 +01:00
arismoko cfff58e66f Reject mismatched content block registries 2026-10-29 17:10:20 -05:00
Ludovic MarieandClaude Opus 5.5 ba59ac2ccf Dedicated server: build only the font atlas, as the integrated server does
A dedicated server draws nothing, yet it built the full HUD atlas (GUI,
particles, maps, skins) and the title panorama. The integrated server's
game already holds only the font, which books and chat measure with, and a
one-pixel panorama; both now come from UiResources::server, and the
dedicated server's synchronous load uses it too.

Idle, fresh world, 3 interleaved runs: RSS 836/836/841 -> 771/792/765 MB,
ready 5.05 -> 4.32 s (median).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 23:06:19 +01:00
levzzz c1255761d6 Merge pull request #3871 from arismoko/upstream/named-hook-catalog
Catalog versioned native hook points
2026-10-30 03:05:20 +05:00
levzzz a88e84f3c9 Merge pull request #3880 from arismoko/native-bundle-data
Allow data files in native mod bundles
2026-10-30 03:05:09 +05:00
Federico TaglianiandClaude Sonnet 5.5 7ca3671df3 tests: the remote_motion fixture knows the hook motion method
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-29 23:02:36 +01:00
Ludovic Marie 33f0fa8c94 Merge pull request #3341 from damnurmum/fix/rich-item-name-colors
🤖 Agent: proven by pr-triage-3 (Ludovic's AI agent): 6da84cf19 was proven on union/pt3-w2; this head only merges main and adapts main's two new held-item tests to the Tag custom name. Workspace check, 1508 tests and the 11 item/save harnesses pass on it.
2026-10-29 23:02:14 +01:00
SylviromiandClaude Opus 5.5 967ec25579 Native mods: block entities for mod blocks (hook points API 3)
A block entity whose id is a mod's loads as BlockEntity::Mod (its id and
saved compound) instead of staying raw chunk data, and saves back as it
was. Three hook points let a native mod give its blocks block entities
(native_block_entity), say which tick (native_block_entity_ticks) and
tick them on the server in vanilla's order (native_block_entity_tick).
Player placement, /setblock, dispensers and horizon-ctl make them through
one Game::new_block_entity.

Fixes #3966.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 01:01:49 +03: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
Ludovic Marie 4f3457a4dc Merge pull request #2689 from levzzz5154/fix/end-gateway-entities
🤖 Agent: verified by Ludovic's AI agent (pr-triage-2, Claude) at c566e69c6, merged with current main: the End gateway follows EndGatewayBlock.entityInside and Entity.handlePortal; the latest commit puts back the tick_items call that the first one had dropped (dropped items fall, merge and despawn again) and, as Entity.setAsInsidePortal does, only restarts the portal cooldown of an entity that is still cooling down (checked against the 26.3 bytecode). clippy -D warnings, the portal/gateway/item unit tests and tools/singleplayer_end_cargo_audit.py on both paths pass (cow and a 7-diamond stack arrive at [1035.5, 76.0, 1.5]). Thanks @levzzz5154.
2026-10-29 22:58:08 +01: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
Ludovic Marie 117241eebb Merge pull request #2872 from levzzz5154/fix/forceload-rings
🤖 Agent: verified by Ludovic's AI agent (Claude): the review point is addressed (decorated protos still save as minecraft:features); the forced ticket rings follow FORCED_TICKET_LEVEL 31 rising one per ring; merged with main, clippy -D warnings and the horizon-client tests (forced_chunks included) pass. Queued; the merger waits for CI parity.
2026-10-29 22:58:03 +01:00
Ludovic Marie 3d23a9ff95 Merge pull request #3360 from levzzz5154/fix/launcher-mixins-public-modules
🤖 Agent: pr-triage-2 checked the pin: 3f99a5c60 is horizon-rust/horizon-mixins main, 8 commits ahead of 0d6b103c7 (preserved standard macros, Windows verbatim output paths), nothing behind; only MIXINS_REV changes and explicit per-bundle pins still win. Thanks @levzzz5154.
2026-10-29 22:57:59 +01:00
Ludovic Marie 4bf65778b6 Merge feat/launcher-night-specimen into feat/launcher-night-sky 2026-10-29 22:56:35 +01:00
Ludovic MarieandClaude Opus 5.5 cd44c4f495 Specimen: plates and the main action cut their corners in pixel steps
plate.rs draws a plate as rectangles that don't overlap (one opacity, the 1 px stroke follows
the steps); the specimen compares it with the rounded controls.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 22:56:27 +01:00
Federico Tagliani 379ec36d52 Merge remote-tracking branch 'origin/main' into fix/fishing-hook-smooth 2026-10-29 22:55:59 +01:00
Ludovic Marie 768099d32c Merge feat/launcher-night-tokens into feat/launcher-night-specimen 2026-10-29 22:55:58 +01:00
Federico TaglianiandClaude Sonnet 5.5 7f70737138 Fishing hook on a server's client: its own movement between the tracker's updates
FishingHook.tick runs on a client, easing towards the last update while it
flies, floats and rides what it hooked. Horizon only eased towards the
updates, which come every five ticks, so the bobber stood still for two
ticks of five and jumped.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-29 22:55:58 +01:00
Ludovic Marie 9d53e6ce35 Merge origin/main into feat/launcher-night-tokens 2026-10-29 22:54:15 +01:00
Ludovic MarieandClaude Opus 5.5 194970773d Night: pixel-cut plate corners, Geist's Cyrillic merged into Bricolage
Plates cut their corners in two 4 px steps (PIXEL_CORNER); RADIUS_PLATE is gone
from the night tokens. tools/bake_bricolage.py now merges Geist's Cyrillic into
every cut, so Russian uses the launcher's own face.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 22:54:06 +01:00
Ludovic Marie c8012dff3a Merge pull request #3119 from NetuosoDEV/fix/enderman-carried-block-tints
🤖 Agent: pr-triage-2 (Ludovic's AI agent, Claude): proven at c32dddb1f: carried and worn blocks draw their block-state model with vanilla's per-face default tints (block_tint harness against 26.3), grass colour once per draw; parity --changed passes, clippy clean, tests pass but for main's own glowing test. Merging.
2026-10-29 22:53:15 +01:00
Ludovic Marie 04da927a44 Merge pull request #3815 from horizon-rust/perf/light-view-cache
🤖 Agent: verified by server-tick-1: proto light views built once per proto state (streaming span 10-24 → 3-4 ms/tick on 25 explore bots, RSS unchanged); new generation_saves test fails on main and passes here; pr_ready --quick green; merging once CI parity passes.
2026-10-29 22:53:11 +01:00
k3yhandClaude Opus 5.5 67c4e5eb1a Merge main into the branch; the guest swap keeps both the acting permission and known_movement
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 16:47:34 -05:00
Ludovic Marie 12930d6237 Merge pull request #3572 from v3nn7/PolishLang
🤖 Agent: pr-triage-1 merged main once more (fast-forward): your Polish checks sit beside main's new test that every key has a Russian value. horizon-client-core's lang tests, the launcher's i18n tests and rustfmt pass on this head; proven before on union/pt1-w4.
2026-10-29 22:46:15 +01: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
Ludovic MarieandClaude Opus 5.5 9bba7215f5 Merge main into #3341: main's new held-item tests set a Tag custom name
Two tests main added (entity_nbt, mobs) set custom_name to a string and compared custom_name() with Option<&str>; with this PR it is the component tag and the plain text is an owned String.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 22:43:55 +01:00
DUzzL f822855c64 docs: add an owner's page mapping server.properties to server.toml 2026-10-29 22:43:10 +01: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
Ludovic Marie 0fff625c22 Merge pull request #3441 from xKumorio/fix-arrow-known-movement
🤖 Agent: proven by pr-triage-3 (Ludovic's AI agent) on union/pt3-w2 db5591ac7: parity --changed all passed, clippy, tests, lan_e2e 11 checks on both paths, and the throws levzzz asked for (walking/jumping carry the movement, none after a tick without a move) for host and guest on both paths. Mojang rules, supply chain, per-tick cost (a Vec3 copy per guest turn) read.
2026-10-29 22:39:35 +01:00
Ludovic Marie d52705984a Merge remote-tracking branch 'origin/main' into fix/pt3-3341 2026-10-29 22:37:25 +01:00
Ludovic MarieandClaude Opus 5.5 73c8eb9b60 Merge main into PolishLang: the Polish checks beside main's every-key test
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 22:34:22 +01:00
Ludovic Marie a305d38fc5 Merge pull request #3581 from v3nn7/fix/furnace-cart-client
🤖 Agent: proven on the integration branch union/pt1-w4 by pr-triage-1. The affected vanilla harnesses, clippy -D warnings and the tests pass, and so does lan_e2e (vehicles, commands, armour, self, tab) on both single-player paths. lan_e2e's minecarts check fails on main's own code too, with or without this PR. Furnace carts now clear their lit flag for clients, and their smoke follows MinecartFurnace.tick. Thank you!
2026-10-29 22:27:41 +01:00
Ludovic Marie 14b7b252ac Merge pull request #3106 from malordin/fix/missing-assets-download
🤖 Agent: proven by pr-triage-3 (Ludovic's AI agent): launcher-only change; on this exact head launcher fmt, clippy -D warnings and 299 tests pass on the Mac mini, clean merge with main. Asset refill only through download_for_owner (owners only).
2026-10-29 22:27:28 +01:00
Ludovic Marie 393ff56184 Merge pull request #3584 from v3nn7/fix/pumpkin-player-head
🤖 Agent: proven on the integration branch union/pt1-w4 by pr-triage-1: the affected vanilla harnesses pass (the only failures are main's own or another PR's), clippy -D warnings and the tests pass, the launcher's checks pass, and lan_e2e passes vehicles, commands, armour, self and tab on both single-player paths. Thank you!
2026-10-29 22:27:21 +01:00
Ludovic Marie e515379b04 Merge pull request #3925 from horizon-rust/feat/claim-respects-kira
🤖 Agent: merging the Kira/GitHub claim alignment (#3859 step 1 follow-up): verify green, claim tests pass.
2026-10-29 22:20:19 +01:00
Kacpherek c1de70edd4 Keep a bred baby fox or goat free of the spawn finalize extras
Breeding spawns the baby through spawning::finalize, which now gives a fox a mouth item and a goat a lost horn and a random screaming roll; Fox.getBreedOffspring and Goat.getBreedOffspring run no finalizeSpawn (Fox.java, Goat.java). The baby fox is emptied, the baby goat keeps both horns and gets a parent's screaming or one chance in fifty.
2026-10-29 22:19:35 +01:00
Kacpherek 149c4b87f8 Reopen the chat with its kept draft
T and / always opened the chat with the caller's text. ChatComponent.createScreen (ChatComponent.java:442) restores the saved draft, and ChatScreen.removed (ChatScreen.java) keeps it on Escape when Save Chat Drafts is on.
2026-10-29 22:19:26 +01:00
Kacpherek a6b3066f85 Kick guests who spam chat or commands
A dedicated server never kicked a spamming guest: chat-spam-threshold-seconds and command-spam-threshold-seconds were read but unused. Vanilla adds 20 per message or command, decays one a tick and disconnects over the threshold with disconnect.spam, except for operators (ServerGamePacketListenerImpl.detectRateSpam, TickThrottler.java:23).
2026-10-29 22:19:23 +01:00
Kacpherek 08918a8ab0 Load a world's own resource pack when the world opens
A save's resourcepacks/resources.zip was never loaded, so its textures were missing. Vanilla WorldOpenFlows.loadBundledResourcePack pushes it as a local pack (ServerPackManager.pushLocalPack, ServerPackManager.java:61) before the world connects.
2026-10-29 22:19:23 +01:00
Kacpherek fe08d02d5d Hold player chat back while Chat Delay is set
Player and disguised chat showed at once, so Chat Delay was ignored. ChatListener.handleMessage, tick and flushQueue (ChatListener.java:75-120) queue the lines and show one per delay.
2026-10-29 22:19:23 +01:00
Kacpherek 359e83b338 Step option cycle buttons with the mouse wheel
The wheel over an option button only scrolled the list. CycleButton.mouseScrolled (CycleButton.java:115) cycles its value, up going back, and the list does not move.
2026-10-29 22:19:19 +01:00
Kacpherek 8b17c0383e Make foxes from snowy biomes snow foxes on natural spawns
Natural and spawner foxes were always red; vanilla Fox.Variant.byBiome picks the snow variant from spawns_snow_foxes (Fox.java, Fox.Variant).
2026-10-29 22:19:10 +01:00
Kacpherek 407dd201f8 Swap another player's held items when it swaps hands
Entity event 55 was ignored, so another player's main and off hand kept their items after a swap on the server; vanilla LivingEntity.handleEntityEvent calls swapHandItems (LivingEntity.java).
2026-10-29 22:19:09 +01:00
Kacpherek 889931c0b9 Let natural foxes hold food or a feather in their mouth
Only structure mobs ran Fox.populateDefaultEquipmentSlots, so a fox from a natural or spawner spawn never held an emerald, egg, rabbit foot or the like (Fox.java).
2026-10-29 22:19:09 +01:00
Kacpherek 509536179d Give natural goats their screaming, lost horn and ram cooldowns
Only structure mobs and the probe ran Goat.finalizeSpawn, so a goat from a natural or spawner spawn never screamed, lost a horn or had its ram cooldowns (Goat.java, GoatAi.initMemories).
2026-10-29 22:19:09 +01:00
Kacpherek a9d2ae39d9 Call only the pets of the same owner when a tamed mob is hurt
HurtByTargetGoal.alertOthers called every nearby wolf, including other owners' and the attacker's own tamed pets; vanilla checks TamableAnimal.getOwner and considersEntityAsAlly (HurtByTargetGoal.java, TamableAnimal.java).
2026-10-29 22:19:04 +01:00
Kacpherek 9d44aa4ba0 Stop a tamed unsaddled skeleton horse from taking steering from its rider
A tamed skeleton horse obeyed its rider without a saddle; AbstractHorse.getControllingPassenger (AbstractHorse.java) needs the saddle, and SkeletonTrapGoal only tames the trap horse.
2026-10-29 22:19:04 +01:00
Kacpherek 0c34e3f36c Refill axolotls' air from a splashed water potion
A splashed water bottle did nothing to axolotls; AbstractThrownPotion.affectEntitiesAround (AbstractThrownPotion.java) rehydrates each axolotl in the box, adding 1800 air up to the maximum.
2026-10-29 22:19:03 +01:00
Kacpherek 4049220715 Make a horse thud when it lands instead of the generic fall sound
A landing horse played the generic fall sound and no thud; AbstractHorse.causeFallDamage (AbstractHorse.java) plays entity.horse.land (baby variant for babies) at 0.4 for falls over one block.
2026-10-29 22:19:03 +01:00
Kacpherek 1139057d3e Play the camel saddle sound when a camel is saddled by hand
Saddling a camel with a saddle in hand played the horse sound; Camel.getSaddleSound (Camel.java) is the camel's saddle equip sound.
2026-10-29 22:19:03 +01:00
Kacpherek 9f3cba308c Send a flap game event from a flying bee every other tick
A bee in the air never gave off its flap vibration, so sculk sensors never heard it; Entity.processFlappingMovement with Bee.isFlapping (Bee.java) emits minecraft:flap.
2026-10-29 22:19:03 +01:00
Kacpherek 80b68bae0c Hold a sitting camel's rotation under its rider
A camel that refuses to move (sitting or changing pose) still turned with its rider's view; Camel.getRiddenRotation (Camel.java) keeps its rotation.
2026-10-29 22:19:03 +01:00
Kacpherek 48efd53e00 Breed axolotl babies with a random parent's colour and a rare blue
Axolotl.getBreedOffspring took the first parent's colour, never blue and never persistent; vanilla rolls one in 1200 for blue, else picks either parent and sets the baby persistent (Axolotl.java).
2026-10-29 22:19:03 +01:00
Kacpherek c7440b78d5 Hang a bee's lead at half its eye height
The lead hung at the default leash offset; Bee.getLeashOffset (Bee.java) puts it at half the eye height, a fifth of the width ahead.
2026-10-29 22:19:03 +01:00
Kacpherek 0703ae6bd4 Keep a mannequin's profile when it is saved and loaded
A saved mannequin lost its profile, so after a reload it wore the default skin. Vanilla saves and reads it as ResolvableProfile (Mannequin.addAdditionalSaveData / readAdditionalSaveData, Mannequin.java:126 and :144).
2026-10-29 22:19:02 +01:00
Kacpherek ec6ea46499 Break item frames caught in an explosion
A blast never reached item frames, so they stayed up and kept their item. Vanilla kills a non-fixed frame in reach and drops it with its item (BlockAttachedEntity.hurtServer, ServerExplosion.hurtEntities; ItemFrame.java:173).
2026-10-29 22:19:02 +01:00
Kacpherek 62c4d91aa5 Send the hat layer change to every player's tab list
A guest who toggled the hat skin layer was not announced: vanilla sends UPDATE_HAT to all players in handleClientInformation (ServerGamePacketListenerImpl.java:2180), and the join entry now carries the hat state too.
2026-10-29 22:19:02 +01:00
Kacpherek d5d42c8a5f Let ravagers trample crops they walk through
Ravagers only broke leaves, so crops in their path survived; CropBlock.entityInside (CropBlock.java:153) destroys a crop a ravager's box enters when mobGriefing is on.
2026-10-29 22:19:02 +01:00
Kacpherek 765ed5d0c2 Fire the thrown-item pickup advancement for the player
Picking up an item the player threw never granted thrown_item_picked_up_by_player, so You've Got a Friend in Me was unreachable; ServerPlayer.onItemPickup (ServerPlayer.java:2324) fires it with the stack left after the take.
2026-10-29 22:19:02 +01:00
Kacpherek a63b69c300 Fire thrown-item pickup advancement for piglins
A piglin taking gold you threw never granted thrown_item_picked_up_by_entity, so Oh Shiny stayed locked; LivingEntity.onItemPickup (LivingEntity.java:3429) fires it for the taker, before the stack is split.
2026-10-29 22:19:02 +01:00
Kacpherek 126f997703 Make live shulkers solid for the player
Walking ignored live shulkers, so the player passed through them; Shulker.canBeCollidedWith (Shulker.java:487) returns isAlive and Entity.canCollideWith uses it.
2026-10-29 22:19:02 +01:00
Kacpherek 6befa8622d Scale the LAN host's entity tracking by the Entity Distance option
The integrated host ignored entityDistanceScaling, so guests saw entities at the default range; IntegratedServer.getScaledTrackingDistance (IntegratedServer.java:451) scales each tracking range by it.
2026-10-29 22:19:02 +01:00
Kacpherek 4fe17c268c Show spectating guests as invisible to others
A guest in spectator mode sent no invisible flag, so others saw a full model; ServerPlayer.updateInvisibilityStatus (ServerPlayer.java:2054) sets it for every spectator.
2026-10-29 22:19:02 +01:00
Kacpherek 1474b118b4 Break a straw bed when its sleeper gets up in the Overworld
A straw bed stayed in place after its sleeper woke up, but the Overworld's straw_bed_rule has destroy_on_leave, so vanilla breaks it (AbstractBedBlock.java:119 onStopSleeping, StrawBedBlock.java destroyBed).
2026-10-29 22:18:57 +01:00
Kacpherek e9808d0d13 Move online guests to the default mode when game mode is forced
MinecraftServer.enforceGameTypeForPlayers sets every player to the server mode when it is forced; guests already online kept their own mode and only got the default at join (MinecraftServer.java:1412).
2026-10-29 22:18:56 +01:00
Kacpherek a8a6485243 Send LAN guests the new difficulty on /difficulty
MinecraftServer.setDifficulty sends ChangeDifficulty to every player; /difficulty changed only the host, so guests kept the old difficulty until they rejoined (MinecraftServer.java:1307).
2026-10-29 22:18:56 +01:00
Kacpherek 1f9fdd9129 Pick a mooshroom calf's colour like MushroomCow
A mooshroom calf took its first parent's colour every time; MushroomCow.getOffspringVariant gives either parent's colour, and the other one 1 in 1024 times when both match (MushroomCow.java:275).
2026-10-29 22:18:56 +01:00
Kacpherek 8d3541a364 Save and load a mooshroom's colour under Type
MushroomCow.addAdditionalSaveData and readAdditionalSaveData store the colour as Type; only foxes wrote it, so a brown mooshroom reloaded red (MushroomCow.java:214, :221).
2026-10-29 22:18:56 +01:00
Kacpherek ed279f27d5 Draw the leather overlay on worn armour and armour stands
EquipmentLayerRenderer.renderLayers draws every layer of an equipment asset; only layer 0 was loaded, so leather armour lost its overlay layer (leather.json, EquipmentLayerRenderer.java:51).
2026-10-29 22:18:56 +01:00
Kacpherek 0d598edee8 Draw tipped arrow particles and the stuck-arrow shake on the client side
The window and LAN guests are clients of the server thread, so the arrow
effects added for Arrow.tick, Arrow.handleEntityEvent and ArrowModel.setupAnim
never reached them: the server's arrow drew its particles into a world nobody
sees, and a client's copy of an arrow never held a shake timer.

The server no longer draws or randomises Arrow.makeParticle (it is the
client's `Level.isClientSide` branch). It sends Arrow.ID_EFFECT_COLOR with a
tipped arrow's entity data and entity event 0 when the arrow has been stuck
for 600 ticks. A client ticks the particles itself (two in flight, one every
fifth tick in a block, the client's own random) and bursts twenty on event 0.
A client's arrow now starts its shake when it lands (AbstractArrow
.onSyncedDataUpdated) and runs it down each tick, so the roll shows.
2026-10-29 22:18:56 +01:00
Kacpherek 0b2e9da854 Burst tipped arrows and drop the potion after 600 ticks
A tipped arrow left stuck for 600 ticks kept its potion and never burst. Vanilla Arrow.tick sends entity event 0 (20 particles in the potion colour) and sets a plain arrow as the pickup item.
2026-10-29 22:18:56 +01:00
Kacpherek e701c9f458 Trail tipped arrows with their potion colour
Tipped arrows left no entity-effect particles in flight or stuck in a block. Vanilla Arrow.tick and Arrow.makeParticle draw two per tick in flight and one every fifth tick in the ground (Arrow.java).
2026-10-29 22:18:55 +01:00
Kacpherek 720381e3cf Crack skulls, shulker boxes, bells, lecterns and statues while broken
Only chests and banners showed the breaking overlay. Vanilla SkullBlockRenderer, ShulkerBoxRenderer, BellRenderer, LecternRenderer and CopperGolemStatueBlockRenderer submit their models for crumbling too.
2026-10-29 22:18:55 +01:00
Kacpherek 6fb33260ae Let empty buckets scoop bubble columns
An empty bucket on a bubble column did nothing; vanilla BubbleColumnBlock.pickupBlock removes the column and returns a water bucket.
2026-10-29 22:18:55 +01:00
Kacpherek 0559503258 Fire thrown-item pickup advancements for allay deliveries
Taking an item an entity threw never fired thrown_item_picked_up_by_player and allay deliveries had no thrower. Vanilla ServerPlayer.onItemPickup and BehaviorUtils.throwItem (PickedUpItemTrigger) do both.
2026-10-29 22:18:55 +01:00
Kacpherek 69d060efe6 Hang an allay's lead at 0.6 of its eye height
Allay.getLeashOffset (Allay.java:484) puts the rope at 0.6 of the eye height and 0.1 of the width; the lead used the default offset.
2026-10-29 22:18:55 +01:00
Kacpherek 0c9e6293ee Roll stuck arrows while their shake timer runs
A stuck arrow kept a fixed model pose; vanilla ArrowModel.setupAnim (ArrowRenderer) rolls the root by -sin(shake*3)*shake degrees while shakeTime lasts.
2026-10-29 22:18:55 +01:00
Kacpherek 0db93570be Apply spectator fog to powder snow
Spectators saw normal powder snow fog; vanilla PowderedSnowFogEnvironment.setupFog uses start -8 and half the render distance for spectators.
2026-10-29 22:18:55 +01:00
Kacpherek 4fcaff9935 Give a water bucket back for a tropical fish bucket fed to an axolotl
Feeding an axolotl a tropical fish bucket used the whole item; vanilla hands back a water bucket (Axolotl.usePlayerItem in Axolotl.java).
2026-10-29 22:18:54 +01:00
Kacpherek 0554db3906 Calm neutral mobs when the player who angered them dies
With forgive_dead_players on, wolves, bees, polar bears and other neutral mobs kept their anger after the player died. ServerPlayer.tellNeutralMobsThatIDied (ServerPlayer.java:991) makes mobs within 32 by 10 blocks run NeutralMob.playerDied, which stops the anger at that player (NeutralMob.java:122).
2026-10-29 22:18:54 +01:00
Kacpherek e94f1917a7 Give bred goats their cooldowns and screaming inheritance
A bred baby goat started without ram or long jump cooldowns and was never screaming. Goat.getBreedOffspring (Goat.java:152) runs GoatAi.initMemories, then takes screaming from a random parent or rolls 2 percent.
2026-10-29 22:18:54 +01:00
Kacpherek 713b2774aa Swim turtles with their own water travel
Turtles used the generic fluid travel in water (0.02 speed, 0.8 drag). Turtle.travelInWater (Turtle.java:266) moves at 0.1 with 0.9 drag and sinks slowly unless it has a target or is going home near its home.
2026-10-29 22:18:54 +01:00
Kacpherek f448660e4c Let rabbits, foxes and silverfish climb out of powder snow
Mobs stuck in powder snow never jumped out: the goal was missing from their registrations. Vanilla ClimbOnTopOfPowderSnowGoal jumps when the block above is powder snow or open (ClimbOnTopOfPowderSnowGoal.java:23).
2026-10-29 22:18:52 +01:00
Kacpherek d77cca177a Show the white trail of a spectral arrow in flight
A spectral arrow in flight left no trail; vanilla adds a white spell particle each tick while it is not in the ground (SpectralArrow.java:34).
2026-10-29 22:18:52 +01:00
Kacpherek 0a353d57c9 Chime amethyst blocks when a projectile hits them
A projectile striking an amethyst block played nothing; vanilla plays block.amethyst_block.chime at the block with a random pitch (AmethystBlock.java:18).
2026-10-29 22:18:52 +01:00
Kacpherek 1f21c74b01 Hide bogged mushrooms once it is sheared
A sheared bogged kept its mushrooms on the head. BoggedModel.setupAnim (BoggedModel.java:62) sets the mushrooms part visible only while the bogged is not sheared.
2026-10-29 22:18:52 +01:00
Kacpherek 882e3a3005 Run the click event of text in dialog messages
Clicking styled text in a dialog message or item description did nothing. DialogBodyHandlers.java:54 and :108 run the text's click event through DialogScreen.runAction.
2026-10-29 22:18:51 +01:00
Kacpherek fecb404485 Hide saddle reins on a horse nobody rides
A saddled horse with no rider drew its left and right saddle lines. EquineSaddleModel.setupAnim (EquineSaddleModel.java:64) shows the riding parts only while the horse is ridden.
2026-10-29 22:18:51 +01:00
Kacpherek d684cafca1 Read the saved version id and name as LevelVersion.parse does
The downgrade check compared Data.DataVersion; vanilla LevelVersion.parse takes Version.Id (no Version reads as 0, no Id as the current one) and names a Version without a Name as the current version. Both world checks now share that reading, and the play doc lists the new outcomes.
2026-10-29 22:18:51 +01:00
Kacpherek 7f19d9f95b Name mobs and armor stands a dispenser places from a renamed item
The dispenser paths for spawn eggs and armor stands skipped the stack's custom name that the hand-placed paths now apply. Entity.applyImplicitComponents (Entity.java:4281) names the entity on every spawn from an item.
2026-10-29 22:18:51 +01:00
Kacpherek 20ad245e02 Ask for a backup before opening a world saved by a newer version
Horizon opened a newer world without asking. WorldOpenFlows.openWorldCheckVersionCompatibility (WorldOpenFlows.java:352) asks for a backup first, with the red downgrade question (BackupStatus.DOWNGRADE in LevelSummary.java).
2026-10-29 22:18:51 +01:00
Kacpherek 54bb09a261 Name an armor stand placed from a renamed item
A renamed armor stand item placed an unnamed stand. EntityType.createDefaultStackConfig (EntityType.java:171) applies the stack's components, custom name included, through Entity.applyImplicitComponents (Entity.java:4281).
2026-10-29 22:18:51 +01:00
Kacpherek 6840b57201 Show the incompatible version alert instead of opening such a world
Horizon opened a world from another version series without a check. WorldOpenFlows.openWorldCheckVersionCompatibility (WorldOpenFlows.java:352) shows the red Incompatible version AlertScreen and keeps the world closed.
2026-10-29 22:18:51 +01:00
Kacpherek 739185e2c9 Make piglins walk only to wanted items they can see
Piglins took the nearest wanted item within 9 blocks even behind a wall. NearestItemSensor.doTick (NearestItemSensor.java:24) takes the nearest visible one within 32, then GoToWantedItem walks to it within 9 (PiglinAi).
2026-10-29 22:18:51 +01:00
Kacpherek c8477304a3 Let mobs slide down honey blocks
Mobs fell through honey blocks; only the player slid. HoneyBlock.entityInside (HoneyBlock.java:75) slows any living entity beside the block, with the slide sound and the entity event 53 particles.
2026-10-29 22:18:51 +01:00
Kacpherek c97f4da629 Make allays pick up only the items they can see
An allay walked to a matching item behind a wall. NearestItemSensor.doTick (NearestItemSensor.java:24) keeps only items the mob has a line of sight to.
2026-10-29 22:18:51 +01:00
Kacpherek dc5695dfa7 Mark worlds another running copy has open as locked
The world list showed a world open in another instance as an ordinary world. DirectoryLock.isLocked (DirectoryLock.java:66) and LevelSummary.createInfo (LevelSummary.java:154) show it as Locked in red and it cannot be joined.
2026-10-29 22:18:51 +01:00
Kacpherek 6ec46d1a81 Pick the piston a piston head belongs to
Pick block on a piston head gave nothing, since the head has no item form. PistonHeadBlock.getCloneItemStack (PistonHeadBlock.java:116) gives a sticky or plain piston by the head's type.
2026-10-29 22:18:51 +01:00
Kacpherek 925580fe3d Name the mob a renamed spawn egg spawns
A spawn egg renamed in an anvil spawned an unnamed mob; the stack's custom name was never applied. Entity.applyImplicitComponents (Entity.java:4281) sets the custom name from the item on spawn.
2026-10-29 22:18:51 +01:00
Kacpherek 8aeb55f0cf Apply x_rotation and y_rotation ranges in entity selectors
Selectors parsed the rotation ranges but never filtered on them, so @e[y_rotation=...] matched regardless of facing. EntitySelectorParser.createRotationPredicate (EntitySelectorParser.java:188) checks the wrapped pitch and yaw.
2026-10-29 22:18:50 +01:00
Kacpherek 17383e6fe0 Hold items in an allay's own hand transform
Allay items were placed with the humanoid hand offset; AllayModel.translateToHand (AllayModel.java:120) uses the body, the right arm's pitch, a 0.7 scale and a shift.
2026-10-29 22:18:50 +01:00
Kacpherek f31e30a081 Select the word under a double click on a book page
A double click on a book page only moved the caret, so no word was selected. Vanilla MultiLineEditBox.onClick and MultilineTextField.selectWordAtCursor (MultiLineEditBox.java:89, MultilineTextField.java:184) select the word before the cursor.
2026-10-29 22:18:46 +01:00
Kacpherek 913ad15462 Let Ctrl plus the mouse wheel change the GUI scale on the Video page
The wheel only scrolled the list there, and Ctrl was ignored. VideoSettingsScreen.mouseScrolled (File.java:263) steps guiScale by one and resets the list to the top instead.
2026-10-29 22:18:46 +01:00
Kacpherek 1976c27870 Join the selected world with Enter or Space on the world list
Enter and Space on a selected world did nothing, so keyboard players could not join from the list; vanilla WorldListEntry.keyPressed joins on selection keys (WorldSelectionList.java:602).
2026-10-29 22:18:46 +01:00
Kacpherek 7af0a21f2d Join the typed server address on Enter in Direct Connection
Enter did nothing on Direct Connection; vanilla DirectJoinServerScreen.keyPressed selects the server when the Join button is active (DirectJoinServerScreen.java:32).
2026-10-29 22:18:46 +01:00
Kacpherek 7a2121d84f Create the world on Enter in Create New World
Enter only confirmed a world rename, so it did nothing on Create New World; vanilla CreateWorldScreen.keyPressed calls onCreate on confirmation (CreateWorldScreen.java:360).
2026-10-29 22:18:46 +01:00
Kacpherek 3d5407ab11 Show the ghost recipe item tooltip on hover
Hovering a ghost ingredient or result in a crafting grid or furnace showed nothing; vanilla GhostSlots.extractTooltip shows the ghost item for the hovered slot (GhostSlots.java:65).
2026-10-29 22:18:46 +01:00
Kacpherek ef890e667f Show the item tooltip on hover in the statistics Items tab
Hovering an item icon in the Items tab showed nothing; vanilla ItemRowWidget.extractTooltip draws the item tooltip at the slot's corner (StatsScreen.java:512).
2026-10-29 22:18:46 +01:00
Kacpherek b5bcf031e5 Name a shield after its banner base colour
A shield with a banner base colour showed as plain Shield; vanilla ShieldItem.getName uses item.minecraft.shield.<colour> (ShieldItem.java:12).
2026-10-29 22:18:46 +01:00
Kacpherek 68cfbd61da Make dolphins flee guardians and ignore their hits
Dolphins ignored guardians and took their hits as attacks; Dolphin.registerGoals (Dolphin.java:158) adds AvoidEntity for Guardian and HurtByTargetGoal ignoring Guardian.
2026-10-29 22:18:45 +01:00
Kacpherek c61911ffcd Play the dried ghast sound when it takes water
A dried ghast filled with water made no place-in-water sound. Vanilla DriedGhastBlock.placeLiquid (DriedGhastBlock.java:174) plays DRIED_GHAST_PLACE_IN_WATER after scheduling the fluid tick.
2026-10-29 22:18:45 +01:00
Kacpherek a9e1f452df Break a lily pad that a boat drives over
A boat passing over a lily pad left it in place. Vanilla LilyPadBlock.entityInside (LilyPadBlock.java:27) destroys the pad with its drops for an AbstractBoat on a server level.
2026-10-29 22:18:45 +01:00
Kacpherek 1e3f05fe8c Make the spear-ai-golem fixes build on current main 2026-10-29 22:18:23 +01:00
Ludovic Marie eb19c2723b Merge pull request #3522 from Domanffe/i18n/own-screens-remainder-2
🤖 Agent: pr-triage-2 (Ludovic's AI agent, Claude): proven at d8e912738 in union/pt2-e3 (17 PRs on main cbea22fbb): parity --changed passes but for main's own failures, lan_e2e on both paths, policy read (Mojang rules, supply chain, speed). Merging.
2026-10-29 22:18:20 +01:00
Ludovic Marie ccc323a1b7 Merge pull request #3374 from ThatCakeIsAPie/feat/upgrade-datapack-4
🤖 Agent: pr-triage-2 (Ludovic's AI agent, Claude): proven at a717d6d58 in union/pt2-e3 (17 PRs on main cbea22fbb): parity --changed passes but for main's own failures, lan_e2e on both paths, policy read (Mojang rules, supply chain, speed). Merging.
2026-10-29 22:18:15 +01:00
Ludovic Marie d242616d68 Merge pull request #3373 from ThatCakeIsAPie/feat/upgrade-datapack-3
🤖 Agent: pr-triage-2 (Ludovic's AI agent, Claude): proven at 1ffe68635 in union/pt2-e3 (17 PRs on main cbea22fbb): parity --changed passes but for main's own failures, lan_e2e on both paths, policy read (Mojang rules, supply chain, speed). Merging.
2026-10-29 22:18:11 +01:00
DenisHumenandClaude Opus 5.5 d1a62e68ec Golems: build a snow or iron golem in any orientation, as vanilla does
CarvedPumpkinBlock.trySpawnGolem uses BlockPattern.find, which tries every
corner in the box from the pumpkin and every forwards/up pair, so the snow
column and the iron T also count lying down or upside down. Horizon only
checked the upright column and the upright T with arms along X or Z.

find_pattern follows find and translateAndRotate; the cells and the spawn
base come from the match (getBlock(0, 2) for snow, getBlock(1, 2) for iron).

Fixes #3929

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 23:18:08 +02:00
Kacpherek 45e1d44be2 Draw the candle snuff smoke on the clicking client
In single player the server side skips the wick smoke, so the client draws it itself, as CandleBlock.useItemOn does on the client (AbstractCandleBlock.extinguish).
2026-10-29 22:15:19 +01:00
Kacpherek e0a1b471ff Draw the block in a copper golem's antenna
The block item in a copper golem's antenna slot was never drawn. BlockDecorationLayer.submit (BlockDecorationLayer.java:33) draws it at CopperGolemModel.applyBlockOnAntennaTransform (CopperGolemModel.java:311).
2026-10-29 22:15:13 +01:00
Kacpherek 131458ee0f Play the head's sound from a note block under a custom head
A note block with a player head on top always clicked, ignoring the head's note sound. NoteBlock.getCustomSoundId (NoteBlock.java:167) takes the sound from the skull above; no skull or sound means silence.
2026-10-29 22:15:13 +01:00
Kacpherek 90db82ba0e Hold a witch's potion at her nose instead of in her hand
A drinking witch's potion was drawn in the hand with no potion contents. WitchItemLayer.applyTranslation (WitchItemLayer.java:17) holds the potion at the nose as a potion item.
2026-10-29 22:15:13 +01:00
Kacpherek d21886b2f3 Snuff a lit candle with an empty hand
An empty hand on a lit candle did nothing; only arrows lit them. CandleBlock.useItemOn (CandleBlock.java:75) calls AbstractCandleBlock.extinguish: unlit, wick smoke, candle extinguish sound, block_change.
2026-10-29 22:15:13 +01:00
Kacpherek f250e84ecf Refuse item counts above the max stack in item commands
item replace/fill/override accepted counts up to 99 above an item's max stack. ItemInput.createItemStack (ItemInput.java:21) throws arguments.item.overstacked instead.
2026-10-29 22:15:05 +01:00
Kacpherek a1454e00a2 Show the entity tooltip for chat hover events with advanced tooltips
Hovering a show_entity chat part showed nothing. With advancedItemTooltips on, vanilla lists the name, the entity type and the UUID (GuiGraphicsExtractor.componentHoverEffect; HoverEvent.java:84).
2026-10-29 22:15:05 +01:00
Kacpherek 994820e697 Hold a spyglass to the eye for other players
A spyglass in use was drawn in the hand, not at the eye. PlayerItemInHandLayer.renderItemHeldToEye (PlayerItemInHandLayer.java:40) clamps the head pitch to -30 to 90 degrees and holds the item on the head.
2026-10-29 22:15:05 +01:00
Kacpherek 75b37506ab Draw skulls worn in the mob head slot
Zombie, skeleton, creeper and player heads in a mob's head slot were not drawn. CustomHeadLayer.submit (CustomHeadLayer.java:44) puts the skull model on the head, and resolveSkullRenderType (CustomHeadLayer.java:69) takes a player head's skin from its profile.
2026-10-29 22:15:05 +01:00
Kacpherek 8ef51c4b69 Sweep a brush in use like vanilla
A brush in use was held still. FirstPersonHandsAndItemsRenderer.applyBrushTransform (FirstPersonHandsAndItemsRenderer.java:286) swings it back and forth over the use.
2026-10-29 22:15:05 +01:00
Kacpherek c2bd40f270 Show the pointing hand over chat insertion text while Shift is held
Hovering a chat part with an insertion never asked for the pointing hand; vanilla requests it in ChatComponent.java:672 when Shift is held (ChatScreen passes insertionClickMode).
2026-10-29 22:15:05 +01:00
Kacpherek 245ea982b9 Show oak plank crumbs when an armor stand breaks
A stand broken by a creative hand or the second hit made no plank particles; ArmorStand.showBreakingParticles sends ten oak plank block particles from the box (ArmorStand.java:369).
2026-10-29 22:15:04 +01:00
Kacpherek 6528d566a1 Hide spell particles near the eye while scoping with a spyglass
Spell particles stayed visible in the spyglass view; SpellParticle.isCloseToScopingPlayer sets their alpha to 0 within 3 blocks of the eye in first person while isScoping (SpellParticle.java).
2026-10-29 22:15:04 +01:00
Kacpherek 89d4b1ce31 Tilt the iron golem body side to side while it walks
Only the arms and legs swung; IronGolemRenderer.setupRotations also rotates the body about Z by 6.5 degrees of a triangle wave of the walk position.
2026-10-29 22:15:04 +01:00
Kacpherek 2010ef412f Draw the wool undercoat on a white jeb_ sheep
A white sheep named jeb_ had no undercoat; SheepWoolUndercoatLayer.submit draws it for isJebSheep as well as for a dyed wool.
2026-10-29 22:15:04 +01:00
Kacpherek 0e42a29fda Cycle the wool of a jeb_ sheep through the dye colours
A sheep named jeb_ kept its dyed colour; SheepRenderState.getWoolColor fades it through ColorLerper.getLerpedColor(SHEEP) (SheepRenderer.java, MobRenderer.checkMagicName).
2026-10-29 22:15:04 +01:00
Kacpherek 56b327bd94 Puff red dust when a lever is switched on
Switching a lever on showed no dust puff at the lever. LeverBlock.useWithoutItem (LeverBlock.java:56) calls makeParticle at full scale on the clicking client when the lever ends up powered.
2026-10-29 22:15:01 +01:00
Kacpherek f6df8b8339 Show the red dust burst when a redstone ore is touched
Hitting, stepping on or right-clicking a redstone ore lit it with no particles. RedStoneOreBlock.interact (RedStoneOreBlock.java:61) calls spawnParticles each time, lit or not.
2026-10-29 22:15:00 +01:00
Kacpherek 6529206304 Keep the light level when a light block is picked
Pick-block on a light gave a level 0 light. LightBlock.getCloneItemStack and setLightOnStack (LightBlock.java:100,104) put the level into the item's block state.
2026-10-29 22:14:42 +01:00
Kacpherek 88dc2c5f8d Make the piglin and hoglin fixes build on current main 2026-10-29 22:14:32 +01:00
Kacpherek bb7b1b27ad Play the dried ghast place sound when it is put down
Placing a dried ghast was silent. DriedGhastBlock.setPlacedBy (DriedGhastBlock.java:189) plays block.dried_ghast.place, or place_in_water when waterlogged.
2026-10-29 22:14:25 +01:00
Kacpherek a68a897af5 Break cushions struck by lightning
A bolt's box left cushions intact. Cushion.thunderHit (Cushion.java:138) kills the cushion and drops its item, as break_cushion does.
2026-10-29 22:14:25 +01:00
Kacpherek 296fd3d839 Break one turtle egg at a time when a survival player mines a stack
Mining a stack of 2 to 4 turtle eggs removed the whole stack; TurtleEggBlock.playerDestroy (TurtleEggBlock.java:138, decreaseEggs) plays the break and keeps the rest.
2026-10-29 22:14:25 +01:00
Kacpherek 33f3444a49 Chain a hanging sign onto a wall hanging sign
Right-clicking the side of a wall hanging sign with a hanging sign opened the sign instead of chaining. WallHangingSignBlock.useItemOn passes the use on when the hit is not on the editable faces (WallHangingSignBlock.java:53).
2026-10-29 22:14:25 +01:00
Kacpherek 2711a4a005 Break pointed dripstone hit by a fast thrown trident
A thrown trident faster than 0.6 that stuck in pointed dripstone left it standing. SpeleothemBlock.onProjectileHit destroys it, reading the deltaMovement from before the hit (SpeleothemBlock.java:181, AbstractArrow.onHitBlock).
2026-10-29 22:14:25 +01:00
Kacpherek c37cab6b6a Show vault connection particles for every connected player
Only the local player got the connection particles from a vault, so guests connected to it saw none. VaultBlockEntity.Client.emitConnectionParticlesForNearbyPlayers loops over every connected player (VaultBlockEntity.java:172).
2026-10-29 22:14:24 +01:00
Kacpherek cbf57eb6ee Keep a copper golem statue's pose when it is picked
Middle-click on a statue gave it standing, whatever its pose. CopperGolemStatueBlock.getCloneItemStack sets the block_state component to the pose (CopperGolemStatueBlockEntity.getItem, CopperGolemStatueBlock.java:134).
2026-10-29 22:14:24 +01:00
Kacpherek e1c7a297bc Pick a decorated pot with its sherds
Middle-click on a decorated pot gave an empty pot. DecoratedPotBlock.getCloneItemStack returns createDecoratedPotInstance(decorations), so the sherds come with the pick; the pot's item only comes with include_data (DecoratedPotBlock.java:211).
2026-10-29 22:14:24 +01:00
Kacpherek bc3161c619 Ring a bell hit by a wind charge burst
A wind charge burst that reached a bell sent a use on its top face, which the bell rejected, so it stayed silent. BellBlock.onExplosionHit calls attemptToRing with no direction, so the bell rings on its facing (BellBlock.java:197).
2026-10-29 22:14:24 +01:00
Kacpherek dd649971c8 Pick the potted plant instead of an empty flower pot
Middle-click on a flower pot with a plant gave an empty pot. FlowerPotBlock.getCloneItemStack returns new ItemStack(potted) when the pot is not empty (FlowerPotBlock.java:115).
2026-10-29 22:14:24 +01:00
Kacpherek 50a1911cea Play the creaking heart idle sound at night with logs on all sides
A non-uprooted creaking heart with pale oak logs on six sides never made its idle creak; CreakingHeartBlock.animateTick (CreakingHeartBlock.java:65) plays it 1 time in 16 while CREAKING_ACTIVE is on.
2026-10-29 22:14:24 +01:00
Kacpherek b82ebb2261 Snuff a lit candle cake with an empty hand
A right-click with an empty hand on the lit candle of a candle cake did nothing. CandleCakeBlock.useItemOn extinguishes it when the hit is above half a block, with the extinguish sound and a block change event.
2026-10-29 22:14:24 +01:00
Kacpherek 8880e0d373 Play the hoglin retreat grunt near a repellent
A hoglin near a repellent made no retreat grunt; vanilla plays entity.hoglin.retreat there (HoglinAi.java:231 getSoundForActivity).
2026-10-29 22:13:47 +01:00
Kacpherek b9ddeb1820 Start a gliding player's rocket half a block out at the rocket hand
The rocket of a gliding player started at the player's feet; vanilla offsets it 0.5 block toward the hand holding it (Entity.java:2688 getHandHoldingItemAngle, FireworkRocketEntity.java:138).
2026-10-29 22:13:41 +01:00
Kacpherek 50121630ce Ignore knockback on a creaking that cannot move
A creaking a player looks at still got shoved by hits; vanilla ignores knockback while it cannot move (Creaking.java:451 knockback).
2026-10-29 22:13:41 +01:00
Kacpherek edd7a9cb61 Spawn the crumbs when a held tool or armour piece breaks
A tool or armour piece that broke made no crumb particles; vanilla spawns five item crumbs for it (LivingEntity.java:1516 breakItem).
2026-10-29 22:13:41 +01:00
Kacpherek 5270924ba9 Show the crumbs of the food a fox is eating
Fox.handleEntityEvent(45) throws eight item crumbs from in front of its mouth; the server broadcasts 45 with the eating sound (Fox.java:289, :227).
2026-10-29 22:13:41 +01:00
Kacpherek 97eda2b7f5 Play the tadpole grow-up sound at volume 0.15
Tadpole.ageUp plays TADPOLE_GROW_UP at 0.15 from the tadpole's position, not at full volume from the eyes (Tadpole.java:227).
2026-10-29 22:13:41 +01:00
Kacpherek 86c1795ec6 Save and load a fox's sleeping, sitting and crouching flags
Fox.addAdditionalSaveData writes Sleeping, Sitting and Crouching from its flags and Fox.readAdditionalSaveData reads them back; a reload no longer wakes a sleeping fox (Fox.java:445). Trusted players are still not ported.
2026-10-29 22:13:41 +01:00
Kacpherek b469038d45 Feed a saddled pig held food instead of mounting it
Pig.mobInteract and Strider.mobInteract take a click holding food to Animal.mobInteract before any ride (Pig.java:167, Strider.java:401).
2026-10-29 22:13:41 +01:00
Kacpherek d17c1bc0e9 Keep a tame nautilus near its home, within 32 or 16 blocks
AbstractNautilus.checkRestriction sets the home at the nautilus with getNautilusRestrictionRadius (32, or 16 for a baby or saddled one) when it is not leashed or ridden (AbstractNautilus.java:237, :241).
2026-10-29 22:13:41 +01:00
Kacpherek ec77633cfe Let a cat lured with fish approach its picked player unscared
CatTemptGoal keeps a selectedPlayer re-rolled every 500 to 600 ticks and does not scare it (Cat.java:677).
2026-10-29 22:13:41 +01:00
Kacpherek 5909dc68b6 Lower a sheep's head to eat grass on every client
Sheep.handleEntityEvent(10) sets eatAnimationTick to 40 and EatBlockGoal.start broadcasts event 10 (Sheep.java:119, EatBlockGoal.java:40); the server now sends it and clients start and count down the eating head.
2026-10-29 22:13:41 +01:00
Kacpherek bb7adf1207 Cull lightning bolts beyond 64 blocks times the view scale
A bolt past 64 blocks (scaled by the entity distance) was still drawn; vanilla keeps only the sky flash and thunder there (LightningBolt.java:250).
2026-10-29 22:13:41 +01:00
Kacpherek 97294e3838 Hurt an Enderman that a splash water potion lands near
A water splash did nothing to an Enderman; AbstractThrownPotion.affectEntitiesAround hurts it 1 and Enderman.hurtWithCleanWater makes it teleport.
2026-10-29 22:13:41 +01:00
Kacpherek 01cf25687f Mix both parents' dye colours for sheep, wolf and cat babies
A baby took one parent's colour by a coin flip; DyeColor.getMixedColor mixes them with the two-dye crafting recipe, else picks one (Sheep.java:281, Wolf.java:608, Cat.java:383).
2026-10-29 22:13:41 +01:00
Kacpherek 2151ef21f2 Put a wolf's leash rope at 0.6 of its eye height
The rope point was at full eye height; Wolf.getLeashOffset (Wolf.java:659) uses 0.6 times it.
2026-10-29 22:13:41 +01:00
Kacpherek d686f4b9d7 Play the iron golem damage sound when it cracks
Hits that moved an iron golem to a new crack level were silent; IronGolem.hurtServer plays IRON_GOLEM_DAMAGE then (IronGolem.java:209).
2026-10-29 22:13:41 +01:00
Kacpherek 2b90acbc0d Play the fox bite sound after each melee strike
A fox's melee strike made no sound; FoxMeleeAttackGoal plays FOX_BITE after doHurtTarget (Fox.java:1101).
2026-10-29 22:13:41 +01:00
Kacpherek 2e49c68051 Keep turtles silent in water and as babies
Turtles in water or babies played the land ambient; Turtle.getAmbientSound plays TURTLE_AMBIENT_LAND only on land and grown up, else Mob's silent default (Turtle.java:176).
2026-10-29 22:13:41 +01:00
Kacpherek aa30c8e9d2 Growl the angry zoglin sound while it has a target
A zoglin with an attack target grunted its ambient instead of entity.zoglin.angry; Zoglin.getAmbientSound picks ZOGLIN_ANGRY when ATTACK_TARGET is set (Zoglin.java:286).
2026-10-29 22:13:41 +01:00
Kacpherek 4dcb669759 Play the fox sleep sound while a fox naps
A sleeping fox played its day ambient; Fox.getAmbientSound returns FOX_SLEEP while sleeping (Fox.java:677).
2026-10-29 22:13:40 +01:00
abit-foggy 40b0467e20 fix(renderer): use flat draws on Intel Gen11 Vulkan to prevent section flicker (#3529) 2026-10-29 11:12:39 -10:00
Ludovic Marie 6175960663 Merge pull request #3372 from ThatCakeIsAPie/feat/upgrade-datapack-2b
🤖 Agent: pr-triage-2 (Ludovic's AI agent, Claude): proven at 217216e26 in union/pt2-e3 (17 PRs on main cbea22fbb): parity --changed passes but for main's own failures, lan_e2e on both paths, policy read (Mojang rules, supply chain, speed). Merging.
2026-10-29 22:12:19 +01:00
DUzzL ada08437e5 Turtle: silent in water, in the air and as a baby (Turtle.getAmbientSound) 2026-10-29 22:12:16 +01:00
Ludovic Marie 7eaa770716 Merge pull request #3370 from ThatCakeIsAPie/feat/upgrade-datapack-2a
🤖 Agent: pr-triage-2 (Ludovic's AI agent, Claude): proven at 825f8f884 in union/pt2-e3 (17 PRs on main cbea22fbb): parity --changed passes but for main's own failures, lan_e2e on both paths, policy read (Mojang rules, supply chain, speed). Merging.
2026-10-29 22:12:15 +01:00
Ludovic Marie 7d6a05e18f Merge pull request #2847 from sirrayi/fix/release-notes-contributors-heading
🤖 Agent: pr-triage-2 (Ludovic's AI agent, Claude): proven at c455bb6ec in union/pt2-e3 (17 PRs on main cbea22fbb): parity --changed passes but for main's own failures, lan_e2e on both paths, policy read (Mojang rules, supply chain, speed). Merging.
2026-10-29 22:12:11 +01:00
Ludovic Marie fab10b1922 Merge pull request #2821 from levzzz5154/fix/death-message-item-name
🤖 Agent: pr-triage-2 (Ludovic's AI agent, Claude): proven at a11e54e7b in union/pt2-e3 (17 PRs on main cbea22fbb): parity --changed passes but for main's own failures, lan_e2e on both paths, policy read (Mojang rules, supply chain, speed). Merging.
2026-10-29 22:12:06 +01:00
Ludovic Marie 8a363878ee Merge pull request #2570 from levzzz5154/fix/world-types
🤖 Agent: pr-triage-2 (Ludovic's AI agent, Claude): proven at c7aca8489 in union/pt2-e3 (17 PRs on main cbea22fbb): parity --changed passes but for main's own failures, lan_e2e on both paths, policy read (Mojang rules, supply chain, speed). Merging.
2026-10-29 22:12:02 +01:00
GIGABAIT93andClaude Opus 5.5 d7f9535406 Entity shadows follow the entity's scale and age
LivingEntityRenderer.getShadowRadius multiplies the type's radius by
minecraft:scale, and MobRenderer by the age scale (babies 0.5, or the
species' own: goat 0.55, camel 0.6...). Villagers and striders halve a
baby's shadow again, turtles take 0.83 of it; slimes, magma cubes,
sulfur cubes and pufferfish size theirs by their size or puff state.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 23:10:55 +02:00
Kacpherek 997bbef785 Walk zombies through their village at night
Zombies, husks and zombie villagers wandered with the plain stroll only. Vanilla Zombie.addBehaviourGoals adds MoveThroughVillageGoal(1.0, onlyAtNight, 4, canBreakDoors) at priority 6 (MoveThroughVillageGoal.java): within 6 sections of a village, pick the land spot (15, 7) whose nearest taken, unvisited village POI is closest, path to that POI (or ten blocks toward it, or only as far as a wooden door), and remember the 16 last POIs reached. The weighted land position now also honours a restricted home like vanilla's LandRandomPos.getPos.
2026-10-29 22:08:19 +01:00
Kacpherek 753660c0b4 Run the click command of a sign when a player uses it
Clicking a sign with a run_command in its text did nothing. SignBlock.useWithoutItem and useItemOn call SignBlockEntity.executeClickCommandsIfPresent (SignBlockEntity.java:182): each line of the side the player faces runs its run_command (as the player, from the block centre, permission level 2, no output: createCommandSourceStack with CommandSource.NULL), opens its show_dialog or logs its custom action, but only when the sign has allow_op_features; otherwise the action bar says click actions are disabled. A waxed sign still plays its failed-interaction sound, and a sign that ran one does not open the editor.
2026-10-29 22:08:07 +01:00
Kacpherek eba7cb0409 Make iron golems return to and patrol their village
Golems strolled anywhere at 0.6 (the plain stroll) and never went home. Vanilla IronGolem.registerGoals (IronGolem.java:68) adds MoveBackToVillageGoal at priority 2 (a stroll every 10 ticks, toward the section within 2 closest to a village, only outside the village) and GolemRandomStrollInVillageGoal at 4 (every 240 ticks: 3 in 10 anywhere, else toward a villager who wants a golem or a taken POI in a village section, from the level's random). Both are RandomStrollGoal subclasses, so Stroll takes an overriding spot picker and canUse test; the village sections and POI tickets come from the villager brain's helpers.
2026-10-29 22:08:07 +01:00
Kacpherek 2d477df4fd Let a waxed sign with a click command stop hanging signs chaining
A hanging sign used on another hanging sign always chained below it, even when the aimed sign had a run_command to run. SignBlockEntity.canExecuteClickCommands (SignBlockEntity.java:178) is a waxed sign whose facing side has one, and CeilingHangingSignBlock and WallHangingSignBlock shouldTryToChainAnotherHangingSign pass only without it. The game now asks the sign, and hands the answer to the wall hanging sign check through PlayerContext.
2026-10-29 22:08:07 +01:00
Kacpherek 71092caa14 Make piglins back off 9 to 11 blocks after a spear charge
A piglin that charged with a spear stayed on its target. SpearRetreat starts on the RETREAT status with a target, a spear and no item in use when LandRandomPos.getPosAway finds a spot 9 to 11 blocks away (checkExtraStartConditions), remembers it as SPEAR_FLEEING_POSITION, walks there for up to 100 ticks while looking at the target, and ends by erasing the status so the next approach starts (SpearRetreat.java).
2026-10-29 22:08:07 +01:00
Kacpherek 5968cd0f09 Make piglins run at the target and reposition mid-charge
A charging piglin stood still. SpearAttack.tick moves it every tick: run at the target (SpearAttack.java), and within 2 blocks of it or at the end of the path pick a spot 6 to 7 blocks past it (LandRandomPos.getPosAway, 2 more blocks on a mount) to run to, erasing it at the end of the path so it runs at the target again, until SPEAR_ENGAGE_TIME ends.
2026-10-29 22:08:07 +01:00
Kacpherek 8ce62d3aaa Make piglins raise the spear to charge a target
After closing in, a piglin with a golden spear never lowered it to attack. SpearAttack starts on the CHARGING status with a target, a spear and no item in use: it becomes aggressive, sets SPEAR_ENGAGE_TIME to the weapon's use time (KineticWeapon.computeDamageUseDuration), erases SPEAR_CHARGE_POSITION and starts using the spear (SpearAttack.start); the charge counts down while it looks at the target and ends in the RETREAT status (SpearAttack.java).
2026-10-29 22:08:07 +01:00
Kacpherek 2153681d47 Make piglins with a golden spear close in on 10 blocks
A piglin holding a golden spear (about 1 in 20 adults) walked up and meleed. PiglinAi.initFightActivity puts SpearApproach before the melee behaviour: with a target, a spear and no item in use, and no SPEAR_STATUS yet (checkExtraStartConditions), it sets the status APPROACH and walks to the target until it is within 10 blocks (SpearApproach.java), then the status is CHARGING. A piglin with a spear no longer melees (MeleeAttack.isHoldingUsableNonMeleeWeapon).
2026-10-29 22:08:07 +01:00
Kacpherek 349127673c Douse fire, candles and campfires where a water bottle lands
A potion that hit a block had no face and never put anything out. Thrown potions now keep the block and the face they entered it by, and AbstractThrownPotion.onHitBlock (server side, before the entity effects) douses the cell in front of that face, the cell behind it and the four horizontal neighbours when the potion is in #douses_fire. AbstractThrownPotion.douseFire destroys a fire block without drops, puts out a lit candle or candle cake (AbstractCandleBlock.extinguish) and a lit campfire (level event 1009, the block-change game event, lit false) (AbstractThrownPotion.java:48, :122).
2026-10-29 22:08:00 +01:00
Kacpherek 32e16761bd Hurt water-sensitive mobs and put out burning ones with a splash
A water potion only hurt endermen, and nothing it splashed on stopped burning. AbstractThrownPotion.affectEntitiesAround goes over every living entity in the potion's box grown by (4, 2, 4) and under 4 blocks away: #hurts_water_sensitive_entities hurts the sensitive ones (blaze, snow golem, strider, enderman) with indirectMagic and #extinguishes_entities calls extinguishFire on the burning ones, players and armor stands included (AbstractThrownPotion.java:94).
2026-10-29 22:08:00 +01:00
Kacpherek 57a2512025 Add putting out a burning mob and a lit candle from outside
Entity.extinguishFire plays the extinguish sound for a burning entity and then clears its fire; AbstractCandleBlock.extinguish unlights a candle or candle cake with its sound and the block-change game event (no source entity, and no smoke from a server). Mobs only lost their fire in water or rain, and candles only to a player's empty hand or a wind charge.
2026-10-29 22:07:51 +01:00
Kacpherek 25d15d2a0a Add the indirect_magic damage type for thrown potions
A thrown potion hurts through DamageSources.indirectMagic(potion, owner), which bypasses armor, is resisted by witches and wolf armor, and, unlike magic, knocks back from the potion. Mobs only had magic (no knockback) or a player attack. Add the kind, its constructor and the damage_type id, and keep Enderman.hurtServer's random teleport chance off it: the thrown-potion branch teleports without it (Entity.java, DamageTypeTags, Enderman.java).
2026-10-29 22:07:51 +01:00
Kacpherek a1a45472c8 Read splash potion effects around the hit from the potion tags
A thrown potion tested for plain water by id to hurt endermen and refill axolotls. Vanilla asks PotionContents.is for #hurts_water_sensitive_entities, #rehydrates_axolotls (and #douses_fire, #extinguishes_entities) in AbstractThrownPotion. Add the lookup over the data pack's tags/potion and use it for the two checks that exist.
2026-10-29 22:07:51 +01:00
Kacpherek 37f9481bb7 Add SignText.hasAnyClickCommands for a sign side
A sign side had no way to say whether its text carries a run_command click event. SignText.hasAnyClickCommands (SignText.java:120) is true when any line's style has a RunCommand click event. Horizon reads the saved click_event the way ClickEvent.CODEC does for the three actions a sign acts on (run_command, show_dialog, custom).
2026-10-29 22:07:19 +01:00
Kacpherek 04fb868e99 Keep click events and allow_op_features on loaded signs
Horizon flattened every sign line to plain text, so a click event in the text was lost the moment a sign loaded. SignText keeps each line's Component and its Style; the sign now keeps each line's click_event next to its text, writes it back with the line (an edited line keeps its style, SignBlockEntity.updateMessages), and reads and writes the allow_op_features flag of SignBlockEntity.loadAdditional and saveAdditional.
2026-10-29 22:07:15 +01:00
Kacpherek 7ed6012c79 Tick potent sulfur geysers on the server and the client
Wet or dormant sulfur never made anyone nauseous, periodic geysers never erupted, and continuous ones neither pushed entities nor showed their plume. Run PotentSulfurBlock.getTicker's tickers by state (PotentSulfurBlockEntity.java): the server gives nausea every 10 ticks, counts the geyser down with its positional random and flips dormant and erupting, and throws mobs, items and primed TNT up the column; the client releases gas clouds, draws the plume and plays its sound from the eruption block event, and pushes the local player, who moves on its own client. A window that simulates the world itself does both.
2026-10-29 22:06:57 +01:00
Kacpherek a8be1d9ee0 Add the potent sulfur block entity and its block events
Potent sulfur had no block entity of its own here, so no countdown was kept or saved and a geyser that began erupting made no sound or block event. Add PotentSulfurBlockEntity's waitingCountdown (saved as countdown) and eruptionTick, create it with the block and keep it through saves. PotentSulfurBlock.onPlace now queues the eruption block event, plays the start sound and signals block_activate for erupting and continuous states, triggerEvent reports the event handled, and updateShape resets the countdown when a block that was no geyser turns dormant (PotentSulfurBlock.java validBlockState).
2026-10-29 22:06:57 +01:00
Ludovic Marie c770074449 Merge pull request #3405 from degepie/fix/issue-3137-suspicious-stew
🤖 Agent: pr-triage-2 (Ludovic's AI agent, Claude): proven at 95c234cc1 in union/pt2-e3 (17 PRs on main cbea22fbb): parity --changed passes but for main's own failures, lan_e2e on both paths, policy read (Mojang rules, supply chain, speed). Merging.
2026-10-29 22:06:55 +01:00
Kacpherek b6f3d91b09 Raise the spear arm of zombies and piglins
HumanoidMobRenderer.getArmPose gives an arm holding a spear the SPEAR pose, which AnimationUtils.animateZombieArms leaves alone; the mobs here drew zombie or piglin arms around it. Zombies, husks, zombie villagers, zombified piglins and piglins now hold the spear out and charge it by their use time, and a piglin admiring gold no longer counts as using the spear in its main hand.
2026-10-29 22:06:55 +01:00
Kacpherek 26f46141c7 Make zombies and zombified piglins charge with a spear
Zombies, husks, zombie villagers and zombified piglins spawned with a spear just melee. SpearUseGoal (priority 2, and 1 for zombified piglins) walks to 10 blocks, lowers the spear for KineticWeapon.computeDamageUseDuration (halved, the goal ticks every other tick), charges along the target, repositions 6 to 7 blocks away, then backs off 9 to 11 blocks (LandRandomPos.getPosAway with a minimum distance) for up to 50 goal ticks. A mount's chargeSpeedModifier and the 2 blocks a passenger adds are kept.
2026-10-29 22:06:55 +01:00
Kacpherek 4f5a70ee53 Take ten points off a goat's fall damage
Goat.calculateFallDamage is the base damage minus 10, as Frog's is minus 5. Goats now leap down to five blocks and the arc lifts them higher still, so without it every ledge jump would hurt them.
2026-10-29 22:06:52 +01:00
Kacpherek d0761d3c50 Let goats long jump between ledges when their cooldown runs out
GoatAi.initLongJumpActivity adds LongJumpMidJump and LongJumpToRandomPos (5 wide, 5 high, velocity 3.5714288) to the activity that runs after the ram activity and before the idle one: no tempting player, no breed target, no walk target and no LONG_JUMP_COOLDOWN_TICKS. It lands on LongJumpToRandomPos.defaultAcceptableLandingSpot (a solid block below, no path malus).
2026-10-29 22:06:52 +01:00
Kacpherek caca2ef2d4 Land a long jumping goat with a thud and a 10 percent slide
LongJumpMidJump.stop slows a mob that lands to a tenth of its horizontal speed (multiply(0.1F, 1.0, 0.1F), the float widened to a double), plays the activity's landing sound at volume 2 (GoatAi: entity.goat.step), stands it up and samples the cooldown. A goat has no swim activity, so water does not end its flight.
2026-10-29 22:06:52 +01:00
Kacpherek 8874140947 Play the goat's long jump sound when it leaps
LongJumpToRandomPos.tick plays the activity's jump sound at the leap. GoatAi.initLongJumpActivity gives goats entity.goat.long_jump and screaming goats entity.goat.screaming.long_jump; the arc, the 40 tick wait and the jump boost are the same as the frog's.
2026-10-29 22:06:52 +01:00
Kacpherek a369faeb32 Let allays remember a note block they heard
An allay never learned a liked note block, so it always brought items to the player. AllayAi.hearNoteblock (AllayAi.java:108), called from Allay.VibrationUser.onReceiveVibration on NOTE_BLOCK_PLAY, stores the block as LIKED_NOTEBLOCK_POSITION with a 600 tick cooldown and renews the cooldown when the same block plays again.
2026-10-29 22:06:52 +01:00
Kacpherek 6dc2f77e99 Make an allay with a liked note block hear only that block
Allay.VibrationUser.canReceiveVibration (Allay.java:564) refuses vibrations without AI and, once a note block is liked, accepts only that block while the allay is within 1024 blocks of it in its dimension. Horizon accepted every vibration.
2026-10-29 22:06:52 +01:00
Kacpherek bb4da395a3 Add GlobalPos.isCloseEnough and the allay's liked note block memory
The liked note block slot (extra.home) was never set and was a bare block position. Vanilla keeps LIKED_NOTEBLOCK_POSITION as a GlobalPos (GlobalPos.java:31) and AllayAi.getItemDepositPosition drops it when it is over 1024 blocks away, in another dimension, no longer a note block or its cooldown ran out; the throw target is BlockPosTracker(pos.above()), the centre of that block. Saved in Brain.memories like vanilla.
2026-10-29 22:06:52 +01:00
Kacpherek 2555b46e7c Give allays a vibration listener that hears note blocks
An allay never listened for vibrations, so note blocks played near it went unnoticed. Vanilla gives it a VibrationSystem listener (Allay.VibrationUser, 16 blocks, GameEventTags.ALLAY_CAN_LISTEN = note_block_play) ticked after super.tick() in Allay.tick, saved under "listener".
2026-10-29 22:06:52 +01:00
Kacpherek 65a23ca98e Port map: the golden dandelion age lock is ported
The map still said a golden dandelion did nothing: drop the missing rows for
AgeableMob.mobInteract, canUseGoldenDandelion, setAgeLockedData, setAgeLocked,
makeAgeLockedParticle and Tadpole.setAgeLockedData (the code now cites them),
and say AgeableMob.defineSynchedData sends AGE_LOCKED too.
2026-10-29 22:06:52 +01:00
Kacpherek 24101477da Let a golden dandelion lock or release a baby animal's growth
Using a golden dandelion on a baby animal did nothing. AgeableMob.mobInteract (reached by Animal, Dolphin, SulfurCube and the other subclasses after their own checks) runs setAgeLocked with setAgeLockedData and returns SUCCESS when canUseGoldenDandelion holds; Tadpole.mobInteract does the same. A scared panda or one on its back (Panda.mobInteract) and an armadillo in its shell (Armadillo.mobInteract) answer first. A server's client treats the click as taken like the server does (AgeableMob.java:68).
2026-10-29 22:06:52 +01:00
Kacpherek e04428b60c Use up the golden dandelion, keep the mob and play the lock sound
Golden dandelion consumption, persistence and the use or unuse sound were missing. AgeableMob.setAgeLocked runs setAgeLockedData, consumes one item (not in creative), setPersistenceRequired when the mob is now locked, and plays golden_dandelion.use or .unuse at the block position (middle of the block, 1.0, 1.0, PLAYERS) (AgeableMob.java:88).
2026-10-29 22:06:52 +01:00
Kacpherek 79ed5ba529 Add the golden dandelion check for age locking a baby
AgeableMob.canUseGoldenDandelion(item, isBaby, cooldown, mob): the held item is a golden dandelion, the mob is a baby, its ageLockParticleTimer is 0 and its type is not in the cannot_be_age_locked entity tag (zombie horse, skeleton horse, villager) (AgeableMob.java:78).
2026-10-29 22:06:52 +01:00
Kacpherek a569559196 Add the age lock toggle a golden dandelion applies to a tadpole
A golden dandelion on a tadpole did nothing. Tadpole.setAgeLockedData sets AGE_LOCKED to its opposite, setAge(0) (back to the start of its 24000-tick growth) and ageLockParticleTimer = 40 (Tadpole.java:168).
2026-10-29 22:06:52 +01:00
Kacpherek ac74644dc9 Add the age lock toggle a golden dandelion applies to an ageable mob
Nothing toggled AgeLocked in play. AgeableMob.setAgeLockedData sets AGE_LOCKED to its opposite, setAge(getBabyStartAge()) (-24000, a sniffer's -48000, through the age boundary) and ageLockParticleTimer = 40 (AgeableMob.java:82).
2026-10-29 22:06:51 +01:00
Kacpherek c419845102 Draw the growth particles above a mob a golden dandelion touched
There was no age lock particle timer, so no pause or reset growth particles. AgeableMob.makeAgeLockedParticle runs at the end of AgeableMob.aiStep and Tadpole.aiStep: while ageLockParticleTimer is above 0, every even tick it sends one PAUSE_MOB_GROWTH (locked) or RESET_MOB_GROWTH particle at getRandomX(1.0), getRandomY(0.2) + bbHeight (+ 0.2F when locked), getRandomZ(1.0), then counts down (AgeableMob.java:225).
2026-10-29 22:06:51 +01:00
Kacpherek a6e91967a1 Hold an age-locked baby as a baby and sync the lock to clients
Nothing honoured AgeLocked in the shared age tick: a locked baby kept growing, a locked baby fed food still aged, and the lock never reached remote clients. AgeableMob.aiStep only counts a baby up when canAgeUp (isBaby && !isAgeLocked), and only for AgeableMob subclasses (it was also counting a tadpole's own age back down, so it never grew up). Animal.mobInteract, Panda.mobInteract and Dolphin.mobInteract feed a baby only when canAgeUp. AgeableMob.AGE_LOCKED (entry 17 after DATA_BABY_ID) and Tadpole.AGE_LOCKED are synched; MushroomCow.DATA_TYPE moves to 18 behind them (AgeableMob.java).
2026-10-29 22:06:51 +01:00
Kacpherek af8751173f Keep a riding piglin to the ride activity's behaviours
PiglinAi.initRideHoglinActivity lists Mount, SetEntityLookTarget(loved item, 8.0), a look at what it sees while a passenger and DismountOrSkipMounting; it has no idle movement or repellent avoidance, so a piglin heading for a hoglin no longer wanders off or strolls. A riding piglin looks at a player holding gold within 8 blocks, not 14.
2026-10-29 22:06:47 +01:00
Kacpherek af0bc10c34 Take a baby piglin off a baby it rides once its ride target ends
A baby piglin stayed on its mount forever. PiglinAi.updateActivity ends with stopRiding when RIDE_TARGET is gone and PiglinAi.isBabyRidingBaby (a baby piglin on a baby piglin or baby hoglin) holds, so a ride lasts as long as the 10 to 30 second memory.
2026-10-29 22:06:47 +01:00
Kacpherek 7fd82c846b Make a riding piglin get off a vehicle it no longer wants
No dismount rule ran. DismountOrSkipMounting keeps the vehicle (or else the ride target) only while it is alive and closer than 8 blocks and PiglinAi.wantsToStopRiding says no: the vehicle is grown, dead, the piglin was hurt in the last 40 ticks (HURT_BY), or the vehicle is a piglin that rides nothing itself. Otherwise the piglin gets off and forgets RIDE_TARGET.
2026-10-29 22:06:46 +01:00
Kacpherek f27a098c2d Walk a baby piglin to its ride target and mount it
No brain sent a mob to another to climb on it. Mount.create (ride activity) does nothing for a passenger, boards the ride target once closer than a block, and otherwise looks at it and sets a 0.8 speed walk to it; MoveToTargetSink ends that walk a block from the target's block and aims again when it moved over 2 blocks.
2026-10-29 22:06:46 +01:00
Kacpherek 08e206c459 Make an idle baby piglin pick a baby hoglin to ride
Nothing ever set RIDE_TARGET. PiglinSpecificSensor remembers the nearest visible baby hoglin, and PiglinAi.babySometimesRideBabyHoglin (idle activity) copies it into RIDE_TARGET for 10 to 30 seconds once SetEntityLookTargetSometimes.Ticker lets a baby through, every 10 to 40 seconds, with draws from the level random.
2026-10-29 22:06:46 +01:00
Kacpherek 5bc9606d99 Give a piglin a ride target and a ride activity
Brain activities wait on their memory: PiglinAi.getActivities lists a RIDE activity that is valid while RIDE_TARGET is there, between celebrating and idle in PiglinAi.updateActivity's order. RIDE_TARGET holds a mob and expires like any memory with an expiry.
2026-10-29 22:06:46 +01:00
Kacpherek c4f0c49c0e Pose a riding piglin sitting
PiglinModel extends HumanoidModel, whose setupAnim bends the legs and arms of a passenger; piglin_pose never set the riding flag, so a piglin on a mount stood stiff.
2026-10-29 22:06:46 +01:00
Kacpherek ff70b164cc Let a mob's brain board another mob and get off it
Only spawning put a mob on another; no brain could. Entity.startRiding checks the boarding cooldown, a free seat and no riding cycle, and Piglin.startRiding sends a baby piglin that boards a hoglin to the top of up to three piglins stacked on it. LivingEntity.stopRiding sets the passenger down at Animal.getDismountLocationForPassenger. A hoglin seats its rider at 1.49375 (0.875 for a baby), a baby piglin hangs 0.1875 below its seat (Piglin.BABY_DIMENSIONS).
2026-10-29 22:06:46 +01:00
Kacpherek 66c20c246d Open the new piglin and hoglin doc comments with their vanilla method
Doc wording only: each function that ports a vanilla method now starts its comment with the method name.
2026-10-29 22:06:46 +01:00
Kacpherek 32f2156b13 Count a piglin's hunted-recently wait down once a tick
Two additions to Piglin's per-tick state each counted the same memory down, so the HUNTED_RECENTLY wait that setAvoidTargetAndDontHuntForAWhile and StartHuntingHoglin start (30 to 120 seconds, PiglinAi.TIME_BETWEEN_HUNTS) ran out twice as fast.
2026-10-29 22:06:46 +01:00
Kacpherek 810a0152d3 Make idle piglins watch a player who holds gold
Piglins never turned to look at a player holding a gold ingot or another loved item. The idle and celebrate activities of PiglinAi.java start with SetEntityLookTarget on a visible player within 14 blocks for whom isPlayerHoldingLovedItem holds; add that look as a goal.
2026-10-29 22:06:46 +01:00
Kacpherek c124b94c8b Keep adult hoglins 8 blocks from the adult piglins they see
Idle hoglins ignored adult piglins and let them walk right up. HoglinAi.initIdleActivity walks an adult away from the nearest visible adult piglin at 0.4 once it is closer than 8 blocks, when it has no walk of its own (SetWalkTargetAwayFrom in HoglinAi.java).
2026-10-29 22:06:46 +01:00
Kacpherek 845343e019 Play a piglin's and hoglin's activity sound as its ambient grunt
Piglins and hoglins played their angry grunt whenever they had a target and otherwise the plain ambient one. Piglin.getAmbientSound and Hoglin.getAmbientSound play the sound of the activity the mob is in (PiglinAi.getSoundForCurrentActivity, HoglinAi.getSoundForCurrentActivity).
2026-10-29 22:06:46 +01:00
Kacpherek 89ecd6cd9d Grunt when a piglin or hoglin changes activity
A hoglin or piglin that started a fight, a retreat, a celebration or an admiring said nothing; only the ambient roll played. HoglinAi.updateActivity and PiglinAi.updateActivity make getSoundForActivity's sound (retreat, angry, admiring, celebrate, jealous) when the activity changes (Hoglin.java, Piglin.java).
2026-10-29 22:06:46 +01:00
Kacpherek e314b5c6e1 Make a hurt piglin flee, retreat or retaliate as vanilla does
A hit piglin ended its celebration and, for a player's hit, its admiring, but babies fought back instead of fleeing, an avoid target of another kind stayed, and an adult piglin took on hoglins it should retreat from. PiglinAi.wasHurtBy (Piglin.hurtServer) drops the admiring and the celebration for any attacker but a piglin, sends a baby into a 100-tick flight and calls the adults, retreats an adult from hoglins that outnumber piglins, and lets it retaliate only when maybeRetaliate allows.
2026-10-29 22:06:46 +01:00
Kacpherek be2963b1d6 Make a hurt hoglin retreat or retaliate as vanilla does
A hit hoglin only ended its pacified state and let the hurt-by goal target the attacker, so a baby never fled and an adult took on piglins and hoglins it should leave alone. HoglinAi.wasHurtBy (Hoglin.hurtServer) sends a baby into the retreat and lets an adult retaliate only when maybeRetaliate allows it.
2026-10-29 22:06:46 +01:00
Kacpherek d68d59f6b6 Make an outnumbered hoglin retreat after it hits a piglin
A hoglin that hit a piglin kept fighting even when piglins outnumbered the hoglins around it. HoglinAi.onHitTarget (Hoglin.doHurtTarget) sets the piglin as its avoid target and broadcasts the retreat to the adult hoglins it sees.
2026-10-29 22:06:46 +01:00
Kacpherek 7a98019450 Set an avoid target and broadcast a retreat to the adults nearby
A hoglin or piglin that decides to retreat had no way to set the avoid target, pick the nearest of its targets or warn the adults around it. Add HoglinAi.setAvoidTarget, PiglinAi.setAvoidTargetAndDontHuntForAWhile, retreatFromNearestTarget and broadcastRetreat; the broadcast reaches the other mob as an event that its next tick handles before its goals.
2026-10-29 22:06:46 +01:00
Kacpherek c40d0112a6 Let piglins and hoglins retreat from their avoid target
Hoglins and piglins never ran from piglins, hoglins or zombified mobs through an avoid target: baby hoglins panicked instead and piglins used a plain avoid goal. Add the retreat activity's walk away (HoglinAi.initRetreatActivity at 1.3 and 15 blocks, PiglinAi.initRetreatActivity at 1.0 and 12), the copies of a nemesis or zombified mob into AVOID_TARGET (PiglinAi.java), wantsToStopFleeing and the outnumbering tests, and keep the idle behaviours and the attack goals out of the retreat.
2026-10-29 22:06:45 +01:00
Kacpherek 89cbb09961 Let mobs stab with a spear they are using
A mob that held a spear could not use it: nothing started its use or ran KineticWeapon.damageEntities for it. A mob's use now counts ticks like the player's, stabs along its view from delay_ticks on (conditions scaled by 0.2, reach by the AttackRange mob_factor), hurts and pushes the player or mobs hit, dismounts riders and broadcasts entity event 2 (LivingEntity.stabAttack, causeExtraKnockback). A piglin's admiring is told apart from a spear in use by the off hand.
2026-10-29 22:06:40 +01:00
Kacpherek d43b957afc Add the sulfur geyser and noxious gas particles
Potent sulfur geysers draw four new particle types and two emitters, and none existed. Add noxious_gas, noxious_gas_cloud, geyser, geyser_base, geyser_poof and geyser_plume the way the vanilla providers build them (GeyserEruptionParticle.java, GeyserPlumeParticle.java, GeyserBaseParticle.java, NoxiousGasParticle.java, NoxiousGasCloudParticle.java): the gas fades from half its life, the base puffs burst upward, the plume shoots up and falls back, and the cloud and the eruption spawn the rest every tick. The cloud asks PotentSulfurBlockEntity.canBeReachedByNoxiousGas, ported with the other level reads of that class in the new potent_sulfur module. Particle packets with geyser data now map to and from the options.
2026-10-29 22:06:36 +01:00
Kacpherek 93dd53e1c6 Brighten a glow squid's body when it is not dark
A glow squid was lit like any mob, so it never glowed in the dark. GlowSquidRenderer.getBlockLightLevel lerps 1 - darkTicks / 10 over 0..15 and uses 15 outright, else the larger of that and the usual block light (GlowSquidRenderer.java:26).
2026-10-29 22:06:36 +01:00
Kacpherek 1a371c3e20 Count down the glow squid dark ticks and shed glow particles
A glow squid kept no dark timer and shed no glow. GlowSquid.aiStep runs after Squid.aiStep on the server and on clients: it lowers a positive dark tick count by one and calls addParticle(GLOW) at getRandomX(0.6), getRandomY(), getRandomZ(0.6) (GlowSquid.java:83). Only a client's level draws it; the server throws the same three random doubles.
2026-10-29 22:06:35 +01:00
Kacpherek f1a0963f2a Dim a glow squid for 100 ticks when it is hit
A hit glow squid never got its dark timer, so it did not dim. GlowSquid.hurtServer sets the dark ticks to 100 once Squid.hurtServer took the damage (GlowSquid.java:97).
2026-10-29 22:06:32 +01:00
Kacpherek 2947c2d71f Share the spear hit search with the mob code
The stab's entity search (ProjectileUtil.getHitEntitiesAlong) lived in the game crate's spear module; mobs that stab need it from the client library, so it moves to the spear module there and the game keeps using it.
2026-10-29 22:06:26 +01:00
Kacpherek 952fc5a1f5 Add LandRandomPos.getPosTowards for mobs that lean toward a spot
Horizon only had getPosAway on land. Vanilla's LandRandomPos.getPosTowards (LandRandomPos.java:23) takes the direction to the point and goes through getPosInDirection, a quarter turn (float cast pi/2) either side. getPosAway now shares that helper, so it uses the same float-cast angle.
2026-10-29 22:06:26 +01:00
Kacpherek ba7f106d53 Sense the adults a piglin or hoglin sees and track its activity
Piglins and hoglins had no brain memories: nothing counted the adult piglins and hoglins in sight (HoglinSpecificSensor, PiglinSpecificSensor), kept an avoid target with an expiry, or noted which activity they were in. Add them as mob state ticked from customServerAiStep (Hoglin.java, Piglin.java).
2026-10-29 22:06:24 +01:00
Kacpherek 9191728d12 Make a goat that cannot long jump wait 300 to 600 ticks
LongJumpToRandomPos.checkExtraStartConditions needs the mob on the ground and out of water, lava and honey, and otherwise sets LONG_JUMP_COOLDOWN_TICKS to half a sample of TIME_BETWEEN_LONG_JUMPS (GoatAi: 600 to 1200). A goat has no swim activity to keep it out of the check, so it runs in water too.
2026-10-29 22:06:23 +01:00
Kacpherek bd89ae3df7 Sync and save the glow squid dark ticks
A glow squid had nowhere to keep DATA_DARK_TICKS_REMAINING: nothing held it, sent it to clients, or saved it. GlowSquid defines it as the first entry after AgeableMob's two (index 18) and writes DarkTicksRemaining (GlowSquid.java). It lives in the mob's counter; the server sends it as synched data, a client applies it and the save keeps it.
2026-10-29 22:06:23 +01:00
Kacpherek 5ea674c6e6 Search an 11 block box for a goat's long jump
LongJumpToRandomPos.start lists every block within the activity's width and height of the mob (GoatAi: 5 and 5, so 11 x 11 x 11; the frog's is 9 x 5 x 9). A goat's activity is a plain LongJumpToRandomPos, so it skips LongJumpToPreferredBlock.start's draw from the mob's random.
2026-10-29 22:06:23 +01:00
Kacpherek d8ac144995 Share the frog's long jump goal pieces with goats
LongJumpToRandomPos and LongJumpMidJump serve both mobs in vanilla. The goal now sets the friction flag and the pose through helpers that know the goat's state, and the default landing spot and the cooldown count down (CountDownCooldownTicks) can be used by a goat. Frogs behave as before.
2026-10-29 22:06:23 +01:00
Kacpherek a7f03f52fd Give goats a long jumping pose with its smaller box and friction flag
A goat that long jumps takes Pose.LONG_JUMPING, which Goat.getDefaultDimensions scales by 0.7, and LongJumpUtil checks the arc with that pose's box (LongJumpUtil.isClearTransition). The pose goes to remote clients like the frog's, and the goat holds LivingEntity.discardFriction.
2026-10-29 22:06:23 +01:00
DUzzL 9226f507a3 Remove dead ctov modular_compat code and unused test helper in horizon-port-pack 2026-10-29 22:05:48 +01:00
Михаил РоговцевandOpenAI Codex 880eb7350f Cover spear swing across a held item switch
Co-Authored-By: OpenAI Codex <[email protected]>
2026-10-30 00:04:53 +03:00
Ludovic Marie 8169314c66 Merge pull request #3369 from ThatCakeIsAPie/feat/upgrade-datapack-1
🤖 Agent: pr-triage-2 (Ludovic's AI agent, Claude): proven at 0771f59bc in union/pt2-e3 (17 PRs on main cbea22fbb): parity --changed passes but for main's own failures, lan_e2e on both paths, policy read (Mojang rules, supply chain, speed). Merging.
2026-10-29 22:04:32 +01:00
Ludovic Marie 7b581fb43c Merge pull request #3366 from ThatCakeIsAPie/perf/worldgen-surface-biomes
🤖 Agent: pr-triage-2 (Ludovic's AI agent, Claude): proven at 52cc53a4f in union/pt2-e3 (17 PRs on main cbea22fbb): parity --changed passes but for main's own failures, lan_e2e on both paths, policy read (Mojang rules, supply chain, speed). Merging.
2026-10-29 22:04:28 +01:00
Ludovic MarieandClaude Opus 5.5 c32dddb1fe Merge main into the branch: default tints through the tint closure
Main's #3401 drew blocks through tinted_block_model_vertices with a per-face
tint closure; block_model_vertices now passes the default tints there
(grass colour once per call). The enderman's carried block uses the default
tints, so block_model_vertices_tinted has no caller left.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 22:03:53 +01:00
Ludovic Marie 5b1d64d4f7 Merge pull request #3401 from JacobRWebb/JacobRWebb/sticky-piston-texture-color
🤖 Agent: pr-triage-2 (Ludovic's AI agent, Claude): proven at 1a01f5ef5 in union/pt2-e3 (17 PRs on main cbea22fbb): parity --changed passes but for main's own failures, lan_e2e on both paths, policy read (Mojang rules, supply chain, speed). Merging.
2026-10-29 22:01:50 +01:00
Federico Tagliani 55db71a9ff Merge remote-tracking branch 'origin/main' into fix/riptide-with-elytra 2026-10-29 22:01:45 +01:00
Federico TaglianiandClaude Sonnet 5.5 9751bdf0ab Riptide in rain on a server's client: isInRain from its own chunks and weather
A client of a server has no live biome features, so the behaviours'
isRainingAt answered false and a Riptide trident never started charging in
rain.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-29 22:01:44 +01:00
Ludovic Marie bf0d6dd423 Merge pull request #3920 from horizon-rust/feat/roadmap-owners
🤖 Agent: merging the short roadmap (#3859 step 4): verify green, roadmap tests and the online check pass. Proposed owners can change their line with a one-line PR.
2026-10-29 21:59:32 +01:00
Ludovic MarieandClaude Opus 5.5 91523637ac CONTRIBUTING: rewrap the claims paragraph
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 21:58:59 +01:00
Ludovic MarieandClaude Opus 5.5 d64f34c96c Claims: an issue claimed on Discord is taken too
Kira's Discord /claim marks an issue with the `in progress` label instead of
assigning it; `/claim` on GitHub now refuses such an issue, so the two
claim paths can't hand the same issue to two people. Kira's side (refusing a
Discord claim on a GitHub-assigned issue) is in horizon-rust/kira.

Part of #3859.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 21:58:34 +01:00
Ludovic MarieandClaude Opus 5.5 fabb6d78ec Parity headline: a plain paragraph, no HTML markers
The website renders the "Project status" intro with its own markdown, which
prints HTML comments as text: the block is now found by its first words.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 21:56:45 +01:00
Ludovic Marie 74904e32c1 Merge pull request #3903 from horizon-rust/feat/overlap-note
🤖 Agent: merging the overlap note (#3859 step 2): verify green, tests pass, false positives measured on the 207 open PRs (21 pairs: 16 related, 4 doubtful, 1 unrelated).
2026-10-29 21:55:40 +01:00
Ludovic MarieandClaude Opus 5.5 3082b36894 Parity headline: the player-visible gaps still open, on the README and the site
tools/parity_gaps.py counts the rows not yet ticked in the port ledger's
ten gap lists (#3512-#3521) and writes them first under "Project status"
(by area, linked) and in a badge beside the 100% one. The website shows the
block already: it renders that section's intro. Today: 833 open of 833.
Refreshed in the weekly roadmap pass (roadmap/README.md); tests check the
block, the badge and the site's heading agree, without the network.

Part of #3859.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 21:54:39 +01:00
Михаил РоговцевandOpenAI Codex 15a22366e3 Test swap scale from displayed item models
Co-Authored-By: OpenAI Codex <[email protected]>
2026-10-29 23:53:29 +03:00
Ludovic MarieandClaude Opus 5.5 7c8e0d8e9b Roadmap: Now at most 8 items, each with an owner and a tracking issue
roadmap.toml items get `owner` (a GitHub login) and `issue` (the tracking
issue); `check` requires both on every Now item, at most 8 Now and 12 Next
items, and the tracking issue to be an open issue. ROADMAP.md shows them,
roadmap.json carries them (the hub ignores unknown keys).

The proposed lists, from the work actually happening (open pull requests,
agents' workstreams):
- Now: smooth-frames, server-tick, mods, port-ledger (the player-visible
  gaps), modpack-worldgen, server-thread, java-instances, lean-code, with
  tracking issues #3909-#3916.
- Mods: one line (Ludovic's decision), owner levzzz5154; native-mods,
  mod-registry, server-mods, mod-screens, modder-tools, machines-api and
  mod-world-items fold into it.
- Shipped (`sync`, all their pull requests in a release): server-dialogs,
  discord-presence, native-shaders, pregen, item-cooldowns, chat-settings,
  pointer-hand, text-field-keys, chicken-flap. phones-and-browser is not
  shipped: it becomes ongoing so `sync` stops taking its first pieces for it.
- Next: 12 (splitscreen, owner martipops, and launcher-redesign come up);
  the rest of Next moves to Later.

Part of #3859.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 21:48:46 +01:00
GIGABAIT93andClaude Opus 5.5 8974e07eec Other players and mannequins take their minecraft:scale
ClientPacketListener.handleUpdateAttributes gives every living entity its
attributes; the client kept only a mob's max health, so a server's scale
on another player or a mannequin was dropped and NPC plugins' enlarged
fake players showed at their normal size. The scale now goes to the
model as the local player's already does (LivingEntityRenderState.scale),
computed like AttributeInstance.calculateValue, now a function with a test.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 22:46:54 +02:00
Ludovic MarieandClaude Opus 5.5 6da84cf195 A plain custom name is saved as a string, as vanilla's codec writes it
Component.tryCollapseToString: ComponentSerialization.CODEC writes plain text with no style and no siblings as a bare string, so a legacy display Name '{"text":"Cool"}' becomes custom_name:"Cool". Keeping the compound made 215 of hotbar's 905 cases differ.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 21:46:31 +01:00
DUzzL c9053670cd Fix camel pose time at spawn and in LastPoseTick saves
A camel kept its pose change as a signed age, so a camel finalized at age 0 had pose time 0 and could not move for 52 ticks, and LastPoseTick was saved as a sign. Keep a signed age anchor plus a sitting flag, and save and load LastPoseTick as a game time.
2026-10-29 21:42:33 +01:00
DUzzL 2403ca40a2 Reject data-pack recipe result counts outside 1..=99
ItemStackTemplate.CODEC bounds the count with intRange(1, 99). Crafting,
cooking and stonecutting results were cast with 'as u32', so 0, 2^31 and
wrapped 2^32 reached item stacks. Such recipes are now not loaded.
2026-10-29 21:40:55 +01:00
Михаил РоговцевandOpenAI Codex a554d6e7c4 Fix item model hand swap animation scale
Co-Authored-By: OpenAI Codex <[email protected]>
2026-10-29 23:39:54 +03:00
DUzzL e7b19995ea Explosion drops merge by item, components and max stack size
ServerExplosion.addOrAppendStack uses ItemEntity.areMergable: same item and
components, and the combined count within the item's max stack size.
2026-10-29 21:39:27 +01:00
Ludovic Marie f47c14d93f Merge pull request #3872 from horizon-rust/feat/kind-labels
🤖 Agent: merging the kind and area labels (#3859 step 3): verify green, workflow-safety and labelling tests pass; labels created and the open pull requests labelled once.
2026-10-29 21:38:13 +01:00
DUzzL 4269899778 Stamp the current DataVersion only after the world data is upgraded
upgrade_world wrote level.dat at the current DataVersion before converting chunks, so a failed conversion was never retried. The level.dat write now comes last.
2026-10-29 21:37:45 +01:00
DUzzL a5f4e32bfc Dedupe function-tag expansion with hash sets and end tag cycles
Fixes #3885
2026-10-29 21:37:00 +01:00
Ludovic MarieandClaude Opus 5.5 ca0888663e Overlap note: one comment naming the open pull requests doing the same work
On open and push, overlap-note.yml (pull_request_target, main's script only)
lists, in one comment edited on every push, the other open pull requests that
close the same issue or change the same functions. Functions come from git's
own rust/python hunk headers, which name indented methods (GitHub's stop at
`impl Game<'_>`). The heads are fetched as objects and diffed, never checked
out; hooks, fsmonitor, external diff and textconv are off.

Measured on the 207 open pull requests: one shared function, or a few of a
big pull request's hundred, was mostly chance, so a pair needs a shared
issue, or 2 shared functions and a tenth of the bigger one's; functions 5
compared pull requests change, probes, tests and stacks don't count.
21 pairs (34 notes): 16 related (4 duplicates), 4 doubtful, 1 unrelated.

Part of #3859.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 21:36:37 +01:00
bloodulonandQoder cdd3254dbb Load item definitions from nested folders
ModelEngine names a bone's item model `modelengine:<model>/<bone>` and
ships it at assets/modelengine/items/<model>/<bone>.json. Only the files
at the top of assets/<ns>/items were read, so every bone stack resolved
to the built-in missing model, whose sprite is absent from the HUD
atlas: the quads were dropped and the NPC rendered nothing while its
interaction hitbox stayed clickable.

Walk the tree like ParsedModels::load already does for models.

Co-Authored-By: Qoder <[email protected]>
2026-10-30 01:36:04 +05:00
ccclosed's AI agentandDeepSeek Harness 410bde729b Mobs: apply the Weakness, Slowness and Health Boost attribute modifiers
effect_modifiers only mapped strength and speed, so Weakness and Slowness
had no effect on a mob: its melee damage and movement speed stayed at the
base values. Map every attribute-carrying effect the way vanilla's
MobEffect.addAttributeModifier does, scaled by amplifier + 1:

- weakness: attack damage -4 per level (ADD_VALUE)
- slowness: movement speed -0.15 per level (ADD_MULTIPLIED_TOTAL)
- health_boost: max health +4 per level (ADD_VALUE)

Move the per-effect table into a testable effect_modifier() and cover all
five rows with a unit test.

Fixes #3887.

Co-Authored-By: DeepSeek Harness (deepseek-v4.1-flash) <[email protected]>
2026-10-29 23:33:35 +03:00
Ludovic Marie 90aded96a3 Merge pull request #3874 from horizon-rust/fix/display-log-linux-only
🤖 Agent: macOS clippy fix (log_display is only called on Linux); clippy clean on macOS.
2026-10-29 21:24:43 +01:00
TJKDev1andClaude Opus 5.5 6eb26eb046 Third person: the player's head pitch follows the mouse between ticks
Entity.turn adds the turn to xRotO as well as xRot, so the local
player's getXRot(partialTick) follows the mouse every frame. Every turn
(mouse, pad, arrow keys, the control `turn`) now goes through
turn_player, which moves both ends of the model's head pitch and ends
in AbstractBoat.onPassengerTurned's clamp. handleRotatePlayer snaps the
model (setOldRot), and handleMovePlayer moves its old pitch by the
packet as setOldPosAndRot does. A change the tick makes still eases in.

Fixes #3844

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 21:24:09 +01:00
Federico Tagliani d1129a9fce Merge remote-tracking branch 'origin/main' into fix/boat-paddle-input-latch 2026-10-29 21:17:14 +01:00
Federico TaglianiandClaude Sonnet 5.5 0cfb0400c8 Boats: steer with the input latched on the previous tick
LocalPlayer.rideTick sets the boat's input after the boat ticked, so
AbstractBoat.tick steers with the previous tick's keys and the paddle
packet it sends describes the motion it just made. Horizon set the keys
before ticking, a tick ahead of that packet (GrimAC Simulation flags).

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-29 21:15:45 +01:00
matydenandClaude Opus 5.5 7b33479e15 Port publish: write the issue body as UTF-8
issue() wrote the body file in the system's code page, while gh reads
it as UTF-8, so on Windows a body with "→" raised UnicodeEncodeError
(cp1251 here) or would reach GitHub garbled. #3587 described this fix,
but the merged pull request kept only its roadmap files.
test_apply_lists_the_new_issue_numbers_in_the_tracking_issue now passes
on Windows without PYTHONUTF8.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 22:15:05 +02:00
matydenandClaude Opus 5.5 230d89f7ec Telemetry: a user named "user" keeps the <user> label whole
The scrubber puts <user> in place of the folder name in /Users/... and
C:\Users\... paths, then replaces the machine's user name wherever it
stands alone. When that name is "user", the second pass matched the
word inside the label it had just written, so a crash report read
/Users/<<user>>/... and telemetry's a_crash_keeps_only_what_says_what_happened
failed on such machines (seen in #578 and #3838). A match between "<"
and ">" is now left as it is.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 22:15:04 +02:00
Ludovic Marie 7c1e0f9794 Merge pull request #3862 from horizon-rust/feat/claim-command
🤖 Agent: merging the claims workflow (#3859 step 1): verify green, Python tests and workflow-safety tests pass, the GraphQL query and release --dry-run tried live.
2026-10-29 21:13:27 +01:00
Ludovic MarieandClaude Opus 5.5 0c9e995257 Render: log_display only where Wayland's HDR reports call it
On macOS and Windows nothing calls it, so clippy -D warnings failed there
(dead code) while Linux CI passed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 21:13:27 +01:00
Ludovic Marie 97032398df Merge pull request #3834 from horizon-rust/perf/scheduled-one-level
🤖 Agent: verified by server-tick-1: due scheduled ticks run in one with_level while a step leaves no level events or spill (identical order and state); tick_blocks span 15.4/14.8 → 10.4/9.3 ms at 50 bots; pr_ready --quick green; merging once CI parity passes.
2026-10-29 21:13:23 +01:00
DUzzL a8f0a64bd6 Credit breeding to the player who fed the animals
On the server thread (the default) breeding runs in the mob tick, outside
any player's turn, and the server has no host player, so the
bred_animals trigger and the animals_bred statistic were dropped and
"The Parrots and the Bats" was never granted.

Remember who fed each animal into love (Animal.loveCause) and credit that
player, the parent's feeder first and else the partner's, as vanilla does.
2026-10-29 21:11:07 +01:00
Aria Figueredo d3e0774ca3 Catalog versioned native hook points 2026-10-29 15:09:42 -05:00
spiderandClaude Opus 5.5 e716347329 Play with Friends: leaving the world ends its invite code
On the server thread (the default single player), Save & Quit goes
through leave_server, which stopped the integrated server but kept the
sharing state: back in a world, the old code still showed with nothing
behind it. leave_server now ends the code, as lan_close already did for
the in-frame path. test_friends checks it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 21:06:22 +01:00
bxnjxmin27andDeepSeek V4.1 Flash c3fdf7192d Screenshots: the key works with chat, a sign or a book open
F2 was handled in `Game::key_pressed`, which a screen with a text box never
reaches: chat, a sign, a book, the command block and the search boxes take the
key first, so F2 did nothing there (#3689).

Vanilla runs `Minecraft.handleGlobalKeyPress` before a screen sees the key:
`key.fullscreen` and `key.screenshot` are window shortcuts that work on every
screen, and a held debug modifier suppresses both. The fullscreen half was
already global here; this adds the screenshot half beside it, driven by the
`key.screenshot` mapping instead of a hard-coded F2, and drops the old arm in
`key_pressed`. `horizon-ctl key` takes the same shortcut, so an agent drives
what a player does.

Co-Authored-By: DeepSeek V4.1 Flash (opencode) <[email protected]>
2026-10-29 21:05:06 +01:00
Ludovic MarieandClaude Opus 5.5 59b1c33bb2 PR labels: a kind from the template's ticked box, areas from the paths
The template gets a "Kind" list (parity, bug, perf, feature, extra, tooling,
docs). pr-labels.yml (pull_request_target, main's script only, never the
pull request's code) sets the ticked kind when the pull request has none, swaps
it when the author changes the tick, and never removes a label set by hand.
Area labels come from the changed paths: at most 3, each a tenth of the lines.
`feature` keeps its release meaning.

pr_labels.py also creates the labels and labels the open pull requests once
(backfill: kind only where clear, else none).

Part of #3859.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 21:04:01 +01:00
spiderandClaude Opus 5.5 be48b7a4a9 LAN and friends worlds authenticate guests when the host is signed in
IntegratedServer.initServer uses authentication, so a vanilla LAN is in
online mode: each guest's hasJoined answer brings their signed textures
property, and everyone sees their skin and cape. Horizon's LAN stayed in
offline mode, so guests joined with no properties and showed as default
skins without capes.

OnlineMode::lan() makes the key pair when an account is signed in (an
offline name keeps offline mode), for the server thread and the in-frame
host alike; friend codes reach the same port through the tunnel. As in
ServerLoginPacketListenerImpl.handleHello, the integrated server's own
player over memory is never asked to authenticate.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 21:03:58 +01:00
Aria Figueredo d1d99be74f Allow data files in native mod bundles 2026-10-29 15:00:57 -05:00
ElyTheCatandAntigravity cc4c49e8f9 Disconnect: do not show saving world screen on external server
In vanilla Minecraft (26.3), exiting or disconnecting from a remote server
(`isLocalServer() == false`) never shows the saving world screen
(`GenericMessageScreen(Gui.SAVING_LEVEL)`). In `disconnect()`, vanilla only
halts the integrated server and displays the saving level screen when
`singleplayerServer != null`.

Previously in Horizon, `begin_closing_save(quit = true)` set `Stage::Writes`
when quitting from a remote server or the title screen, causing
`render_closing_save` to draw "Saving world...", and `save_for_exit` called
`save_game()` on exit even for remote worlds.

- In `begin_closing_save`: introduce `Stage::Quit` for clean termination
  without drawing the save progress screen. When quitting while connected
  to a remote server, immediately disconnect the session, mark
  `saved_for_exit`, and transition directly to `Stage::Quit`. Quitting
  from the title screen also uses `Stage::Quit`.
- In `close_world`: directly call `leave_server(None)` when on an external
  server (`self.external_server()`), skipping `begin_closing_save`.
- In `save_for_exit`: cleanly disconnect `remote` and return without saving
  when on an external server.

Co-Authored-By: Antigravity <[email protected]>
2026-10-29 23:30:08 +03:30
Aria Figueredo df440f90b2 Content mods: set per-state outline and collision shapes 2026-10-29 14:59:03 -05:00
Aria Figueredo 791ac3d928 Content mods: allow a block default state override 2026-10-29 14:58:30 -05:00
DenisHumenandClaude Opus 5.5 6774e47aae Launcher: moving a world to the trash no longer closes it on Windows
The trash crate starts COM on the calling thread and panics when that
thread already runs COM in the other mode (RPC_E_CHANGED_MODE). Deleting a
world runs on a worker of GPUI's thread pool, which can, and a panic in that
callback cannot unwind, so the launcher aborted and the world stayed.
The system trash now runs on a thread of its own, and a panic there comes
back as an error.

Fixes #3784

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 21:56:57 +02:00
Ludovic MarieandClaude Opus 5.5 f9e5264df7 Claims: /claim and /unclaim on issues, stale claims released after 5 days
Anyone comments `/claim` on an issue to be assigned (GitHub lets an issue's
commenters be assigned, no role needed); an assigned issue is taken.
`/unclaim` gives it back; the trusted reviewers can `/unclaim @login`.
A daily run releases claims this workflow made over 5 days ago when no open
pull request links the issue; hand assignments are never released.

Same shape as triage-command: main's copy runs, no pull request is checked
out, the comment reaches the script as env vars, the token writes issues only.

Part of #3859.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 20:54:06 +01:00
Ludovic Marie 668419b92a Merge pull request #3526 from kacpherek/tools/port-ledger-harness
🤖 Agent: verified by pr-triage-3. Dev tooling only (tools/port_ledger.py harness coverage + gaps --kind untested, docs, port_map paths moved to the split crates: every new path exists on main). Merges cleanly with main; tests/test_port* 30 pass; skip-release.
2026-10-29 20:52:30 +01: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
DUzzL bfda2bf165 Only a piston base stands in for a retracting moving_piston 2026-10-29 20:38:51 +01:00
DUzzL 5782df1c03 Lever on a piston's back no longer drops when the piston retracts
A retracting piston is a moving_piston block for two ticks. The support
facts are exported without a block entity, so the block read as empty and
every face as unsupported, and the lever attached to the piston's back
popped off. Read the extended base for a retracting source piston, which
is what vanilla's moving-piston block entity shape contains.
2026-10-29 20:36:08 +01:00
Ludovic MarieandClaude Opus 5.5 d3c6f0d937 Merge main into #2987 (lan_entities.rs: lan_display_data beside main's lan_entity_uuids)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 20:34:23 +01:00
Ludovic Marie baa379d717 Merge pull request #3578 from d3grando/fix-3432
🤖 Agent: proven on union/pt1-w3 by pr-triage-1: every vanilla harness this branch reaches passes (mob_ai and the equine ones included), clippy -D warnings and the horizon-client tests pass (the one failing test is main's own, fixed by #3851), and the last commit only reorders an import for rustfmt. Thank you!
2026-10-29 20:31:31 +01: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
Ludovic MarieandClaude Opus 5.5 5fb9092073 Merge main into #3341: inline objects (#3327) beside the component shadow colour
The glyph loop takes both: an inline object draws and skips the shadow passes; other glyphs use the component's shadow_color. Both sides' tests kept.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 20:28:16 +01:00
Ludovic Marie 28a65af5fc Merge pull request #3848 from horizon-rust/perf/block-light-check-node
🤖 Agent: merging. Block light follows vanilla BlockLightEngine.checkNode (an emitter in its own light only brightens): in-frame relight during the RD 32 ocean flight 17–30 ms per run down to 7–8 ms; light harness and the light tests pass; CI verify green.
2026-10-29 20:26:44 +01:00
Ludovic Marie a6751f2aa9 Merge pull request #2960 from kacpherek/fix/shelf-interaction
🤖 Agent: proven by pr-triage-3 (Ludovic's AI agent) on union/pt3-w1 7e05a39a9 (main 62a23ba02 + 15 PRs): pr_ready green (fmt, clippy -D warnings, 1810 tests, doc, content root), parity --changed 192/192, lan_e2e 13 checks on both paths. Mojang rules, supply chain and per-frame cost read.
2026-10-29 20:24:20 +01:00
Ludovic Marie ea60e1e062 Merge pull request #3327 from damnurmum/fix/inline-object-text
🤖 Agent: proven by pr-triage-3 (Ludovic's AI agent) on union/pt3-w1 7e05a39a9 (main 62a23ba02 + 15 PRs): pr_ready green (fmt, clippy -D warnings, 1810 tests, doc, content root), parity --changed 192/192, lan_e2e 13 checks on both paths. Mojang rules, supply chain and per-frame cost read.
2026-10-29 20:24:16 +01:00
Ludovic Marie 047815c5d7 Merge pull request #3303 from doodlyyousuf/fix/container-use-swing
🤖 Agent: proven by pr-triage-3 (Ludovic's AI agent) on union/pt3-w1 7e05a39a9 (main 62a23ba02 + 15 PRs): pr_ready green (fmt, clippy -D warnings, 1810 tests, doc, content root), parity --changed 192/192, lan_e2e 13 checks on both paths. Mojang rules, supply chain and per-frame cost read.
2026-10-29 20:24:11 +01:00
Ludovic MarieandClaude Opus 5.5 d22aecd526 Format the equine tests' imports
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 20:20:19 +01:00
DUzzL 5f931e3659 Draw a worn elytra on armour stands (#3843)
ArmorStandRenderer adds WingsLayer after the armour layers, but
armor_stand_geometry never drew the chest slot's wings. Split the
wings drawing out of player_elytra_geometry into wings_geometry and
call it for stands, at the resting wing angles (a stand never glides
or crouches) and with the baby elytra model for a small stand.
2026-10-29 20:20:07 +01:00
DUzzL b9ed9d58de Fix axolotl NBT test: query glowing effect by namespaced id
Mob::add_effect normalizes ids to their minecraft: form (08b97652), but the
test asked has_effect for the bare "glowing", so the assertion failed.

Fixes #3840
2026-10-29 20:18:27 +01:00
Ludovic MarieandClaude Opus 5.5 e40ea748d1 Merge main into this branch: Polish beside main's Chinese, Spanish and Ukrainian
Language::ALL holds eight and every list of codes names pl. Polish gets the
seven launcher strings main added since (instance trash, native mods, the
Java Edition line) and the build-time line main reworded.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 20:17:49 +01: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
Ludovic MarieandClaude Opus 5.5 01b4e5cbc4 Light: an emitter placed in its own light only brightens
Vanilla's BlockLightEngine.checkNode clears a changed block's light and
decreases from it only when the new emission is below the stored level;
otherwise the level stays, the neighbours' light is pulled in and the
emission increases from there. Horizon's relight cleared every changed
block, so each flowing lava block placed next to lava (fluids settling
in fresh chunks while flying over the ocean) darkened and relit up to
14 blocks around it on the render thread, and marked every section
there for remeshing.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 20:13:11 +01:00
FauxandClaude Opus 5.5 96877dcf48 Far terrain: no dark band in the sea where chunks were loaded
A chunk's summary kept the chunk's own light, which fades a level per block
of water, while the seed's far columns light water and its floor as open
sky. The chunks a player passed then drew a darker band through the far sea.
Summaries now light open water and its floor as open sky, and the water is
one span again.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 12:11:33 -07:00
GIGABAIT93 7d227270a2 Merge branch 'main' of https://github.com/horizon-rust/horizon into feat/launch-api-client
# Conflicts:
#	crates/horizon-client/src/game.rs
2026-10-29 21:11:06 +02: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
degepieandClaude Opus 5.5 bcfeeeeb2a Merge main into fix/issue-3024-worn-armor-glint
main added render_layers (armour without the body) and a body_visible flag;
its extra layers now carry the overlay too, like render_with's.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
2026-10-30 00:03:43 +05:00
Ludovic MarieandClaude Opus 5.5 84c3af5122 Merge main into this branch: the pumpkin beside main's cape and glowing changes
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 20:00:31 +01:00
Ludovic MarieandClaude Opus 5.5 e28ed6d877 Spawners: no level built while no player is in range
`BaseSpawner.serverTick` returns first thing without an alive player within
its range (`isNearPlayer`) or with `spawner_blocks_work` off, before any
draw. Each spawner still built the spawn code's level every tick for it:
every player with its box and mode, the nearest-player list and the mob
world. Ask the players directly first, without collecting them.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 19:58:32 +01:00
GIGABAIT93andClaude Opus 5.5 b6b9a5a307 Text displays riding the local player sit over its head too
/ride a text_display mount @s makes the window's own player the
vehicle, which isn't in mobs: the display stayed where it spawned. The
local player seats it at the top of its box in its pose and scale, its
passenger attachment (Avatar.POSES), as for other players.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 20:48:57 +02:00
Ludovic Marie 7ebf7d26cb Merge pull request #3350 from horizon-rust/draft/remove-wasm-mods-game
🤖 Agent: WebAssembly removal 1/4 (game), merged up to date with main c7065027b. The official mods are native in the registry (Zoom, FPS Counter, Keystrokes, Minimap, Recipes, Coordinates HUD 2.0.0, verified in the published index), and the authors of the WebAssembly mods with no native version were told (list in the body). Each layer compiles; pr_ready --quick on the tip: only main's own macOS clippy (log_display) and axolotl test fail. Parity: 340/340 locally on the previous heads; CI parity on these. Merge before #3351, #3353, #3371.
2026-10-29 19:48:54 +01:00
Ludovic MarieandClaude Opus 5.5 d8e912738b Merge main into the branch: the HDR graphics page shares the video title key
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 19:46:02 +01:00
GIGABAIT93andClaude Opus 5.5 e855ded821 Test that an offscreen game never grabs, warps or hides the pointer
The capture fixture compiles the client's own capture body; it now has
the game's options, and an offscreen game must keep its grab as its own
state without touching the window's pointer.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 20:44:20 +02:00
Ludovic Marie cbea22fbb6 Merge pull request #3831 from horizon-rust/perf/streaming-results-no-wait
🤖 Agent: merging. The frame no longer waits on a descheduled worker's mpsc send: 8–24 ms stalls in Streaming::update gone in the RD 32 ocean flight (0–1 per run vs 5–14), 109/109 affected harnesses pass, CI verify green.
2026-10-29 19:43:45 +01:00
Ludovic MarieandClaude Opus 5.5 c7aca8489f Merge main into the branch: keep the world icon beside the preset check
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 19:42:46 +01:00
Ludovic Marie 319876a186 Merge pull request #2795 from s1f102500012/launcher-simplified-chinese
🤖 Agent: correction from pr-triage-1: my first push of the main merge missed one line (`Language::ALL` still said six), so it didn't build; 46ac49ad0 adds it and merges main once more. On this exact head the launcher's fmt, clippy -D warnings and all its tests pass, including the check that every language has every string. Sorry for the noise.
2026-10-29 19:38:07 +01:00
levzzz 9093960719 Merge pull request #3355 from Sticky-Bug/feat/audio-output-device
options: choose the audio output device in Music & Sounds (#1035)
2026-10-29 23:37:50 +05: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
GIGABAIT93andClaude Opus 5.5 4264d34f1c --offscreen: hide the window and leave the system pointer alone
An offscreen game renders into a texture, so its window only ever showed
white, yet entering a world grabbed the system pointer: on Windows it was
clipped to one pixel even while the window wasn't in front, until the
person alt-tabbed. The window is now created hidden, the grab is the
game's own state (agents still turn the view with mouse-move), and the
real mouse's raw motion never steers an offscreen game.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 20:32:38 +02:00
Ludovic MarieandClaude Opus 5.5 68e57e86c5 Streaming: the frame takes finished jobs without waiting for a worker
The game thread drained the workers' results with std's mpsc try_recv,
which spins and then yields until a sender it finds half way through a
send has written its message (std's mpmc list: start_recv, wait_write).
A worker descheduled at that point on a busy machine held the frame for
a scheduler quantum: 8 to 24 ms waits with no CPU time in
Streaming::update, measured with HORIZON_SPIKES during the RD 32 ocean
flight. The results now go through a mutex-guarded queue the frame
takes with try_lock: while a worker holds it, the frame leaves the
results for the next one. The blocking waits (save, wait, drop) keep
their timeouts on a condvar.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 19:28:52 +01: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
Ludovic MarieandClaude Opus 5.5 20ffb882bd Merge the launcher layer (main c7065027b) into the docs layer
main's edit to the WebAssembly server mods section goes with the section.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 19:23:40 +01:00
Ludovic MarieandClaude Opus 5.5 38a315331d Merge the crates layer (main c7065027b) into the launcher layer
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 19:22:50 +01:00
Ludovic MarieandClaude Opus 5.5 36ca9df57b Merge the game layer (main c7065027b) into the crates layer
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 19:22:04 +01:00
Ludovic MarieandClaude Opus 5.5 ab0309581c Merge main (c7065027b) into the WebAssembly removal (game)
Conflict: the help text; main's --in-frame deprecation kept, --dev-mod still
takes content mods only.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 19:21:29 +01:00
Ludovic MarieandClaude Opus 5.5 20acff0b3c Scheduled ticks: run the due ticks against one level
`tick_scheduled` built the level (`with_level`: the boxes of every cart,
arrow and guest, the fire-spreading players, the game rules, the ticket
levels) again for each due block and fluid tick. Run the due ticks in one
`with_level` while each leaves nothing for the game to apply between
them, level events or a spill; then the game handles those as before, so
every tick still sees what the ones before it did.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 19:20:16 +01:00
Ludovic MarieandClaude Opus 5.5 1ea5ec5964 Merge main into hdr-tiers: the tiers paragraph beside HDR's own screen and Auto
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 19:19:29 +01: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
Ludovic MarieandClaude Opus 5.5 46ac49ad05 Launcher: Language::ALL holds seven languages, formatted
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 19:02:12 +01:00
Ludovic Marie d23ab5ae50 Merge remote-tracking branch 'origin/main' into pt1-2795 2026-10-29 19:02:03 +01:00
Ludovic MarieandClaude Opus 5.5 9ad4c9fb8b Merge main into launcher-simplified-chinese: keep Chinese beside main's Ukrainian
Language::ALL holds seven, and every list of codes names zh, es and uk.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 19:01:03 +01:00
bloodulonandQoder 27e16c3e01 Merge origin/main into pr/packs-overlay
Resolved zip.rs: main added a_name_falls_back_to_its_folder_entry where this
branch added its deflate tests; kept both.

Co-Authored-By: Qoder <[email protected]>
2026-10-29 22:54:48 +05:00
GIGABAIT93 88e10db8e5 Merge branch 'main' of https://github.com/horizon-rust/horizon into feat/launch-api-client
# Conflicts:
#	crates/horizon-client/src/game.rs
2026-10-29 19:54:47 +02:00
Ludovic MarieandClaude Opus 5.5 1c4b029b95 Merge the launcher change (main d009ebd) into the docs
main's content mod docs (items patch their template's components) kept; its
change to the WebAssembly API reference goes with the page.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 18:54:01 +01:00
Ludovic MarieandClaude Opus 5.5 31c0c3dfe4 Merge the crate removal (main d009ebd) into the launcher change
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 18:52:54 +01:00
levzzz c7065027b1 Merge pull request #3461 from senjinthedragon/hdr-wayland
HDR: Auto Paper White and Peak Brightness, from what the compositor reports
2026-10-29 22:52:21 +05:00
rdbt b97e39e074 Merge pull request #3800 from horizon-rust/chore/deprecate-in-frame
Deprecate --in-frame (unsupported, will be removed soon)
2026-10-29 17:49:28 +00:00
Ludovic MarieandClaude Opus 5.5 2fb276d5f6 Merge the game branch (main d009ebd) into the crate removal
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 18:48:54 +01:00
levzzz 1deaac7263 Merge pull request #3820 from horizon-rust/fix/prepare-output-version-2
horizon-prepare: OUTPUT_VERSION 2 (the command registries now export selected_entities)
2026-10-29 22:48:11 +05:00
Ludovic MarieandClaude Opus 5.5 26f099d168 Merge main (d009ebd) into the WebAssembly removal (game)
Conflict: main made chat_tree callable from command blocks, with the
WebAssembly mods for its argument; it stays callable, without them.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 18:47:49 +01:00
rdbt ef66d5b892 Merge branch 'main' into chore/deprecate-in-frame 2026-10-29 17:41:01 +00:00
Ludovic MarieandClaude Opus 5.5 b732e582b9 Merge main into feat/audio-output-device: keep soundDevice beside main's new option keys
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 18:38:36 +01:00
levzzzandClaude Opus 5.5 f145c903b6 Merge main into hdr-wayland (with #3176 and #3203 merged)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 22:38:32 +05:00
levzzz d009ebd140 Merge pull request #3203 from senjinthedragon/hdr-screen
HDR: its settings get a screen of their own
2026-10-29 22:38:18 +05:00
Federico Tagliani fea840e0d4 Merge origin/main into fix/break-effects-for-popped-blocks 2026-10-29 18:38:08 +01:00
k3yhandClaude Opus 5.5 7830c6de4a Merge main into the branch; keep both spawner.rs additions (acting_player_is_op, tick_remote_spawners)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 12:37:58 -05:00
Ludovic Marie 0c49146fc8 Merge remote-tracking branch 'origin/main' into fix/prepare-output-version-2 2026-10-29 18:37:49 +01:00
Ludovic MarieandClaude Opus 5.5 cf558b0c0a Merge the game branch (main 1b38710) into the crate removal
main's change to the SDK's horizon.wit goes with the SDK.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 18:37:38 +01:00
Ludovic MarieandClaude Opus 5.5 1fb8e84561 Merge main into the WebAssembly removal (game)
Conflicts: main's changes to mod_render.rs, server_mods.rs and the runtime
part of mods.rs sit in WebAssembly code this branch deletes. Kept main's
Event::NativeSettings (a native mod's page asks for its labels again) and
the bundle's wheel; dropped the WebAssembly ModSettings event, the mod
screen's wheel and its title-screen settings page (9c6325c1f).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 18:36:42 +01:00
Ludovic MarieandClaude Opus 5.5 0f9adac090 Merge main into hdr-tiers: keep #3176's UI brightness beside the light tiers
UI Brightness takes params[6]; the tier lift also skips pixels the hand or
HUD covers, as main's lift does.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 18:34:26 +01:00
Ludovic Marie cbbdf773e2 Merge pull request #3817 from GIGABAIT93/fix/launcher-language-count
Launcher: Language::ALL holds six languages (main doesn't build)
2026-10-29 18:34:25 +01:00
Ludovic Marie e1207bb017 Merge pull request #3453 from d3grando/fix-3357
🤖 Agent: proven on the integration branch union/pt1-w2 (23 PRs on main 89f949155) by pr-triage-1: tools/pr_ready.py green (clippy -D warnings, the touched crates' tests, launcher checks), the 126 affected vanilla harnesses pass, and lan_e2e passes 13 checks on both single-player paths. Thank you!
2026-10-29 18:34:17 +01:00
Ludovic Marie 94a737918a Merge pull request #3443 from d3grando/fix-shulker-box-menu
🤖 Agent: proven on the integration branch union/pt1-w2 (23 PRs on main 89f949155) by pr-triage-1: tools/pr_ready.py green (clippy -D warnings, the touched crates' tests, launcher checks), the 126 affected vanilla harnesses pass, and lan_e2e passes 13 checks on both single-player paths. Thank you!
2026-10-29 18:34:10 +01:00
Ludovic MarieandClaude Opus 5.5 7ed5855e4a horizon-prepare: OUTPUT_VERSION 2 (command registries export selected_entities)
#3410 made the command parse oracle, which also exports
command-registries.json during preparation, write "selected_entities". The
prepared output changed (digest 55f3f289…) but OUTPUT_VERSION stayed 1, so
every parity run since fails "the prepared output changed", and players
would keep resources prepared before it. #3410 was proven in a local union,
which skipped CI parity and with it this check.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 18:32:53 +01:00
Federico TaglianiandClaude Sonnet 5.5 582331c3ea lan_e2e: popped_break_effects mines by hand in survival, torch on top
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-29 18:32:47 +01:00
levzzz e3cbe2688f Merge pull request #2911 from masa-masa-0314/fix/world-select-icons
Select World: show each world's preview icon
2026-10-29 22:30:37 +05:00
levzzz 7dc72d6c33 Merge pull request #3582 from Username59138/fix/command-block-screen
Important fix: command blocks keep their command, suggest commands and dim the screen as vanilla
2026-10-29 22:30:23 +05:00
Ludovic Marie f4e1a98127 Merge pull request #3446 from d3grando/fix-3407
🤖 Agent: proven on the integration branch union/pt1-w2 (23 PRs on main 89f949155) by pr-triage-1: tools/pr_ready.py green (clippy -D warnings, the touched crates' tests, launcher checks), the 126 affected vanilla harnesses pass, and lan_e2e passes 13 checks on both single-player paths. Thank you!
2026-10-29 18:27:36 +01:00
levzzzandClaude Opus 5.5 e5087bdb5a Merge main into hdr-screen (with #3176 merged)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 22:27:34 +05:00
levzzz 0e7918fb88 Merge pull request #3176 from senjinthedragon/hdr-ui
HDR: UI Brightness, and the HUD and menus never lift
2026-10-29 22:27:20 +05:00
GIGABAIT93 8b65dc4516 Merge branch 'main' of https://github.com/horizon-rust/horizon into feat/launch-api-client
# Conflicts:
#	crates/horizon-client/src/game.rs
2026-10-29 19:26:44 +02:00
levzzz e5076cd6ab Merge pull request #3230 from Mik00000/fix/spawner-mob-display
Display mob inside spawners and trial spawners (fixes #3107)
2026-10-29 22:23:08 +05:00
Ludovic MarieandClaude Opus 5.5 73558e3673 Merge main into hdr-tiers: keep #3145's black level beside the light tiers
The black level takes params[5], before the tiers' vec4s at 8, 12 and 16.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 18:22:59 +01:00
levzzz 3d4cade73b Merge pull request #3544 from degepie/fix/issue-2841-frog-jump
Frogs long jump: FrogAi's long jump activity (fixes #2841)
2026-10-29 22:22:45 +05:00
levzzzandClaude Opus 5.5 1332c90868 Merge main into the branch; the command box types AltGr characters as main's other text boxes do
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 22:22:27 +05:00
Federico TaglianiandClaude Sonnet 5.5 eed02f0cda lan_e2e: popped_break_effects check for blocks that pop off a mined support
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-29 18:20:27 +01:00
levzzz 1b38710e92 Merge pull request #2970 from kacpherek/fix/multiplayer-safety-notice
Show the multiplayer safety notice on the first Multiplayer click
2026-10-29 22:20:02 +05:00
levzzz aaa301b2e5 Merge pull request #2924 from elythepreciouscat/feat/vanilla-controls-26-3
Options: add Java 26.3 controls
2026-10-29 22:19:46 +05:00
levzzzandClaude Opus 5.5 3052227936 HDR Auto: watch the display only while HDR output is on
The Wayland watch was made whenever the surface offered HDR, so with
HDR off (the default) the renderer still did a blocking dispatch on
winit's connection at startup. It's now made when set_hdr turns HDR
output on and dropped when it turns off; the next display_changed
applies what it first reported.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 22:19:42 +05:00
levzzz 95a366fe89 Merge pull request #3049 from kacpherek/feat/render-distance-128
Raise the render distance cap from 64 to 128 chunks
2026-10-29 22:19:30 +05:00
levzzz fc0e275414 Merge pull request #3229 from Mik00000/fix/bow-arrow-types
Support firing spectral and tipped arrows from bows and crossbows
2026-10-29 22:19:13 +05:00
levzzz 5dc0515248 Merge pull request #3148 from echo-ilovech3ss/fix/middle-click-pick-block-selection
Select the picked hotbar slot in singleplayer and LAN
2026-10-29 22:18:57 +05:00
levzzzandClaude Opus 5.5 20a3c3e399 Merge main, #3176 and #3203 into hdr-wayland: Auto beside Black Level and UI Brightness
Paper White and Peak keep their Auto step (0) on Graphics: HDR, where
#3203 moved them; Black Level and UI Brightness are saved, parsed and
passed to set_hdr beside the Auto nits. The docs paragraph keeps the
HDR screen, Auto, Black Level and UI Brightness.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 22:18:50 +05:00
levzzz 0a6020695b Merge pull request #3131 from Mik00000/fix/invisible-frames-custom-heads
Fix invisible item frames (#2925)
2026-10-29 22:18:42 +05:00
levzzzandClaude Opus 5.5 dfde0ac091 Merge main and #3176 into hdr-screen: all five HDR settings on HDR's screen
Main gained Black Level (#3145) and #3176 adds UI Brightness; both
move from Display to Graphics: HDR with the rest, in main's order
(Black Level, Peak, Paper White, UI Brightness), and wait for the HDR
switch like the other sliders.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 22:16:24 +05:00
Claude f4bdc66781 Never wait on the owner's profile fetch: list its skin once it lands
The owner's login to its server thread is main's again: no properties
and no wait. The player list gives the integrated server's owner the
signed-in account's properties instead (Hub::player_info), taken from
the background fetch only if it has already answered, so a slow, failing
or absent network can't delay opening a world. A LAN guest joins after
the world is open, well after the fetch.

Tests: the owner's login doesn't wait on a fetch that never answers; the
player list's owner entry before, after and without the fetch; and in
tests/lan_bot.rs (needs the prepared resources) a TCP guest's player
list carries the owner's textures and none of its own.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CxQvrTahuwEpRxJpTvWpjG
2026-10-29 17:15:39 +00:00
GIGABAIT93andClaude Opus 5.5 dc274d681c Launcher: Language::ALL holds six languages
Merging main into #3625 put Spanish and Ukrainian both in Language::ALL
but left its length at 5, so the launcher no longer builds on main
(E0308: expected an array with a size of 5, found one with a size of 6).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 19:14:26 +02:00
levzzzandClaude Opus 5.5 246a071dbc Merge main into hdr-ui: UI Brightness beside main's Black Level
Main gained HDR Black Level (#3145). Both settings are kept: the
params slot after `pq` is the black level and the next one is the UI
brightness; set_hdr takes both; options save hdrBlackLevel and
hdrUiBrightness; the UI Brightness row follows Paper White.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 22:14:22 +05:00
levzzz 1e965ec5a8 Merge pull request #3749 from reservedbytes/fix/jmath-atan2
fix(core): atan2 matches StrictMath
2026-10-29 22:12:08 +05:00
Federico TaglianiandClaude Sonnet 5.5 bc5fd427ab Armor stands: isFalling before the move, isInWater from the fluid surface
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-29 18:12:03 +01:00
levzzzandClaude Opus 5.5 a7ff35cf7b Merge horizon-rust/main into fix/world-select-icons
Resolves two conflicts:
- gfx.rs: main made padded_row/copy_frame free functions (copy_frame takes
  the device and size) and dropped write_screenshot's COPY_SRC check for
  the OpenGL readback target; take_readback now calls the free copy_frame
  and write_screenshot keeps the ScreenshotRequest signature.
- remote.rs leave_server: keeps main's hud_disconnected (which resets the
  boss overlay), overlay_time and item_activation reset, plus this branch's
  world_icon = None.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 22:10:21 +05:00
levzzz 3e31a55950 Merge pull request #3644 from iLostName/invisible-player-third-person
invisibility: the player's own body is hidden in third person and the inventory, its armour still drawn
2026-10-29 22:10:15 +05:00
levzzz 4e910746ce Merge pull request #3625 from GIGABAIT93/feat/ukrainian
Launcher: Ukrainian
2026-10-29 22:10:04 +05:00
levzzz 06254caea3 Merge pull request #3751 from leifiyoo/fix/creative-drop-items
Creative display drops reach the server on a server connection (#3523)
2026-10-29 22:07:59 +05:00
levzzz 7b78be8d4e Merge pull request #3738 from GIGABAIT93/fix/server-pack-overlay-cache
Server resource packs: keep the overlay when the pack is only touched
2026-10-29 22:07:53 +05:00
levzzz 73602536bb Merge pull request #3717 from 1038april/feat/own-name-tag
Extras: show your own name tag in third person
2026-10-29 22:07:47 +05:00
levzzz d601fc2e54 Merge pull request #3701 from Domanffe/fix/launcher-nightly-label
Launcher: a Nightly row in Versions reads as its commit
2026-10-29 22:05:21 +05:00
levzzz 9ca1b032ce Merge pull request #3673 from d3grando/fix-3001
Type AltGr characters into chat and search boxes on Windows
2026-10-29 22:05:16 +05:00
levzzz 5b8898d018 Merge pull request #3660 from xFedeT/fix/mod-settings-at-title
Mods: Options > Mods > Settings opens the mod's page at the title screen
2026-10-29 22:05:10 +05:00
levzzz 28a4e5443f Merge pull request #3652 from d3grando/fix-3211
The mouse wheel selects a bundle's item in the creative inventory
2026-10-29 22:05:04 +05:00
levzzz dc05ba0cc7 Merge pull request #3641 from iLostName/pack-mcmeta-folder-entry
resource packs: a zip that stores pack.mcmeta as pack.mcmeta/ loads, as ZipFile.getEntry finds it
2026-10-29 22:04:58 +05:00
Ludovic MarieandClaude Opus 5.5 2c8b7ee172 Merge main into the branch: keep both lang test blocks
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 18:04:37 +01:00
levzzz 74cf4f8c53 Merge pull request #3646 from d3grando/fix-3525
Shader packs: a program the shader compiler panics on no longer crashes the game
2026-10-29 22:04:15 +05:00
DenisHumenandClaude Opus 5.5 e4c1af1fff Launcher: the version and account menus close on a click outside them
Both menus only closed from their own button or a row. They now close on
a mouse-down anywhere outside them; a press that does so on the menu's
own button no longer reopens it when it ends. Fixes #3216.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 19:02:25 +02:00
levzzz 335d11e35e Merge pull request #3640 from d3grando/fix-3552
Graphics settings: the scrollbar can be held and dragged
2026-10-29 22:02:16 +05:00
levzzz cc7bcbf4e5 Merge pull request #3731 from Joansitoh/refactor/launcher-fields-share-line-edit
Launcher: every text field uses the shared LineEdit
2026-10-29 22:01:18 +05:00
levzzz 182b9a1473 Merge pull request #3775 from martipops/split/05-viewports
Render: the splitscreen layout model (where each player's view goes)
2026-10-29 22:01:03 +05:00
levzzzandClaude Opus 5.5 cb3f4387db Merge main into the invisibility branch: render_layers beside main's cape, and no cape while invisible
Main added CapeLayer (mobs::render::cape) at the spot this branch adds render_layers;
both stay. CapeLayer draws only when !state.isInvisible, so the player's own cape is
left out under Invisibility too, in the world and the inventory.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 22:00:59 +05:00
levzzz db0d65ee3c Merge pull request #3771 from elythepreciouscat/fix/linux-opengl-taa
Linux: OpenGL falls back when TAA pipeline cannot compile
2026-10-29 22:00:57 +05:00
levzzz b328f4d660 Merge pull request #3765 from iLostName/far-terrain-render-distance-drop
far terrain: chunks past a shrunk render distance stay drawn until their tiles are ready
2026-10-29 22:00:51 +05:00
levzzz 37f8b6a525 Merge pull request #3145 from senjinthedragon/hdr-black
HDR: a black level setting, for displays whose black isn't 0
2026-10-29 22:00:46 +05:00
levzzz 3a9d71b2db Merge pull request #3726 from martipops/split/01-compositor
Render: present offscreen views through a window compositor
2026-10-29 22:00:40 +05:00
levzzz 0a1a6908c0 Merge pull request #3705 from arismoko/upstream-mod-id-order
Allocate mod item IDs in deterministic registry order
2026-10-29 22:00:34 +05:00
Ludovic MarieandClaude Opus 5.5 95c234cc1e Merge main into the branch
Keep main's Component::Instrument beside this branch's StewEffects (enum,
test and item_wire_probe).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 18:00:27 +01:00
levzzz 201ae4d255 Merge pull request #3774 from martipops/split/04-pointer-ownership
Input: one player owns the keyboard, mouse and pointer (splitscreen groundwork)
2026-10-29 21:59:25 +05:00
levzzz 918ff50c7a Merge pull request #3752 from xFedeT/fix/player-scale-attribute
Player model: /attribute minecraft:scale scales the model, not only the camera
2026-10-29 21:59:19 +05:00
levzzz e2ce8e42a6 Merge pull request #3739 from UMCEKO/mods/item-components
Content mods: items patch their template's data components
2026-10-29 21:59:13 +05:00
levzzzandClaude Opus 5.5 5815bf4f11 jmath::atan2: use the fdlibm port geometry::trig already has
horizon_core::geometry::trig::fdlibm_atan2 is the same e_atan2.c port, and it
passes every vector of atan2_matches_strictmath, so jmath calls it instead of
keeping a second copy.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 21:58:48 +05:00
levzzzandClaude Opus 5.5 9c21a95409 Merge main into feat/ukrainian: Ukrainian beside main's Spanish
Both languages stay in Language; the translation test walks Language::ALL; the CLI's
help, its error and the docs now list es too.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 21:57:20 +05:00
levzzz 91ae68828a Merge pull request #3699 from jaszkus/fix/creative-trash-tooltip
Creative inventory: the Destroy Item tooltip follows the language
2026-10-29 21:55:58 +05:00
levzzz a6bcea674c Merge pull request #3667 from xFedeT/fix/mod-structure-suggestions
Mods: structures, pools and templates from content mods show in command suggestions
2026-10-29 21:55:52 +05:00
levzzz 938ae3ca47 Merge pull request #3650 from d3grando/fix-3171
Draw lightning bolts: sample inside the white sprite
2026-10-29 21:55:47 +05:00
levzzz 5b87045fee Merge pull request #3632 from SU4G3/fix/horizon-ctl-drop-remote
horizon-ctl drop: let the server throw the stack on a client of a server
2026-10-29 21:55:41 +05:00
Federico TaglianiandClaude Sonnet 5.5 dacde01da2 lan_e2e: merchant-close check for the trade screen closing out of reach
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-29 17:55:16 +01:00
levzzz 25776d1a4c Merge pull request #3602 from GIGABAIT93/fix/pack-cache-test-mtime
Pack cache tests: wait for the filesystem clock's next tick after each build
2026-10-29 21:54:52 +05:00
levzzz 247b67a77d Merge pull request #3587 from GIGABAIT93/fix/windows-pr-ready
Python tools and tests: the same results on Windows
2026-10-29 21:54:45 +05:00
levzzz 47e8375fd1 Merge pull request #3547 from NikitaRTN/test/nonblocking-disconnect
Net: guard fast server disconnect against a stalled reader
2026-10-29 21:54:39 +05:00
levzzz e4a131f5c7 Merge pull request #3470 from d3grando/bench-windows
perf_bench: run on Windows (and pass vanilla the manifest's -XX flags)
2026-10-29 21:54:33 +05:00
Ludovic Marie 4f792eb7de Merge pull request #3803 from horizon-rust/docs/agent-attribution-template
🤖 Agent: docs only (AGENTS.md + PR template); verify, launcher and parity-scope green; queued.
2026-10-29 17:53:05 +01:00
levzzz e431a41ab1 Merge pull request #3698 from iLostName/smooth-chunk-arrival
chunk loading: a worker frees the meshes the frame has uploaded
2026-10-29 21:52:02 +05:00
levzzz dfabf62567 Merge pull request #3691 from 1038april/fix/map-hands
Draw the first-person arms under a held map
2026-10-29 21:51:54 +05:00
levzzz 7dd05db9be Merge pull request #3683 from repositorykreml1n/main
Add files via upload
2026-10-29 21:51:46 +05:00
levzzz d0f6223a90 Merge pull request #3654 from d3grando/fix-3510
Screenshots on OpenGL draw the frame where it can be read back
2026-10-29 21:51:40 +05:00
levzzz c457259ae2 Merge pull request #3649 from d3grando/fix-3597
startup: report why resource loading failed instead of "loader stopped"
2026-10-29 21:51:32 +05:00
Ludovic MarieandClaude Opus 5.5 3e1f39899c Merge main into fix-3153: keep both the wither breaks and ender dragon corpus groups
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 17:51:27 +01:00
levzzz 088107b407 Merge pull request #3626 from GIGABAIT93/feat/ukrainian-game
Game: Horizon's own strings in Ukrainian
2026-10-29 21:51:26 +05:00
Muhammad Asif 26c139f901 Keep launcher links on horizon-rust/horizon in release notes
Player-facing notes used GITHUB_REPOSITORY, so fork CI rewrote the
launcher releases href and failed
test_a_game_release_opens_with_the_way_to_the_launcher.
2026-10-29 21:51:25 +05:00
levzzzandClaude Opus 5.5 ec86a343c5 Merge main into fix-3552
graphics_ui.rs: main passes the settings to layout, content_height and scroll;
the held scrollbar's code and tests take them too.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 21:51:11 +05:00
levzzzandClaude Opus 5.5 21fdd33cdf Merge main into fix-3525
crash.rs: keep main's report_watchdog beside this branch's payload_text.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 21:49:57 +05:00
DenisHumenandClaude Opus 5.5 07cd6c90a8 rea_vanilla: run npx by its path, so it starts on Windows
On Windows npx is npx.cmd, and subprocess (CreateProcess) does not find
it by name: the script stopped with FileNotFoundError. Resolve it with
shutil.which, as the script already does for rea, and say what is
missing when Node.js isn't installed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 18:49:33 +02:00
levzzzandClaude Opus 5.5 d4f466623d Merge main into fix/issue-3024-worn-armor-glint: the armour glint moves to overlay 36
Main gave overlay 35 to text drawn with a polygon offset (TEXT_POLYGON_OFFSET_OVERLAY,
its own material in gfx/entity_batch.rs), so armour with 35 would be drawn as that
material. The test now lists it among the overlays the glint must not take.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 21:47:51 +05:00
levzzz ee6cf6acb8 Merge pull request #3609 from degepie/fix/issue-3040-cursor-shapes
Title buttons and creative tabs show the pointing hand under the mouse (#3040, #3042)
2026-10-29 21:47:46 +05:00
levzzz b0831e54c8 Merge pull request #3606 from degepie/fix/issue-3028-splash-sounds
Splash sounds and particles when the player falls into water (#3028)
2026-10-29 21:47:40 +05:00
levzzz f1341139ff Merge pull request #3556 from d3grando/fix-3173
Jukebox: a server's client plays the song of level event 1010
2026-10-29 21:47:34 +05:00
Ludovic MarieandClaude Opus 5.5 4f9cc17e91 AGENTS.md: contributors' agents name their own person
The attribution rule gave Ludovic's exact line first, so contributors' agents
copied it: #3800 (rdbtCVS) opened as "Ludovic's AI agent", #3499 as "AI
agent (Codex), not by himself". The rule is now a template filled with the
login the PR is opened from; Ludovic's line stays as a labelled example. The
PR template carries the same line as a comment.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 17:47:28 +01:00
levzzz 8913deaa76 Merge pull request #3384 from SolomonNumb1/fix/smithing-ingredients
сlean up station recipes
2026-10-29 21:46:25 +05:00
levzzz 66d731cbf0 Merge pull request #3608 from degepie/fix/issue-3008-phantom-hitbox
Draw the phantom's model inside its box (#3008)
2026-10-29 21:44:48 +05:00
levzzz acb209dbf7 Merge pull request #3337 from degepie/fix/perf-bench-javac-java-home
perf_bench: compile the driver mod with JAVA_HOME's javac
2026-10-29 21:44:03 +05:00
rdbtCVSandClaude Sonnet 5.5 74f0265db1 Deprecate --in-frame, --no-server-thread and HORIZON_SERVER_THREAD=0
The in-frame single-player path is unsupported and will be removed soon.
The game prints a deprecation warning when it is requested, the help text
and docs say never to use it, and the e2e drivers warn on their flags.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-29 16:43:43 +00:00
levzzzandClaude Opus 5.5 fe47804f28 Merge main into hdr-wayland: display_changed beside main's TAA set_world_post
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 21:43:21 +05:00
levzzz e8214a02aa Merge pull request #3177 from senjinthedragon/screenshot-log
Screenshots are named in local time, and the log says where they went
2026-10-29 21:41:49 +05:00
levzzz 49f4359fcf Merge pull request #3089 from Expected333/vr/view-set
render: draw from a ViewSet of per-eye views (VR groundwork)
2026-10-29 21:41:44 +05:00
levzzzandClaude Opus 5.5 0503ee0cbf gfx: rustfmt's order for the frame_gen module
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 21:40:52 +05:00
levzzzandClaude Opus 5.5 f12d87b5d9 Merge main into hdr-screen: the HDR offer beside main's log, and the test on main's slider API
Main's graphics screen now takes the settings in layout and extra_rows, and a
slider press starts a drag (SliderDrag) that saves on release (#2711).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 21:40:34 +05:00
DenisHumenandClaude Opus 5.5 01bf07dd5d Buckets and bottles pick up the water the eyes are in
BlockGetter.traverseBlocks visits the block a ray starts in first, and
VoxelShape.clip hits a shape that contains the ray's start. pick_fluid
began stepping from the eyes' block without testing it, so under water
a bucket took the next source along the ray (the feet's, looking down)
and no air pocket could be made. Fixes #3787, #3233.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 18:40:25 +02:00
levzzzandClaude Opus 5.5 0b63d040ea Merge main into hdr-ui: hdrUiBrightness beside main's taa and upscaling
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 21:38:13 +05:00
levzzzandClaude Opus 5.5 b19d8659ff Merge main into hdr-black: settings now also save taa and upscaling
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 21:37:24 +05:00
ThatCakeIsAPieandClaude Opus 5.5 c9999e3336 Worldgen: data packs' block state providers join the state_providers bundle
worldgen/block_state_provider was missing from overrides::KINDS, so a
pack's own providers (WWOO's wythers:stony_cliffs) never reached the
state_providers bundle the generator already reads, and every feature
using one was skipped. pack_files_go_to_their_bundles covers the folder.

Reported by AdachiReiYay's agent on #3476.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 11:36:10 -05:00
levzzzandClaude Opus 5.5 f9b002f804 Merge horizon-rust/main into fix/spawner-mob-display
Resolves the conflict in game/remote.rs tick_remote: keeps both the
remote spawner tick and main's client_tick_beacons.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 21:31:36 +05:00
bloodulon 1ab99bec27 Packs: an unreadable zip entry costs that file, not the whole overlay 2026-10-29 21:26:49 +05:00
ThatCakeIsAPie 6b01498127 Merge feat/level-shape-11 into feat/datapack-biomes-1: data packs' block state providers 2026-10-29 11:24:10 -05:00
ThatCakeIsAPie 5983bae947 Merge feat/level-shape-10 into feat/level-shape-11: data packs' block state providers 2026-10-29 11:24:10 -05:00
ThatCakeIsAPie ab82d71a96 Merge feat/level-shape-9 into feat/level-shape-10: data packs' block state providers 2026-10-29 11:24:09 -05:00
ThatCakeIsAPie c3daaa8762 Worldgen: data packs' block state providers join the state_providers bundle
worldgen/block_state_provider was missing from overrides::KINDS, so a
pack's own providers (WWOO's wythers:stony_cliffs) never reached the
state_providers bundle the generator already reads, and every feature
using one was skipped. pack_files_go_to_their_bundles covers the folder.

Reported by AdachiReiYay's agent on #3476.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit bd92abce3b0ba8f209f8a613acd5741cb9e4b825)
2026-10-29 11:24:09 -05:00
ThatCakeIsAPieandClaude Opus 5.5 3a37c32d8d Merge upstream main into feat/level-shape-1
world.rs keeps main's FxHashMap/FxHashSet imports beside this branch's
SectionMask. Main's writes_mark_exactly_the_reachable_sections sorted
(column, mask) pairs, and a SectionMask has no order: it sorts by the
column, which each pair has once.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 11:23:30 -05:00
Ludovic Marie 64e298952d Merge pull request #3054 from kacpherek/fix/elytra-flying-sound
🤖 Agent: PR triage (Ludovic's AI agent, Claude): main merged in (ff). ElytraOnPlayerSoundInstance.tick line for line (stop when removed or after 20 ticks without isFallFlying, volume clamp(speed/4), silent 20 ticks then a 20-tick fade-in, pitch 1+(v-0.8) above 0.8), started like LocalPlayer.onSyncedDataUpdated when gliding starts. #3227 (same port) closed as its duplicate. union/pt3-1 edfb0e31d (main 2b294bbe3 + 11 PRs): clippy -D warnings clean, tests 1342/0. CI parity on this head.
2026-10-29 17:22:36 +01:00
Ludovic MarieandClaude Opus 5.5 2286b52f01 Streaming: build each proto chunk's light view once
A column at the edge of the loaded area lights against its decorated
neighbours past the area, so the streamer turns those proto chunks into
columns (`light_view`) on the tick thread. It built them again for every
light job and for every retry of a column still waiting on its last
neighbour: on a dedicated server with 50 bots, 59 220 views for 5 345
proto chunks. Keep each view until its proto is checked out, replaced or
removed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 17:20:51 +01:00
GIGABAIT93andClaude Opus 5.5 91591e4dcf Player skins: skin_entries as a function, with a test
skin_entries holds the lookup update_remote_skins does, so a test can
play an NPC plugin's add, spawn and remove: the entity keeps the skin
its entry gave, a player spawned before its entry takes it when it
comes, and a removed entity forgets it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 18:14:55 +02:00
Federico TaglianiandClaude Sonnet 5.5 6180a78195 Ghast: target position, move control timing, look angle and shot sounds as vanilla
getSuitableFlyToPosition returns the first random position (no mirror under
the heightmap unless 64 tries fail); GhastMoveControl tests floatDuration
before decrementing it; GhastLookGoal turns with the float 180/pi and
Mth.atan2; the warning cry and the shot are level events whose pitch is the
client's, not draws from the ghast's random.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-29 17:14:38 +01:00
Ludovic Marie cad2a29d2a Merge pull request #3759 from horizon-rust/feat/official-server-launcher
🤖 Agent: merging. Horizon Official at the top of the launcher's Multiplayer list, once, sharing the game's marker (Ludovic, 2026-10-29). pr_ready --quick green (launcher 273 tests); verify and the launcher matrix green.
2026-10-29 17:14:12 +01:00
levzzz a7fa9ad7bf Merge pull request #2964 from kacpherek/fix/tutorial-hints
Show tutorial hints and track their progress
2026-10-29 21:09:53 +05:00
levzzz c9cfa4a192 Merge pull request #3391 from ThatCakeIsAPie/perf/far-saved-chunks
Far terrain: stop decoding saved chunks for every distant column
2026-10-29 21:08:48 +05:00
Ludovic Marie 8d4196a29b Merge pull request #3761 from horizon-rust/feat/official-server-game
🤖 Agent: merging. Horizon Official at the top of Multiplayer, once (Ludovic, 2026-10-29). pr_ready --quick green (945 tests); verify green; parity runs in CI.
2026-10-29 17:06:50 +01:00
levzzz 7c681e0ed2 Merge pull request #3336 from matyden/mobs/rabbit-ocelot-avoid-player
Rabbits and wild ocelots run from players
2026-10-29 21:04:29 +05:00
Ludovic MarieandClaude Opus 5.5 a11e54e7b8 Merge main into the branch
Keep #3543's hurt flash for other players beside the main-hand stack.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 17:01:41 +01:00
levzzz 42e7d94082 Merge pull request #3479 from ThatCakeIsAPie/feat/datapack-worldgen-4
Harness: chunks generated with a world's data packs against vanilla
2026-10-29 20:59:01 +05:00
levzzz 5f7341487f Merge pull request #3478 from ThatCakeIsAPie/feat/datapack-worldgen-3
Data packs: a world's packs change its world generation
2026-10-29 20:58:44 +05:00
levzzz a5fba2ddac Merge pull request #3477 from ThatCakeIsAPie/feat/datapack-worldgen-2
Worldgen: the generator loads a world's data packs over the bundles
2026-10-29 20:58:25 +05:00
GIGABAIT93andClaude Opus 5.5 731896f222 Player entities keep the skin they found: NPCs show theirs
NPC plugins add an NPC's profile to the player list, spawn it, and take
the entry off the list a moment later to hide it there. Vanilla keeps
it: AbstractClientPlayer.getPlayerInfo looks the PlayerInfo up once and
holds it, so getSkin still reads its textures. PlayerInfoRemove dropped
the profile's textures here, so on tensa.co.ua every NPC showed its own
skin for about two seconds, then a default one (efe, zuri, sunny, kai).

level_player_textures keeps, per player entity, the textures it found
first, as level_player_names already keeps its name; update_remote_skins
resolves a player whose entry is gone from them. A new entity, a removed
one and a new level forget theirs.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 17:58:21 +02:00
levzzz f93b7cd0fa Merge pull request #3476 from ThatCakeIsAPie/feat/datapack-worldgen-1
Worldgen: data packs' entries over the prepared bundles
2026-10-29 20:58:07 +05:00
Federico TaglianiandClaude Sonnet 5.5 8b5145eaa5 Armor stands: bubble columns push them, and water slows and floats them
An armor stand used the air travel in water too and ignored bubble columns,
so one standing in a soul sand column never rose. LivingEntity.travel now has
its water branch (slow down 0.8, gravity / 16 through getFluidFallingAdjusted
Movement), and the blocks the stand is inside (a bubble column) push it
between the move and the friction, as Entity.move's checkInsideBlocks does.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-29 16:55:45 +01:00
levzzzandClaude Opus 5.5 bda07e1a84 Tutorial: an in-frame host acting for a guest leaves its own tutorial alone
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 20:55:22 +05:00
levzzzandClaude Opus 5.5 f065bab2e4 Merge main into the branch; the zombie villager's cure step gets the level random
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 20:55:13 +05:00
levzzz cefa6153d1 Merge remote-tracking branch 'hr/main' into fix-3544 2026-10-29 20:54:41 +05:00
Ludovic Marie ae175cc968 Merge pull request #3769 from horizon-rust/fix/launcher-keychain-portal
🤖 Agent: launcher matrix 40903932985 green at this head (Linux build compiles the new keychain store); queued.
2026-10-29 16:54:22 +01:00
levzzz 42ad9192e3 Merge remote-tracking branch 'hr/main' into HEAD 2026-10-29 20:53:35 +05:00
levzzz 8065b2830a Merge pull request #2927 from hectoreccles/perf/terrain-rustgen-ports
Worldgen: cut aquifer, carver and fill costs on the terrain stage
2026-10-29 20:53:22 +05:00
levzzz 0b3cf8d4f0 Merge pull request #3390 from ThatCakeIsAPie/perf/far-tiles-coarse
Far tiles: with Reduced far detail, approximate wide cells
2026-10-29 20:53:00 +05:00
levzzz 93921eccc4 Merge pull request #2981 from kacpherek/fix/dialogs-packs-parity
Server dialogs and resource packs: draw every dialog control, open links, keep pack order and animation metadata like vanilla
2026-10-29 20:52:26 +05:00
levzzz d1f4cd22ce Merge pull request #3409 from degepie/fix/issue-3142-totem-activation
Totem of undying: the item activation animation on screen (#3142)
2026-10-29 20:51:50 +05:00
levzzz 29c7ce8c7c Merge pull request #3380 from degepie/fix/goat-horn-instrument-sync
Goat horns keep their instrument from the server (#3181)
2026-10-29 20:51:39 +05:00
levzzz 869ffec2a1 Merge pull request #3543 from degepie/fix/issue-2827-hurt-red-tint
Hurt players flash red for the other players too (#2827)
2026-10-29 20:51:29 +05:00
levzzz adfff67c88 Merge pull request #3410 from degepie/fix/issue-3223-tp-uuid-suggestion
Suggest the crosshair entity's UUID for entity arguments (#3223)
2026-10-29 20:51:17 +05:00
levzzz 732f6eff1b Merge pull request #2982 from kacpherek/fix/server-core-parity
Server core: keep-alive timeout, pause grace, watchdog and function permission like vanilla
2026-10-29 20:51:07 +05:00
levzzz 15469c5501 Merge pull request #2818 from sirrayi/fix/rate-limit-property
Dedicated server: apply server.properties rate-limit
2026-10-29 20:50:56 +05:00
Federico TaglianiandClaude Sonnet 5.5 2f804ed55f Merge main into fix/wither-immune-to-wither-friends
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-29 16:48:18 +01:00
Ludovic Marie 0ac347d5b0 Merge pull request #2823 from sirrayi/fix/port-pack-unsafe-names
🤖 Agent: verified by Ludovic's AI agent (Claude): output paths stay inside the output folder; main merged, verify green. Queued for the merger.
2026-10-29 16:44:46 +01:00
levzzzandClaude Opus 5.5 80405878b5 Cats and ocelots sprint when they dash
Cat and Ocelot.customServerAiStep set sprinting while the move control
runs at 1.33 (fleeing a player) and the crouching pose at 0.6 (tempted).
LivingEntity.setSprinting adds SPEED_MODIFIER_SPRINTING (+0.3F,
ADD_MULTIPLIED_TOTAL) to the movement speed, which Horizon only used for
the model pose, so a fleeing ocelot or stray cat walked 1.3x too slowly.

The step now runs between navigation and the move control, as in
Mob.serverAiStep, and sets Extra.sprinting and the pose. A sprinting mob
also gets the rest of vanilla's isSprinting: the speed modifier, the
sprint jump impulse, the 0.9 water slowdown, no fluid-falling gravity
and the two random draws of spawnSprintParticle. The server sends shared
flag 0x08, the CROUCHING pose and the sprinting modifier; the client
reads them, and the feline model takes its pose from them.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 20:43:04 +05:00
Federico TaglianiandClaude Sonnet 5.5 7703ebcf95 Cat: avoidance goal last, sprint while dashing
Cat.reassessTameGoals adds the player-avoidance goal after registerGoals, so
it is evaluated last. Cat.customServerAiStep sets sprinting while the move
control's speed modifier is 1.33: +30% movement speed, and Entity.baseTick
draws twice from the mob's random for the sprint dust. The probe names the
goal CatAvoidEntityGoal as vanilla's subclass.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-29 16:42:19 +01:00
ThatCakeIsAPieandClaude Opus 5.5 52cc53a4f9 Surface rules: an empty biome list keeps the name lookup
An empty `biome_is` list on a surface that does not number its biomes
compiled to the id comparison, since collecting no indices never asks
`biome_index`. It then failed with MissingBiomeProvider where vanilla and
main answer false (material_condition's codec-corpus condition 49). It
keeps the name lookup main uses. Review on #3366.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 10:41:38 -05:00
NetuosoDEVandOpenAI Codex 267b92182e Format the hoisted block tint lookup
Apply rustfmt to the shorter default_tint call after hoisting the grass colour.

Co-Authored-By: OpenAI Codex <[email protected]>
2026-10-29 17:41:19 +02:00
ElyTheCatandAntigravity 1cb3aa5af6 Linux: OpenGL falls back when TAA pipeline cannot compile
The OpenGL backend's GLSL translator (naga) does not support `textureLoad`
from depth textures. When starting on OpenGL (such as on Linux with Intel
HD 4000 via Mesa crocus), `Post::new` panicked while compiling `fs_taa`
in `world_post.wgsl` (`WGSL textureLoad from depth textures is not
supported in GLSL`), crashing the game on startup.

Wrap TAA pipeline creation in an error scope, matching the animated
sprites compute pipeline fallback in `animate.rs`. When TAA cannot
compile, log a warning and disable TAA, letting the world post pass fall
back to FXAA or direct presentation without panicking.

Co-Authored-By: Antigravity <[email protected]>
2026-10-29 19:10:48 +03:30
NetuosoDEVandOpenAI Codex 240ac19e3c Compute default grass colour once per block-model draw
Move the active colormap lookup outside the model/face loops, as requested in
review. All tinted faces in this draw share the same default grass colour.

Implemented by OpenAI Codex on behalf of NetuosoDEV.

Co-Authored-By: OpenAI Codex <[email protected]>
2026-10-29 17:40:45 +02:00
Ludovic MarieandClaude Opus 5.5 7db3ecb67f Launcher: keep Microsoft sign-ins on Linux desktops that set GTK_USE_PORTAL, and say when the keychain doesn't
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 16:39:36 +01:00
Ludovic MarieandClaude Opus 5.5 32967a2f0f Merge main into this branch; the ominous bottle test's registries get dialog_tags
main's new item_sync test builds a Dynamic by hand; it takes the rest
(dialog_tags) from Default, as this branch's other tests do.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 16:38:38 +01:00
Ludovic Marie 62a23ba025 Merge pull request #3325 from ThatCakeIsAPie/feat/create-world-data-packs
🤖 Agent: verified by Ludovic's AI agent (Claude): follows CreateWorldScreen's temp data pack folder flow; main merged, clippy and the datapack/title/pack screen tests pass, verify green. Queued for the merger.
2026-10-29 16:37:10 +01:00
Ludovic Marie fff16ae2ed Merge remote-tracking branch 'origin/main' into pt3/2981 2026-10-29 16:35:43 +01:00
Ludovic Marie 2cacd5561c Merge pull request #3725 from horizon-rust/feat/launcher-java-join
🤖 Agent: launcher matrix 40899770886 green at this head; queued.
2026-10-29 16:32:56 +01:00
GIGABAIT93andClaude Opus 5.5 bf9928423c Text displays ride their vehicles: an NPC's name sits over its head
NPC plugins hang a text display on a fake player with SetPassengers, and
vanilla seats it every tick (Entity.rideTick, positionRider) at the
vehicle's passenger attachment, the top of its box, less the display's
own vehicle attachment, which is 0. remote_set_passengers placed only
mobs and the local player, so a ridden display stayed where it spawned,
at the NPC's feet (tensa.co.ua's lobby: the glyph titles under the NPCs).

A remote text display now keeps its vehicle and position_riders seats
it after the tick, from the last tick's seat, so it follows a moving
vehicle smoothly; a teleport sent meanwhile no longer drags it off.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 17:32:24 +02:00
spiderandClaude Opus 5.5 a91763379f FSR 3.1 optical flow, ported to WGSL (frame generation 1/3)
AMD FidelityFX SDK 1.1.4's optical flow (MIT): luma pyramids, scene change
detection, and a 7-level block search with filter and scale, as WGSL compute
passes that run on WebGPU's default limits. Not used by the game yet; the
interpolation and the setting follow.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 16:32:10 +01:00
Gitlab Main 8fdbad2d4b Merge origin/main into feat/audio-output-device 2026-10-29 20:58:32 +05:30
leifiyooandClaude Haiku 5.5 39b6aa46bc Furnace minecart keeps its push length, as vanilla does
MinecartFurnace.calculateNewPushAlong projects the push onto the
movement and scales it back to the push's own 3D length. Horizon kept
the push as x/z only and normalized with the vertical part, so the
push lost length every tick while gravity gave the movement a vertical
part (the cart moved with a 0.04 per tick fall). A cart fuelled from
about half a block away slowed down to about 0.56 to 0.79 of vanilla's
distance over its 3 minutes of coal.

Keep the push as a Vec3 between ticks. Saving still writes only PushX
and PushZ, and loading and fuelling set y to 0, as vanilla does.

Vanilla: MinecartFurnace.applyNaturalSlowdown, calculateNewPushAlong,
addFuel, addAdditionalSaveData, readAdditionalSaveData;
Vec3.projectedOn, Vec3.normalize.

Co-Authored-By: Claude Haiku 5.5 <[email protected]>
2026-10-29 16:26:53 +01:00
leifiyooandClaude Haiku 5.5 f9cac972ad Template placement clears the block entity it replaces
/place template wrote a block's saved data over whatever block entity
was at its position and dropped the old one without removal handling.
Over the same block state, place_state returns early, so a playing
jukebox kept its song and an open container screen stayed on the
replaced entity.

StructureTemplate.placeInWorld first sets a barrier where a block with
saved data goes, which makes LevelChunk.setBlockState remove the entity
there. Do the same before placing: remove the entity and its custom name,
stop a removed jukebox (place_state does that itself when the block's
name changes) and close a container screen open on it.

Refs #3708

Co-Authored-By: Claude Haiku 5.5 <[email protected]>
2026-10-29 16:25:28 +01:00
Ludovic MarieandClaude Opus 5.5 f5811c797a Merge main into the branch
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 16:25:16 +01:00
Ludovic MarieandClaude Opus 5.5 34c5dce056 Merge main into the branch
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 16:25:16 +01:00
Ludovic MarieandClaude Opus 5.5 38da7d8d6e Merge main into this branch
brush.rs: main's player_generates_container_loot trigger (#33c15a6d7) is
kept in unpack_brush_loot, which now reads the Brushable block entity.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 16:24:59 +01:00
Ludovic MarieandClaude Opus 5.5 5e6bed2e42 Merge main into this branch; the column fill uses the palette scan
#3253 (on main) replaced BlockSection's local_ids map with a scan of the
live palette and a last-lookup cache. fill_column now finds the state's id
with id_in, clears the cache when the old state's slot is freed, and sets
it when the section becomes uniform.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 16:23:23 +01:00
Ludovic Marie 8bb23aa1f7 Merge remote-tracking branch 'origin/main' into pt3/2927 2026-10-29 16:21:14 +01:00
levzzz a491181961 Merge pull request #3253 from hectoreccles/perf/opt-01-storage
perf: storage primitives (dirty sections, packed index, palette scan, Fx maps)
2026-10-29 20:18:07 +05:00
levzzz 4f69f45167 Merge pull request #3450 from d3grando/fix-3319
End the dragon fight when /kill removes the dragon
2026-10-29 20:16:49 +05:00
levzzz 2f9d889dcc Merge pull request #3367 from ThatCakeIsAPie/perf/worldgen-decoration-reads
Decoration: inlined block reads, column scans, per-chunk heightmap reads
2026-10-29 20:16:24 +05:00
levzzz a459a00584 Merge pull request #3365 from ThatCakeIsAPie/perf/worldgen-biome-stage
Biome stage: build free neighbour biome chunks first, cheaper id lookups
2026-10-29 20:16:13 +05:00
levzzz ede6d6dfbd Merge pull request #3364 from ThatCakeIsAPie/perf/worldgen-structure-climate
Structure checks: one climate volume per column, and each worker keeps its samples
2026-10-29 20:16:02 +05:00
levzzz 2cf38dd095 Merge pull request #3368 from ThatCakeIsAPie/perf/far-tiles-biome-memo
Far tiles: resolve each biome quart once per tile, and a far-tile bench
2026-10-29 20:15:34 +05:00
Ludovic Marie 89f4b34c1f Merge remote-tracking branch 'origin/main' into feat/launcher-java-join
# Conflicts:
#	launcher/src/main.rs
2026-10-29 16:15:13 +01:00
Ludovic MarieandClaude Opus 5.5 8ca96ad967 Server brand: Horizon's server names itself "horizon"
The integrated, LAN and dedicated server sent minecraft:brand "vanilla"
(MinecraftServer.getServerModName) while Horizon's client sends "horizon".
A modded server names itself, so Horizon's does too: F3 shows it, and a
server admin can tell Horizon servers apart (bunchy7s, Discord).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 16:14:47 +01:00
Ludovic MarieandClaude Opus 5.5 929e75181b Launcher Multiplayer: Horizon Official on top of the list, once
The Multiplayer section puts "Horizon Official" (horizon.bunchy7s.dev) first in the instance's
servers.dat, as the game does: once (horizon_official_server.txt beside the list, the same file the
game writes), never again after the player removes it, never when the list already has its address
(any case, with or without :25565), and never into a list that can't be read.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 16:12:59 +01:00
Federico TaglianiandClaude Sonnet 5.5 647e7c2aca Player model: /attribute minecraft:scale scales the model, not only the camera
LivingEntityRenderer.submit scales the whole pose stack by the entity's
scale. Horizon scaled the player's eye height, box and camera distance but
drew the model at scale 1, so a player with scale 2 looked half its size
from the pulled-back camera. The local player's model, and what hangs off it
(arms, held items, cape, elytra, armour), now takes the attribute; the
inventory preview divides it out as extractEntityInInventory does.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-29 16:11:10 +01:00
martipopsandAntigravity db34a746d1 Render: viewports and layout model for shared windows (split_layout)
- Add split_layout with place, place_ordered, ordered, and rows
- Calculate viewports for 1 to 16 players sharing a window (halves, thirds, quarters, balanced rows)
- Add 8 unit tests verifying boundary tiling, non-overlapping views, and area preservation
- Re-export split_layout in horizon-client

Co-Authored-By: Antigravity <[email protected]>
2026-10-29 11:10:42 -04:00
Ludovic Marie 84c5e0b63b Merge pull request #3561 from d3grando/fix-3243
🤖 Agent: read in full (pr-triage-1). A keyboard key bound to attack/use keeps clicking while held, as KeyboardHandler.keyPress sends every repeat to KeyMapping.set/click; horizon-ctl 'repeat' keeps the key held. Unit test added.
2026-10-29 16:07:52 +01:00
Ludovic MarieandClaude Opus 5.5 77a6cf7182 Multiplayer: Horizon Official, bunchy7s's survival server, listed once at the top
At startup, when a person is playing (not a server, benchmark or automated run), the game puts
"Horizon Official" (horizon.bunchy7s.dev) first in servers.dat, once: horizon_official_server.txt
beside it remembers that it was offered, so a player who deletes it never gets it back, and a list
that already has its address (any case, with or without :25565) is left as it is.

servers::prepend inserts the entry at the root level, so hidden entries and fields the codec
doesn't know are kept, and leaves an unreadable file alone.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 15:59:26 +01:00
Ludovic Marie 7e42cc8ece Merge pull request #2745 from levzzz5154/fix/flowing-water-mobs
🤖 Agent: verified by Ludovic's AI agent (Claude): the review points are addressed (tagged pigs, enclosed arenas, cleanup); flow-push and bubble-column pass on the server thread and in-frame (current carries 2.8/2.7 blocks vs 0 in still water; the column lifts higher than plain water). Tools only. Queued for the merger. Re-run at 9039a268f (arenas now built at y 100, husk line back to main's): lan_e2e flow-push and bubble-column PASS on the server thread (flowing 2.87 vs still 0.0; column 1.404 vs plain 0.785) and in-frame (2.776 vs 0.0; 1.42 vs 0.725).
2026-10-29 15:54:46 +01:00
leifiyooandClaude Haiku 5.5 bb6305da25 Keep creative display drops on a server connection
A throw on an item tab's display slot (Q or a right-click in the creative
screen) dropped the stack with a local Player.drop. On a server connection
(single player's server thread, LAN, multiplayer) that item entity was
never sent to the server, so it vanished. Send the stack as
SetCreativeModeSlot to slot -1 instead, as the survival tab's throw and
vanilla's handleCreativeModeItemDrop already do.

Vanilla: CreativeModeInventoryScreen.slotClicked (item tab THROW),
MultiPlayerGameMode.handleCreativeModeItemDrop.

Co-Authored-By: Claude Haiku 5.5 <[email protected]>
2026-10-29 15:54:24 +01:00
leifiyooandClaude Haiku 5.5 978c42216a Replace block entities in a loaded chunk by block state
load_chunk put each loaded block entity into the map over any entity
already at its position and kept that position's ticker slot whatever
the new block state was. Vanilla's LevelChunk.setBlockEntity drops the
previous entry, and updateBlockEntityTicker rebinds the ticker only when
the new state has one, removing it otherwise.

Mark replaced positions and, at the next tick, free their slot unless
the block state ticks. Add a test covering a replacement that keeps the
slot and one that frees it.

Vanilla: LevelChunk.setBlockEntity, LevelChunk.updateBlockEntityTicker,
LevelChunk.registerAllBlockEntitiesAfterLevelLoad.

Co-Authored-By: Claude Haiku 5.5 <[email protected]>
2026-10-29 15:53:51 +01:00
Ludovic Marie 0a95d1c047 Merge pull request #3404 from degepie/fix/issue-3306-zombify-villager
🤖 Agent: verified by Ludovic's AI agent (Claude) on union/pt2-a 7e322f591 (main + 8 PRs): fmt, workspace clippy -D warnings, horizon-client/protocol/command/client-core/world tests, fast build; lan_e2e hurt, cure-discount, commands, piston-push, hands, helduse on the server thread and in-frame, plus portals (server thread): all pass (cure-discount: 14 wheat for the cured villager's emerald, host and guest). Queued; the merger waits for CI parity on this head.
2026-10-29 15:51:13 +01:00
martipopsandAntigravity dd7414486b Input: pointer ownership and view offset per player
- Track keyboard_mouse ownership and view_at viewport offset on Game
- Ignore cursor grabs, visibility changes, and pointer warps when not owning keyboard/mouse
- Offset cursor warps by view_at in shared windows
- Track physical_buttons and clear on focus loss
- Add capture unit tests for controller-only games and viewport offsets

Co-Authored-By: Antigravity <[email protected]>
2026-10-29 10:47:58 -04:00
reservedbytes d385d188f3 fix(core): atan2 matches StrictMath 2026-10-29 13:47:57 -01:00
Federico TaglianiandClaude Sonnet 5.5 c11d57d1f8 Break effects: a block that pops off when its support is mined plays its break sound and particles for the miner
The server sent every level event of an acting player's action with that
player excluded, so a torch or flower that popped off when the player mined
the block it stood on was silent (no break sound, no particles) for the
miner. Block.playerWillDestroy excludes the player only from the destroy
events of the block it breaks (and its other half); what the break sets off
afterwards goes through destroyBlock with no player. Only the events up to the
broken block's are the player's own now.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-29 15:46:59 +01:00
iLostName d81f99ba8f far terrain: chunks past a shrunk render distance stay drawn until their tiles are ready
Lowering the render distance (the Performance preset after 32 or 64) let the coarsest far tiles stand in for the chunks it left: the tiles planned there had been inside the full-detail area, so never sampled, and the 64-block cells of the roots showed flat plateaus with stone walls for minutes. Those columns now stay left out, their meshes drawn, until the tile planned over each is ready.
2026-10-29 15:37:26 +01:00
levzzz 0dea3a89e2 Merge remote-tracking branch 'hr/main' into pr-3336 2026-10-29 19:36:08 +05:00
Ludovic Marie 2286d42b7f Merge pull request #2973 from kacpherek/fix/advancement-triggers-summon-loot-craft
🤖 Agent: requeued by contrib-gate-28 after merging main (B9 render crate split; stopped on a conflict or a known flake). fmt, clippy -D warnings and tests pass on the new head; proven earlier by cg26/cg27.
2026-10-29 15:31:29 +01:00
NetuosoDEVandOpenAI Codex ddc2a6d7e6 Merge origin/main into fix/enderman-carried-block-tints
Resolve the carried-block overlap with #2986 while preserving upstream's iron
golem flower layer, fox item context, minecart changes and renderer crate split.
Keep the captured carry transform and per-face BlockTintSource defaults; remove
the superseded single-tint helper that only the old enderman path used.

Carry the tint implementation into horizon-client-render/src/mesher.rs and
update the differential harness's source path after the upstream module move.

Resolved by OpenAI Codex on behalf of NetuosoDEV. No additional local tests
were run, as requested by NetuosoDEV; the updated PR head is checked by CI.

Co-Authored-By: OpenAI Codex <[email protected]>
2026-10-29 16:27:13 +02:00
levzzz b869bdcacd Merge pull request #3591 from d3grando/fix-3185
Wolves: any target makes a wolf angry, so an alerted pack looks angry
2026-10-29 19:25:29 +05:00
Federico TaglianiandClaude Sonnet 5.5 0b6c48be77 Zombified piglin: it runs Zombie.registerGoals too, with the turtle egg goal
ZombifiedPiglin extends Zombie and only overrides addBehaviourGoals, so it
keeps the turtle egg goal (priority 4) that Zombie.registerGoals adds. Horizon
left it out, so the goal never drew its start delay and the mob's random
stream drifted from vanilla's on its second tick.

Adds a zombified_piglin_player scenario to the mob_ai harness.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-29 15:23:54 +01:00
Claude ba6575314c Draw the empty main hand on the main arm, and hide it while invisible
renderArmWithItem draws an empty main hand with renderPlayerArm on the
player's main arm (the left one when the main hand is set to left) and
only when the player isn't invisible. hand_geometry always drew the right
arm and drew it while invisible. The map arms (#3444) already follow
both rules.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_018ksHJwFp11RfYk6S9xnyCf
2026-10-30 00:23:45 +10:00
NetuosoDEVandOpenAI Codex d3e2fe8bf3 Render enderman carried block states with vanilla tints
Use the baked block-state model instead of the block item's model so flowers,
fungi and roots keep their cross geometry and state properties survive. Remove
the item-centering compensation while retaining the vanilla carry transform.

Apply BlockTintSource.color(state) to tinted block-model faces using the active
grass colormap and vanilla default foliage colours. Keep in-world sugar cane
and lily pad colours unchanged. Use BlockModelResolver's fixed model seed 42.

Add a differential adapter checking 6,616 state/tint-index combinations against
Minecraft 26.3 and a scratch-world offscreen screenshot driver. The existing
captured-vertex transform regression now checks block coordinates directly.

Implemented by OpenAI Codex on behalf of NetuosoDEV. NetuosoDEV manually tested
the fix in game and confirmed it works.

Co-Authored-By: OpenAI Codex <[email protected]>
2026-10-29 16:23:20 +02:00
GIGABAIT93andClaude Opus 5.5 f383fdf74c Server resource packs: keep the overlay when the pack is only touched
Joining a server whose resource pack is already in the download cache
marks the file used (HttpUtil.updateModificationTime, ported as touch),
and the overlay cache keyed a ZIP pack by its stamp, modified time
included. Every join therefore rebuilt the whole overlay and reloaded
from thousands of new files: on Windows, where Defender scans each, a
join waited about a minute on "Preparing resources" (a 1.2 MB Nexo
pack on tensa.co.ua). A ZIP is now known by its bytes, one file read;
directory packs keep their stamps.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 16:22:06 +02:00
bunchy7s 815beda680 Refresh command block edit screen with server data
Send the command block's block-entity data when opening its screen and
apply incoming block-entity updates to the open edit screen. This makes
the command and output settings match the server instead of a stale
client copy, mirroring vanilla's openCommandBlock and
handleBlockEntityData behaviour.

Files:
- crates/horizon-client/src/game.rs
- crates/horizon-client/src/game/command_blocks.rs
- crates/horizon-client/src/game/remote.rs
- crates/horizon-client/src/lan/chunks.rs
2026-10-29 09:19:03 -05:00
levzzz cef8fa7e6d Merge pull request #3240 from some-du6e/fix/spear-jab-animation
Spears: the jab animates in first and third person
2026-10-29 19:17:10 +05:00
Joansitoh 1582cc638f Merge origin/main into refactor/launcher-fields-share-line-edit
The Post-exit field main added is a LineEdit too.
2026-10-29 15:15:34 +01:00
ThatCakeIsAPieandClaude Opus 5.5 8e0939f3e8 Merge upstream main into feat/level-shape-1
Main's horizon-client-render split (#3186) moved chunk.rs, world.rs,
streaming.rs and gfx.rs, which this branch changes: section_mask.rs moves
with them into horizon-client-render, and horizon-client re-exports it
beside the other moved modules. lan/chunks.rs keeps both imports (main's
spawner traits and SectionMask).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 09:14:39 -05:00
umcekoandClaude Opus 5.5 a9d14449e8 Equipment assets and their textures load from content mods' namespaces
Worn armour found its equipment asset only under minecraft, and the atlas
packed only minecraft's entity textures, so a content mod's armour could
not draw as itself. EquipmentAssetManager reads `equipment/` in every
namespace: the armour layers now load from the content mods' namespaces
too, under `<ns>:<name>`, and their textures pack as
`<ns>:entity/equipment/<layer>/<path>`. A root without content mods reads
exactly what it did.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 17:13:19 +03:00
umcekoandClaude Opus 5.5 d39715cca0 Port pack: items of vanilla tool and armour classes keep their stats
With the oracle's facts, every item came over as a stick: a ported sword
had no damage, no durability and no mining rules. An item of a vanilla
tool or armour class now gets the iron item of its class and slot as its
template, and the oracle's durability, attribute modifiers (1.21.1's ids
upgraded), tool rules, enchantability and repair items as a component
patch, minus what the template already has. The mod's tool rules replace
the template's whole (its tier names its own incorrect-for-drops tag);
what 1.21.1 kept in the class, a sword sparing blocks in creative, stays
the template's. Simplest Emerald Gear's item stats: 4/49 -> 64/64 against
the mod on NeoForge.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 17:13:19 +03:00
umcekoandClaude Opus 5.5 f904f44047 Mod port harness: compare items' stats with the NeoForge oracle
The items check only saw an item exist with its stack size, so a ported
sword carried as a stick passed. The probe now dumps what Horizon's loaders
make of each item (durability, attribute modifiers, tool rules,
enchantability, repair items, equipment slot, food) and the harness checks
them against the oracle's components, as `item_stats`, with 1.21.1's
`generic.` attribute ids read as 26.3's and vanilla's newer tool rules
allowed beside the mod's own.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 17:13:19 +03:00
umcekoandClaude Opus 5.5 df96b6787d NeoForge oracle: export items' data components and armour facts
Each item now carries its default components (vanilla's codecs, 1.21.1's
form), its effective attribute modifiers (armour's come from a method,
not a component), enchantability and repair items, and for armour its
slot, defense, toughness, knockback resistance, equip sound and texture
layers: the ground truth an item port is compared against. The export
also creates its output folder instead of failing to write.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 17:13:19 +03:00
umcekoandClaude Opus 5.5 5c66b27c9a Content mods: items patch their template's data components
An item file may carry `components`, a 26.3 data component patch over its
template's: `minecraft:<type>` sets one, `!minecraft:<type>` with `{}`
removes it. A modded sword gets its own damage and durability, a tool its
mining speed, without a template per stat. Types no vanilla item has are
refused, so a typo fails the load. Issue #3305, item 3.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 17:13:19 +03:00
Ludovic MarieandClaude Opus 5.5 1a01f5ef59 Merge main into the branch
Main added block_model_vertices_tinted (one tint for every tinted face)
for mob_blocks.rs; it now calls this branch's tinted_block_model_vertices
with that tint for tint indices >= 0.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 15:10:01 +01:00
ThatCakeIsAPie a717d6d583 Merge feat/upgrade-datapack-3: refuse pack entries that leave the pack 2026-10-29 09:08:50 -05:00
ThatCakeIsAPie 1ffe686350 Merge feat/upgrade-datapack-2b: refuse pack entries that leave the pack 2026-10-29 09:08:50 -05:00
ThatCakeIsAPie 217216e263 Merge feat/upgrade-datapack-2a: refuse pack entries that leave the pack 2026-10-29 09:08:48 -05:00
ThatCakeIsAPie 825f8f8845 Merge feat/upgrade-datapack-1: refuse pack entries that leave the pack 2026-10-29 09:08:47 -05:00
ThatCakeIsAPieandClaude Opus 5.5 0771f59bca upgrade-datapack: refuse pack entries that leave the pack
A zip entry named `../x` or `/x` (or `C:\x` on Windows) was written as it
was, outside `<world>/datapacks/<name>-26.3/`. `read_pack` now takes each
entry's name from `enclosed_name()` and refuses a pack with one that has
anything but plain components, and `upgrade_into` refuses to write such a
path. Review on #3369.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 09:08:46 -05:00
levzzz de8ffbb294 Merge pull request #3288 from hectoreccles/feat/beacon-beam-lod
Keep beacon beams visible outside the render distance
2026-10-29 19:07:51 +05:00
Federico TaglianiandClaude Sonnet 5.5 0c79bae87d Wither skeleton: its piglin target goal comes first among the priority 3 goals
WitherSkeleton.registerGoals adds its AbstractPiglin target goal before it
calls super.registerGoals, so among the priority 3 target goals it is
evaluated before the iron golem and turtle ones. Horizon added it last, so
the goals drew their random intervals in another order and a wither
skeleton's random stream drifted from vanilla's on its second tick.

Adds a wither_skeleton_player scenario to the mob_ai harness.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-29 15:04:55 +01:00
Ludovic Marie c177fef871 Merge pull request #2986 from kacpherek/fix/entity-rendering-parity
🤖 Agent: PR triage (Ludovic's AI agent, Claude): read in full, building on rdbt's approving review. Enderman's carried block is its baked model (seed 42, not centred) as CarriedBlockLayer, FoxHeldItemLayer draws the mouth item in the ground context (sleeping/baby offsets), IronGolemFlowerLayer the poppy while getOfferFlowerTick() > 0, Skeleton.DATA_STRAY_CONVERSION_ID (16) is sent and read so freezing skeletons shake, and the shadow radii match GiantMobRenderer (0.5 x 6), ElderGuardian 1.2, HappyGhast 2.0, Armadillo 0.4, Shulker 0. verify green; CI parity on this head.
2026-10-29 15:04:46 +01:00
Ludovic MarieandClaude Opus 5.5 c455bb6ec7 Merge main into the branch
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 15:04:30 +01:00
xKumorioandClaude Opus 5.5 a91952a8c0 Merge main: keep only what #3085 doesn't have
#3085 already moved the bow, trident, throws, potions and wind charge to
known_player_movement(), and main has the Windows test fixes (#3088, and
#3587 for port_publish.py). This branch takes main's version of all of
those. What it keeps: Guest::known_movement, set on every accepted move
and zeroed by a ClientTickEnd without one (handleClientTickEnd), and the
save and restore of Game::known_movement in enter_guest/leave_guest.
The moves.last() copies in UseItem, the spear's jab and the per-tick
using turn go, since enter_guest sets the value for every turn.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 18:58:55 +05:00
Ludovic Marie 45ecf7bef0 Merge pull request #3718 from horizon-rust/feat/launcher-java-mod-toggle
🤖 Agent: launcher matrix 40883773220 green on all platforms at this head; queued.
2026-10-29 14:57:38 +01:00
Ludovic Marie 181040539d Merge pull request #3713 from horizon-rust/feat/launcher-post-exit
🤖 Agent: launcher matrix 40884459459 green on all platforms at this head; queued.
2026-10-29 14:57:33 +01:00
Joansitoh 17864b867d Merge remote-tracking branch 'origin/main' into refactor/launcher-fields-share-line-edit 2026-10-29 14:56:12 +01:00
Joansitoh 09146bfecf Launcher: rustfmt 2026-10-29 14:56:09 +01:00
Federico TaglianiandClaude Sonnet 5.5 5071edb248 Mobs: the path maluses vanilla's constructors set (fire, lava, powder snow, cocoa, trapdoors)
Animal, AbstractVillager, AbstractPiglin and CopperGolem set FIRE to -1 and
FIRE_IN_NEIGHBOR to 16; Fox, Parrot, Frog, Goat, Wolf, Blaze, ZombifiedPiglin
and WitherSkeleton set their own. Horizon left every mob on PathType's
defaults (FIRE 16, FIRE_IN_NEIGHBOR 8), so animals, villagers and piglins
walked through fire that vanilla paths around.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-29 14:55:47 +01:00
Ludovic MarieandClaude Opus 5.5 f2337d0a73 Launcher: a server's Join starts a Java instance into it
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 14:52:27 +01:00
levzzzandClaude Opus 5.5 e852db5e60 A wolf angry at the player stays angry across a reload
On the server thread a reloaded wolf ticks before the player joins, and
the anger ended because no player counted as no valid target. Vanilla's
updatePersistentAnger ends it only once it finds the player and the player
is in creative or spectator, or the difficulty is peaceful.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 18:50:37 +05:00
levzzz f781769177 Merge remote-tracking branch 'hr/main' into pr-3591 2026-10-29 18:49:06 +05:00
vvnikita74 c5a734eace fix: sleep only when it is dark outside, not whenever the thunder flag is set (#3709)
Beds checked the level's raw thundering flag, whose timer also runs in
clear weather, so players could sleep through a sunny day. The Overworld's
bed_rule sleeps WHEN_DARK, as vanilla's Level.isDarkOutside: skyDarken >= 4,
from the time of day and the rain and thunder levels.
2026-10-29 16:47:08 +03:00
Joansitoh 3e83f8a48a Merge origin/main into refactor/launcher-fields-share-line-edit
The proxy field and the new Java-instance, launch, switch and multiplayer
fields use LineEdit too.
2026-10-29 14:46:50 +01:00
Federico TaglianiandClaude Sonnet 5.5 51005638e0 Skeletons: a path out of the shade stops at the first node the sky sees
GroundPathNavigation.trimPath, with avoidSun set by RestrictSunGoal, cuts a
path that starts out of the sun at its first sunlit node, so a skeleton
chasing a player by day waits in the shade. Horizon never trimmed a path.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-29 14:46:18 +01:00
Alexander KrylovandHermes Agent 3b3560d71d Launcher: remove several versions at once
App::removing held one version, and remove() returned early while it was
set, so a second row's Remove did nothing until the first delete ended.
It becomes the list of versions in flight: each row starts its own
delete, Msg::Removed drops its own tag, and the rows, Play and Install
ask whether that tag is in the list. The gate is removable(), so the
two-at-once case has a test.

Fix #3719.

Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
2026-10-29 16:45:47 +03:00
levzzz 57d4801bff Merge pull request #3583 from d3grando/fix-3430
Mobs: undead with NoAI burn in daylight
2026-10-29 18:44:13 +05:00
Ludovic Marie 0050a65600 Merge remote-tracking branch 'origin/main' into feat/launcher-post-exit
# Conflicts:
#	launcher/src/cli.rs
#	launcher/src/main.rs
2026-10-29 14:42:02 +01:00
Ludovic MarieandClaude Opus 5.5 0914c90455 Launcher: the post-exit test's Windows command keeps its last word
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 14:38:52 +01:00
Ludovic MarieandClaude Opus 5.5 2dd4903b5e Launcher: turn single mods on and off in Java instances
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 14:37:50 +01:00
Federico TaglianiandClaude Sonnet 5.5 94eff29428 Villagers: the trade screen closes when the trader is gone or out of reach
AbstractVillager.stillValid keeps a trade screen open only while the trader
lives and the player is within the entity interaction range plus 4 of its
box. Neither the host's own trade screen nor a guest's (which includes the
single-player window on the server thread) was checked, so the screen
stayed open after walking away.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-29 14:36:20 +01:00
levzzz a3b4d135aa Merge pull request #2706 from RoyalPixelsDE/perf/surface-skip-same-writes
Worldgen: the surface step skips writes that change nothing
2026-10-29 18:35:56 +05:00
martipopsandCodex f9a97f264f Add a compositor for offscreen frames on one GPU device
Expose renderer frames and surface handoff, with a standalone two-view GPU smoke example. Part 1 of the splitscreen series; normal game rendering is unchanged.

Co-Authored-By: Codex <[email protected]>
2026-10-29 09:34:38 -04:00
levzzz 8474ac55c9 Merge pull request #3423 from d3grando/fix-3362
GUI: screens keep vanilla's int layout at any window size
2026-10-29 18:33:46 +05:00
levzzz bcb3d50624 Merge pull request #3349 from degepie/fix/world-list-hardcore
World list: show Hardcore Mode in red, and adventure and spectator by name (#3298)
2026-10-29 18:33:40 +05:00
Ludovic Marie 9c62d4f850 Merge pull request #3688 from horizon-rust/feat/launcher-java-instance-settings
🤖 Agent: launcher matrix 40876178731 green on all platforms at this head; queued.
2026-10-29 14:33:04 +01:00
degepie 894848c17b Merge remote-tracking branch 'origin/main' into fix/goat-horn-instrument-sync 2026-10-29 18:29:19 +05:00
jaszkusandjaszkus's AI agent 4c1f8f24bd Creative inventory: the Destroy Item tooltip follows the language
CreativeModeInventoryScreen builds the destroy slot's tooltip from inventory.binSlot; the screen drew the English literal instead, so it stayed English in every language. Read the key through the same translate helper the tab titles use, keeping the English text as the fallback.

Co-Authored-By: jaszkus's AI agent <[email protected]>
2026-10-29 14:29:05 +01:00
Ludovic Marie ed45e27d15 Merge pull request #3697 from horizon-rust/fix/parity-test-macos-tmp
🤖 Agent: test-only fix (macOS temp symlink); verify and launcher green, 40 tests OK on macOS; queued.
2026-10-29 14:27:30 +01:00
Claude f4f9897c9e Extras: show your own name tag in third person
A new extra, Own name tag (ownNameTag, off by default), under a Third
person header on the Extras page. With it on, the player's own model in
third person gets the name tag other players see: the profile name in
its team's colours, the below-name score, faded while crouching. It goes
through the same checks as other players' tags (team name tag
visibility, distance, F1) and hides in first person, while sleeping, in
spectator mode and while invisible. Vanilla never names the camera
entity, so this stays off by default.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_018ksHJwFp11RfYk6S9xnyCf
2026-10-29 23:26:36 +10:00
Ludovic MarieandClaude Opus 5.5 edd340a621 Launcher: a command after the game exits, and the instance's variables
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 14:22:49 +01:00
Federico TaglianiandClaude Sonnet 5.5 136349ae95 Villagers: a baby that grows up gets the adult brain (Villager.ageBoundaryReached)
Horizon built a villager's brain once, with its baby flag, so a villager
that grew up kept the baby schedule (play) and never went to work. At the
age boundary the brain is now rebuilt as Villager.refreshBrain does: running
behaviours stop, the packed memories carry over and the sensors' first scans
are delayed by draws from the mob's random.

Adds villager_grows_up scenarios to the mob_ai harness.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-29 14:22:45 +01:00
Ludovic MarieandClaude Opus 5.5 201ea8b393 Merge main into the branch
Main's hud_disconnected() now resets the boss overlay; the item
activation reset stays after it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 14:19:40 +01:00
levzzz 9c15e18c6f Merge pull request #2951 from kacpherek/fix/list-damage-datapack-commands
Fix /list, the missing /damage types and /datapack create
2026-10-29 18:14:54 +05:00
levzzz e5338ff202 Merge pull request #2713 from iLostName/reset-hud-between-servers
remote: titles, tab list, scoreboard and effects don't carry over to the next server
2026-10-29 18:14:33 +05:00
Ludovic Marie b13b4f2c7f Merge pull request #3679 from horizon-rust/feat/launcher-import-apps
🤖 Agent: launcher matrix 40874504029 green on all platforms at this head; queued.
2026-10-29 14:14:00 +01:00
Alexander KrylovandHermes Agent 9cbde2b525 Launcher: a Nightly row in Versions reads as its commit
Every row on that page is a Nightly now, and its title was the install
folder name (nightly-2026-10-29-4baa571) because version_tag only strips
the v of a release tag. nightly::label turns it into "Nightly · 4baa571";
the date stays beside it, where both call sites already print it.

Fix #3700.

Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
2026-10-29 16:12:53 +03:00
levzzzandClaude Opus 5.5 35eb60e2f1 Merge main into launcher-simplified-chinese: keep Spanish and Chinese
Both languages stay in Language::ALL; the PR's loop over every language
replaces main's fixed list in the completeness test.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 18:12:09 +05:00
Ludovic MarieandClaude Opus 5.5 558904775f Merge main into this branch
Text displays: main's text_display::draw and Lines (#2812) replace this
branch's inline drawing, with this branch's display_light (the same light);
this branch's display_data and transformation are kept beside them.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 14:10:43 +01:00
Ludovic MarieandClaude Opus 5.5 5cdf9a9151 Docs: Coordinates HUD joins Horizon's official native mods
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 14:09:02 +01:00
Federico TaglianiandClaude Sonnet 5.5 01125c3023 mob_ai: chicken and pig panic scenarios, one beside a pool
chicken_panic_192 fails without the Chicken water malus (nav path of 7
nodes through the pool instead of vanilla's 5).

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-29 14:06:08 +01:00
degepieandClaude Opus 5.5 8537e29c92 Merge remote-tracking branch 'origin/main' into fix/issue-3137-suspicious-stew
Conflicts:
- game/stations.rs: keep both tooltip lines, the ominous bottle's before
  the suspicious stew's, in ItemStack.addDetailsToTooltip's component order.
- item_tooltip.rs: keep both tests; the stew test now expects main's
  zero-padded durations ("00:03", "00:07").

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
2026-10-29 18:06:00 +05:00
Ludovic MarieandClaude Opus 5.5 191b66303a Merge main into this branch
tools/port_publish.py: main already writes the store path with Posix
separators and ~ (the same fix), so main's version is kept; the roadmap
UTF-8 reads and writes remain.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 14:05:50 +01:00
levzzz 2730949b41 Merge pull request #3339 from josegil1909/feat/launcher-i18n-poc
Launcher: Spanish language support
2026-10-29 18:05:50 +05:00
levzzz 73cbd863af Merge pull request #2411 from kacpherek/fix/death-advancement-messages-to-guests
Send the host's death and advancement messages to guests and follow team visibility
2026-10-29 18:05:31 +05:00
Ludovic MarieandClaude Opus 5.5 9c67dd3b76 tests: resolve the temp root in the parity dependency-path test
On macOS the temp folder is under /var, a symlink to /private/var. dependencies()
resolves each path before comparing it with ROOT, so an unresolved temp ROOT
dropped every file and the test failed on macOS (Linux CI didn't see it).
The real ROOT is already resolved.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 14:03:24 +01:00
levzzz 702f7e2b93 Merge pull request #3359 from hamza-cskn/fix/invisible-armor-stands
Invisible armor stands: draw their equipment, and send their names to clients
2026-10-29 18:01:20 +05:00
Ludovic MarieandClaude Opus 5.5 5a1c095724 Merge main into this branch
hud.rs: main's name_tag (and its test) and this branch's draw_inline_object
(and its tests) both kept.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 14:00:51 +01:00
levzzzandClaude Opus 5.5 765cdb6bb0 Merge main into fix-3362: keep both new multiplayer tests
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 18:00:27 +05:00
Ludovic MarieandClaude Opus 5.5 bc78cba5b9 Merge main into this branch
remote.rs: main's spectator check and off-hand anchor charge are kept with
this branch's InteractionResult: an anchor charged from the off hand is
SUCCESS (RespawnAnchorBlock.useItemOn) and swings, a jukebox taking a disc
is SUCCESS (JukeboxPlayable.tryInsertIntoJukebox), an anchor left to the
off hand's glowstone is PASS. level/tests.rs: both new tests kept.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 13:59:19 +01:00
Federico TaglianiandClaude Sonnet 5.5 398b1dd2d8 Chicken: water costs nothing to path through, as in vanilla's constructor
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-29 13:59:17 +01:00
Ludovic MarieandClaude Opus 5.5 62c0834880 Merge main into this branch (rearing beside the horse finalize tests, one tests module)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 13:57:27 +01:00
Ludovic MarieandClaude Opus 5.5 92b8883039 Merge main into the branch
Keep main's target_point (absolute coordinates, block hits only) beside
the crosshair entity's UUID for entity suggestions.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 13:56:51 +01:00
levzzz 4103382062 Merge pull request #2995 from ASOwnerYT/fix/2869-horse-max-health
Horses, donkeys, mules and llamas sync their rolled max health
2026-10-29 17:56:44 +05:00
levzzz 7f9cc4084d Merge pull request #2345 from kacpherek/fix/burn-in-daylight-list
Follow the burn in daylight tag for mobs that burn in the sun
2026-10-29 17:56:40 +05:00
Ludovic Marie c784e572d7 Merge remote-tracking branch 'origin/main' into pt3/3253 2026-10-29 13:56:14 +01:00
levzzzandClaude Opus 5.5 479d10e794 Tutorial hints are recorded by the window only: the server thread no longer saves options.txt from its stale copy of the settings when a guest picks up a log or breaks one.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 17:56:06 +05:00
Claude 26fa54369c Show your own skin: offline skins from the launcher, the host's skin on LAN
- The game now reads HORIZON_OFFLINE_SKIN / HORIZON_OFFLINE_SKIN_MODEL,
  which the launcher already sets for an offline name, and draws that skin
  (slim or classic) on the local player where neither the server nor a
  signed-in account gives one. It was read for capes only.
- On the window's own server thread (single player, and the world opened
  to LAN), the owner's memory connection now logs in with the signed-in
  account's profile properties (IntegratedServer.getSingleplayerProfile),
  so the player list carries the owner's signed skin to LAN guests instead
  of the default one. The profile is fetched while the world loads; the
  login waits for it at most 10 s.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CxQvrTahuwEpRxJpTvWpjG
2026-10-29 12:55:49 +00:00
levzzz 89f949155c Merge pull request #3589 from d3grando/fix-3427
Commands: coordinate suggestions skip entities and offer the hit point
2026-10-29 17:55:49 +05:00
levzzz 129ad4fdf8 Merge pull request #3573 from Vadyanik/fix/xkb-caps-swapescape
Follow the XKB keymap for Escape and Caps Lock
2026-10-29 17:55:43 +05:00
levzzz ea3c3532b6 Merge pull request #3570 from d3grando/fix-3426
Creative tooltips list the tabs that hold the item, as vanilla does
2026-10-29 17:55:36 +05:00
levzzz 46a5e18630 Merge pull request #3569 from d3grando/fix-3431
Villager gossip: hits and murders count against the player who did them
2026-10-29 17:55:29 +05:00
levzzz 703ba156da Merge pull request #3565 from d3grando/fix-2862
Copper golem eyes glow on the body model, not the empty eyes layer
2026-10-29 17:55:22 +05:00
levzzz dd40744480 Merge pull request #3564 from d3grando/fix-3158
Key binds: a moved key no longer keeps its default action
2026-10-29 17:55:16 +05:00
levzzz cca5298864 Merge pull request #3560 from d3grando/fix-3429
Count a zombie villager's cure down in its tick, with or without AI
2026-10-29 17:55:09 +05:00
Ludovic MarieandClaude Opus 5.5 98c0883c8f Merge main into this branch; suspicious blocks are not containers
remote.rs: main's spectator check (a spectator's use predicts nothing) and
this branch's shelf prediction are both kept, the spectator first.

machines.rs: the new Brushable block entity made the dispenser harness's
dropper under suspicious sand treat it as an empty container and drop
nothing; BrushableBlockEntity is no Container, so the dropper drops in
front of it as vanilla does.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 13:55:05 +01:00
levzzz 44c88b6c7b Merge pull request #3559 from d3grando/fix-3209
Attack indicator shows at a spear's extra range
2026-10-29 17:55:02 +05:00
levzzz 6a9ff4a38f Merge pull request #3558 from d3grando/fix-3136
Draw the arms under a map held in first person
2026-10-29 17:54:56 +05:00
levzzz 4c33a0fdfe Merge pull request #3557 from d3grando/fix-3289
Invisible mobs cast no shadow
2026-10-29 17:54:49 +05:00
levzzz 2e01c336a3 Merge pull request #3554 from d3grando/fix-3293
Draw glowing sign text in front of its outline
2026-10-29 17:54:42 +05:00
levzzz d26740a103 Merge pull request #3553 from d3grando/fix-3381
Projectiles hit a seated enderman
2026-10-29 17:54:36 +05:00
levzzzandClaude Opus 5.5 fdcefc0d90 /list uuids in single player shows the player's saved profile id, as the other commands do, instead of the offline id of the name.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 17:54:35 +05:00
levzzz 1c87226ac9 Merge pull request #3511 from Domanffe/fix/windows-overlay-hardlink-render
Windows: link the overlay's mirrored files instead of copying them
2026-10-29 17:54:28 +05:00
levzzz 9c86860e40 Merge pull request #3498 from JustKarkat/fix/mob-drop-scatter
Mob-origin item drops get the ItemEntity constructor's random motion
2026-10-29 17:54:19 +05:00
levzzz ca3d2d32af Merge pull request #3467 from AHHhHHHHH-H/pr/far-sampling-threads
Far terrain: sample on up to 6 threads when the machine has 12 or more, behind the game
2026-10-29 17:54:10 +05:00
levzzz 1b0923d8cf Merge pull request #3464 from d3grando/native-settings-refresh-on-open
Native mod settings: ask for the labels again when the page opens
2026-10-29 17:54:04 +05:00
levzzz 68f50bbd41 Merge pull request #3463 from d3grando/native-hooks-f3-corners
native_corner_shown: F3 takes the top right and its charts' corners
2026-10-29 17:53:55 +05:00
levzzz 47bce76078 Merge pull request #3456 from d3grando/fix-3312
Tooltips: potion effect and armour trim lines in the selected language
2026-10-29 17:53:48 +05:00
levzzz 125e540126 Merge pull request #3449 from d3grando/fix-3302
Keep the player model's legs still while flying straight up
2026-10-29 17:53:41 +05:00
levzzz 1578a97c79 Merge pull request #3444 from d3grando/fix-3408
Fog option off clears the Nether's fog too
2026-10-29 17:53:34 +05:00
levzzz dbd5fb7f62 Merge pull request #3438 from Joansitoh/fix/windows-overlay-junctions
Windows: link untouched directories into the pack overlay with junctions
2026-10-29 17:53:27 +05:00
Ludovic Marie 4812518327 Merge main into the branch
Co-Authored-By: Claude Opus 5.5 <[email protected]>

# Conflicts:
#	crates/horizon-client/src/game/creatures.rs
2026-10-29 13:53:24 +01:00
levzzz 2a8dc29c69 Merge pull request #3437 from d3grando/fix-resize-absurd-size
Window: a size above the GPU's largest texture no longer crashes
2026-10-29 17:53:20 +05:00
levzzz 81a0e7e9a5 Merge pull request #3428 from d3grando/win-parity-sighup
parity: catch SIGHUP only where it exists, so parity.py starts on Windows
2026-10-29 17:53:13 +05:00
levzzz ce980a90de Merge pull request #3416 from d3grando/fix-3084-cyrillic-bold
Name tags: draw text as Font.prepareText does (bold, tall glyphs)
2026-10-29 17:53:07 +05:00
Ludovic MarieandClaude Opus 5.5 7daca92c4a rustfmt: sulfur cubes join the baby-flag group
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 13:53:01 +01:00
levzzz dd32d3dc38 Merge pull request #3399 from d3grando/fix-mod-screen-list-clip
Mod screens: a list's rows are cut at its frame
2026-10-29 17:53:00 +05:00
levzzz 29f427dabe Merge pull request #3363 from ThatCakeIsAPie/fix/launcher-windows-unused-import
launcher: drop an unused import in the Windows make_link
2026-10-29 17:52:54 +05:00
levzzz b156515edd Merge pull request #3330 from josegil1909/fix/launcher-clipboard-linux
Launcher: sync clipboard on Linux Wayland and X11
2026-10-29 17:52:42 +05:00
levzzz 0fdf1aeee1 Merge pull request #3328 from echo-ilovech3ss/fix/creative-piston-head-drop
Piston head: destroy fitting base without drops in creative mode
2026-10-29 17:52:35 +05:00
levzzz ee2fb39605 Merge pull request #3241 from SolomonNumb1/fix/modifier-tooltip
tooltip fix
2026-10-29 17:52:27 +05:00
levzzz 559e36ee04 Merge pull request #3193 from SlaVKsVolks/contribute/horizon-windows-parity-20261028
Parity: select affected harnesses from Windows dependency records
2026-10-29 17:52:20 +05:00
levzzz 7037510e65 Merge pull request #3167 from sleepe229/fix/3143-sunset-sky-cost
Build the sunset fan once a frame instead of in every sky pixel
2026-10-29 17:52:13 +05:00
Username59138andClaude Opus 5.5 fa77b35140 Creative inventory: the Saved Hotbars tab, as vanilla shows it
The HOTBAR tab was skipped since the creative tabs came in (#49, "Not
covered: ... the saved-hotbars tab"), while saving and loading toolbars
with C or X and a hotbar key already worked.

- CreativeModeInventoryScreen.selectTab on the HOTBAR tab: the nine
  saved rows of hotbar.nbt; an empty row shows a paper in its own slot
  with creative_slot_lock and the item name inventory.hotbarInfo, named
  after the bound save and hotbar keys. The rows are read when the tab
  is selected and when the inventory opens on it.
- CustomCreativeSlot.mayPickup: a locked stack can't be clicked.
- Creative::shown gives a grid cell's stack, None for a toolbar's empty
  slot, for the clicks, the hover, the drawing and horizon-ctl.
- VanillaCreativeSearchOracle compared the tabs without the HOTBAR one,
  which Horizon didn't show; it now compares every tab vanilla shows
  (shouldDisplay). The tab's display items are empty on both sides: its
  contents come from HotbarManager when it is selected.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 15:52:12 +03:00
levzzz 417a1cebc1 Merge pull request #3161 from jaszkus/fix/block-outline-open-screens
Keep the block outline while a screen is open
2026-10-29 17:52:06 +05:00
levzzz 9edc7eb80a Merge pull request #3130 from d3grando/mod-sprites-nine-slice
Mod API: a sprite shape draws as vanilla's blitSprite, nine-sliced by its gui.scaling
2026-10-29 17:52:00 +05:00
Ludovic MarieandClaude Opus 5.5 5667e9ebd1 Merge main into the branch
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 13:51:56 +01:00
levzzz d9628314ab Merge pull request #3113 from kacpherek/fix/ender-eye-debug-hitbox
Show a thrown eye of ender's hitbox with F3+B
2026-10-29 17:51:53 +05:00
levzzz 4984a1329a Merge pull request #3103 from Domanffe/fix/creative-hotbar-save-message
Creative hotbar: say 'Saved' only when it was saved
2026-10-29 17:51:47 +05:00
levzzz d22b63b9ca Merge pull request #3101 from Domanffe/fix/scoreboard-write-failure
Scoreboard: report a failed write instead of dropping it
2026-10-29 17:51:41 +05:00
levzzz dc5e8c45f7 Merge pull request #3099 from Domanffe/fix/launcher-servers-atomic-list
Launcher: write the server list beside it and rename
2026-10-29 17:51:34 +05:00
levzzz 2782a9057a Merge pull request #3076 from Domanffe/fix/port-publish-posix-path
Port publish: a body names the diff store with Posix separators
2026-10-29 17:51:27 +05:00
levzzz 66620f22a3 Merge pull request #2996 from feeldev12/feat/book-click-events
fix(client): book text runs and copies its click commands and shows its hover text
2026-10-29 17:51:20 +05:00
levzzz 18070bb4ce Merge pull request #2814 from sirrayi/fix/snbt-depth-and-words
SNBT: bound nesting depth, keep nan/inf words as strings
2026-10-29 17:51:14 +05:00
levzzz b575402076 Merge pull request #2753 from iLostName/world-access-failure-toast
title: a world open in another instance shows the access failure toast instead of crashing
2026-10-29 17:51:07 +05:00
Ludovic MarieandClaude Opus 5.5 0c06082dfa Merge main into this branch
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 13:51:05 +01:00
Federico TaglianiandClaude Sonnet 5.5 e0fecd8b8e Fish: swim targets pass the water malus check, and FishMoveControl turns as vanilla
Cod, salmon, tropical fish, pufferfish, tadpoles, squid and dolphins keep
PathType.WATER at 0 (WaterAnimal and AgeableWaterCreature constructors).
Horizon left it at 8, so DefaultRandomPos rejected every water position
and RandomSwimmingGoal retried ten times, ending on a spot in the air.
FishMoveControl also computes its yaw like MoveControl does.

Adds cod, salmon and tropical fish scenarios to the mob_ai harness; the
probe names the fish goal FishSwimGoal as vanilla does.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-29 13:51:02 +01:00
Ludovic MarieandClaude Opus 5.5 e2de446128 Merge main into this branch
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 13:51:02 +01:00
levzzz 5b3c3684b1 Merge pull request #2721 from senjinthedragon/hdr-f3
HDR: show the output on the F3 screen and in the log
2026-10-29 17:51:00 +05:00
levzzz 29b4b72dd5 Merge pull request #2712 from iLostName/server-list-player-tooltip
server list: the player count's tooltip lists the status sample
2026-10-29 17:50:52 +05:00
levzzz 9bc8447871 Merge pull request #2711 from iLostName/options-slider-save-on-release
options: sliders apply while dragged and save on release
2026-10-29 17:50:46 +05:00
levzzzandClaude Opus 5.5 39592697c7 The launcher's Spanish covers the strings added on main: native mods, moving an instance to the Trash, and the build's new time estimate.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 17:50:21 +05:00
Ludovic MarieandClaude Opus 5.5 d785041833 Launcher: Java instances get a window size and join a server on launch
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 13:46:35 +01:00
levzzzandClaude Opus 5.5 f7efb0d710 The launcher's Simplified Chinese covers the strings added on main: native mods, moving an instance to the Trash, and the build's new time estimate.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 17:45:38 +05:00
iLostName 75600ca046 chunk loading: a worker frees the meshes the frame has uploaded
Freeing the hundreds of meshes a chunk border brings made the render thread wait on the allocator behind the busy meshing and generation workers, up to 20 ms of a frame at each border while flying at render distance 32. The renderer now hands them back to Streaming, whose workers free them ahead of other jobs, as vanilla's compile task closes its mesh data on its own thread. Refs #1890.
2026-10-29 13:45:28 +01:00
k3yhandClaude Opus 5.5 a839b0cef1 An op's spawn egg sets its entity data on a spawner
SpawnEggItem.useOn on a spawner takes the type from getType, the egg's
entity data id when it has one, and for an op (PlayerList.isOp) calls
Spawner.setEntityData: TypedEntityData.loadInto sets the next spawn
data's id and merges the egg's data into it. Horizon set only the egg's
own type, so a custom egg gave a plain mob.

The acting server guest's permission level is kept beside its name and
UUID (lan_acting_permission), so the check is the acting player's own.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 07:42:40 -05:00
masa-masa-0314andZCode Agent 845dbae91b Fix a recursive fallback in the world content tables' accessors
`Game::items`/`recipes`/`stations` fell back to themselves when the world
had no data packs instead of to `Resources.rest`, so a world without packs
recursed until the thread spun at 100%: the window never joined its
integrated server and showed "Loading terrain..." forever.

Co-Authored-By: ZCode Agent <[email protected]>
2026-10-29 20:42:34 +08:00
Ludovic Marie 56c1100644 Merge pull request #3678 from horizon-rust/feat/launcher-java-mods-page
🤖 Agent: Mods page for a selected Java instance (its version and loader; no Horizon port check). Horizon instances unchanged. Verify green; matrix 40873026396 green on Windows, macOS and Linux.
2026-10-29 13:41:31 +01:00
Ludovic Marie a881d284a7 Merge pull request #3433 from xKumorio/fix-kelp-sheet-mips
🤖 Agent: verified by pr-triage-3. Now only the kelp mipmap fix in gfx/animate.rs (the save.rs part is on main via #3088, port_publish.py is in #3587, as the author offered). union/pt3-2 285ecb6e8 (main 2b294bbe3 + #2417 #2951 #2981 #3000 #3417 #3433 #2544 #2586): fmt, clippy -D warnings (horizon-client, -client-render, -client-core, -world, -items, all targets), their tests pass.
2026-10-29 13:41:27 +01:00
levzzz 9da77d9d4b Merge remote-tracking branch 'hr/main' into pr-2795
# Conflicts:
#	launcher/src/i18n/tests.rs
2026-10-29 17:41:21 +05:00
repositorykreml1n 01791aba51 Add files via upload 2026-10-29 15:41:10 +03:00
levzzz b5b762b840 Merge remote-tracking branch 'hr/main' into pr-2964
# Conflicts:
#	crates/horizon-client/src/game/remote.rs
2026-10-29 17:41:04 +05:00
levzzz 08c0fc1286 Merge remote-tracking branch 'hr/main' into pr-2951 2026-10-29 17:40:55 +05:00
levzzz e81e23e64e Merge remote-tracking branch 'hr/main' into pr-3339 2026-10-29 17:40:54 +05:00
Federico TaglianiandClaude Sonnet 5.5 0a770a0e14 Merge main into fix/mod-structure-suggestions
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-29 13:40:17 +01:00
Ludovic Marie a167a89215 Merge branch 'draft/remove-wasm-mods-crates' into draft/remove-wasm-mods-launcher 2026-10-29 13:39:00 +01:00
Ludovic Marie 953a5a84ee Merge branch 'draft/remove-wasm-mods-launcher' into draft/remove-wasm-mods-docs 2026-10-29 13:39:00 +01:00
Ludovic Marie 2cd11c926f Merge branch 'draft/remove-wasm-mods-game' into draft/remove-wasm-mods-crates 2026-10-29 13:38:59 +01:00
Ludovic Marie a7f1f70508 Merge remote-tracking branch 'origin/main' into draft/remove-wasm-mods-game 2026-10-29 13:38:59 +01:00
jaszkusandjaszkus's AI agent 37a2cc0057 Add the Debug Stick
DebugStickItem: a left click selects a block-state property and a right click cycles its value (Shift reverses it), stored in the minecraft:debug_stick_state component. The client never breaks the block and swings on use; the server cycles the state and sends the block update.

The sneak direction follows the acting player's sneak input (host, toggled sneak or guest), a guest's command permission is swapped for its turn so game-master checks use the guest's level, and useOn returns the vanilla SUCCESS/FAIL, triggers ITEM_USED_ON_BLOCK on success and only swings the local actor's client.

Co-Authored-By: jaszkus's AI agent <[email protected]>
2026-10-29 13:37:04 +01:00
Ludovic Marie 5005a7ad9f Merge remote-tracking branch 'origin/main' into feat/launcher-import-apps
# Conflicts:
#	launcher/src/cli.rs
#	launcher/src/other_launchers.rs
2026-10-29 13:34:39 +01:00
Ludovic Marie 029870bb90 Merge branch 'draft/remove-wasm-mods-launcher' into draft/remove-wasm-mods-docs
# Conflicts:
#	README.md
#	ROADMAP.md
#	roadmap/roadmap.json
#	roadmap/roadmap.toml
2026-10-29 13:32:46 +01:00
Aria FigueredoandGPT-6.1 Terra 4808d801fe Allocate mod item IDs in deterministic registry order
Co-Authored-By: GPT-6.1 Terra <[email protected]>
2026-10-29 07:31:30 -05:00
Shreyas d27a7cbce3 Sulfur cubes: align babyhood and egg offspring with vanilla 26.3
- Route sulfur cube baby flag through baby_index (entry 16) in lan.rs
- Remove client-side baby state derivation from size in remote_mob_data.rs
- Revert finalize.rs age_boundary_reached and set_baby to vanilla behavior
- Ensure spawn_egg_offspring sets size 1 on new sulfur cube offspring
- Drop lists.rs change from this branch
2026-10-29 18:00:54 +05:30
damnurmumandCodex 0d0411fc7a Merge latest main into rich item names and shadows
Pick up launcher and dragon changes that landed while the earlier merge was being tested.

Co-Authored-By: Codex <[email protected]>
2026-10-29 15:29:59 +03:00
damnurmumandCodex 8c739d2bc2 Merge main into rich item names and shadows
Resolve HUD shadow rendering conflict by keeping styled shadow colors and the new world text depth behavior.

Co-Authored-By: Codex <[email protected]>
2026-10-29 15:29:52 +03:00
Ludovic Marie 87a6a7a161 Merge pull request #3634 from horizon-rust/fix/launcher-java-loader-follow-ups
🤖 Agent: Java instances use their loader for mods; copy-from keeps a Fabric or Quilt loader. Verify green; matrix 40858947625 green on Windows, macOS and Linux.
2026-10-29 13:28:14 +01:00
Ludovic Marie 5ceba01475 Merge pull request #3630 from horizon-rust/fix/launcher-new-account-join
🤖 Agent: new accounts' token from launcher/login, falling back to login_with_xbox; the owner gate is unchanged. martipops confirmed it with a new account on an online-mode server. Verify green; matrix 40858081820 green on Windows, macOS and Linux.
2026-10-29 13:28:09 +01:00
Ludovic Marie 1afbdca0f0 Merge branch 'draft/remove-wasm-mods-crates' into draft/remove-wasm-mods-launcher 2026-10-29 13:26:49 +01:00
da9d0dcbe1 The dragon follows the server that flies it (#2765)
* The dragon follows the server that flies it

The client draws the dragon from its own state (game/dragon.rs), but a
server's dragon only sent its health, its phase and its damage: its
moves never reached that state, so the dragon was drawn where it had
spawned while the server's dragon flew around the island, and the local
flight machine flew it away from the server's moves as well.

The server's move packets now move the dragon the client draws, the
way every other entity of a server's follows them, and a server's
dragon is no longer flown by the client's own phase machine: the
server flies it, the client flaps its wings, keeps its flight history
and places its parts.

Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>

* Keep the steering comment

Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>

* Ease the server's dragon through the lerp tick_remote_dragon reads

The frozen dragon came from move_entity: the dragon also has a copy in the
mob list, so its moves took the mob path and returned before r.lerps, and
tick_remote_dragon never had a target. The moves now skip that copy and
reach r.lerps, so tick_remote_dragon eases the drawn dragon over the three
steps of LivingEntity's interpolation, as the code it already had meant to.

This replaces the snap to each move and the remote flag in tick_dragon:
a server's client never runs tick_dragon (tick_end is the local level's),
and the snap left the dragon still between the server's updates.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Code <[email protected]>
Co-authored-by: minimax-m3.1-flash (Claude Code) <[email protected]>
Co-authored-by: Ludovic Marie <[email protected]>
2026-10-29 17:26:37 +05:00
Ludovic MarieandClaude Opus 5.5 dc66e37847 Launcher: copy CurseForge app and Modrinth App instances into Java instances
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 13:25:16 +01:00
Ludovic Marie b25d588c91 Merge branch 'draft/remove-wasm-mods-game' into draft/remove-wasm-mods-crates 2026-10-29 13:25:05 +01:00
Ludovic MarieandClaude Opus 5.5 e2a9909521 Launcher: the Mods page searches and installs for a selected Java instance
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 13:24:18 +01:00
Ludovic Marie 6e8ee3ed07 Merge remote-tracking branch 'origin/main' into draft/remove-wasm-mods-game
# Conflicts:
#	crates/horizon-client/src/game/mods.rs
2026-10-29 13:23:27 +01:00
levzzzandClaude Opus 5.5 9039a268f6 Build the fluid arenas up in the air, and leave the husks to main
Both fluid checks built their walls at the host's feet, in the host's chunk,
so a player standing there could be shut in stone and suffocate. They now
build at y 100, as check_piston_push does, and the last clear takes the
floor away as well. The boat dismount check's husks go back to NoAI, as on
main: that change belongs to no fluid check.

flow-push, bubble-column, digging and frames pass in that order on the
server thread and --in-frame.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 17:23:08 +05:00
levzzzandClaude Opus 5.5 c566e69c6c Tick dropped items again, and keep a cooling-down entity out of a gateway
The first commit replaced the tick_items span with the gateway handler, so
dropped items stopped falling, merging and despawning in every mode. The
call is back, next to the gateway's.

An entity that has just come through a portal carries PortalCooldown 300.
EndGatewayBlock.entityInside still triggers the gateway's own cooldown, but
Entity.setAsInsidePortal then only restarts the entity's cooldown, whatever
the portal: the gateway now asks the shared portal state first.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 17:21:13 +05:00
Ludovic MarieandClaude Opus 5.5 461a8757e3 Merge main into the branch
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 13:19:44 +01:00
damnurmumandCodex a5a0e1bb98 Merge main into inline object text
Resolve HUD text rendering conflict by preserving inline player and atlas objects alongside the new world text shadow and depth behavior.

Co-Authored-By: Codex <[email protected]>
2026-10-29 15:18:19 +03:00
Federico TaglianiandClaude Sonnet 5.5 d9e70d5ca7 Commands: suggest the templates of every namespace, not only minecraft's
/place template listed worldgen/structure/minecraft only, so a content
mod's templates (worldgen/structure/<ns>/...) were never suggested.
Also drop the unused templates additions from the content merge.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-29 13:12:07 +01:00
Ludovic Marie 19faf89699 Merge pull request #2720 from ASOwnerYT/claude/2697-horse-make-mad-4431aa
🤖 Agent: proven on the integration branch union/pt1-wave1 (22 PRs on main 13d7d9995): tools/pr_ready.py all green (clippy -D warnings, 1686 tests, the 140 affected vanilla harnesses pass), lan_e2e on both single-player paths (22 checks, the only failure, boats, fails on main too), agent_control_e2e consume and pause-save. Thank you!
2026-10-29 13:08:15 +01:00
Ludovic Marie f230017ec1 Merge pull request #2839 from iLostName/sounds-index-without-stat
🤖 Agent: PR triage (Ludovic's AI agent, Claude): main merged in (ff). sounds.json files are listed once per namespace instead of a lookup per sound, same resolution; its earlier parity failure was the remote_fireball wind-charge load flake, and every other selected harness passed. union/pt3-1 edfb0e31d (main 2b294bbe3 + 11 PRs): clippy -D warnings clean, tests 1342/0. CI parity on this head.
2026-10-29 13:08:11 +01:00
Ludovic MarieandClaude Opus 5.5 3b261d1081 Merge main into the branch
Keep main's 'names' check beside flow-push and bubble-column.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 13:07:56 +01:00
Ludovic Marie ba86a4d76d Merge pull request #3653 from horizon-rust/ci/world-spawn-vanilla-reuse
🤖 Agent: ci-queue-1: world_spawn run by hand on 3 seeds (3/3 match with the cache empty, then reused: 12 s -> 4.7 s a seed; PARITY_JAVA_CACHE=0 reruns vanilla); new unit tests; pr_ready --quick green (411 Python tests).
2026-10-29 13:02:18 +01:00
Ludovic Marie a734609ab4 Merge pull request #2807 from levzzz5154/fix/shield-blocking-pose
🤖 Agent: gate cg29: the parity failure at e9c6751a1 was command_exec's vanilla oracle (it threw 'entities around the player did not load' on playhorizon), not this PR; the other 47 selected harnesses passed in that run, and command_exec passes on main + this head on the Mac mini. Head unchanged since cg27's proof.
2026-10-29 13:02:13 +01:00
Ludovic Marie eab3195848 Merge pull request #2799 from iLostName/join-without-rescanning-registries
🤖 Agent: gate cg29: the parity failure at cf0b111bd was remote_fireball's wind charge (3 of 6 samples), a load flake of that harness seen on unrelated PRs today; this PR only touches chat.rs. union/cg29-1 (main b19b4e92d + #3104 #2799 #2819 #2812): fmt, clippy -p horizon-client/-client-render/-client-core --all-targets -D warnings, tests 1334/0, fast build; lan_e2e team-push pvp frames minecarts boats deflect cushions commands tab on the server thread and --in-frame all pass. CI parity rerun on this head.
2026-10-29 13:02:09 +01:00
Ludovic Marie 0d2f7b86d5 Merge pull request #2812 from socksprox/fix/text-display-depth
🤖 Agent: proven by contrib-gate-28 (requeued after a cancelled parity run): gate fix for a trailing § in a part plus a test, main merged after B9; fmt, clippy -D warnings, tests pass; text_display_render 600/600 cases match vanilla; CI parity for the rest.
2026-10-29 13:02:04 +01:00
hashtheboardandClaude Opus 5.5 c487bbce9e Merge origin/main into vr/view-set
gfx.rs: the frame's view_proj comes from the ViewSet's primary view, then
gets main's TAA jitter.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 12:59:32 +01:00
Ludovic Marie e79992e80b Merge pull request #3448 from d3grando/fix-3307
🤖 Agent: proven on the integration branch union/pt1-wave1 (22 PRs on main 13d7d9995): tools/pr_ready.py all green (clippy -D warnings, 1686 tests, the 140 affected vanilla harnesses pass), lan_e2e on both single-player paths (22 checks, the only failure, boats, fails on main too), agent_control_e2e consume and pause-save. Thank you!
2026-10-29 12:59:23 +01:00
Ludovic Marie 248aa8b302 Merge pull request #3415 from d3grando/fix-3245
🤖 Agent: proven on the integration branch union/pt1-wave1 (22 PRs on main 13d7d9995): tools/pr_ready.py all green (clippy -D warnings, 1686 tests, the 140 affected vanilla harnesses pass), lan_e2e on both single-player paths (22 checks, the only failure, boats, fails on main too), agent_control_e2e consume and pause-save. Thank you!
2026-10-29 12:59:19 +01:00
Ludovic Marie cf586e4159 Merge pull request #3442 from d3grando/fix-copper-chest-double
🤖 Agent: proven on the integration branch union/pt1-wave1 (22 PRs on main 13d7d9995): tools/pr_ready.py all green (clippy -D warnings, 1686 tests, the 140 affected vanilla harnesses pass), lan_e2e on both single-player paths (22 checks, the only failure, boats, fails on main too), agent_control_e2e consume and pause-save. Thank you!
2026-10-29 12:59:14 +01:00
Ludovic Marie 7443236850 Merge pull request #3445 from d3grando/fix-3414
🤖 Agent: proven on the integration branch union/pt1-wave1 (22 PRs on main 13d7d9995): tools/pr_ready.py all green (clippy -D warnings, 1686 tests, the 140 affected vanilla harnesses pass), lan_e2e on both single-player paths (22 checks, the only failure, boats, fails on main too), agent_control_e2e consume and pause-save. Thank you!
2026-10-29 12:59:09 +01:00
Federico TaglianiandClaude Sonnet 5.5 9c5377a571 Mods: structures, sets, pools and templates from content mods reach command suggestions
The content merge wrote a mod's worldgen into the worldgen bundles but
never into command-registries.json, so /locate structure and /place
structure/jigsaw/template suggested vanilla ids only, though the commands
themselves accepted the mod's.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-29 12:58:28 +01:00
martipopsandClaude Opus 5.5 88e7afbedf Controller movement on servers: the stick moves as the keys it sets do
A server only hears the movement keys (ServerboundPlayerInputPacket), and
vanilla's client moves exactly as they do (KeyboardInput.tick: 8 directions,
full speed). With analog movement a player walked at angles and speeds the
keys can't produce, and a server that predicts movement from them could pull
it back. On a server that isn't this game's own server thread the stick now
moves as its keys; single player keeps analog movement.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01JLHp82HU35NWsN14K4taHT
2026-10-29 07:57:25 -04:00
Ludovic Marie 5e391eb673 Merge pull request #3500 from spidercraft/fix/firework-gaps
🤖 Agent: proven on the integration branch union/pt1-wave1 (22 PRs on main 13d7d9995): tools/pr_ready.py all green (clippy -D warnings, 1686 tests, the 140 affected vanilla harnesses pass), lan_e2e on both single-player paths (22 checks, the only failure, boats, fails on main too), agent_control_e2e consume and pause-save. Thank you!
2026-10-29 12:54:07 +01:00
Ludovic Marie c10553eebf Merge pull request #3439 from spidercraft/taa
🤖 Agent: proven on the integration branch union/pt1-wave1 (22 PRs on main 13d7d9995): tools/pr_ready.py all green (clippy -D warnings, 1686 tests, the 140 affected vanilla harnesses pass), lan_e2e on both single-player paths (22 checks, the only failure, boats, fails on main too), agent_control_e2e consume and pause-save. Thank you!
2026-10-29 12:54:03 +01:00
Ludovic Marie 215d8e779a Merge pull request #3411 from spidercraft/cape-rendering
🤖 Agent: proven on the integration branch union/pt1-wave1 (22 PRs on main 13d7d9995): tools/pr_ready.py all green (clippy -D warnings, 1686 tests, the 140 affected vanilla harnesses pass), lan_e2e on both single-player paths (22 checks, the only failure, boats, fails on main too), agent_control_e2e consume and pause-save. Thank you!
2026-10-29 12:53:59 +01:00
Ludovic Marie 58a4fd8263 Merge pull request #3571 from d3grando/fix-3501
🤖 Agent: proven on the integration branch union/pt1-wave1 (22 PRs on main 13d7d9995): tools/pr_ready.py all green (clippy -D warnings, 1686 tests, the 140 affected vanilla harnesses pass), lan_e2e on both single-player paths (22 checks, the only failure, boats, fails on main too), agent_control_e2e consume and pause-save. Thank you!
2026-10-29 12:53:55 +01:00
martipopsandClaude Opus 5.5 5601fea75e Let /damage hurt players other than the acting one
cmd_damage only handled the window's own player and mobs: any other player
(a LAN guest, a dedicated server's player, a splitscreen player) fell through
to "Target is invulnerable". Run it as that player through for_player, as
/effect does.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01JLHp82HU35NWsN14K4taHT
2026-10-29 07:53:41 -04:00
jaszkusandjaszkus's AI agent e83ef39df4 Place and collect water in waterlogged blocks
A water bucket fills the clicked LiquidBlockContainer (stairs, slabs, fences, trapdoors and other SimpleWaterloggedBlocks) instead of the block next to it, and an empty bucket's SOURCE_ONLY ray now targets waterlogged blocks too, so their water can be picked up again. The empty-bucket use also reports the vanilla FAIL/PASS result, applies the sneak rule, keeps creative's filled-result behaviour and lets only creative players waterlog a barrier.

Co-Authored-By: jaszkus's AI agent <[email protected]>
2026-10-29 12:52:43 +01:00
Ludovic MarieandClaude Opus 5.5 3c63c5913e run-parity: a push cancels the old head's run, other labels cancel nothing; cancel-on-close takes every head (#3651)
- run-parity.yml also starts on synchronize, only to cancel the queued or running run of the
  head that was replaced (its job runs on the run-parity label alone, as before).
- Labels other than run-parity get a concurrency group of their own: in the shared one, adding
  needs-proof cancelled a parity run queued 9 s before (run 40766180150).
- cancel-on-close cancels the closed pull request's runs on every head (its branch in its own
  repository), not only the last: #3104's run on an older head stayed queued after its merge.

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-29 12:52:10 +01:00
Ludovic Marie 7f870d7447 Merge pull request #3563 from d3grando/fix-3322
🤖 Agent: proven on the integration branch union/pt1-wave1 (22 PRs on main 13d7d9995): tools/pr_ready.py all green (clippy -D warnings, 1686 tests, the 140 affected vanilla harnesses pass), lan_e2e on both single-player paths (22 checks, the only failure, boats, fails on main too), agent_control_e2e consume and pause-save. Thank you!
2026-10-29 12:51:30 +01:00
Ludovic Marie f49659c78b Merge pull request #3586 from d3grando/fix-3246
🤖 Agent: proven on the integration branch union/pt1-wave1 (22 PRs on main 13d7d9995): tools/pr_ready.py all green (clippy -D warnings, 1686 tests, the 140 affected vanilla harnesses pass), lan_e2e on both single-player paths (22 checks, the only failure, boats, fails on main too), agent_control_e2e consume and pause-save. Thank you!
2026-10-29 12:51:26 +01:00
Ludovic Marie 62ff5241a4 Merge pull request #3566 from d3grando/fix-3034
🤖 Agent: proven on the integration branch union/pt1-wave1 (22 PRs on main 13d7d9995): tools/pr_ready.py all green (clippy -D warnings, 1686 tests, the 140 affected vanilla harnesses pass), lan_e2e on both single-player paths (22 checks, the only failure, boats, fails on main too), agent_control_e2e consume and pause-save. Thank you!
2026-10-29 12:51:22 +01:00
Ludovic Marie 4d460c7374 Merge pull request #3577 from d3grando/fix-3505
🤖 Agent: proven on the integration branch union/pt1-wave1 (22 PRs on main 13d7d9995): tools/pr_ready.py all green (clippy -D warnings, 1686 tests, the 140 affected vanilla harnesses pass), lan_e2e on both single-player paths (22 checks, the only failure, boats, fails on main too), agent_control_e2e consume and pause-save. Thank you!
2026-10-29 12:51:19 +01:00
Ludovic Marie d99f7d6d7b Merge pull request #3579 from d3grando/fix-3147
🤖 Agent: proven on the integration branch union/pt1-wave1 (22 PRs on main 13d7d9995): tools/pr_ready.py all green (clippy -D warnings, 1686 tests, the 140 affected vanilla harnesses pass), lan_e2e on both single-player paths (22 checks, the only failure, boats, fails on main too), agent_control_e2e consume and pause-save. Thank you!
2026-10-29 12:51:15 +01:00
Ludovic Marie 49c6c47f98 Merge pull request #3562 from d3grando/fix-3474
🤖 Agent: proven on the integration branch union/pt1-wave1 (22 PRs on main 13d7d9995): tools/pr_ready.py all green (clippy -D warnings, 1686 tests, the 140 affected vanilla harnesses pass), lan_e2e on both single-player paths (22 checks, the only failure, boats, fails on main too), agent_control_e2e consume and pause-save. Thank you!
2026-10-29 12:48:31 +01:00
Ludovic Marie b029efa3d3 Merge pull request #3454 from d3grando/fix-3308
🤖 Agent: proven on the integration branch union/pt1-wave1 (22 PRs on main 13d7d9995): tools/pr_ready.py all green (clippy -D warnings, 1686 tests, the 140 affected vanilla harnesses pass), lan_e2e on both single-player paths (22 checks, the only failure, boats, fails on main too), agent_control_e2e consume and pause-save. One small follow-up for later, not blocking: Player.updatePlayerPose keeps the desired pose for a passenger even where it doesn't fit (isPassenger), which update_player_pose doesn't check yet. Thank you!
2026-10-29 12:48:27 +01:00
Ludovic Marie 37b4ee3abd Merge pull request #3585 from d3grando/fix-3280
🤖 Agent: proven on the integration branch union/pt1-wave1 (22 PRs on main 13d7d9995): tools/pr_ready.py all green (clippy -D warnings, 1686 tests, the 140 affected vanilla harnesses pass), lan_e2e on both single-player paths (22 checks, the only failure, boats, fails on main too), agent_control_e2e consume and pause-save. Thank you!
2026-10-29 12:48:22 +01:00
Ludovic Marie b07f2775a2 Merge pull request #3555 from d3grando/fix-3424
🤖 Agent: proven on the integration branch union/pt1-wave1 (22 PRs on main 13d7d9995): tools/pr_ready.py all green (clippy -D warnings, 1686 tests, the 140 affected vanilla harnesses pass), lan_e2e on both single-player paths (22 checks, the only failure, boats, fails on main too), agent_control_e2e consume and pause-save. Thank you!
2026-10-29 12:48:17 +01:00
Ludovic Marie e831cb0333 Merge pull request #3504 from ASOwnerYT/fix/3425-potion-duration-padding
🤖 Agent: read in full (pr-triage-1), checked against vanilla 26.3 StringUtil.formatTickDuration (Mth.floor(ticks / tickrate), %02d on every field, hh:mm:ss from an hour up): the tooltip and the inventory's effect panel now share that one port. Client text only.
2026-10-29 12:48:13 +01: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
Federico TaglianiandClaude Sonnet 5.5 9c6325c1f8 Mods: Options > Mods > Settings opens the mod's page at the title screen
open_mod_screen returned early while the title was up, so the Settings
button at the title did nothing, and the mod screen was neither drawn nor
given clicks or keys there. Draw it like Options (before the title), route
its clicks and keys, and blur the panorama behind it. A mod's own screen
(ui.open-screen) still waits for a world.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-29 12:45:56 +01:00
Ludovic Marie 6a5cd3cc28 Merge pull request #2875 from elythepreciouscat/fix/linux-opengl
🤖 Agent: verified by Ludovic's AI agent (Claude): main merged (README conflict resolved), clippy and tests pass, verify green. Queued for the merger.
2026-10-29 12:44:44 +01:00
JustKarkatandJustKarkat's AI agent (ZCode) 16090d3d4a Drop motion from the ItemEntity's own random, not the level's
Review fix on #3498: the 5-arg ItemEntity(level, x, y, z, stack)
constructor draws its push from this.random, a fresh source — not
this.level().random. Death loot, gift loot and MobEvent::DropItem now
use dispenser::new_item_motion() (already the item's own random), and
the DropGift arm on main had the same level-random draw and joins them.
The level random stays untouched for whatever reads it next (orb merge
ids, block ticks).

Co-Authored-By: JustKarkat's AI agent (ZCode)
2026-10-29 06:41:30 -05: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
Ludovic MarieandClaude Opus 5.5 dc8f01ea19 Merge main into the branch
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 12:28:51 +01:00
Ludovic MarieandClaude Opus 5.5 b65c319b2a Merge main into the branch
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 12:28:50 +01:00
Ludovic MarieandClaude Opus 5.5 8fb51b0c4a Merge main into the branch
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 12:28:50 +01:00
Ludovic MarieandClaude Opus 5.5 0a00df7640 Merge main into the branch
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 12:28:49 +01:00
Ludovic MarieandClaude Opus 5.5 3b5643628a world_spawn: reuse a seed's vanilla spawn under parity.py's oracle reuse
Vanilla's server is ~11 s of each seed's ~13 s, 60 seeds a run, and world_spawn
was 25 % of the PR parity runners' harness time on the Mac mini (37 % on
playhorizon): parity_shim.py never caches it, since a server's console is a pipe.
Its spawn depends only on the pinned jar, the JDK, the command line and the
seed's server.properties, which make the key. --fresh, --all and parity.py's
fresh retry of a failure create every vanilla world as before.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 12:27:23 +01: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
Ludovic Marie 4a9501d2d9 Merge pull request #2383 from kacpherek/fix/fog-lava-and-night-vision
🤖 Agent: PR triage (Ludovic's AI agent, Claude): main merged in (ff). LavaFogEnvironment.setupFog: spectator -8..renderDistance*0.5, Fire Resistance 0..5, else 0.25..1, sky/cloud end = environmental end; FogRenderer.computeFogColor's brightenFactor = nightVisionScale (endsWithin(200) flicker) unless Darkness. Proven in cg19 union R1; union/pt3-1 edfb0e31d (main 2b294bbe3 + 11 PRs): clippy -D warnings clean, tests 1342/0. CI parity on this head.
2026-10-29 12:22:41 +01:00
iLostName 4e5b26f9fb elder guardian: a player already cursed isn't cursed again every minute
The check looked up `minecraft:mining_fatigue`, but the player's effects are kept by their bare name, so it never matched: the curse, its sound and the apparition came back at every 1200-tick pulse. As MobEffectUtil.addEffectToPlayersAround does, a curse as strong that doesn't end within the next minute now stays.
2026-10-29 12:19:58 +01:00
degepieandClaude Opus 5.5 7e459e5fb1 Frogs long jump: level and unseeded randoms, activity gate TODO
Address the review on #3544:
- goals::Cx carries the level random (the one the brains already get);
  the weighted jump-candidate pick and the three TIME_BETWEEN_LONG_JUMPS
  samples draw from it, as LongJumpToRandomPos/LongJumpMidJump do.
- The angle shuffle in calculateOptimalJumpVector uses a fresh unseeded
  random (Collections.shuffle's global one), not the frog's.
- The preferred-block roll stays on the frog's random (vanilla uses the
  body's random there).
- TODO in LongJump::can_use for FrogAi.initJumpActivity's TEMPTING_PLAYER /
  BREED_TARGET gate and LAY_SPAWN priority: frogs have no tempting,
  breeding or frogspawn here yet.

Other mobs only get the new field threaded through; nothing else reads it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
2026-10-29 16:15:54 +05:00
SolomonNumb1 0dcc9a15fd smithing cleanup 2026-10-29 16:14:17 +05: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
Federico TaglianiandClaude Sonnet 5.5 9828599067 Wither: take no damage from wither friends (skulls, blasts)
WitherBoss.hurtServer rejects damage caused by a non-player mob in
#wither_friends (the undead, withers included). Horizon only filtered
the wither's own targeting, so skulls and explosions from one wither hurt
its neighbours, which then retaliated through HurtByTarget.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-29 12:10:07 +01: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
Ludovic Marie 8a290d810a Merge pull request #2533 from kacpherek/fix/server-mob-animation-sync-2
🤖 Agent: PR triage (Ludovic's AI agent, Claude): main merged in (ff, no conflict). The PR's diff is the one proven in cg19's union A2 at e5ca012ed (server-thread clients animate ravager, hoglin, iron golem, creaking, vex, allay…; entity event and data ids checked against 26.3 then). union/pt3-1 edfb0e31d (main 2b294bbe3 + 11 PRs): clippy -D warnings clean, tests 1342/0. CI parity on this head.
2026-10-29 12:07:58 +01:00
Ludovic MarieandClaude Opus 5.5 dffd3973bc Merge main into perf/far-tiles-biome-memo
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 12:07:12 +01:00
Ludovic MarieandClaude Opus 5.5 84185641bc Merge main into perf/worldgen-surface-biomes
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 12:07:11 +01:00
Ludovic MarieandClaude Opus 5.5 e60455d12c Merge main into perf/worldgen-biome-stage
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 12:07:11 +01:00
Ludovic MarieandClaude Opus 5.5 043f9c7b7b Merge main into perf/worldgen-structure-climate
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 12:07:11 +01: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
Ludovic Marie 43e7700791 Merge pull request #3447 from d3grando/fix-3396
🤖 Agent: read in full (pr-triage-1). Item names use Util.makeDescriptionId's key in the item's own namespace (item.<ns>.<path with / as .>, then block.), so mod items translate; vanilla items unchanged. Approved by rdbtCVS.
2026-10-29 12:00:14 +01:00
Ludovic MarieandClaude Opus 5.5 c0b3b5b3e9 Launcher: the night sky behind every page
sky.rs draws the night look's backdrop: the sky in texel bands, stars that
twinkle in steps, three layers of hand-drawn clouds drifting a texel at a
time, a sun cut in slices and two skylines. Sprites are baked once per
appearance and scale into tiles that don't depend on the window's size; a
frame is asked for only when something on screen changes, never under
reduced motion (macOS, or HORIZON_LAUNCHER_MOTION=off), and not 10 s after
the window loses focus. It replaces the stone texture and Home's landscape.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 11:58:28 +01:00
iLostName 3c9b2599d8 block placement: a click on a block is handled before the move behind it, so pillar blocks are heard
The hub applied a player's move before the game ran the UseItemOn sent ahead of it, so BlockItem.canPlace checked the box after the move: mid-jump the server placed a block the client had refused, and only the client plays the place sound. The move now waits for the next tick, as it already does behind a dig. Fixes #3568.
2026-10-29 11:50:46 +01:00
iLostName 88a001b7fa resource packs: a zip that stores pack.mcmeta as pack.mcmeta/ loads, as ZipFile.getEntry finds it
Java's ZipFile.getEntry falls back to the folder entry `name/` when no file has the name, and vanilla reads its bytes like a file's; Horizon left folder entries out, so Loka's server pack had no metadata and the reload failed. Fixes #3413.
2026-10-29 11:50:45 +01:00
iLostName ce2b51090f minecart: right-clicking a tnt minecart uses the held item, so a crossbow shoots at it
MinecartTNT has no interact of its own, so Entity.interact passes and Minecraft.startUseItem goes on to the item; the server client ended every click on a minecart there. A command block minecart without a game master, and a rideable one when sneaking or ridden, pass the same way. Fixes #3588.
2026-10-29 11:48:45 +01:00
Ludovic Marie e932a2da61 Merge remote-tracking branch 'origin/main' into upd/2924
# Conflicts:
#	crates/horizon-client-render/src/options_ui.rs
2026-10-29 11:47:35 +01:00
Ludovic Marie edd11b6103 Merge remote-tracking branch 'origin/main' into upd/3401 2026-10-29 11:47:06 +01:00
Ludovic Marie 73ae9f6cf1 Merge branch 'upd/3478' into upd/3479 2026-10-29 11:46:40 +01:00
Ludovic Marie 69fc342a1a Merge remote-tracking branch 'origin/main' into upd/3478
# Conflicts:
#	crates/horizon-client/src/game/world_loading.rs
#	crates/horizon-client/src/vanillagen.rs
#	crates/horizon-world/src/worldgen/generator.rs
2026-10-29 11:46:37 +01:00
Ludovic Marie ad7904946e Merge remote-tracking branch 'origin/main' into upd/3477
# Conflicts:
#	crates/horizon-world/src/worldgen/generator.rs
2026-10-29 11:45:59 +01:00
iLostName 30d33e077f off hand: filling a composter or using bone meal ends the click before the off-hand torch
ComposterBlock.useItemOn and BoneMealItem.useOn are SUCCESS on vanilla's client, so Minecraft.startUseItem never tries the off hand; the server client predicted both as passing. A sapling, azalea or mushroom still passes there, as isValidBonemealTarget needs a ServerLevel. Fixes #3536.
2026-10-29 11:44:34 +01:00
Ludovic Marie 4baa571566 Merge pull request #2819 from dreadpiratejoelius/fix/remote-player-teams
🤖 Agent: gate cg29: parity failure at 5f80ddffc was the remote_fireball wind-charge load flake. Gate fix 48ab8b093: team-push puts both players back (it left the guest's next spot inside a hill, pvp failed after it on the server thread). Team by profile name matches Player.getScoreboardName / createEntityFromPacket. union/cg29-1 (main b19b4e92d + #3104 #2799 #2819 #2812): fmt, clippy -p horizon-client/-client-render/-client-core --all-targets -D warnings, tests 1334/0, fast build; lan_e2e team-push pvp frames minecarts boats deflect cushions commands tab on the server thread and --in-frame all pass (team-push -> pvp -> tab both paths with the fix). CI parity on this head.
2026-10-29 11:43:56 +01:00
iLostName 2677a397e5 invisibility: the player's own body is hidden in third person and the inventory, its armour still drawn
The model check looked up `minecraft:invisibility`, but the player's effects are kept without the namespace, so the body always showed. As LivingEntityRenderer.submit does, the body is now skipped unless the player's team sees friendly invisibles, while armour, elytra and held items still draw. Fixes #3615.
2026-10-29 11:42:52 +01:00
Ludovic MarieandClaude Opus 5.5 a7e279cfed Launcher: Java instances' mods follow their loader, and copies keep theirs
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 11:36:30 +01:00
Ludovic Marie 7c3364d30d Merge pull request #3627 from horizon-rust/ci/nightly-own-runner
🤖 Agent: the nightly's untrusted macOS jobs move to ludovics-mac-mini-nightly (registered, online, same trust as horizon-game, never the release runner). test_nightly updated; verify green.
2026-10-29 11:33:22 +01:00
Ludovic MarieandClaude Opus 5.5 2fe5fa0939 Launcher: new Microsoft accounts get a token that joins online-mode servers
The Minecraft token now comes from launcher/login, with login_with_xbox as the fallback.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 11:30:01 +01:00
damnurmumandCodex be2d3582e7 Keep rich item names in inventory sync and match shadow channel floors
Send the stored custom-name component through the server inventory codec and use vanilla float-to-channel conversion for RGBA shadow colors. Cover both paths with focused tests.

Co-Authored-By: Codex <[email protected]>
2026-10-29 13:28:40 +03:00
SU4G3andClaude Opus 5.5 2445108f10 horizon-ctl drop: let the server throw the stack on a client of a server
On a server, and in single player on its server thread, the window's Game
is a client: `drop` called drop_held there, which spawned a client-only
item entity and left the server's inventory untouched (the server still
counted the dropped items). It now goes through remote_drop, as the Q key
does: predict the removal and send PlayerAction DROP_ITEM / DROP_ALL_ITEMS.

The agent-control e2e drops a plank and asks the server how many are left.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 13:27:53 +03:00
Ludovic Marie 64990cd321 Merge pull request #3619 from horizon-rust/feat/launcher-fabric-quilt
🤖 Agent: Fabric and Quilt for Java instances: profiles from their meta services, libraries sha1-checked, paths through mojang::relative; inheritsFrom loop guard with its test. Verify green; matrix 40855348257 green on Windows, macOS and Linux.
2026-10-29 11:27:53 +01:00
Ludovic Marie 78b778463f Merge pull request #3295 from RVZO6/fix/menu-separator
🤖 Agent: PR triage (Ludovic's AI agent, Claude): main merged in (ff). header_separator/footer_separator are 32x2 textures: tiling them as 32x32 squares sampled 2/32 of the height. tiled_sized tiles them at [32, 2] (both rows, last column clipped), with a unit test; ui::tiled keeps its square form. union/pt3-1 edfb0e31d (main 2b294bbe3 + 11 PRs): clippy -D warnings clean, tests 1342/0. CI parity on this head.
2026-10-29 11:27:49 +01:00
Ludovic Marie 76864807ba Merge pull request #3111 from senjinthedragon/hdr-gallery
🤖 Agent: read in full (pr-triage-1). A dev-only Python tool (tools/hdr_gallery.py) that writes a datapack of every light-giving block into a world folder for tuning HDR; no network, no subprocess, reads only the prepared facts files, writes only under the given world's datapacks/. Nothing players run changes (skip-release).
2026-10-29 11:27:45 +01:00
Ludovic MarieandClaude Opus 5.5 1f3cc4b80e Nightly: build on the Mac mini's own nightly runner, not behind PR parity
The untrusted (nightly) build ran its macOS jobs on the horizon-game runner,
which also takes every pull request's parity job. On 2026-10-29 the nightly's
macOS build sat queued from 08:17Z behind 100 run-parity runs (oldest
00:24Z), so no nightly shipped for hours.

It now runs on ludovics-mac-mini-nightly (labels self-hosted, macOS,
horizon-nightly): the same machine and trust as the horizon-game runner,
still never the release runner. The runner is registered and online.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 11:22:05 +01:00
Ludovic MarieandClaude Opus 5.5 3c6bdc2e5b Launcher: the night look's specimen (HORIZON_LAUNCHER_PAGE=specimen)
Draws the night tokens' type and colours in the launcher itself, over the
sky's stops, with each text colour's contrast on a plate over the brightest
stop and the rounded and pixel-cut plate corners side by side. Not a page
players reach; for checking the tokens in the real renderer.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 11:19:38 +01:00
Ludovic MarieandClaude Opus 5.5 99071a1160 Launcher: night tokens and Bricolage Grotesque in horizon-theme
horizon_theme::night: the night sky look's colours (NIGHT, and DAWN for Light),
type steps, radii and the Bricolage Grotesque cuts (tools/bake_bricolage.py,
OFL), with AA tests over every sky stop. tokens.css gains --night-*. The
launcher reads them: Night::blocks() dresses the screens not rebuilt yet in
the night's colours, and Bricolage replaces Geist and Pixelify Sans there.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 11:19:37 +01:00
Mik00000 e0f11804f2 Address review: drop legacy readers; keep flag sync and entity_data placement 2026-10-29 12:17:39 +02:00
GIGABAIT93andClaude Opus 5.5 88ee948bc2 Game: Horizon's own strings in Ukrainian
crates/horizon-client/assets/horizon/lang/uk_ua.json translates the
game's 13 own strings (the controller options), bundled like ru_ru;
vanilla's uk_ua covers the rest of the game.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 12:15:43 +02:00
GIGABAIT93andClaude Opus 5.5 e7e5413667 Launcher: Ukrainian
launcher/lang/uk.json translates the launcher's 194 strings, Horizon's
own, written from the English; Language::Ukrainian ("uk", Українська)
joins the picker, the system language pick and `settings language`. The
translation test now covers every language but English.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 12:15:38 +02:00
Ludovic Marie 1816cf0b52 Merge pull request #3612 from horizon-rust/feat/launcher-import-launchers
🤖 Agent: imports from Prism/MultiMC/official launcher, read-only on their folders (copy into a new Java instance), formats from public docs. Verify green; matrix 40853481205 green on Windows, macOS and Linux.
2026-10-29 11:15:16 +01:00
Ludovic Marie 258c5946b5 Merge pull request #3603 from horizon-rust/feat/launcher-java-modrinth
🤖 Agent: Modrinth for Java instances, filtered by version and loader; downloads go through download::save (safe names, checksums). Verify green; matrix 40853843158 green on Windows, macOS and Linux.
2026-10-29 11:15:11 +01:00
deloskiyt c6fbd42aee Audio: play break sound and track stats when tools break while mining (#3524)
Fixes missing item break sound effect and broken statistic tracking:
- When a tool breaks from mining a block (mine_block_with_held), route
  durability wear through damage_held rather than clearing the slot silently.
- Route weapon wear in mace.rs and brush wear on armadillos in creatures.rs
  through damage_held.
- Play entity.item.break sound and re-apply equipment when worn armor
  breaks from Thorns in enchant_effects.rs.
2026-10-29 11:13:20 +01:00
Ludovic Marie 452224a28a Merge pull request #3617 from horizon-rust/docs/native-mods-on-after-install
🤖 Agent: docs only (native-mods.md); pr_ready --quick green (408 Python tests); skip-release.
2026-10-29 11:09:54 +01:00
degepieandClaude Opus 5.5 35345be102 Fish form schools: port FollowFlockLeaderGoal (#3026)
Cod, salmon and tropical fish had only panic, player avoidance and random
swimming. AbstractSchoolingFish adds FollowFlockLeaderGoal (priority 5, no
flags): after a 100-110 tick start delay a fish joins a nearby school with
room or gathers the free fish around it, and followers path to their
leader while it lives and is within 11 blocks. FishSwimGoal stops while the
fish follows (canRandomSwim), a leader forgets an empty school on a 1/200
level-random roll, and schools hold 8 (5 for salmon).

Recruiting changes other fish, so it goes through two MobEvents that
apply_brain_events applies after the recruiter's tick.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
2026-10-29 15:09:49 +05:00
Ludovic Marie ca1b200c79 Merge remote-tracking branch 'origin/main' into upd/3391
# Conflicts:
#	crates/horizon-world/src/worldgen/generator.rs
2026-10-29 11:09:33 +01:00
Ludovic Marie b603db6176 Merge remote-tracking branch 'origin/main' into upd/3390
# Conflicts:
#	crates/horizon-world/src/worldgen/generator.rs
2026-10-29 11:09:18 +01:00
Ludovic Marie 779b773530 Merge remote-tracking branch 'origin/main' into upd/3367
# Conflicts:
#	crates/horizon-world/src/worldgen/generator_decoration.rs
2026-10-29 11:08:25 +01:00
Mik00000 7fb1f77870 Address review: drop overlapping spectral arrow paths and remove double arms 2026-10-29 12:06:20 +02:00
GIGABAIT93andClaude Opus 5.5 8a9db2dc7b Fonts: Pixelify Sans gets the Cyrillic І
Ukrainian headings, buttons and navigation need the capital І (U+0406),
which upstream doesn't map, so it came from the fallback font, smooth
and taller than the pixel letters around it. As #2844 did for О, it
takes each weight's own Latin I glyph, the same shape. Only cmap changes
(and head's checksum adjustment); every other table, the timestamps
included, is byte-identical to the files it replaces.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 12:06:06 +02:00
Ludovic Marie f8988d5a73 Merge pull request #3000 from hectoreccles/lod-small-end-islands
🤖 Agent: requeued by pr-triage-3 after a main merge (was dc3fc6961, cg27-verified, rdbt approved; stopped on a conflict with #2894). Both LOD tests kept. union/pt3-2 285ecb6e8 (main 2b294bbe3 + #2417 #2951 #2981 #3000 #3417 #3433 #2544 #2586): fmt, clippy -D warnings (horizon-client, -client-render, -client-core, -world, -items, all targets), their tests pass.
2026-10-29 11:05:54 +01:00
Ludovic MarieandClaude Opus 5.5 184e1ee3d6 Launcher: refuse a version whose inheritsFrom loops
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 11:05:00 +01:00
Ludovic MarieandClaude Opus 5.5 c5702d6c29 Launcher: Fabric and Quilt for Java instances
A Java instance can play with a mod loader: `instance create
--minecraft 1.21.4 --loader fabric|quilt [--loader-version V]`, or
`instance loader <instance> fabric|quilt|none [--version V]`, with
`--list` for the loader's versions (the newest stable one by default;
Quilt's list comes unordered, so versions are sorted).

Play installs the loader's profile once from its metadata service into
versions/, beside the game's, with its libraries in libraries/, each
checked against its sha1 (the profile's, else the one its Maven
repository publishes), then starts the profile merged into the game.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 11:04:59 +01:00
malordinandClaude Sonnet 5.5 ff0c089ac5 Launcher: refill the asset store at start-up, not on Play
`complete_assets` also runs when Play is pressed, so a store with most of its objects
missing made the player wait for thousands of downloads and a sign-in with nothing on
screen. `refill_own_store` now runs in `reuse_prepared_everywhere`, on the start-up
thread, and Play only copies what is there; the gate's test pins both places.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-29 15:03:16 +05:00
Mik00000 4d13a4ebcc Address review comments: empty Spawner default, remove legacy fallbacks, use block light 2026-10-29 12:00:54 +02:00
Ludovic MarieandClaude Opus 5.5 e6412d6e5c Launcher: the CLI test expects mods search to take --instance
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 10:57:49 +01:00
Ludovic MarieandClaude Opus 5.5 90b583896a Docs: native mods turn on when a player installs one; Linux builds them
Since #3596 installing a native mod turns native mods on after a notice;
the intro and the Trust section still said they were off by default. And
a full game build on Linux is proven (an x86_64 server with no Rust of its
own, the launcher's toolchain, Zoom's proof passing on the built game), so
it leaves "Left to do" for the measurements.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 10:55:27 +01:00
Ludovic MarieandClaude Opus 5.5 7db6a085bf Launcher: copy Prism, MultiMC and official launcher instances into Java instances
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 10:55:17 +01:00
Ludovic Marie 0b74b68aac Merge pull request #3598 from horizon-rust/feat/launcher-java-play
🤖 Agent: Play for Java instances. Diff reviewed: the owner gate holds (a session only for an account that owns the game; downloads via download_version_for_owner); #3451's guards kept. Verify green; build matrix 40851406886 green on Windows, macOS and Linux.
2026-10-29 10:51:41 +01:00
Ludovic MarieandClaude Opus 5.5 a20aaf5396 Text objects: vanilla's default fallback when none is given
ObjectContents.visit uses the object's defaultFallback without one:
[item/porkchop], [porkchop@gui] off the blocks atlas, [Steve head].

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 10:51:40 +01:00
Ludovic Marie 1d01a2d696 Merge remote-tracking branch 'origin/main' into upd/3325 2026-10-29 10:51:09 +01:00
Ludovic Marie 1db57affe6 Merge pull request #3600 from horizon-rust/feat/launcher-java-inherits
🤖 Agent: inheritsFrom merge (child libraries first with parent duplicates dropped, arguments appended, parent jar). Diff reviewed; verify green; build matrix 40849739616 green on Windows, macOS and Linux.
2026-10-29 10:47:10 +01:00
Ludovic MarieandClaude Opus 5.5 5c18ae844b Drop a stray doc line after MAX_LEVEL
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 10:45:13 +01:00
Ludovic Marie 584a59ad8c Merge remote-tracking branch 'origin/main' into upd/3467 2026-10-29 10:45:08 +01:00
degepieandClaude Opus 5.5 d9f40c6942 Adventure: don't predict breaking a block the player may not break
MultiPlayerGameMode.startDestroyBlock refuses before predicting when
Player.blockActionRestricted holds: an adventure player without a
can_break item. The client predicted the break anyway (sound, crack,
the block gone) and the server sent it back, and continueAttack swung
every tick. A stack the server sent with can_break still breaks; the
server judges its predicate.

Fixes #3027.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
2026-10-29 14:43:19 +05:00
masa-masa-0314andZCode Agent 6e2ac2522d Re-trigger the pull request checks (no run arrived for the head commit)
Co-Authored-By: ZCode Agent <[email protected]>
2026-10-29 17:42:05 +08:00
masa-masa-0314andZCode Agent e72a8c5c6e Data packs: load the world's packs into the content tables
A world's data packs now change what the game plays with, the way
`ReloadableServerResources` loads a server's: recipes, loot tables,
advancements and tags from `<world>/datapacks` merge over the resource
root's own, at world open and on `/reload`. A pack's recipe or
advancement replaces the game's file for the same id
(`SimpleJsonResourceReloadListener` hands out the top resource of each
stack); tag files join the game's entries with `replace` dropping the
lower sources (`TagLoader.load`). Worlds without packs keep loading
exactly as before.

`datapack::DataStore` collects the packs' `recipe/`, `loot_table/`,
`advancement/` and `tags/…/` files (`DataStore.files`, the built-in
pack excluded: its files are the resource root's own, which the tables
read from disk). `horizon_world::loot::PackOverlay` carries them into
the loaders: `Items` (loot tables, item/block/entity_type tags),
`Recipes`, `StationData` (smithing, stonecutting, brewing, the recipe
book index), `advancements::Tree` and `LootData` (structure-block
containers). The game keeps the pack tables in `Game::world_tables`
and reads them through `Game::items`/`recipes`/`stations` (the
`world_items!`-family macros give the same field-level borrows at
conflict sites), rebuilding them when the world opens and on
`/reload`, with the advancement tree and the loot and LAN sync caches
forgotten alongside.

Not covered yet: worldgen registries, trade/banner/enchantment
registry files from packs, and the prepare-time behavior bundles
(mining facts stay with `block-behaviours.json`).

Co-Authored-By: ZCode Agent <[email protected]>
2026-10-29 17:42:05 +08:00
Ludovic Marie e1844d064e Merge pull request #2971 from kacpherek/fix/skin-online-sound-options
🤖 Agent: requeued by contrib-gate-28 after merging main (B9 render crate split; stopped on a conflict or a known flake). fmt, clippy -D warnings and tests pass on the new head; proven earlier by cg26/cg27.
2026-10-29 10:41:47 +01:00
deloskiyt eaa5549a3b Render: draw carved pumpkins and mob heads on player and humanoid heads (#3549)
Ports CustomHeadLayer.submit for players and humanoid mobs:
- Renders carved pumpkins, jack o'lanterns, and mob/player heads worn
  in the head equipment slot on the player model and humanoid mobs.
- Adds head_item_geometry to mob_block_geometry, player_model_geometry
  (third person), and draw_player_preview (inventory preview).
- Correctly skips armor helmets (handled by HumanoidArmorLayer) and
  body-slot species (equines, wolves, etc.).
- Keeps player model extra.head and extra.armor synchronized in
  tick_player_model.
- Updates tools/port_map/client-005.tsv mapping.
2026-10-29 10:41:02 +01:00
iLostName 0bf91b8822 ender pearl: one that hits an end crystal still takes its thrower there
A thrown projectile that hit a crystal or a dragon part stopped at onHitEntity; a pearl, an egg and a snowball now go on to their onHit. Fixes #3081.
2026-10-29 10:40:17 +01:00
Ludovic MarieandClaude Opus 5.5 1231679c4d Launcher: Modrinth mods and resource packs for Java instances
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 10:40:02 +01:00
Ludovic Marie 9e9b9862ee Merge remote-tracking branch 'origin/main' into feat/launcher-java-play
# Conflicts:
#	launcher/lang/de.json
#	launcher/lang/fr.json
#	launcher/lang/ru.json
2026-10-29 10:39:44 +01:00
Ludovic Marie 7c31259376 Books: suggest_command does nothing on a book, as vanilla's insertText there (every click event is matched) 2026-10-29 10:39:00 +01:00
Ludovic Marie 40c1ab898f Merge remote-tracking branch 'origin/main' into upd/2996
# Conflicts:
#	crates/horizon-client/src/game/books.rs
2026-10-29 10:38:25 +01:00
damnurmumandCodex c01f4f646b Merge main into rich item name colors
Preserve styled custom names in loot component updates and cover the merged behavior.

Co-Authored-By: Codex <[email protected]>
2026-10-29 12:36:47 +03:00
Muhammad Asif c74d170fbc rustfmt pearl and wind-charge boxes 2026-10-29 14:36:21 +05:00
GIGABAIT93andClaude Opus 5.5 ea01f563b4 Pack cache tests: wait for the clock's next tick after each build
The first fix covered same-length rewrites through Fixture::write only;
directory_inputs_config_and_owned_output_recovery still failed about
one run in seven under WSL2, through its own fs::write and set_times
and through files added to a directory in the build's tick. Waiting for
the filesystem's clock to move after each rebuild_matches covers every
edit that follows, as a person's next edit comes after the build. The
wait writes new bytes each time (NTFS keeps the time of a write that
changes nothing) and gives up after a second.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 11:35:18 +02:00
Ludovic Marie 13d7d9995d Merge pull request #3596 from horizon-rust/fix/launcher-native-mods-auto-on
🤖 Agent: Ludovic's decision 10-29 (native mods on when one is installed, notice once). Diff reviewed; verify green; build matrix 40848959744 green on Windows, macOS and Linux.
2026-10-29 10:35:00 +01:00
Ludovic Marie 945a98f8bc Merge remote-tracking branch 'origin/main' into upd/2875
# Conflicts:
#	README.md
2026-10-29 10:34:30 +01:00
Ludovic MarieandClaude Opus 5.5 7fa0fe50f7 Format modifier lines with format_translation
TranslatableContents.decomposeTemplate is already ported as
text::format_translation (%s, %n$s, %%), so the modifier line uses it instead
of a second, partial parser.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 10:34:19 +01:00
Ludovic MarieandClaude Opus 5.5 21773ca7f5 Merge main into this branch
streaming.rs moved to horizon-client-render (#3186) and main now sweeps far
columns only on some updates (if sweep); this branch's changes (no area at a
negative radius, rank_full_task, its test) are applied there.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 10:33:55 +01:00
Ludovic MarieandClaude Opus 5.5 49cf77a312 Merge main into this branch, stub the tutorial in the remote mining test
game.rs/remote.rs: main's creative screen reopen (#3187) and code of conduct
reset are kept beside this branch's tutorial steps. tests/test_remote_mining.py
compiles remote.rs's mining against its own small Game, which now needs a
no-op tutorial_destroy (verify failed with E0599 there).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 10:33:29 +01:00
Ludovic Marie 611d1ef51e Merge remote-tracking branch 'origin/main' into upd/3103 2026-10-29 10:32:04 +01:00
Ludovic MarieandClaude Opus 5.5 49866a4482 Leave tools/port_publish.py to #3587
The same Windows path fix is in #3587 (with the UTF-8 body file), so this
branch keeps only the kelp mipmap change.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 10:32:04 +01:00
Ludovic Marie 6a06853032 Merge remote-tracking branch 'origin/main' into upd/3076 2026-10-29 10:32:02 +01:00
Ludovic MarieandClaude Opus 5.5 25f4545038 Merge main into this branch
lod.rs: main's chorus plant test (#2894) and this branch's small island test are
both kept; docs/graphics.md names chorus plants and small islands.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 10:29:46 +01:00
Ludovic MarieandClaude Opus 5.5 54e56ca215 Merge main into this branch
save.rs: main already reads session.lock after the lock is dropped (#3088), so
main's version of that test is kept.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 10:29:20 +01:00
Ludovic MarieandClaude Opus 5.5 d026f1eb37 Merge main into this branch
net.rs: main's configuration loop now hands the code of conduct to the game
(Event::CodeOfConduct) instead of accepting it; this branch's UpdateTags arm is kept.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 10:28:23 +01:00
Ludovic Marie 92c462a9b1 Merge pull request #2845 from levzzz5154/fix/structure-solid-render
🤖 Agent: verified by contrib-gate-27. Read in full; matches StructurePiece's isSolidRender checks and the reorient order (N, E, S, W). union/cg27-1 (main + this wave): fmt, clippy -D warnings (horizon-client, horizon-world, horizon-client-core), their tests and the fast build pass; lan_e2e damage, minecarts, projectiles, sculk_container, shield, push and hazards pass on the server thread and in-frame. Harness parity runs in CI on this exact head (the Mac mini's load is too high for local parity).
2026-10-29 10:27:57 +01:00
Ludovic MarieandClaude Opus 5.5 cdefed7a19 Merge main into this branch
#damages_helmet holds falling_anvil, falling_block and falling_stalactite (26.3's
tag): main added the stalactite, this branch the anvil; both kept.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 10:27:54 +01:00
Ludovic MarieandClaude Opus 5.5 29d24f48df Merge main into this branch
Main's handleUseItem sets the guest's known movement (ServerPlayer.getKnownMovement)
before the item use; this branch's horn sound is kept in the same closure.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 10:27:37 +01:00
Ludovic Marie b8f48f3ad0 Merge remote-tracking branch 'origin/main' into upd/2814 2026-10-29 10:27:22 +01:00
Ludovic Marie 5ff61e1c42 Merge remote-tracking branch 'origin/main' into upd/2823 2026-10-29 10:26:31 +01:00
Ludovic MarieandClaude Opus 5.5 abca441ad4 Launcher: Java versions that inherit from another (loader profiles)
A version file with `inheritsFrom` (what Fabric, Quilt and other mod
loaders install) is merged into the version it names, as the official
launcher reads it: its fields over the parent's, its libraries first (a
parent's library it names again is left out), its arguments after the
parent's, and the parent's client jar (`jar`). On a version from before
1.13, a profile's own argument lists join the old line instead of
replacing it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 10:25:28 +01:00
Ludovic MarieandClaude Opus 5.5 f44042e0eb Sound device: vanilla's own option names, and a missing device asks once
options.audioDevice and options.audioDevice.default come from vanilla's
language files ("Device", "System Default"): the game's own lang no longer
overrides them. Audio keeps the device asked for, so a saved device that is
unplugged plays on the default without reopening the stream at every
settings change.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 10:25:05 +01:00
Ludovic MarieandClaude Opus 5.5 4a2a5b9977 Merge main into this branch after the render crate split (#3186)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 10:25:05 +01:00
levzzzandOpenAI Codex 6534db4f6d Specify the equipment regression fixture's mob registry type
Co-Authored-By: OpenAI Codex <[email protected]>
2026-10-29 14:24:18 +05:00
GIGABAIT93andClaude Opus 5.5 479f53624c Python tools: read and write UTF-8 in roadmap.py and port_publish.py
roadmap.py reads and writes ROADMAP.md and the roadmap TOML as UTF-8, so
build writes the · as git has it and build --check doesn't call a fresh
checkout out of date on Windows. port_publish.py decodes gh's and
node's output as UTF-8, so an issue body's arrows survive an update, and
writes the store path as #3441 does, so the two pull requests agree.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 11:23:53 +02:00
Muhammad Asif ce45e065c3 Let thrown pearls hit wind charges
AbstractWindCharge is pickable, so ThrownEnderpearl's
ProjectileUtil.getEntityHitResult can strike it and onHit teleports.
Horizon only targeted mobs and players, so a pearl passed through a
charge. Add the charge's entity box to the pearl's targets.
2026-10-29 14:23:49 +05:00
levzzzandOpenAI Codex 69689b3fdd Cover named equipment replacement through save and reload
Co-Authored-By: OpenAI Codex <[email protected]>
2026-10-29 14:23:48 +05:00
Ludovic Marie 99b52c2617 Merge remote-tracking branch 'origin/main' into feat/launcher-java-play
# Conflicts:
#	launcher/lang/fr.json
#	launcher/lang/ru.json
#	launcher/src/main.rs
2026-10-29 10:23:05 +01:00
Ludovic Marie 77bfff8eb5 Merge pull request #3595 from horizon-rust/docs/roadmap-triage-10-29
🤖 Agent: docs only (roadmap.toml + generated ROADMAP.md/roadmap.json + README). roadmap.py build --check and check --offline pass; verify green; parity correctly skipped.
2026-10-29 10:22:55 +01:00
Ludovic Marie 8ea9a2c849 Merge remote-tracking branch 'origin/main' into fix/launcher-native-mods-auto-on
# Conflicts:
#	launcher/lang/fr.json
#	launcher/lang/ru.json
2026-10-29 10:20:52 +01:00
Ludovic MarieandClaude Opus 5.5 cc15e8ebb3 Rename the hitbox view's eye_positions to eye_motions
Main's c5fe5d4bc added EndState::eye_positions (id and position, for
status); the hitbox view needs the previous and current positions.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 10:19:58 +01:00
GIGABAIT93andClaude Opus 5.5 5410e79811 Pack cache tests: rewrite files as an edit would, a clock tick later
On Unix the overlay cache compares stamps (length, mtime, ctime, inode),
and Linux takes them from a coarse clock, so the fixture's same-length
rewrites within one tick looked unchanged:
normalized_and_external_overlay_roots_keep_merged_bytes_current failed
about three runs in five on upstream main under WSL2. Fixture::write now
waits until the file's stamp moves when the bytes change.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 11:19:22 +02:00
levzzzandOpenAI Codex c6dfae6dd7 Keep live enchantments when displaying and saving named equipment
Co-Authored-By: OpenAI Codex <[email protected]>
2026-10-29 14:18:51 +05:00
Ludovic MarieandClaude Opus 5.5 db8fa8377d Merge main into this branch
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 10:18:46 +01:00
Ludovic MarieandClaude Opus 5.5 dbeb13ff6b Merge main into this branch
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 10:18:44 +01:00
Ludovic MarieandClaude Opus 5.5 39d23ac256 Merge main into this branch
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 10:18:43 +01:00
Ludovic MarieandClaude Opus 5.5 d900488d12 Merge main into this branch after the render crate split (#3186)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 10:18:43 +01:00
Ludovic MarieandClaude Opus 5.5 e7827deabb Merge main into this branch
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 10:18:41 +01:00
Ludovic MarieandClaude Opus 5.5 1eb34fa68e Merge main into this branch
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 10:18:40 +01:00
Ludovic MarieandClaude Opus 5.5 d2d5dd00ad Merge main into this branch
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 10:18:38 +01:00
Ludovic MarieandClaude Opus 5.5 916150f02c Merge main into this branch
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 10:18:37 +01:00
Ludovic MarieandClaude Opus 5.5 d7662a97da Merge main into this branch
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 10:18:35 +01:00
Ludovic MarieandClaude Opus 5.5 4623d2e59f Merge main into this branch
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 10:18:33 +01:00
Ludovic Marie 5302f6a22a Creative piston head test: check the base drops nothing (main's shape update now removes the head, which drops its empty loot) 2026-10-29 10:18:33 +01:00
Ludovic MarieandClaude Opus 5.5 aa7a149858 Merge main into this branch
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 10:18:31 +01:00
Ludovic MarieandClaude Opus 5.5 504aa1dca6 Merge main into this branch
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 10:18:30 +01:00
Ludovic MarieandClaude Opus 5.5 3497737ecc Merge main into this branch after the render crate split (#3186)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 10:18:05 +01:00
Ludovic Marie 04cd6ece23 Drop the test that only exercised std::mem::replace (the fix is the one line in as_guest) 2026-10-29 10:16:08 +01:00
Ludovic Marie 9db690b1e0 Merge remote-tracking branch 'origin/main' into upd/3328 2026-10-29 10:15:48 +01:00
Ludovic Marie 9218dc3e7d Leave horizon-server's lists test as on main (not part of this fix) 2026-10-29 10:15:47 +01:00
Ludovic Marie 5b66bd0ec0 Merge remote-tracking branch 'origin/main' into upd/3148 2026-10-29 10:15:46 +01:00
Ludovic MarieandClaude Opus 5.5 70a6aa45aa Merge main into this branch after the render crate split (#3186)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 10:15:19 +01:00
Ludovic MarieandClaude Opus 5.5 8c0b674fe8 Merge main into this branch (German launcher strings, old-version pruning)
Keeps Spanish beside German, adds the six strings main added since, and drops
the nightly strings main removed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 10:14:54 +01:00
levzzzandOpenAI Codex f75896ec45 Retry unloaded gateway arrivals and identify the fixture entities by tag
Co-Authored-By: OpenAI Codex <[email protected]>
2026-10-29 14:13:49 +05:00
Ludovic Marie 2b294bbe3a Merge pull request #3506 from horizon-rust/fix/launcher-delete-instance
🤖 Agent: launcher-java-1 verified this head: PR checks green (verify, launcher), launcher build matrix green on Windows, macOS and Linux (dry run https://github.com/horizon-rust/horizon/actions/runs/40817273793), pr_ready --quick green with the new test deleted_instances_go_to_the_trash_and_are_forgotten; docs/launcher-commands.md regenerated.
2026-10-29 10:13:16 +01:00
levzzzandOpenAI Codex ae72e1f6ca Add a two-path saved vehicle tree and passenger restoration audit
Co-Authored-By: OpenAI Codex <[email protected]>
2026-10-29 14:12:50 +05:00
levzzzandOpenAI Codex 164cfd6b11 Restore server riders after their chunk is sent and refresh entity pairing
Co-Authored-By: OpenAI Codex <[email protected]>
2026-10-29 14:12:19 +05:00
levzzzandOpenAI Codex a2e111a461 Keep fluid-test pigs mobile with zero AI movement speed
Co-Authored-By: OpenAI Codex <[email protected]>
2026-10-29 14:12:18 +05:00
Ludovic MarieandClaude Opus 5.5 f220157a0f Launcher: installing a native mod turns native mods on
Ludovic's decision (2026-10-29). Native mods were on only once the
player found Settings > Native mods (experimental). Now installing a
native mod turns them on, so Settings offers its Build at once; players
who never install one keep them off.

The first native mod's Install shows a notice once, before anything is
installed: native mods compile into a game of their own, the first
build downloads Rust and can take 20 to 30 minutes, later builds are
faster (Install and turn on native mods · Cancel). `mods install`
turns them on the same way and prints the notice. The build itself
stays behind its trust warning, now saying 20 to 30 minutes too, and
turning a mod off in the game's Options > Mods is unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 10:12:09 +01:00
jaszkusandjaszkus's AI agent ddd1dbdf91 Keep the block outline while a screen is open
GameRenderer.shouldRenderBlockOutline() checks the hidden HUD (F1) and the game mode, not the open screen, and Minecraft.tick() keeps the block raycast current while chat is up. Horizon also required Screen::Playing, so the outline vanished as soon as chat opened.

Co-Authored-By: jaszkus's AI agent <[email protected]>
2026-10-29 10:10:54 +01:00
Ludovic MarieandClaude Opus 5.5 3f28d6dbd1 Roadmap: triage the suggestion queue, native mods and Java instances; README refresh
Roadmap (from the 59 suggestions in the queue and what merged since):
- Microsoft sign-in shipped in [email protected].
- New: Minecraft Java instances in the launcher, native mods built by the
  launcher, the port ledger, a new launcher design, hosting panel setups
  (Pterodactyl, AMP), joining older-version servers through a native
  protocol adapter, Windows ARM64, splitscreen (#2054).
- Mods items reworded for native hook points instead of the WebAssembly
  API; scripts for mods moved to Later; asked-for client mods added.

README: more than 300 harnesses, the Nightly channel, native mods, and
the port ledger as a second, method-by-method measure.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 10:10:47 +01:00
Ludovic MarieandClaude Opus 5.5 5bc76cbba9 Launcher clipboard: one loop over the desktop's copy tools
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 10:10:03 +01:00
Ludovic Marie c3dceac2aa Merge remote-tracking branch 'origin/main' into upd/3101
# Conflicts:
#	crates/horizon-client/src/game/scoreboard.rs
2026-10-29 10:09:08 +01:00
degepieandClaude Opus 5.5 e4568abf6e Title buttons and creative tabs show the pointing hand under the mouse
26.3 asks for a cursor from every hovered widget (AbstractWidget
handleCursor: the pointing hand, or not-allowed when inactive) and from
the creative inventory's unselected tab buttons
(CreativeModeInventoryScreen.extractTabButton: the pointing hand while
the whole-pixel mouse is strictly inside the 26x32 button). #1347 did
this for ui::button and the pack screen, but the title screen draws its
own buttons and the creative tabs never asked, so both kept the arrow.

The title screen's button loop now asks through widgets::widget_cursor
with the mouse test apart from the enabled check, and draw_creative asks
for the hand through the new Creative::tab_wants_hand.

Fixes #3040.
Fixes #3042.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
2026-10-29 14:07:23 +05:00
Ludovic MarieandClaude Opus 5.5 64981e95b7 Screenshots: the name's doc says local time, as the code now does
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 10:07:01 +01:00
levzzzandOpenAI Codex 7d0e5aa953 Do not save replaced main-hand components from the raw entity
Co-Authored-By: OpenAI Codex <[email protected]>
2026-10-29 14:04:47 +05:00
levzzzandOpenAI Codex 09a9735630 Keep unsupported saved vehicle types pending without respawning them
Co-Authored-By: OpenAI Codex <[email protected]>
2026-10-29 14:04:46 +05:00
levzzzandOpenAI Codex a80973335c Merge current main before rerunning End gateway cargo proof
Co-Authored-By: OpenAI Codex <[email protected]>
2026-10-29 14:03:47 +05:00
levzzzandOpenAI Codex fe867a6132 Track tagged fluid-test pigs and always clean enclosed arenas
Co-Authored-By: OpenAI Codex <[email protected]>
2026-10-29 14:02:58 +05:00
degepieandClaude Opus 5.5 d32e7ff887 Draw the phantom's model inside its box
PhantomRenderer.scale was not ported: the phantom's model is laid out about
1.3 blocks above the feet and vanilla moves it down (and 0.1875 along z)
after the size scale, so Horizon drew it above its 0.9x0.5 box. Port the
scale (1 + 0.15 * size) and the translate, and PhantomRenderer.setupRotations'
pitch.

Fixes #3008.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
2026-10-29 14:02:41 +05:00
levzzzandOpenAI Codex ace27c11a9 Keep named held stacks current and use full death-message display names
Co-Authored-By: OpenAI Codex <[email protected]>
2026-10-29 14:01:23 +05:00
levzzzandOpenAI Codex 8742e1768e Merge current main before isolating fluid checks
Co-Authored-By: OpenAI Codex <[email protected]>
2026-10-29 14:01:20 +05:00
levzzzandOpenAI Codex c91ca07115 Merge current main and use its preset-aware terrain interface
Co-Authored-By: OpenAI Codex <[email protected]>
2026-10-29 13:58:50 +05:00
levzzzandOpenAI Codex c001f56d0f Merge current main before death-message review fixes
Co-Authored-By: OpenAI Codex <[email protected]>
2026-10-29 13:58:45 +05: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
levzzzandOpenAI Codex b36a63fb39 Merge current main and retain vanilla features save status
Co-Authored-By: OpenAI Codex <[email protected]>
2026-10-29 13:58:03 +05:00
levzzzandOpenAI Codex f86c946a3c Merge current main and preserve saved vanilla vehicle trees
Co-Authored-By: OpenAI Codex <[email protected]>
2026-10-29 13:57:42 +05:00
v3nn7andCodex 65043ba092 Sync furnace minecart fuel exhaustion
Co-Authored-By: Codex <[email protected]>
2026-10-29 09:56:12 +01:00
GIGABAIT93andClaude Opus 5.5 0f4880bacd Python tools and tests: the same results on Windows
port_publish names the version store with forward slashes, so an issue
reads the same whoever publishes it (test_port_publish expected
~/code/... and Windows wrote ~\code\...), and writes the issue body as
UTF-8 for gh whatever the code page. test_roadmap reads ROADMAP.md as
UTF-8. All 408 Python tests now pass on Windows, with or without
PYTHONUTF8.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 10:55:09 +02:00
Ludovic Marie d203ca04be Merge pull request #2894 from hectoreccles/lod-chorus-vegetation
🤖 Agent: requeued by contrib-gate-28 after merging main (B9 render crate split; stopped on a conflict or a known flake). fmt, clippy -D warnings and tests pass on the new head; proven earlier by cg26/cg27.
2026-10-29 09:54:49 +01: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
v3nn7andCodex f2edc255d8 Render carved pumpkins on player heads
Co-Authored-By: Codex <[email protected]>
2026-10-29 09:54:31 +01:00
v3nn7 f6144df193 Show powered furnace minecarts on remote clients 2026-10-29 09:53:17 +01:00
Username59138andClaude Opus 5.5 3451e4e29a Command blocks: keep the command in the edit screen, suggest as vanilla
Command blocks barely worked from the default single player (the window
is a client of its server thread), a LAN guest or a dedicated server:

- ServerPlayer.openCommandBlock: the server now sends the block's data
  (saveCustomOnly) to the player whose use opened it, and the open
  screen refills from it (handleBlockEntityData, updateGui). The chunk's
  update tag leaves the command out, so the screen showed an empty box
  and Done then wiped the block's command.
- Screen.extractBackground of an isInGameUi screen: the dimmed
  gradient, not the in-world menu background.
- CommandSuggestions in the command box with the arguments
  AbstractCommandBlockEditScreen passes (commands without a slash,
  completions from the start, 7 lines under the box, fill 0x80000000):
  the chat's suggestions take a SuggestLayout. Server completions
  (ask_server) reach the box too.
- horizon-ctl key presses reach the command block screen.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 11:53:14 +03: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
degepieandClaude Opus 5.5 861be0b075 Armour: worn pieces and the elytra show the enchantment glint
The player's armour layers (local player, other players, LAN guests)
never carried the glint, and the elytra drew with overlay 0. The armour
glint's overlay code, 13, was also the boat water mask's, so a glinting
layer drew depth only: an armour stand's enchanted chestplate was
invisible.

EquipmentLayerRenderer.renderLayers: each layer and its trim take the
armour glint when ItemStack.hasFoil, never the hurt tint (NO_OVERLAY).
ARMOR_GLINT moves to 35, past every material overlay of the entity
batch.

Fixes #3024.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
2026-10-29 13:50:58 +05:00
levzzzandOpenAI Codex bd59c0bab2 Pick up validated standard macros for body hooks
Co-Authored-By: OpenAI Codex <[email protected]>
2026-10-29 13:47:44 +05:00
levzzzandOpenAI Codex d3cb2f5c29 Pick up HEAD macro preservation in the launcher mixins pin
Co-Authored-By: OpenAI Codex <[email protected]>
2026-10-29 13:47:44 +05: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
v3nn7andCodex e851af06d0 Add Polish translations
Co-Authored-By: Codex <[email protected]>
2026-10-29 09:43:11 +01:00
vadyanikandClaude Sonnet 5.5 6255c02096 Follow the XKB keymap for Escape and Caps Lock
Key events carry the scancode's US-layout code, which ignores options like
caps:swapescape. Take Escape and Caps Lock from the resolved keysym instead.

Fixes #3133

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-29 10:42:32 +02:00
degepieandClaude Opus 5.5 81445f5218 Splash sounds and particles when the player falls into water
Singleplayer and multiplayer run the player through the remote client
path, which dropped the motion's Splash and SplashParticle events. In
vanilla the local player's water-entry splash (Entity.doWaterSplashEffect)
is heard only on its own client: Player.playSound passes the player as
the exception, so the server's copy skips it, and the particles are the
client level's own. Play them from the remote tick as the local tick does.

Fixes #3028.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
2026-10-29 13:39:03 +05: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
SenjinandClaude Opus 5.5 ef2ec4c0dc CI: run the checks again (horizon-server's lists test got no answer from Mojang's services)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 09:33:06 +01: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
v3nn7andCodex c4f7b4f964 Fix teleport commands without explicit targets
Co-Authored-By: Codex <[email protected]>
2026-10-29 09:23:48 +01:00
d3grandoandClaude Opus 5.5 ab640c7646 Respawn anchor: glowstone charges it on a server instead of being placed
On a server (and the default server-thread single player) the client runs
the block's useItemOn on a copy of its level to decide whether the click
ends there. horizon-world's use_item_on had no RespawnAnchorBlock case, so
glowstone on an anchor fell through to useWithoutItem (PASS on an empty
anchor) and the client predicted a glowstone block that the server then
took back.

- horizon-world use_item_on: glowstone on an anchor below 4 charges charges
  it (RespawnAnchorBlock.charge) and returns SUCCESS.
- The client's off-hand turn runs useItemOn too, so off-hand glowstone
  charges the anchor instead of predicting a placement.
- The main hand PASSes when the off hand's glowstone is to charge the
  anchor, so a charged anchor isn't used (useWithoutItem) and the off-hand
  turn is sent.

Fixes #3034

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 15:23:20 +07:00
Ludovic Marie 11494e17f9 Merge pull request #3104 from Expected333/vr/aim-ray
🤖 Agent: gate cg29: re-checked after your second main merge (70b51333d): same-ray refactor, no behaviour change. fmt, clippy -D warnings, horizon-client tests 796/0 and the 33 harnesses its files reach (33/33) on this head; union/cg29-1 (with #2799 #2819 #2812): parity --changed 124/124 and lan_e2e frames minecarts boats deflect cushions pvp commands on the server thread and --in-frame.
2026-10-29 09:23:11 +01:00
d3grandoandClaude Opus 5.5 b2a5abb885 Key binds: a moved key no longer keeps its default action
KeyBinds::translate_all turns a pressed key into the default keys of the
mappings bound to it, so the game's code follows the player's bindings.
It also followed the debug mappings (F3+A reload chunks, F3+S, F3+1 the
profiler chart...), whose defaults are the gameplay keys A, S, D, T, L
and 1 to 4. So after moving key.hotbar.1 off 1, or key.left off A, the
old key was still "bound" to a debug mapping whose default was itself,
and kept selecting slot 1 or strafing left.

The debug keys act only with F3 held (KeyboardHandler.handleDebugKeys),
and debug_keys.rs already reads the physical key and the binding for
them, so translation now leaves them out.

Fixes #3158

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 15:21:02 +07:00
d3grandoandClaude Opus 5.5 1da38a5c46 Villager gossip: hits and murders count against the player who did them
On the server thread every player is a guest of the server's Game, but a
villager's hurt gossip (minor_negative) and its witnesses' murder gossip
(major_negative) were filed under self.level.player_uuid, the world's
singleplayer UUID. Trading reads the reputation under the guest's own UUID,
so hurting or killing villagers never raised that player's prices.

Both now go to the player in last_hurt_by_player (the host, or the guest by
connection), as Villager.setLastHurtByMob and
tellWitnessesThatIWasMurdered report the attacking player.

Fixes #3431

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 15:20:21 +07:00
d3grandoandClaude Opus 5.5 2bb3903b49 Draw the arms under a map held in first person
renderOneHandedMap draws the holding arm (renderPlayerArm, turned 10
degrees outwards) and renderTwoHandedMap both arms (renderMapHand), each
with renderPlayerHand, unless the player is invisible. Horizon drew only
the map. first_person::map_arm_poses gives those poses; the bare arm's
pose (player_arm_pose) and the two-handed map's base
(two_handed_map_base) move to first_person.rs so the empty hand and the
map share them. The game keeps both arms of the player models, and
first_person_arm draws either with the skin and renderHand's 0.1 radian
zRot.

Fixes #3136

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 15:19:44 +07:00
GIGABAIT93andClaude Opus 5.5 cbd1b1c93e Launch API: horizon-client's options for other launchers (#3545)
The game's side of the Launch API, experimental, version 0.1.0:
--game-dir (vanilla's --gameDir), --home, --fullscreen (vanilla's, not
saved to options.txt), --session-stdin and --launch-api, which prints
the API version and the options a launcher may pass as JSON.

A refused command line now exits 2 without a crash report; errors keep
1 and panics 101. The crash report's path stays the last stderr line,
its prefix now a constant (crash::SAVED_LINE).

docs/api/launch.md is the surface page; docs/api/launch-reference.md is
generated from game/launch_api.rs and a test keeps the two together.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 10:19:25 +02:00
d3grandoandClaude Opus 5.5 c8915be58c Attack indicator shows at a spear's extra range
The crosshair's living target came only from mob_hit, capped at the
3 blocks of entityInteractionRange, so the attack indicator never showed
for a mob a spear could reach at 4 blocks. LocalPlayer.raycastHitResult
first picks along the held item's AttackRange (getClosesetHit: from the
min reach to the max plus the player's movement, boxes grown by the
hitbox margin, stopped by block outlines) and only falls back to the
plain pick on a miss. crosshair_living now does the same, then keeps
Hud's AttackRange.isInRange check.

The spear's stab search (getHitEntitiesAlong) becomes free functions
over mob boxes with a block clip, shared by the stab (collider) and the
crosshair (outline).

Fixes #3209

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 15:14:13 +07:00
Ludovic Marie ff0e16e3b9 Merge pull request #2955 from kacpherek/fix/server-code-of-conduct-screen
🤖 Agent: verified by contrib-gate-27. Read in full; matches handleCodeOfConduct and ServerData.acceptedCodeOfConduct. Main merged. Note: adjacent to #2981 in net.rs. union/cg27-3 (main + this wave): fmt, clippy -D warnings, horizon-client/horizon-protocol tests (1395/0) and the fast build pass; lan_e2e sleep, bed-occupied, sleep-villager, sleep-reload, arrows, objects, commands and tab pass on the server thread and in-frame. Harness parity runs in CI on this exact head (the Mac mini's load is too high for local parity).
2026-10-29 09:13:24 +01:00
d3grandoandClaude Opus 5.5 b1d3c9b92b Rockets: a rocket used while gliding no longer raises the off-hand shield
FireworkRocketItem.use answers SUCCESS while the player is fall flying
(it launches the boosting rocket), so Minecraft.startUseItem stops at the
main hand. On a server client Horizon only counted items with a use of
their own as ending the click, so the rocket passed and the off hand's
shield went up too, holding the use until the button let go. On foot the
rocket still passes (PASS), as in vanilla, and the shield still rises.

Fixes #3424

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 15:11:39 +07:00
d3grandoandClaude Opus 5.5 e2146572a6 Repeat attack and use clicks while a key bound to them is held
KeyboardHandler.keyPress sends a held key's repeats to KeyMapping.set and
KeyMapping.click like the press, so a keyboard key bound to key.attack
keeps attacking while it is held. Horizon dropped the repeats of keys
bound to a mouse mapping, so such a key hit once. horizon-ctl's
`key KEY repeat` now keeps a mouse-mapped key held too, as the window's
repeat does.

Fixes #3243

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 15:10:33 +07:00
d3grandoandClaude Opus 5.5 d94e4cb7a4 Jukebox: a server's client plays the song of level event 1010
A jukebox on a server (a multiplayer server, a LAN host, or single
player's own server thread) plays its song for the clients with level
event 1010, whose data is the song's id in the jukebox_song registry.
LevelEventHandler looks the song up in the registry the server sent and
calls playJukeboxSong: the sound, the "Now Playing" overlay and the
dancing parrots. Horizon only made the parrots dance, and started songs
only from its own ticking jukeboxes, which a server's client never has,
so the note particles showed and nothing was heard.

Level event 1010 now starts the song it names, and 1011 stops it
(stopJukeboxSongAndNotifyNearby). A server's client keeps its song until
1011 or until the song ends, instead of dropping it at once because its
own copy of the jukebox isn't playing.

Fixes #3173

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 15:09:38 +07:00
d3grandoandClaude Opus 5.5 8dffd2b847 Count a zombie villager's cure down in its tick, with or without AI
ZombieVillager.tick counts villagerConversionTime down before super.tick
for every living, converting zombie villager; NoAI only skips
serverAiStep. Horizon counted it in customServerAiStep, so a NoAI zombie
villager never finished curing.

Fixes #3429

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 15:08:17 +07:00
d3grandoandClaude Opus 5.5 2a7ee57bab Leave the vehicle when a dead player is removed or respawns
A player who died in a boat stayed its passenger: after the respawn the
server sat the new player back in the boat (and in the void it died
again). In vanilla the dead body is removed 20 ticks after death
(LivingEntity.tickDeath's remove(KILLED)), and PlayerList.respawn
removes it at once (ServerLevel.removePlayerImmediately); both go
through Entity.setRemoved, which calls stopRiding.

- Guest::tick_death takes the body out of its vehicle at 20 ticks, and
  Hub::respawn does it for an earlier respawn (immediate_respawn).
- The client's Respawn after a death (no KEEP_ENTITY_DATA) leaves the
  vehicle, as vanilla's new LocalPlayer rides nothing; a dimension
  change (keep 3) keeps it.
- In-frame, respawn_player stops the riding of the window's player.

Fixes #3474

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 15:08:11 +07:00
Kacpherek 25e39a2f2e Follow the crate split: repoint 1,272 map rows to their files' new crates 2026-10-29 09:07:15 +01:00
Kacpherek 630f3feec5 Merge branch 'main' of https://github.com/ludovic111/horizon into tools/port-ledger-harness
# Conflicts:
#	docs/port-ledger.md
#	tools/port_ledger.py
2026-10-29 09:07:15 +01:00
d3grandoandClaude Opus 5.5 4245540711 Invisible mobs cast no shadow
EntityRenderer.extractShadow skips an entity whose render state is
invisible (Entity.isInvisible). The shadow pass took every mob: the
casters now come from shadows::mob_casters, which leaves out a mob with
the shared invisible flag from a server or the Invisibility effect on a
mob ticked in the window (Mob::is_invisible).

Fixes #3289

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 15:05:01 +07:00
d3grandoandClaude Opus 5.5 6bd34b64d6 Draw glowing sign text in front of its outline
draw_text emitted the eight outline copies and the text on the same
plane in one mesh, so glowing sign text z-fought with its outline and
broke up. Vanilla's TextFeatureRenderer.renderText draws the outline
NORMAL and the text over it with Font.DisplayMode.POLYGON_OFFSET; the
block-entity mesh has no such pass, so an outlined call's text is now
lifted a twentieth of a font pixel towards the reader.

Fixes #3293

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 15:04:14 +07:00
NikitaRTN 2b32b9142f Net: guard non-blocking disconnect against a stalled reader 2026-10-29 14:00:42 +06:00
hashtheboardandClaude Opus 5.5 70b51333d3 Merge origin/main into vr/aim-ray
lan_pvp.rs: keep main's mace::Spared import, drop view_vector (aim() replaces it).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 08:58:22 +01:00
d3grandoandClaude Opus 5.5 a690ccef3c Projectiles hit a seated enderman
Enderman.hurtServer teleports away from projectile damage only when
!isPassenger(). Horizon's on_damage rejected every projectile, so an
enderman riding a boat or a mob made arrows vanish and shrugged off
snowballs and eggs. A free enderman still dodges.

Fixes #3381

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 14:55:02 +07:00
Ludovic Marie 8f0c178957 Merge pull request #2850 from deezhizyu/fix/sweep-attack-2764
🤖 Agent: verified by contrib-gate-27. Read in full; the sweep matches Player.attack and doSweepAttack (sword, charged, on the ground, not sprinting, the entities in the sweep box). union/cg27-2 (main c6af85743 + this wave): fmt, clippy -D warnings, horizon-client tests (1293/0) and the fast build pass; lan_e2e team-push, pvp, thrown, projectiles, mounts and riding pass on the server thread. Harness parity runs in CI on this exact head (the Mac mini's load is too high for local parity).
2026-10-29 08:53:36 +01:00
Ludovic Marie 5a30084c1d Merge pull request #2798 from iLostName/options-languages-without-reading
🤖 Agent: verified by contrib-gate-27. Read in full: the language list now comes from pack.mcmeta without reading every lang file (LanguageManager). union/cg27-1 (main + this wave): fmt, clippy -D warnings (horizon-client, horizon-world, horizon-client-core), their tests and the fast build pass; lan_e2e damage, minecarts, projectiles, sculk_container, shield, push and hazards pass on the server thread and in-frame. Harness parity runs in CI on this exact head (the Mac mini's load is too high for local parity).
2026-10-29 08:33:22 +01:00
Senjin a0964a8010 Merge remote-tracking branch 'origin/main' into hdr-wayland 2026-10-29 08:32:57 +01:00
Senjin d91bd61281 Merge remote-tracking branch 'origin/main' into hdr-screen 2026-10-29 08:32:57 +01:00
Senjin c075240fa3 Merge remote-tracking branch 'origin/main' into hdr-screenshot 2026-10-29 08:32:56 +01:00
Senjin 289c2df527 Merge remote-tracking branch 'origin/main' into hdr-ui 2026-10-29 08:32:56 +01:00
Senjin 5134f8459e Merge remote-tracking branch 'origin/main' into hdr-black 2026-10-29 08:32:56 +01:00
Senjin 624be7df86 Merge remote-tracking branch 'origin/main' into hdr-f3 2026-10-29 08:32:55 +01:00
Senjin 84d49077c0 Merge remote-tracking branch 'origin/main' into hdr-tiers 2026-10-29 08:29:53 +01:00
時穂andCodex 6b96bb96b6 Merge main into launcher-simplified-chinese
Keep German and Simplified Chinese together, retain token-based translation
checks, and refresh Chinese strings for the current launcher.

Co-Authored-By: Codex <[email protected]>
2026-10-29 16:29:05 +09:00
Ludovic MarieandClaude Opus 5.5 48ab8b0934 lan_e2e team-push: put both players back where they stood
The check left the host 3 blocks east of the guest's spot; pvp's
face_each_other then put the guest inside the hill east of the spawn, where
it suffocated and both attacks hit the wall (server thread, after team-push).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 08:28:49 +01:00
Ludovic Marie 9e49037875 Merge pull request #2947 from kacpherek/feat/launcher-prune-old-versions
🤖 Agent: proven by contrib-gate-28: main merged (Nightly-only install path: nightly::after_update then prune_old_versions), German strings added; launcher fmt, clippy -D warnings, cargo test 247+45+… passed; isolated-home run of horizon-launcher update pruned v0.2.80/v0.2.88 and the old Nightly, kept the 2 newest releases and the selected one. Orchestrator approved the feature on 10-22 (cg22).
2026-10-29 08:20:00 +01:00
Ludovic Marie 9169aec896 Merge pull request #3088 from Domanffe/test/session-lock-reads-after-release
🤖 Agent: needs-proof check by Ludovic's AI agent (Claude), not by Ludovic himself. Passed on a union of 11 contributor PRs on main 430e5e4c9: fmt, clippy -D warnings, the horizon-client and horizon-items tests (all pass), and lan_e2e tab chest drops signs frames containers bubble-column with the host on its server thread and in-frame (digging and frames passed on their rerun, after #2745's checks had left water behind). Test-only. Parity: 46 of 268 harnesses passed locally before the Mac mini got too busy, so CI's run-parity on this head decides.
2026-10-29 08:19:40 +01:00
Kacper cddd325067 Port ledger: every vanilla method next to the Rust code that ports it, with a verdict for all of them (#3139)
* Add the port ledger: vanilla methods next to the Rust functions that port them

tools/port_ledger.py indexes the decompiled sources and the Rust tree into .local/port-ledger/ledger.json and answers where/what/class/coverage queries both ways, with the gaps where a port isn't wired like vanilla (seam, client, class, callee, stale).

* Document the port ledger

* Port ledger: bytecode calls, wider matching, drift, issue and check

Read the game jar's bytecode for exact call targets, constants and ids, and
resolve Rust calls through the receiver's type. Match more methods: same-named
fns in an impl or file named for the class (the impl type must fit the class),
and callees of known pairs linked through the call graph. Leave trivial methods
out of coverage.

New gap kind drift: a port that never names a sound, particle, item, block,
effect or game event its vanilla method uses, or lacks one of its rare numbers.
Seam gaps on the old in-frame path sort last; maths and geometry callees no
longer make seam or callee gaps.

New commands: issue (the classes, Rust code and gaps a bug report points at),
check (what a branch touches, gaps it opens and closes against its base), diff,
and a searchable offline report.html.

* Document the port ledger's new matching, gaps and commands

* Port ledger: class homes, structural matches and the verdict map

Give every vanilla class its Rust home files, weighed from all its matches.
In those files, match a method to the fn that touches the same fields and
reaches the ports of the same callees. Read tools/port_map/*.tsv, where a
person or agent records a verdict for what nothing else finds (port, inline,
missing, skip with a reason), and list the next unanswered batch with todo.
Count field-only constructors as trivial and fold initializer lambdas into
<clinit> and <init>.

* Map the first three batches: animals, inventory screens, world generation

* Document class homes, structural matches and the verdict map

* Map 13 game-logic batches and add map-check

map-check checks verdict-map files against the last build without
rebuilding, so several mappers can check their own rows at once.

* Map the rest of the game logic: classes with no Rust trace, then method batches

* Map client packet handling, entity renderers and models, block renderers

Also let PORT_LEDGER_NO_MAP build without the verdict map, to measure the
automatic matches on their own.

* Map client rendering, particles, sounds and resources

* map-check: accept full Class#method keys

* Map client screens, toasts, recipe book and world screens

* Add a vanilla-parity skill: start parity fixes from the port ledger

* issue: read registry names, so a mooshroom report finds MushroomCow

* Map the remaining vanilla methods: commands, network, packs, advancements and the last ten

* Follow the furnace code to furnace.rs on main; answer Fox.getBreedOffspring
2026-10-29 08:15:06 +01:00
Ludovic MarieandClaude Opus 5.5 7d43179850 Docs: the official native mods in the hook points section
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 07:46:57 +01:00
Ludovic MarieandClaude Opus 5.5 64ff13890a Launcher: WebAssembly mods need a native port
The registry keeps its WebAssembly mods (Mod Menu, lookat, blockscope…)
in the index, so the modpacks that need them keep installing. The browser
neither lists nor installs one: its versions say "needs a native port".
A modpack still installs it, with its code left out (the game doesn't run
it), and Installed shows "Not ported: its code (needs a native port…)".
A WebAssembly mod is a `.hmod` that may do more than add content; content
mods are unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 07:46:57 +01:00
Ludovic MarieandClaude Opus 5.5 0c9a49390e Crates: remove the WebAssembly mod runtime, its SDK and the official WebAssembly mods
The game no longer runs WebAssembly code mods (the previous commit), so the
workspace drops what made and ran them: horizon-mods' runtime, commands and
test harness (wasmtime, wit-component, wit-parser), the SDK and its WIT
contract and examples, the sandbox's guest crates, and mods/official (the
official mods are native now, one native/<name>/ folder each in horizon-rust/horizon-mods).

horizon-mods keeps content mods, modpacks and the registry index (whose
api_level still reads published .hmod files). `cargo horizon-mod` makes
content mods only: `new` has one template, `build` refuses a crate,
`check` refuses a .hmod with code and points to native mods, and `test`
(the sandbox) is gone. CI no longer installs the wasm32 target.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 07:46:57 +01:00
Ludovic MarieandClaude Opus 5.5 e7614d2197 Game: WebAssembly code mods don't run any more
Horizon's code mods are native now (game/native_hooks.rs, built in by the
launcher). The game drops the WebAssembly runtime and everything around
it: the `mods` feature (wasmtime and cranelift), the mods' hooks, HUD
panels and screens, the item panel and its recipes, input claims, camera
shakes, far terrain, server mods and the extras that moved to WebAssembly
mods. Content mods (blocks, items, data and assets) load as before.

A WebAssembly code mod's row on the Mods page says it doesn't run any more,
or that its native version replaced it. The first world after the update
says once in the chat which stopped and where to get their native
versions; their files stay where they are.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 07:46:57 +01:00
Ludovic MarieandClaude Sonnet 5.5 708f0b48bf Docs: WebAssembly code mods are gone; content and native mods remain
docs/mods.md is now about content mods (`cargo horizon-mod`, the manifest, the
content fence), native mods, publishing, the official native mods and a short
note on the removed WebAssembly mods. docs/api/mods.md and mods-reference.md
(the code API) are deleted. Other pages, the CLAUDE.md crate table and the
roadmap (five WebAssembly API items removed, others reworded for native mods)
no longer describe the sandbox, the SDK or the WIT.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-29 07:46:57 +01:00
Ludovic Marie b19b4e92d3 Merge pull request #3015 from DanielGodriNeto/perf/sprite-disk-cache
🤖 Agent: requeued by contrib-gate-28 after merging main (B9: sprite_cache.rs moved into horizon-client-render with sprites.rs); fmt, clippy -D warnings, horizon-client and horizon-client-render tests pass. Proven earlier by cg27 (sprite_cache_parity OK).
2026-10-29 07:35:53 +01:00
Ludovic Marie 1727cea578 Merge pull request #2989 from kacpherek/fix/save-format-parity
🤖 Agent: needs-proof check by Ludovic's AI agent (Claude), not by Ludovic himself. Passed on a union of 11 contributor PRs on main 430e5e4c9: fmt, clippy -D warnings, the horizon-client and horizon-items tests (all pass), and lan_e2e tab chest drops signs frames containers bubble-column with the host on its server thread and in-frame (digging and frames passed on their rerun, after #2745's checks had left water behind). world_border.dat lands in the overworld data folder, the border from a main world carries over, and previousPlayerGameType and the .dat_old files are written. Downgrading to an older build keeps the level.dat border: expected. Parity: 46 of 268 harnesses passed locally before the Mac mini got too busy, so CI's run-parity on this head decides.
2026-10-29 07:33:44 +01:00
malordinandClaude Sonnet 5.5 2d98bf8dae Launcher: fetch the assets a stopped download left out, and stop waiting on quiet connections
A download from Mojang that stopped on the way (the assets' failure is one log line)
leaves the asset store with some of its files, and every completion that copies from
it (the sounds, the languages, #1870) then finds nothing to copy and the game plays
silent and in English. Asking for it again did not help: the folder is found as it is,
so Prepare again never reached Mojang.

`complete_assets` now asks the owner gate again (`download_for_owner`, the one door to
Mojang's files) when Horizon's own store exists and a prepared version has no sounds or
no languages, once per run. The download keeps the files already whole, checked by sha1,
and fetches the rest; a non-owner fetches nothing, which the gate's test now also covers
for a store that lacks assets.

In that download each asset is tried three times and one that keeps failing does not
stop the rest: the log says how many failed and the first one's reason, and the next
run fetches what is left.

Requests had no timeouts, so one connection that went quiet held the whole download
for ever: the agent now gives up after 15 s to connect and 60 s without data.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-29 11:04:15 +05:00
hashtheboardandClaude Opus 5.5 5c0f6aa56c Merge origin/main into vr/aim-ray
minecarts.rs: both sides dropped an import (view_vector here, BlockInteraction on main).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 06:57:47 +01:00
Ludovic Marie 364922fa5e Merge pull request #2993 from kacpherek/fix/world-rules-parity
🤖 Agent: verified by contrib-gate-27. Read in full; checked against Mob.isSunBurnTick, ServerLevel.globalLevelEvent, Projectile.mayInteract/mayBreak and the DecoratedPot/ChorusFlower hits; small fix pushed (sculk mounts from the tick's snapshot). Note: #3104 touches the same minecarts.rs imports, so the second to land needs main merged. union/cg27-1 (main + this wave): fmt, clippy -D warnings (horizon-client, horizon-world, horizon-client-core), their tests and the fast build pass; lan_e2e damage, minecarts, projectiles, sculk_container, shield, push and hazards pass on the server thread and in-frame. Harness parity runs in CI on this exact head (the Mac mini's load is too high for local parity).
2026-10-29 06:52:45 +01:00
Ludovic Marie 1d8f853946 Merge pull request #2954 from kacpherek/fix/nautilus-inventory
🤖 Agent: needs-proof check by Ludovic's AI agent (Claude), not by Ludovic himself. Passed on a union of 12 contributor PRs on main 77fa9fb89: fmt, clippy -D warnings, the tests of horizon-client/-items/-protocol/-world (all pass) and the Python tests, and 19 lan_e2e checks with the host on its server thread (all pass) and in-frame (pass; frames passed on its rerun). lan_e2e mounts PASS on both paths. Parity: CI's run-parity on this head.
2026-10-29 06:44:24 +01:00
Kacpherek 451720bb56 Port ledger: link parity harnesses to the methods they cover; list untested ports 2026-10-29 06:40:06 +01:00
Alexander KrylovandHermes Agent 5485e189f8 Options: Horizon's own captions read the bundled language
The options pages, the mod and shader pack lists, the agent button and the
multiplayer friend screens drew their own labels as literals, so a client with
ru_ru stayed English there. They read `horizon.*` keys now, and the bundled
namespace carries both languages for every one of them.

Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
2026-10-29 08:35:09 +03:00
Alexander KrylovandHermes Agent 3c7e4ba574 Windows: link the overlay's mirrored files instead of copying them
`link` copied every file the mirror hands the overlay, so a selection with a
pack on wrote the prepared root out again: 639 MB and 28 181 files here, and a
cold overlay build of 34.4 s against 20.8 s when the same files are hardlinks.
Unix already links them and the overlay only reads them.

A different volume, or a filesystem without hardlinks, still copies.

Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
2026-10-29 08:21:34 +03:00
Ludovic MarieandClaude Opus 5.5 bc33470d5c Merge main into this branch
Main now has the render crate split (#3186); Git followed the moved files,
and nothing in this branch needed changing.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 06:13:15 +01:00
Ludovic MarieandClaude Opus 5.5 c48c2f2167 Merge main into this branch
Main now has the render crate split (#3186); Git followed the moved files,
and nothing in this branch needed changing.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 06:13:14 +01:00
Ludovic MarieandClaude Opus 5.5 fb27a63dce Merge main into this branch
Main now has the render crate split (#3186); Git followed the moved files,
and nothing in this branch needed changing.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 06:13:14 +01:00
Ludovic MarieandClaude Opus 5.5 cef862471f Merge main into this branch
Main now has the render crate split (#3186); Git followed the moved files,
and nothing in this branch needed changing.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 06:13:13 +01:00
Ludovic MarieandClaude Opus 5.5 33f3347d84 Merge main into this branch
Main now has the render crate split (#3186); Git followed the moved files,
and nothing in this branch needed changing.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 06:13:13 +01:00
Ludovic MarieandClaude Opus 5.5 8ce20ba5bd Merge main into this branch
Main now has the render crate split (#3186); Git followed the moved files,
and nothing in this branch needed changing.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 06:13:12 +01:00
Ludovic MarieandClaude Opus 5.5 74d986ca44 Merge main into this branch
game/golems.rs: main's #3123 marks a built iron golem player-created; this
branch's SUMMONED_ENTITY trigger runs after it, as in
CarvedPumpkinBlock.trySpawnGolem (setPlayerCreated, then spawnGolemInWorld).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 06:13:05 +01:00
Alexander KrylovandHermes Agent b27d4b85e1 Launcher: flush the server list before renaming it
`fs::write` hands the bytes to the page cache and returns, so the rename
can be journalled while the data is not: after a power cut `servers.json`
comes back at the right length and full of zeros, which `list()` reads as
no servers at all — the case the rename was meant to prevent.

Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
2026-10-29 08:10:50 +03:00
Ludovic Marie 8f69fb313f Merge pull request #3123 from kacpherek/fix/player-built-golem-forgives
🤖 Agent: proven by contrib-gate-28: main merged (kept #2838's anger fields), vanilla IronGolem.canAttack, a built golem spares the player on the server thread and --in-frame (a summoned one fights back), union parity --changed 81/81 PASS, clippy and tests clean.
2026-10-29 06:00:25 +01:00
asowneryt d5a7e8420b Merge branch 'main' of https://github.com/horizon-rust/horizon into fix/3425-potion-duration-padding 2026-10-29 17:47:19 +13:00
Ludovic MarieandClaude Opus 5.5 bd80ffd540 Merge main into this branch after the render crate split (#3186)
hud.rs, font.rs, gfx.rs and gfx/entity_batch.rs moved to
horizon-client-render and took this branch's changes with them; the render
harness hashes hud.rs and font.rs at their new paths.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 05:44:39 +01:00
hashtheboardandClaude Opus 5.5 cd1d9ba23b Merge origin/main into vr/view-set
gfx moved to horizon-client-render; view.rs follows it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 05:44:23 +01:00
d3grandoandClaude Opus 5.5 1df28d1793 Mod API: a sprite shape draws as vanilla's blitSprite, nine-sliced by its gui.scaling
render's sprite shape stretched every sprite over its rect, so a mod drawing
tooltip/background and tooltip/frame (or widget/button) in a box of another
size got smeared borders. It now goes through ui::blit_sprite, which does what
GuiGraphicsExtractor.blitSprite does with the sprite's GuiSpriteScaling:
nine-sliced when its .png.mcmeta says nine_slice (47 vanilla GUI sprites),
stretched otherwise. Tile, which no vanilla sprite uses and Horizon doesn't
read yet, still stretches.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 11:42:14 +07:00
d3grandoandClaude Opus 5.5 168bc8ffd5 Native mod settings: ask for the labels again when the page opens
A native mod's settings page showed the labels native_settings gave
when Options > Mods was listed, refreshed only after a click on one of
its buttons, so after a language change (or a change in the mod's own
state) the page kept the old labels until clicked. Opening the page now
sends Event::NativeSettings, and the game asks the mod again
(native_settings_refresh, split from native_settings_event).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 11:42:07 +07:00
Ludovic MarieandClaude Opus 5.5 2d966fb3ad Merge main into this branch after the render crate split (#3186)
sprites.rs, assets.rs and atlas_sources.rs moved to horizon-client-render,
so sprite_cache.rs moves there too (mod sprite_cache; in that crate's lib.rs)
and horizon-client's lib.rs loses its line.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 05:41:59 +01:00
d3grandoandClaude Opus 5.5 8bb8239603 Copper chests join into double chests
ChestBlock.combine pairs every ChestBlock, and copper chests in each
weathering state, waxed or not, are ChestBlocks. Horizon placed and drew
them as double chests but only paired chests and trapped chests when
opening a menu, so a double copper chest opened as one 27-slot half. Hoppers,
comparators and the spectator crosshair missed them the same way.

The pairing now lives in horizon_client::containers (is_chest_block,
ChestHalf) and follows DoubleBlockCombiner.combineWithNeigbour: the same
block, the other half's type and the same facing (the facing check was
missing for every chest). The menus, hoppers and the in-frame chest screen
use it.

New harness tools/test_chest_pair_parity.py (VanillaChestPairOracle,
chest_pair_fixture): 3660/3660 cases, 3640/3660 with the old rule.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 11:41:57 +07:00
d3grandoandClaude Opus 5.5 0352ad0e19 Name tags: draw text as Font.prepareText does (bold, tall glyphs)
Name tags had their own glyph loop: every glyph an 8x8 quad at the line
top and no style. accented.png's 9x12 glyphs (й, Й, Ё...) were squeezed
into the line, bold, italic, underline, strikethrough, obfuscated and the
font style were ignored, the line was centred at -trunc(width / 2) and the
background stopped 8 pixels below the line top.

Vanilla (SubmitNodeCollection.submitNameTag -> TextFeatureRenderer ->
Font.prepareText) draws them like any text, centred at -Font.width / 2,
on a background from y - 1 to y + 9 (PreparedTextBuilder.markBackground).
HudBatch::name_tag lays the line out with text_styled, and names.rs maps
its quads into the world. Fixes #3084.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 11:41:56 +07:00
d3grandoandClaude Opus 5.5 702d7cf2cc Show custom names as container titles (#3245)
The server opened every block container with its default title
(container.chest, container.shulkerBox...), so renamed shulker boxes,
copper chests and every other container lost their names on the
server thread, on LAN and on dedicated servers. The menu title now
follows BaseContainerBlockEntity.getDisplayName (the custom name, else
the default name) and, for double chests, ChestBlock's
MENU_PROVIDER_COMBINER (right half, then left, then
container.chestDouble).

The shulker box screen always drew "Shulker Box", and the furnace and
crafter screens ignored the server's title; they now draw it like the
other container screens.

New harness tools/test_container_title_parity.py
(VanillaContainerTitleOracle against container_title_fixture, 148
cases), and a lan_e2e "names" check.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 11:41:56 +07:00
d3grandoandClaude Opus 5.5 526887be89 Tooltips: potion effect and armour trim lines in the selected language
stack_tooltip_lines read the potion effect names and the armour trim
lines through Enchantments::translate, which holds en_us only, so a
Russian game showed "Slowness IV" and "Upgrade: Coast Armor Trim" among
Russian lines. They now go through the same translator as the other
tooltip lines: the selected language, then the pinned English names.

Vanilla builds these lines from translatable components
(PotionContents.addPotionTooltip and getPotionDescription,
ArmorTrim.addToTooltip).

Part of #3312 (the raw %2$s: %1$s attribute lines are #3241).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 11:41:55 +07:00
d3grandoandClaude Opus 5.5 a5ae075a5d Fog option off clears the Nether's fog too
The Horizon Fog option only cleared the distance fog when that fog ended
at or beyond the render distance. The Nether's visual/fog_end_distance is
96 blocks, so its fog stayed at any render distance over 6 chunks. Clear
it whenever the camera's fog is atmospheric and no blindness, darkness or
boss world fog applies; the fog of lava, powder snow and water keeps its
limits, as before.

Fixes #3408

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 11:41:54 +07:00
d3grandoandClaude Opus 5.5 14a4d2726d Window: a size above the GPU's largest texture no longer crashes
Windows lets a program size a window up to 65535 pixels tall with
SetWindowPos; its client area (984x65496 here) is past the 32768 an RTX
5080 takes, and Renderer::resize handed it to Surface::configure, which
panics ("width and height must be within the maximum supported texture
size"). Renderer::resize now treats a size the swapchain can't take like
a minimized window: it keeps the last size and draws nothing until the
next resize. Renderer::new clamps its first size to the same limit.

Vanilla: Window.onFramebufferResize ignores a size that is not positive
and keeps the last one.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 11:41:48 +07:00
d3grandoandClaude Opus 5.5 44d9bddebd Open shulker boxes with the shulker box menu on servers
ShulkerBoxBlockEntity.createMenu opens a ShulkerBoxMenu
(minecraft:shulker_box), whose ShulkerBoxSlots refuse shulker boxes. The
server (single player on its server thread, LAN, dedicated) sent a
generic_9x3 chest menu instead, so it took a shulker box put into a
shulker box, and a vanilla client drew a chest screen.

lan_open_container now sends the block entity's menu type from
horizon_client::containers::block_menu_type; the server already refuses
shulker boxes in that menu, and the client already draws it.

New harness tools/test_storage_menu_parity.py (VanillaStorageMenuOracle,
storage_menu_fixture): 31/31 blocks, 14/31 with the old menu type. New
lan_e2e check shulker_menu.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 11:41:34 +07:00
Ludovic MarieandClaude Opus 5.5 984273f5c8 Merge main into this branch after the render crate split (#3186)
containers.rs moved to horizon-client-render (#3186): this branch's SkullSkin fallback (the "Name" key and a plain string profile) is carried over to the moved file.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 05:41:28 +01:00
Ludovic MarieandClaude Opus 5.5 38712c49ec Merge main into this branch after the render crate split (#3186)
gfx/sunrise_fan.rs follows gfx/ to crates/horizon-client-render/src/gfx/.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 05:40:48 +01:00
Ludovic MarieandClaude Opus 5.5 9f666edc44 Merge main into this branch after the render crate split (#3186)
lib.rs: hud, hud_layout, hud_overlays and inventory_effects moved to horizon-client-render; only this branch's pub mod item_activation stays.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 05:40:18 +01:00
Ludovic Marie 62a01db8f0 Merge pull request #3186 from horizon-rust/refactor/horizon-client-render
🤖 Agent: crate split B9: 83 modules move to crates/horizon-client-render (pure move, re-exported under the old paths), announced in #horizon-team at 00:5xZ. Full pr_ready green (2377 workspace tests, parity 356 affected / 0 failed; rustfmt fixed in this head). The PR body lists the open PRs that need a fix and the exact change for each.
2026-10-29 05:37:12 +01:00
Ludovic MarieandClaude Opus 5.5 a287f701e6 Text displays: a § ending a part breaks only that part; doc back on format_translation
StringDecomposer.iterateFormatted stops at a § with no code after it and the
line-break finder goes on with the next part; split_formatted gave up on the
whole text instead. legacy_format had been inserted under
format_translation's doc comment.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 05:33:53 +01:00
Ludovic MarieandClaude Opus 5.5 8871ba0b28 Launcher: German strings for old-version pruning
main added de.json after this branch; its i18n test asks every language
for every string.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 05:31:29 +01:00
Ludovic Marie 299c0b495d Merge pull request #3419 from horizon-rust/feat/launcher-java-command
🤖 Agent: launcher-java-1 verified this head: PR checks green (verify, launcher), launcher build matrix green on Windows, macOS and Linux (dry run https://github.com/horizon-rust/horizon/actions/runs/40793866687, after the Windows path fix), pr_ready --quick green.
2026-10-29 05:24:17 +01:00
Ludovic Marie c29d37de1d Merge pull request #3451 from horizon-rust/fix/launcher-join-while-preparing
🤖 Agent: launcher-java-1 verified this head: PR checks green (verify, launcher), launcher build matrix green on Windows, macOS and Linux (dry run https://github.com/horizon-rust/horizon/actions/runs/40793347222), pr_ready --quick green with the new test shortcuts_start_only_when_play_would.
2026-10-29 05:24:13 +01:00
Ludovic MarieandClaude Opus 5.5 cbacadae22 Launcher: delete an instance, and forget ones whose folder is gone
Instances could be made but never deleted (#horizon-bugs: one stayed
in the list). The instance row gets Delete…: after a confirmation, the
instance's folder (an own instance's, or an imported or registry pack's
work folder) goes to the Trash, where the player can get it back, and
its choices and the selection go with it. Horizon's own instance, the
game's home, stays. Not while a game runs. `instance delete` does the
same from the command line.

An own instance whose folder was removed outside the launcher is
dropped from instances.json when the list is next read, so a new one
of that name starts clean (only when instances/ itself can be read).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 05:20:50 +01:00
Ludovic MarieandClaude Opus 5.5 caa1be7abe Merge main into feat/launcher-prune-old-versions
main went Nightly-only (#3342): install_tag runs nightly::after_update (it
removes the older Nightlies) and then prunes the old release versions; the
Settings page drops the Nightly row main removed and keeps the pruning switch.
Both sets of new strings are kept in fr.json and ru.json.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 05:17:21 +01:00
Ludovic MarieandClaude Opus 5.5 1970ab1a7b Merge main into fix/player-built-golem-forgives
Keeps both new Extra fields: main's anger_end/angry_at (#2838) and this
branch's player_created.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 05:15:37 +01:00
Claude 0e4c964d0a Keep Player.attack's no-damage guard before the attacker's sprint-hit cut
Player.attack ends before hurtOrSimulate and causeExtraKnockback when
neither the scaled ATTACK_DAMAGE (clamped to [0, 2048], so Weakness can't
push it below 0) nor the scaled enchantment damage is left. The remote
path cut the attacker's speed on knockback alone, so an empty-hand sprint
hit under Weakness I still slowed the client.

The decision moves to attack_strength::attacker_slows_after_hit, with the
knockback formula shared with the local path as
attack_strength::extra_knockback, and unit tests for a charged and an
uncharged sprint hit, a knockback weapon, no sprint, a no-damage hit
under Weakness, enchantment damage alone, and a server-raised attack
speed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_018ksHJwFp11RfYk6S9xnyCf
2026-10-29 14:06:14 +10:00
Claude 2228a7456e Apply the attacker's 0.6 speed cut after a knockback hit on another player
MultiPlayerGameMode.attack runs Player.attack on the client too. For a
mob the client's hurtOrSimulate fails, so the hit has no attacker side,
but RemotePlayer.hurtClient is true: a sprinting (or knockback-enchanted)
hit on another player runs causeExtraKnockback's attacker side on the
client, cutting horizontal speed to 0.6 and stopping the sprint. The
remote attack path only sent the packet, so after such a hit the client
kept its speed while the server's player slowed, and the server pulled
the player back (seen on Hypixel when sprint-hitting a nearby player).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_018ksHJwFp11RfYk6S9xnyCf
2026-10-29 13:57:52 +10:00
asownerytandClaude Opus 5.5 85032102a8 Potion tooltips zero-pad effect durations as vanilla does
Potion, splash and lingering potion and tipped arrow tooltips printed
durations as m:ss ("Speed (3:00)"); vanilla's StringUtil.formatTickDuration
pads every field to two digits ("Speed (03:00)", "01:00:00" from an hour up).

brewing.rs now has one port of formatTickDuration, with vanilla's floor of
ticks / tick rate, and the inventory effect panel uses it too instead of its
own copy, so the two agree on the same screen. Tooltips still format at
20 TPS until their callers pass the level's rate.

Fixes #3425.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 16:53:25 +13:00
Gitlab Main 48e731483a options: choose the audio output device in Music & Sounds (#1035)
Adds audio output device selection matching vanilla options.txt format:
- Bind soundDevice option in Settings with serialization roundtrip
- Enumerate output devices and support live stream switching in Audio
- Add SoundDevice option button to Music & Sounds page cycling devices
- Apply audio device changes dynamically on options save and startup
- Unit tests for settings parsing and options UI button cycling
2026-10-29 09:13:13 +05:30
Ludovic Marie 0e57b1f6cf Merge pull request #2838 from levzzz5154/fix/neutral-mob-anger
🤖 Agent: needs-proof check by Ludovic's AI agent (Claude), not by Ludovic himself. Passed on a union of 11 contributor PRs on main 430e5e4c9: fmt, clippy -D warnings, the horizon-client and horizon-items tests (all pass), and lan_e2e tab chest drops signs frames containers bubble-column with the host on its server thread and in-frame (digging and frames passed on their rerun, after #2745's checks had left water behind). Save round trip main -> PR -> main -> PR keeps the mobs. Follow-up, not a blocker: vanilla NeutralMob.readPersistentAngerSaveData falls back to the old AngerTime key when anger_end_time is missing, and this reads only anger_end_time, so an angry wolf from an older world calms down. angry_at is kept only for the offline single player. Parity: 46 of 268 harnesses passed locally before the Mac mini got too busy, so CI's run-parity on this head decides.
2026-10-29 04:41:16 +01:00
SenjinandClaude Opus 5.5 cd70f518e9 HDR Auto: what Hyprland 0.57 reports, in the docs and the code comment
Tested on Hyprland 0.57 (always-on HDR, NVIDIA): it reports reference white
203 and 0..1060 nits, so Auto gives Paper White 203 / Peak 1060, and it shows
HDR windows below its desktop's SDR white (sdr_max_luminance 430, which the
protocol doesn't carry): a white browser page is brighter than the game's
white and than a 1000-nit patch in mpv.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 04:35:38 +01:00
socksproxandClaude Opus 5.5 c3987df0b2 Text displays: vanilla text depths and polygon offset, with a render harness
A text display's background, shadow and glyphs were coplanar, so they
z-fought (server holograms looked speckled). World text now gets vanilla's
depths (background -0.01, shadow 0, glyph 0.03, bold copy +0.001, lines
+0.01) and text displays draw with TEXT_POLYGON_OFFSET's depth bias.

The new text_display_render harness runs vanilla's TextDisplayRenderer and
Font on baked glyphs and compares every quad; it found and this fixes: § codes
kept across wrapped lines (StringSplitter.splitLines on the raw parts), line
effects starting 1 left at each style run, and no shadow when its colour is 0.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 04:04:44 +01:00
Ludovic Marie c9d61dcab7 Merge pull request #2976 from kacpherek/fix/2756-thrown-trident-frozen
🤖 Agent: needs-proof check by Ludovic's AI agent (Claude), not by Ludovic himself. Passed on a union of 12 contributor PRs on main 77fa9fb89: fmt, clippy -D warnings, the tests of horizon-client/-items/-protocol/-world (all pass) and the Python tests, and 19 lan_e2e checks with the host on its server thread (all pass) and in-frame (pass; frames passed on its rerun). lan_e2e thrown and projectiles PASS on both paths. Parity: CI's run-parity on this head.
2026-10-29 03:58:45 +01:00
spiderandClaude Opus 5.5 6c50b7d980 Firework rockets: explode on the entities they hit, credit their owner, and load from NBT
FireworkRocketEntity.tick checks its move for entities (getHitResultOnMoveVector)
and goes off on the first it meets; a breeze turns it back. Its owner (the player
who launched it from a block, a glide or a crossbow) is credited with what the
blast hurts, as the guest on a server. /summon and saved chunks now load a real
rocket (Life, LifeTime, FireworksItem, ShotAtAngle, Owner, LeftOwner).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 03:56:00 +01:00
asowneryt 1fa4a780f0 Merge branch 'main' of https://github.com/ludovic111/horizon into claude/2697-horse-make-mad-4431aa
# Conflicts:
#	crates/horizon-client/src/mobs/ai.rs
#	crates/horizon-client/src/mobs/equine.rs
2026-10-29 15:55:44 +13:00
Ludovic Marie 8dfce86201 Merge pull request #3105 from malordin/fix/language-list-check
🤖 Agent: verified by contrib-gate-27. Read in full: the language list is missing when assets/pack.mcmeta is (lang.rs reads languages from it, and horizon-prepare takes it from the asset index); the jar's lang folder always holds en_us.json and deprecated.json, so the old file count never told. Launcher clippy -D warnings is clean, and prepared_versions_get_what_the_asset_store_adds_without_preparing_again passes. CI parity runs on this head.
2026-10-29 03:44:52 +01:00
Ludovic Marie 7a8c5a2642 Merge pull request #2974 from kacpherek/fix/advancement-triggers-distance-effects
🤖 Agent: verified by contrib-gate-27. Read in full; checked against ServerPlayer's fall, lava, Nether and explosion tracking; fix pushed: the durability trigger fires only when wear is left (ItemStack.hurtAndBreak). union/cg27-1 (main + this wave): fmt, clippy -D warnings (horizon-client, horizon-world, horizon-client-core), their tests and the fast build pass; lan_e2e damage, minecarts, projectiles, sculk_container, shield, push and hazards pass on the server thread and in-frame. Harness parity runs in CI on this exact head (the Mac mini's load is too high for local parity).
2026-10-29 03:44:32 +01:00
Ludovic Marie dd9c9433b6 Merge pull request #3085 from kacpherek/fix/throws-use-known-movement
🤖 Agent: verified by contrib-gate-27. Read in full; throws take getKnownMovement (main's rider movement kept). Main merged. union/cg27-2 (main c6af85743 + this wave): fmt, clippy -D warnings, horizon-client tests (1293/0) and the fast build pass; lan_e2e team-push, pvp, thrown, projectiles, mounts and riding pass on the server thread. Harness parity runs in CI on this exact head (the Mac mini's load is too high for local parity).
2026-10-29 03:40:22 +01:00
JustKarkatandJustKarkat's AI agent (ZCode) d372e9ece9 Mob-origin item drops get the ItemEntity constructor's random motion
A mob's death loot, its gift table and spawnAtLocation drops (a fox's
berries, a panda's held food, a goat's horn) spawned with a fixed
[0, 0.2, 0] motion, so every item landed stacked at one exact point; the
5-arg ItemEntity(level, x, y, z, stack) constructor throws each one with
a random horizontal push of ±0.1 and 0.2 up, like vanilla. Block drops
(already going popResource's path) unaffected.

Fixes #3144.

Co-Authored-By: JustKarkat's AI agent (ZCode)
2026-10-28 21:37:01 -05:00
Ludovic MarieandClaude Opus 5.5 e037d39314 rustfmt: rewrap horizon-client's re-export of the render modules
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 03:31:26 +01:00
spiderandClaude Opus 5.5 3c514d5c17 Merge main into cape-rendering
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 03:31:04 +01:00
Ludovic Marie 9b12c89191 Merge pull request #3037 from ASOwnerYT/claude/dreamy-khayyam-1553ca
🤖 Agent: verified by contrib-gate-27. Read in full; matches AbstractBedBlock.useWithoutItem (OCCUPIED), kickVillagerOutOfBed, checkBedExists and the saved sleeping_pos; its own lan_e2e checks pass. union/cg27-3 (main + this wave): fmt, clippy -D warnings, horizon-client/horizon-protocol tests (1395/0) and the fast build pass; lan_e2e sleep, bed-occupied, sleep-villager, sleep-reload, arrows, objects, commands and tab pass on the server thread and in-frame. Harness parity runs in CI on this exact head (the Mac mini's load is too high for local parity).
2026-10-29 03:22:46 +01:00
Ludovic Marie ac17fd2b0f Merge pull request #2949 from kacpherek/fix/saved-world-preset
🤖 Agent: needs-proof check by Ludovic's AI agent (Claude), not by Ludovic himself. Passed on a union of 11 contributor PRs on main 430e5e4c9: fmt, clippy -D warnings, the horizon-client and horizon-items tests (all pass), and lan_e2e tab chest drops signs frames containers bubble-column with the host on its server thread and in-frame (digging and frames passed on their rerun, after #2745's checks had left water behind). A saved amplified world generates amplified terrain in new chunks at 3000/3000 (main: normal), screenshots taken. Parity: 46 of 268 harnesses passed locally before the Mac mini got too busy, so CI's run-parity on this head decides.
2026-10-29 03:03:35 +01:00
rdbt e9027c77c2 Merge pull request #3440 from kerimgrande/main
adding german translation to the launcher (+ tidied up my formatting :P)
2026-10-29 01:46:10 +00:00
rdbt 95bd72abaf fix translation 2026-10-29 01:37:26 +00:00
Ludovic Marie 0fc7603da8 Merge pull request #2963 from kacpherek/fix/end-portal-frame-effects
🤖 Agent: needs-proof check by Ludovic's AI agent (Claude), not by Ludovic himself. Passed on a union of 12 contributor PRs on main 77fa9fb89: fmt, clippy -D warnings, the tests of horizon-client/-items/-protocol/-world (all pass) and the Python tests, and 19 lan_e2e checks with the host on its server thread (all pass) and in-frame (pass; frames passed on its rerun). Parity: CI's run-parity on this head.
2026-10-29 02:34:14 +01:00
Ludovic MarieandClaude Opus 5.5 833b9385ee Merge main into world-access-failure-toast
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 02:33:08 +01:00
d3grandoandClaude Opus 5.5 aded903dd2 benchmarks: Horizon v0.2.97 vs vanilla 26.3 on Windows 11 (9800X3D, RTX 5080)
The first perf_bench run on Windows: Horizon v0.2.97, vanilla 26.3 and vanilla with Sodium,
Lithium, FerriteCore and ImmediatelyFast, render distances 8, 16 and 32 at 1280x720, three
games each. The reports are in benchmarks/perf. The page next to them gives the setup, the CPU
load and compiler activity during each run (Windows has no load average), and the tables.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 08:29:52 +07:00
d3grandoandClaude Opus 5.5 cc243fabd8 perf_bench: pass vanilla the manifest's -XX flags
The 26.3 manifest gives every OS -XX:StackShadowPages=32, and the official launcher passes it.
perf_bench only took the manifest's OS-conditional arguments, so vanilla started without it. On
Windows about half of those starts crash before the title screen: an access violation under
shaderc.dll while the client compiles its shaders, with no hs_err file. Measured on Windows 11
(Ryzen 7 9800X3D, RTX 5080, Java 25.0.5), 8 starts each through run_vanilla: 4 crashed without the
flag, none with it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 08:29:52 +07:00
d3grandoandClaude Opus 5.5 f44af70080 perf_bench: run on Windows
tools/perf_bench.py stopped on Windows before or right after the first game:

- the control socket is AF_UNIX at /tmp, which Python on Windows doesn't have; the game
  there listens on a localhost TCP port named in a .json endpoint file (control.rs), so
  the driver now uses that transport when there are no Unix sockets;
- memory was read with `ps`; Windows uses the process's working set (GetProcessMemoryInfo);
- os.getloadavg doesn't exist; the report leaves load_avg_1m out there, as horizon-bench does;
- hardware() read /etc/os-release and /proc; Windows gets `ver`, the CPU brand string
  and the total RAM;
- nvidia-smi reports per-process memory as [N/A] under WDDM, which raised ValueError;
- the binary is horizon-client.exe.

A release binary passed with --build reports its own version (from its first log line)
instead of this checkout's version and commit.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 08:29:52 +07:00
Ludovic MarieandClaude Opus 5.5 f7300bd96b Merge main into fix/multiplayer-safety-notice
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 02:12:57 +01:00
Ludovic Marie 68b626fc9d Merge pull request #2950 from kacpherek/fix/loot-set-components-trim
🤖 Agent: needs-proof check by Ludovic's AI agent (Claude), not by Ludovic himself. Passed on a union of 11 contributor PRs on main 430e5e4c9: fmt, clippy -D warnings, the horizon-client and horizon-items tests (all pass), and lan_e2e tab chest drops signs frames containers bubble-column with the host on its server thread and in-frame (digging and frames passed on their rerun, after #2745's checks had left water behind). Parity: 46 of 268 harnesses passed locally before the Mac mini got too busy, so CI's run-parity on this head decides.
2026-10-29 02:08:18 +01:00
Ludovic MarieandClaude Opus 5.5 5edfd95da6 Sprite cache test: escape the corrupt header's bytes
The raw 0x01 and NUL bytes in the byte string made git, grep and GitHub treat sprite_cache.rs as a binary file.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 02:05:22 +01:00
Ludovic MarieandClaude Opus 5.5 21ba38e633 Merge main into fix/list-damage-datapack-commands
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 02:00:37 +01:00
Ludovic MarieandClaude Opus 5.5 ae4ffbea8f Merge main into fix/server-code-of-conduct-screen
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 02:00:37 +01:00
SenjinandClaude Opus 5.5 8202f02f4e HDR: log what the compositor reports once per change
The renderer is made anew a few times while the game starts, and each asked
the compositor again and logged the same line; a slider dragged in the
desktop's HDR settings logged a line per step. Now a line comes only when the
values changed, and a change within a second of the last line waits until the
values have held for a second, so only the value they settled on is logged.
The values themselves still apply at once.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 01:50:57 +01:00
AHHhHHHHH-HandClaude Sonnet 5.5 940b642233 Far terrain: sample on up to 6 threads when the machine has 12 or more, behind the game
A cold 1024-chunk LOD loaded about a quarter sooner with 6 sampling threads than with 3. The extra slots only open once the nearby chunks are in, and the far threads now run behind the server like the chunk workers, so frames and ticks keep their cores.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-28 21:50:15 -03:00
d3grandoandClaude Opus 5.5 d192946a6f native_corner_shown: F3 takes the top right and its charts' corners
Vanilla's debug screen draws a column in each top corner
(DebugScreenOverlay.extractLines, left- and right-aligned), but the
helper only reported the top left as taken, so a native mod's box in the
top right was drawn under F3's right column. It now reports both top
corners while F3 is open, and a bottom corner while a debug chart is
drawn there (frame and tick times, bandwidth and ping, the lightmap
view), from the new DebugCharts::corners, which follows draw().

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 07:50:00 +07:00
Ludovic MarieandClaude Opus 5.5 b6cd09442e World icon: wait for more than 10 rendered sections
GameRenderer.takeAutoScreenshot saves only when countRenderedSections() > 10
and every section is rendered. A window that joined its server thread
had nothing pending before its first chunks, so the icon was only sky.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 01:48:51 +01:00
Ludovic MarieandClaude Opus 5.5 af3aa00377 Merge main into fix/2869-horse-max-health
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 01:43:53 +01:00
Ludovic MarieandClaude Opus 5.5 5f80ddffce Merge main into fix/remote-player-teams
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 01:43:53 +01:00
Ludovic MarieandClaude Opus 5.5 b9449b2348 Merge main into fix/skin-online-sound-options
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 01:43:53 +01:00
Ludovic MarieandClaude Opus 5.5 8428ff0f59 Merge main into fix/advancement-triggers-summon-loot-craft
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 01:43:53 +01:00
Ludovic MarieandClaude Opus 5.5 c2c364f195 Merge main into fix/throws-use-known-movement
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 01:43:52 +01:00
Ludovic MarieandClaude Opus 5.5 b1532a179a Merge origin/main into refactor/horizon-client-render
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 01:38:42 +01:00
Ludovic MarieandClaude Opus 5.5 ff85f100ec refactor: datapack, audio and sound_engine join horizon-client-render
They use only modules already in the render crate (zip, snbt, settings,
sound_events), and open PRs call them from options_ui (#3325: datapack,
#3355: audio). Pure move: cpal and symphonia and the `audio` feature
move with them (horizon-client's `audio` forwards to it), and
sound_engine's test backend is also built for the `test-support` feature
that horizon-client's tests turn on. audio_harness and
docs/architecture.md point at the new paths.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 01:38:29 +01:00
Ludovic MarieandClaude Opus 5.5 e3b1ae512f Single player: the world icon keeps its save folder through the async start
Since #3011 the window joins the server thread in poll_integrated_start,
where open_integrated's folder is no longer in scope: Starting carries it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 01:37:33 +01:00
Ludovic Marie 5b4dd8b72e Merge main into fix/world-select-icons 2026-10-29 01:37:27 +01:00
Ludovic Marie c6af857433 Merge pull request #2969 from kacpherek/fix/backup-before-world-upgrade
🤖 Agent: needs-proof check by Ludovic's AI agent (Claude), not by Ludovic himself. Passed on a union of 11 contributor PRs on main 430e5e4c9: fmt, clippy -D warnings, the horizon-client and horizon-items tests (all pass), and lan_e2e tab chest drops signs frames containers bubble-column with the host on its server thread and in-frame (digging and frames passed on their rerun, after #2745's checks had left water behind). A world with DataVersion 3955 shows the backup prompt (vanilla text and buttons). Create Backup and Load writes backups/<date>_W.zip and opens the world, now at 5023. Parity: 46 of 268 harnesses passed locally before the Mac mini got too busy, so CI's run-parity on this head decides.
2026-10-29 01:36:49 +01:00
Ludovic MarieandClaude Opus 5.5 dc3fc6961e Decoration: World keeps its own doc comment
OriginWorld was inserted between World's doc comment and World, so the
comment documented both structs as one.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 01:35:11 +01:00
Ludovic MarieandClaude Opus 5.5 1a71386829 Sculk: take each mob's vehicle from the tick's snapshot
The mount events looked every mob up again in the mob list, once per mob
and tick; the snapshot the loop walks already reads the mob.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 01:34:57 +01:00
Ludovic MarieandClaude Opus 5.5 e12c5d4f23 Advancements: the durability trigger fires only when wear is left
ItemStack.hurtAndBreak calls applyDamage, and so ItemDurabilityTrigger,
only when processDurabilityChange leaves damage: a hit Unbreaking saves
fires nothing. One helper, Game::wear, takes the loss and fires the
trigger for every call site.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 01:34:42 +01:00
Ludovic Marie 8f91683323 Merge pull request #3435 from horizon-rust/feat/native-hooks-v2-recipes
🤖 Agent: hook points v2, part 2/2 (the item panel and recipe pages for native mods, NATIVE_HOOKS_API 2), rebased onto main after #3434. pr_ready --quick green on 11f22e2 (clippy, 1289 client tests); full pr_ready with parity green on the pre-rebase head (33/33 selected). The native Recipes mod's proof passes on it (horizon-mods#24).
2026-10-29 01:24:38 +01:00
degepieandClaude Opus 5.5 5f269772d9 Frogs long jump: FrogAi's long jump activity (fixes #2841)
Frogs had no long jump on the server, so they only ever walked. Port the
LONG_JUMP activity as one goal: LongJumpToPreferredBlock searches the
9x5x9 box (weighted by squared distance, lily pads and big dripleaf first
half the time) for a spot FrogAi.isAcceptableLandingSpot accepts and an
8-block path cannot reach, faces it for 40 ticks and leaps along the
LongJumpUtil arc (now with its collision check); LongJumpMidJump holds the
LONG_JUMPING pose without friction until the frog lands. The
LONG_JUMP_COOLDOWN_TICKS memory counts down and is reset as vanilla does.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
2026-10-29 05:23:58 +05:00
Ludovic MarieandClaude Opus 5.5 c5221cc92d Launcher: java_launch's Windows paths, one part at a time
The log config and old versions' asset folder were joined from
"a/b" strings; on Windows their tests compared those mixed
separators with paths joined part by part. Join them part by part.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 01:23:12 +01:00
Ludovic Marie 471d815aeb Merge pull request #3187 from horizon-rust/refactor/spawner-hive-traits
🤖 Agent: crate split B8: spawner, trial spawner and beehive behaviour become extension traits (pure move). Main merged, including #3064 and #2952 (their new methods now live on the traits). Full pr_ready green: 1286 tests, parity 82 affected / 0 failed.
2026-10-29 01:20:26 +01:00
degepieandClaude Opus 5.5 955e7fe48d Hurt players flash red for the other players too
The server broadcast LivingEntity.hurtServer's damage event for mobs only,
so in multiplayer and on LAN (and single player's server thread) nobody saw
another player turn red when hit. The LAN flush now sends it for the host
and each guest whose hurt time restarted (a hit that was not blocked by a
shield), to the players around and the hurt player itself. An in-frame
host's models of its guests also carry their hurt time.

Fixes #2827.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
2026-10-29 05:19:11 +05:00
Ludovic MarieandClaude Opus 5.5 40073ab9c0 Launcher: Join and other quick starts wait for the version's setup
A server's Join started the game while its version was still being
downloaded and prepared for the first time: a white window opened and
the download went on. The button only looked disabled while a game
ran, and its click (like a world's Play, a friend's code and a pack's
Play) reached `play()`, which never asked whether a preparation was
under way or the version was prepared.

Every quick start now asks what Play asks (`starts_now`, from
`play_press`): prepared, no preparation running, no game running or
starting. `play()` and `play_pack` refuse otherwise, and the buttons
are drawn from the same rule.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 01:18:24 +01:00
d3grandoandClaude Opus 5.5 fd0aeb5331 Stop swimming and sit up when boarding a boat while swimming
A riding player skips the movement tick, which held the swimming
update, so a player who boarded a boat while swimming kept the swimming
pose: eye 0.4 above the feet, the camera at the waterline and the model
lying under the boat.

Vanilla's Entity.rideTick runs Player.tick for a passenger:
Entity.updateSwimming clears the swim (it needs !isPassenger()) and
Player.tick ends with updatePlayerPose. The riding branch of the remote
tick now runs the same two steps (MotionWorld::passenger_pose_tick).

Fixes #3308

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 07:18:00 +07:00
d3grandoandClaude Opus 5.5 7211d029ab Swing the hand when Q drops a stack from an inventory slot on a server
On a server (single player included) a menu click is predicted by the
menu engine and sent, and what the prediction dropped was ignored, so
Q over a slot threw the item without the arm swing. Vanilla's
AbstractContainerMenu.doClick drops with Prediction.PREDICTED, and
LivingEntity.drop then swings the main hand on the client itself; the
server shows that swing only to the other players.

The prediction loop moves to predict_clicks, which also says whether a
click dropped something; remote_menu_ops swings the main hand then.

Fixes #3357

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 07:17:59 +07:00
Ludovic Marie e9c6751a1b Merge main into fix/shield-blocking-pose 2026-10-29 01:14:31 +01:00
Ludovic Marie cf0b111bdd Merge main into join-without-rescanning-registries 2026-10-29 01:14:31 +01:00
Ludovic Marie 5c2bc0af4a Merge pull request #2966 from kacpherek/fix/arrow-weapon-knockback
🤖 Agent: needs-proof check by Ludovic's AI agent (Claude), not by Ludovic himself. Passed on a union of 11 contributor PRs on main 430e5e4c9: fmt, clippy -D warnings, the horizon-client and horizon-items tests (all pass), and lan_e2e tab chest drops signs frames containers bubble-column with the host on its server thread and in-frame (digging and frames passed on their rerun, after #2745's checks had left water behind). Parity: 46 of 268 harnesses passed locally before the Mac mini got too busy, so CI's run-parity on this head decides.
2026-10-29 01:12:34 +01:00
Ludovic MarieandClaude Opus 5.5 11f22e26fa Native mods: the item panel and recipe pages for native mods
The item panel beside container screens and its recipe pages move to
game/native_panel.rs and game/native_panel_recipes.rs, built in every
build, with their state in Game::panel and native types (NativeStack,
NativeRecipeView, NativeRecipeLayout, NativePanelEvent). A native mod
shows them with native_panel_items and native_panel_recipes and hears
the player's clicks, searches, page turns and keys over items through
the new hook point native_panel_event: NATIVE_HOOKS_API is 2.

A WebAssembly mod's panel feeds the same state through game/mod_panel.rs
(feature `mods`) and hears its events as before; a native mod's panel
wins when both show one. Players see no change.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 01:12:08 +01:00
d3grandoandClaude Opus 5.5 48342a7ef3 End the dragon fight when /kill removes the dragon
kill_dragon only dropped the entity, so the fight, still not killed,
spawned a fresh dragon on its next tick and never opened the exit
portal. EnderDragon.kill calls dragonFight.setDragonKilled after
removing the dragon: call dragon_killed there too, in the End.

The command execution corpus gets an End group that kills the fight's
dragon and probes the dragon, the exit portal and the egg.

Fixes #3319

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 07:08:10 +07:00
d3grandoandClaude Opus 5.5 bf7179c48b Keep the player model's legs still while flying straight up
The walk animation counted vertical movement while flying or gliding.
Vanilla's LivingEntity.calculateEntityAnimation only counts it for
omnidirectional air movers, and Entity.omnidirectionalAirMover is false
for every player, so only the horizontal distance walks the model.

Fixes #3302

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 07:08:09 +07:00
Senjin 25e00a0ecf Merge remote-tracking branch 'origin/main' into hdr-wayland
# Conflicts:
#	crates/horizon-client/src/game.rs
#	crates/horizon-client/src/game/options.rs
2026-10-29 01:05:23 +01:00
d3grandoandClaude Opus 5.5 dbb03a42d3 Creative inventory tab: click locally on a server, as vanilla does (#3307)
On a server (single player's server thread included) the creative screen's
Survival Inventory tab sent its clicks as ContainerClick, so the server held
the picked-up stack while the Destroy Item slot only cleared the client's:
the stack came back on the next click or on close, and moving a stack
doubled it. CreativeModeInventoryScreen.slotClicked clicks the player's own
InventoryMenu and sends each changed slot (SetCreativeModeSlot); Horizon now
does the same.

- remote_click: the creative screen clicks its own menu, never a ContainerClick;
  a Shift double click there quick-moves locally too.
- Q / Ctrl+Q over a slot of the tab sends the thrown stack
  (handleCreativeModeItemDrop) instead of losing it.
- The creative slot sync covers the armour slots (InventoryMenu 5..=8).
- The server marks a creative-set slot as sent (InventoryMenu.setRemoteSlot),
  so it no longer echoes it back over a newer click, and the client marks only
  the slots a server packet changed as synced.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 07:04:12 +07:00
d3grandoandClaude Opus 5.5 01f3d91d04 Item names: translate items outside the minecraft namespace
`item_tooltip::name_line` stripped only a `minecraft:` prefix and looked
up `item.minecraft.<path>` and `block.minecraft.<path>`. A content mod's
item (`ruby:ruby`) never found its name: the name above the hotbar
showed the raw id ("ruby:ruby"), and the recipe book and creative
searches matched that instead of the item's name.

The keys are now `Util.makeDescriptionId`'s, in the item's own
namespace with `/` as `.`: `item.<namespace>.<path>`, then the block's
for a block item. Without a translation the name falls back to the path
with spaces, as `creative::Names::item` does.

Vanilla: Item.getDescriptionId, Item.Properties (ITEM_DESCRIPTION_ID,
BLOCK_DESCRIPTION_ID), Util.makeDescriptionId.

Fixes #3396

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 06:58:55 +07:00
d3grandoandClaude Opus 5.5 1d2d495686 Shields: an enchanted banner shield keeps its patterns under the glint
An enchanted shield with a banner turned white: after the patterns, the
renderer drew the shield base again with the foil for vanilla's
`patternedShieldGlint` pass. In vanilla that pass draws the glint alone
(`RenderPipelines.GLINT` with only the glint texture), but a foil quad
here is its texture with the glint over it, so the plain base covered the
base colour and every pattern.

Every layer of a patterned shield now carries the foil instead, and the
extra pass is gone. The GUI adds the glint masked by each layer's alpha
and the world shader composites it per layer, so each pixel ends with
the glint once over the layers below it, as vanilla's single glint pass
over the finished plate gives.

Vanilla: ShieldSpecialRenderer.submit, BannerRenderer.submitPatterns,
RenderTypes.patternedShieldGlint.

Fixes #3407

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 06:55:53 +07:00
d3grandoandClaude Opus 5.5 99b6b8f3e8 Spectators open chests and other containers, read-only
A spectator's right click on a chest did nothing: the client predicted
the use like any player's, and the server dropped a spectator's
UseItemOn. Follow vanilla instead:

- MultiPlayerGameMode.performUseItemOn: a spectator's use goes to the
  server, predicts nothing and ends the click (CONSUME, no swing).
- ServerPlayerGameMode.useItemOn: the server opens the block's menu
  (BlockState.getMenuProvider) for a spectator, with no lid, sound,
  game event, stat or piglin anger. Chests, trapped chests, barrels,
  shulker boxes, hoppers, dispensers, droppers, furnaces, the brewing
  stand and the crafter open; a blocked chest still doesn't.
- ServerGamePacketListenerImpl.handleContainerClick: a spectator's
  click changes nothing, and sendAllDataToRemote now takes back what
  the client predicted.

Fixes #3414

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 06:52:13 +07:00
Ludovic Marie e6bb2f74c7 Merge pull request #2941 from ASOwnerYT/tools/bat-oracle-final-fields
🤖 Agent: needs-proof check by Ludovic's AI agent (Claude), not by Ludovic himself. Passed on a union of 12 contributor PRs on main 77fa9fb89: fmt, clippy -D warnings, the tests of horizon-client/-items/-protocol/-world (all pass) and the Python tests, and 19 lan_e2e checks with the host on its server thread (all pass) and in-frame (pass; frames passed on its rerun). Parity: CI's run-parity on this head.
2026-10-29 00:51:04 +01:00
Ludovic Marie 08c67f3d2f Merge pull request #2851 from levzzz5154/fix/dyed-armor-colour
🤖 Agent: needs-proof check by Ludovic's AI agent (Claude), not by Ludovic himself. Passed on a union of 12 contributor PRs on main 77fa9fb89: fmt, clippy -D warnings, the tests of horizon-client/-items/-protocol/-world (all pass) and the Python tests, and 19 lan_e2e checks with the host on its server thread (all pass) and in-frame (pass; frames passed on its rerun). Main merged in (95ecc9d77): armour stands keep main's layer_color. Parity: CI's run-parity on this head.
2026-10-29 00:47:12 +01:00
Senjin 3ea82790ea Merge remote-tracking branch 'origin/main' into hdr-ui
# Conflicts:
#	crates/horizon-client/src/game/options.rs
#	crates/horizon-client/src/gfx.rs
2026-10-29 00:45:46 +01:00
Senjin fb0454d001 Merge remote-tracking branch 'origin/main' into hdr-black
# Conflicts:
#	crates/horizon-client/src/game/options.rs
#	crates/horizon-client/src/gfx.rs
2026-10-29 00:43:18 +01:00
Ludovic Marie db8c6d6331 Merge pull request #3225 from masa-masa-0314/fix/dialog-command-packet
🤖 Agent: needs-proof check by Ludovic's AI agent (Claude), not by Ludovic himself. Passed on a union of 12 contributor PRs on main 77fa9fb89: fmt, clippy -D warnings, the tests of horizon-client/-items/-protocol/-world (all pass) and the Python tests, and 19 lan_e2e checks with the host on its server thread (all pass) and in-frame (pass; frames passed on its rerun). Parity: CI's run-parity on this head.
2026-10-29 00:41:16 +01:00
spiderandClaude Opus 5.5 fc409d28f4 TAA: no jitter shake, and Upscaling shows only with TAA
The resolve looked the history up at this frame's jittered position, so a
still camera wobbled in an 8-frame pattern (twice the frame-to-frame change
of TAA off in a still view), worse when upscaling widens the jitter. The
history is unjittered now: each output pixel looks it up at its own centre,
takes the world pixel whose jittered sample lands nearest that centre, and
scales the frame's 10% share by a Gaussian of the distance, so frames whose
sample lands far away barely move it.

The graphics screen drops the Upscaling row while Anti-aliasing is not TAA.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 00:38:26 +01:00
kerim e665b6bf00 Merge branch 'main' of https://github.com/kerimgrande/horizon 2026-10-29 00:35:16 +01:00
kerim 0048f04f1b edit formatting 2026-10-29 00:35:07 +01:00
spiderandClaude Opus 5.5 fa727a627a TAA upscaling: Native, Quality, Balanced, Performance and Ultra Performance
An Upscaling button under Anti-aliasing (`upscaling` in horizon-graphics.txt,
Native by default). The world renders 1.5x, 1.7x, 2x or 3x smaller along each
axis than the resolution scale's size (FSR's and DLSS's ratios) and TAA resolves
into a history at full size, each output pixel taking the world pixel it falls
in; the jitter runs over 8 phases per output pixel. Picking a ratio turns TAA
on, leaving TAA goes back to Native, and nothing is upscaled on the title
screen or under a shader pack.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 00:31:15 +01:00
Joansitoh dc44f98818 Packs overlay: quote mklink's paths, compile the junction only on Windows
cmd reads its own command line and Rust quotes only paths with spaces, so a
path with `&` or `^` split or changed the mklink call and that player fell back
to the slow mirror. The overlay test now runs under a path with `&` and checks
the untouched directory is a link, not a copy.
2026-10-29 00:30:13 +01:00
spiderandClaude Opus 5.5 511eb49629 TAA: a sub-pixel jitter, camera motion vectors from depth, and a history clamped to the frame
Anti-aliasing now cycles Off, FXAA and TAA (`taa` in horizon-graphics.txt, off by
default). The world's projection is jittered over a Halton (2, 3) sequence of 8
frames; the resolve reprojects each pixel through the depth buffer and the
camera's motion since the last frame, samples the Rgba16Float history with a
5-tap Catmull-Rom, clamps it to the frame's 3x3 neighbourhood and blends 10% of
the new frame in. It runs at the resolution scale, before the hand and HUD, and
stays off under a shader pack.

Motion vectors come from the camera alone: mobs and other moving things lean on
the clamp. Per-object motion is the follow-up.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 00:23:04 +01:00
xKumorioandClaude Opus 5.5 f9494ef93e Arrows shot mid-jump carry the jump: projectiles add the known movement
Projectile.shootFromRotation adds source.getKnownMovement() to the
projectile, the vertical part only when the shooter is off the ground. For
a ServerPlayer that is lastKnownClientMovement: the move the client last
sent (handlePlayerKnownMovement), reset to zero by handleClientTickEnd when
no move came that tick.

Horizon added the player's PlayerMotion velocity instead. On the server
(single player's server thread, LAN, dedicated) a guest's velocity only
holds knockback, not the player's own movement, so an arrow shot while
jumping or running lost the shooter's motion and flew lower and shorter
than vanilla's.

Guests now keep known_movement: set on each accepted move, zeroed at a
ClientTickEnd without one. enter_guest puts it in Game::known_movement for
the turn and leave_guest restores the outer turn's, so nested turns keep
their own. Bows, tridents, snowballs/eggs/ender pearls, thrown potions
and wind charges take known_player_movement(), which the spear already
used. The spear and the per-tick using/digging turn no longer copy the
last of the tick's moves themselves, which was zero once those moves had
been consumed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 04:18:46 +05:00
Ludovic Marie 791d8b8950 Merge pull request #3434 from horizon-rust/feat/native-hooks-v2-tables
🤖 Agent: hook points v2, part 1/2 (recipe tables and helpers for native mods). pr_ready green on the stack's head 1dba99f: clippy, 1286 client tests, parity 33/33 selected; the native Recipes mod builds on it and its proof passes. Merging it queues part 2, #3435.
2026-10-29 00:16:10 +01:00
Joansitoh 90014f63d2 Windows: link untouched directories into the pack overlay with junctions, not a file-by-file mirror
Refs #3385, #3234
2026-10-29 00:15:45 +01:00
Lyle ColeandClaude Opus 5.5 a7771347e6 Biomes: a world's data-pack biome files over the built-in table
with_packs replaces a built-in biome in place and adds new ones after the
built-in ids, which never move; registry_order gives vanilla's network
order (Identifier: path, then namespace, as RegistryDataLoader sorts).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 18:12:59 -05:00
ThatCakeIsAPieandClaude Opus 5.5 83b958a824 docs/world-height.md, a 2096-high fixture pack, and the tall parity run
tools/fixtures/tall_datapacks/horizon_tall_test is Horizon's own pack
(MIT): an Overworld dimension type of min_y -64, height 2096 (To The
Max's, the highest top vanilla allows) and noise settings that raise
terrain to about y 1900. VanillaChunkGenerationOracle builds each level
at its dimension type's range instead of a fixed -64..320, so
HORIZON_PARITY_DATAPACKS=tools/fixtures/tall_datapacks
tools/test_datapack_worldgen_parity.py compares Horizon's tall generation
with vanilla's: 16 chunks over two seeds, terrain and decorated, the same.

docs/world-height.md describes the model (one LevelShape per level, from
its dimension type, read everywhere), the network path, the checks and
what is left; world-generation.md and the README say packs' dimension
types now apply.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 18:12:58 -05:00
ThatCakeIsAPieandClaude Opus 5.5 148d592567 Uniform sections are shared, and the sky-source scan skips them
Sections of one block, one biome and one light value are one allocation
per kind, shared by every column (new chunks, lighting, saved light);
edits copy them, as Arc::make_mut already does. A tall level is mostly
such sections: air with full sky light above the terrain. The relighter's lowest_sky_source passes
over a uniform section that light goes through instead of testing each
of its 16 blocks.

Memory only: every value read is the same.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 18:12:58 -05:00
ThatCakeIsAPieandClaude Opus 5.5 0a72ffbf5c Each level's shape from the world's dimension types, local and remote
Opening a world reads its levels' shapes from its dimension types, the
enabled data packs' over vanilla's (world_loading::level_shapes), and the
game keeps them (Game.level_shapes): each level's World, SaveDir,
streaming and flat terrain get its shape, and the teleport and portal
searches read a level's logical height from them. A host encodes each
level at its shape (HubConfig.levels) and sends the data of a dimension
type the packs changed even to a client that knows the vanilla pack
(NetConfig.levels, SyncData::registry_packets), so the integrated server
of single player and LAN guests see the same heights. A remote client
takes each level's shape and its chunks' column from the server's
dimension type, which now carries its logical height; the vanilla End's
fallback has sky light, as 26.3's the_end.json does.

A world with To The Max's dimension type is 2032 blocks high from here.
Vanilla worlds read vanilla's types: nothing changes there.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 18:12:58 -05:00
ThatCakeIsAPieandClaude Opus 5.5 7a082c2176 Gameplay reads each level's bounds from its world
About ninety places took "where the level starts and ends" from the fixed
column: void damage, build limits, spawn and surface searches, falling
blocks, wind charges, commands, mob goals, navigation and paths, raids,
villagers' POI scans, fluid scans and the portal search. Those that used
the column for every dimension read the world's (World::bottom_y, top_y),
which is -64..320 in all three vanilla levels; those that chose per
dimension read the level's shape (LevelAccess from world.shape, Dim's
bounds from Dim::shape). Each level's World now holds its shape where it
is made. chunk::every_section replaces the per-file [true; SECTIONS]
arrays of the fluid scans.

Every value read is the one in use today.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 18:12:57 -05:00
ThatCakeIsAPieandClaude Opus 5.5 72317bb0c6 Worldgen: a level's build range from its dimension type, a data pack's over vanilla's
A pack's dimension_type/<id>.json joins the overrides (DIMENSION_TYPES).
LevelHeight is a dimension type's build range, logical height and
skylight, checked as its codec checks them; WorldgenOverrides::level_height
gives a vanilla level's, from the type its level stem names. The
generator takes the level's range from the same data instead of the
fixed table (dimension_type_height goes), so a pack's taller dimension
type no longer clamps the noise, and datapack::worldgen_overrides no
longer reports dimension types as left out.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 18:12:57 -05:00
ThatCakeIsAPie 8e445b4c3d Merge the data pack worldgen series (feat/datapack-worldgen-4) under the level-shape series 2026-10-28 18:12:57 -05:00
ThatCakeIsAPieandClaude Opus 5.5 c4e54ad254 Section caches, generation glue, distant terrain, minimap and flat worlds read the column
The random-tick section cache keeps a section list as long as the chunk's
and counts its Y from the chunk's min_y. A generated chunk becomes a
column of its level's (OverworldGenerator::level gives the build range,
ColumnLayout::of_level its column), copying the whole level range, as
does a failed one. Distant terrain from proto chunks spans the level's
range, the minimap and saved-chunk far tiles scan from the chunk's own
bottom, and flat worlds lay their layers from the Overworld's minimum up
to its height (FlatTerrain::from_settings takes the level's shape). A
test ticks the sections of a 131-section chunk near its top.

Vanilla's levels keep their column and ranges, so nothing changes there.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 18:12:56 -05:00
ThatCakeIsAPieandClaude Opus 5.5 9e91278794 Chunk packets map between the level and the client's column by the level's shape
The LAN encoder encodes a level of a LevelShape: its chunks hold the
shape's column, of which the level's own sections go on the wire, and the
heightmaps it sends count the column's sections from the chunk's min_y
(client_heightmaps_in takes it). A section's block entities sit at the
chunk's min_y. The remote client decodes chunk and light packets into the
column its ChunkFormat holds (the vanilla one until the server's
dimension types set it) instead of the fixed -64..320. A test encodes a
131-section column and checks its sections and light layers all go out.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 18:12:56 -05:00
ThatCakeIsAPieandClaude Opus 5.5 fdb4e917c0 Saves read and write a chunk's own column, and SaveDir takes its level's shape
SaveDir::new takes the level's LevelShape from its caller instead of
guessing it from the folder name (LevelShape::for_dir goes). A saved chunk
is decoded into its level's column (decode_with, decode_proto), the
sections outside it left out; encoding, POI sections, heightmaps and the
saved light layers count sections from the chunk's own min_y, and distant
terrain read from saves spans the level's column. A test saves a
131-section chunk (yPos -4, 12-bit heightmaps) and reads it back whole,
and into the vanilla column without the sections above it.

Vanilla levels pass their vanilla shapes, so saves are unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 18:12:55 -05:00
ThatCakeIsAPieandClaude Opus 5.5 c2b55479f1 World, streaming, meshing and the renderer read the level's column
World holds its level's LevelShape, and its chunks' column is the shape's.
Writes check the chunk's own range, the heightmap fallbacks and may_hold
read the column, and dirty section indexes are bounded by the mask instead
of the vanilla constant. Streaming marks the column's sections dirty, the
mesher places sections from the chunk's min_y, and the renderer builds its
occlusion graph, complete-column mask and section origins for the column
of the level it was cleared for (clear_sections takes it). A test streams
a 131-section level (To The Max's height) and checks that columns light
and mesh to y 2031, and that an edit at y 1800 relights and remeshes.

Every level is still the vanilla Overworld's shape here.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 18:12:54 -05:00
ThatCakeIsAPieandClaude Opus 5.5 a6d8409466 Light reads the chunk's column: flood fill, layers and relighting span its sections
light_chunk floods up to the centre chunk's own top, and the light it
returns, the saved and received light layers and the relighter's section
keys span the chunk's sections instead of the vanilla constants. The
relighter takes the column from the world it lights (LightWorld::layout,
the column its chunks hold), and LightChanges counts section indexes in
that column, so a taller level's sections near its top get relit and
remeshed too. Vanilla's column is unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 18:12:54 -05:00
Lyle ColeandClaude Opus 5.5 46c62bdf2a LevelShape: each level's shape as one value, and the column its chunks hold
LevelShape (skylight, min_y, height, and now logical_height) moves from
save.rs to chunk.rs, where every subsystem can read it, and checks a
dimension type as its codec does. Its column is the span of sections a
chunk of the level holds: the level's own range widened to the vanilla
Overworld's -64..320, so vanilla's three levels keep the 24 sections
they have always had. ColumnLayout is that span
as a value; Chunk records its own min_y, and get, set, highest, light,
biome and the heightmap cache read the chunk's sections instead of the
constants.

Nothing makes a chunk of another column yet: vanilla is unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 18:12:54 -05:00
Lyle ColeandClaude Opus 5.5 0c639904f1 Section sets past 32 sections: SectionMask replaces the u32 column masks
Dirty sections, the light edit log, random-tick and portal section caches,
the renderer's meshed-column masks and the LAN encoder's light masks held a
bit per section in a u32 or u64, which capped a column at 32 sections. A
256-bit SectionMask holds every layout a dimension type allows (254 sections
plus the two light borders). No behaviour change: the column is still 24
sections.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 18:12:53 -05:00
ThatCakeIsAPieandClaude Opus 5.5 a849545f63 Harness: chunks generated with a world's data packs against vanilla
test_datapack_worldgen_parity.py runs the whole-chunk oracle and
chunk_dump with HORIZON_DATAPACKS: the oracle selects every pack of the
folder after vanilla through vanilla's pack repository, chunk_dump reads
them through worldgen::overrides into GeneratorData::load_with.
tools/fixtures/datapacks holds Horizon's own pack (MIT), overriding a
noise, a density function, the overworld noise settings, a placed
feature and a biome tag, and adding a noise and two density functions.
Each site must match vanilla with the pack and differ from Horizon
without it. HORIZON_PARITY_DATAPACKS compares another folder of packs.

docs/world-generation.md describes what a pack changes and what Horizon
leaves out; the README lists world generation among what data packs do.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 18:12:53 -05:00
ThatCakeIsAPieandClaude Opus 5.5 b0e65dec06 Data packs: a world's packs change its world generation
The enabled packs of <world>/datapacks (in datapacks.json's order) now
reach the chunk generator: their worldgen files replace and join the
prepared bundles, and the log lists what Horizon leaves out (new biomes,
dimension types, folders the generator does not read, noise taller than
the level). Generator data is cached per set of pack entries, beside the
plain bundle the world screens preload. datapack::enabled_ids reads which
packs are on, as /datapack does.

A world without data packs reads the same bundle as before. Sapling
growth and bone meal still place vanilla's features.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 18:12:53 -05:00
ThatCakeIsAPieandClaude Opus 5.5 d1ef7b4fb0 Worldgen: data packs' entries over the prepared bundles
WorldgenOverrides maps a data pack's worldgen files (noise, density
functions, noise settings, material rules and conditions, carvers,
features, placed features, structures, sets, pools, processor lists,
biomes, level stems) to the prepared bundle each joins, the highest pack
winning, and its block, fluid and biome tag files to the tag bundles in
pack order, as vanilla builds its registries from the enabled packs.
Carvers and features use 26.3's folder names (worldgen/carver,
worldgen/feature). A pack's new biome is left out, with the tag entries
and climate points that name it. Dimension types are reported, not
applied: each level keeps its vanilla build height for now.

Nothing calls it yet.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 18:12:52 -05:00
ThatCakeIsAPieandClaude Opus 5.5 25662576ff Worldgen: the generator loads a world's data packs over the bundles
GeneratorData::load_with reads every bundle through WorldgenOverrides
(the loader threads included), so a data pack's noises, density
functions, noise settings, rules, carvers, features, structures and
biome changes reach generation. Pack tag files join the prepared tags;
a tag that names something missing is left out with a warning, as
vanilla's TagLoader does, where the prepared tags alone must still all
resolve. A pack's level stem replaces the world preset's dimension.
Noise settings that reach past the dimension's build height fill only
the part inside it (NoiseSettings.clampToHeightAccessor), and the
generator says so, since every level keeps its vanilla height for now.

load is load_with with no packs, so vanilla generation is unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 18:12:52 -05:00
SenjinandClaude Opus 5.5 31ead52d21 HDR: follow the compositor's changes while the game runs
The renderer keeps the window's surface feedback and looks at it every frame
(only events winit already read; no waiting). On preferred_changed it asks
again, and the game applies its settings, so Auto follows a move to another
output or a recalibrated desktop without a restart. Docs: the Auto values.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 00:08:10 +01:00
kerim b9a040dc21 Merge branch 'horizon-rust:main' into main 2026-10-29 00:07:46 +01:00
Ludovic Marie fe3333f3f0 Merge pull request #2945 from kacpherek/fix/creative-slot-item-components
🤖 Agent: needs-proof check by Ludovic's AI agent (Claude), not by Ludovic himself. Passed on a union of 12 contributor PRs on main 77fa9fb89: fmt, clippy -D warnings, the tests of horizon-client/-items/-protocol/-world (all pass) and the Python tests, and 19 lan_e2e checks with the host on its server thread (all pass) and in-frame (pass; frames passed on its rerun). Against a vanilla 26.3 server (127.0.0.1, offline): moving a renamed/unbreakable/lored sword and a shulker box holding 5 diamonds in the creative inventory kept every component and did not kick. Parity: CI's run-parity on this head.
2026-10-29 00:07:34 +01:00
kerim 1dce2ab885 added german translation to the launcher 2026-10-29 00:04:23 +01:00
Cosmic b56048d996 Merge pull request #3221 from 1038april/fix/remote-team-push-names
Look up players teams by name for client-side pushes on servers
2026-10-28 19:02:05 -04:00
xKumorioandClaude Opus 5.5 ad644778bc Three Windows-only test failures in pr_ready
session_lock_is_exclusive read session.lock while its lock was held.
Windows file locks are mandatory, so that read failed with OS error 33.
It now reads the file once the first lock is dropped.

port_publish wrote the store path as str(path) with the home replaced by
"~", which kept Windows backslashes (~\code\...). The path under the home
is now written as ~/ plus its POSIX form, as on Linux. The temporary body
file is opened as UTF-8, so the "→" in a facts line no longer fails to
encode in the Windows code page.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 04:01:17 +05:00
JacobRWebbandClaude Opus 5.5 ee90b3b34b Moving blocks take their light from the cells their faces look at
A finished piston's block is drawn over the chunk mesh until that mesh
shows it, but it was lit with the light stored at the moving piston's own
cell. Once the placed block stands there and the edit is relit, that cell
is opaque and holds 0: the pushed or pulled block and the piston base
flashed black for the frames until the mesh came up (#3002).

Vanilla's PistonHeadRenderer.createMovingBlock gives each block a
MovingBlockRenderState (the lone block at the cell it moves from, the
level's light engine), and MovingBlockFeatureRenderer tesselates it with
ModelBlockRenderer.tesselateBlock: in tesselateFlat each quad reads
BlockModelLighter.getLightCoords at its cull-face neighbour, or
prepareQuadFlat's position (the neighbour when cubic, else the block).
Moving blocks now follow that through MeshTables::flat_light, the chunk
mesher's own flat-light rules; a finished one is lit as the block placed
at its cell, which it stands in for.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 17:46:24 -05:00
xKumorioandClaude Opus 5.5 a278572906 Three Windows-only test failures in pr_ready
session_lock_is_exclusive read session.lock while its lock was held.
Windows file locks are mandatory, so that read failed with OS error 33.
It now reads the file once the first lock is dropped.

port_publish wrote the store path as str(path) with the home replaced by
"~", which kept Windows backslashes (~\code\...). The path under the home
is now written as ~/ plus its POSIX form, as on Linux. The temporary body
file is opened as UTF-8, so the "→" in a facts line no longer fails to
encode in the Windows code page.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 03:44:55 +05:00
Ludovic MarieandClaude Opus 5.5 78e449714a Merge refactor/spawner-hive-traits (B8 with main) into refactor/horizon-client-render
crash.rs: main's separator-neutral manifest path, with the render crate's suffix.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 23:43:22 +01:00
Ludovic MarieandClaude Opus 5.5 ab0f591673 Merge origin/main into refactor/spawner-hive-traits
#3064's TrialConfig::encode and TrialSpawner::override_entity become
methods of TrialConfigExt and TrialSpawnerExt; game/spawner.rs (#3064)
and lan/chunks.rs (#2952) import the extension traits they now need.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 23:42:52 +01:00
SenjinandClaude Opus 5.5 7ef092fabc HDR: Auto paper white, at the desktop's SDR white
Paper White 0 is Auto, the new default: where the system puts SDR white (on
Wayland the compositor's reference white, 203 nits in HDR output's scale), else
203. Both Auto values come from one report the renderer makes at surface open.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 23:42:34 +01:00
d3grandoandClaude Opus 5.5 2882f0764d parity: catch SIGHUP only where it exists, so parity.py starts on Windows
Python's signal module has no SIGHUP on Windows, so exit_on_termination
raised AttributeError and every parity.py command (--list, --changed,
--status, and pr_ready's local harness run) stopped before doing anything.
SIGTERM is still caught everywhere, SIGHUP wherever the platform has it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 05:41:08 +07:00
degepie 8740d3897c Merge remote-tracking branch 'origin/main' into fix/goat-horn-instrument-sync
# Conflicts:
#	crates/horizon-client/src/lan/guest.rs
2026-10-29 03:34:06 +05:00
xKumorioandClaude Opus 5.5 859b6be783 Kelp stops blinking out far away: animated sprites mipmap over the whole sheet
Vanilla's SpriteContents mipmaps the animation's original image, every
frame of the sheet at once (byMipLevel = {originalImage}, then
MipmapGenerator.generateMipLevels), and AnimatedTexture uploads each frame
from that chain at frameX/frameY >> level. Horizon mipmapped each frame on
its own, so solidify and each level's alpha-to-coverage scale were the
frame's. A 16px frame has a single texel square at its 2x2 level: its
coverage there is 0 or a multiple of 1/16, and the bisection could settle
on a scale that left the whole level under the cutout. Kelp frames 0 and 18
did, so far kelp vanished every time the animation reached them (only with
mipmaps on, as reported).

source_chains stacks the frames into one sheet, runs mipmap::generate over
it and cuts each level back into per-frame chains, so every frame shares
the sheet's coverage scale. A frame alone (source_chain) is unchanged.

Fixes #1703. Fixes #3073.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 03:33:32 +05:00
Ludovic MarieandClaude Opus 5.5 326719539d Merge main into fix/shelf-interaction
main's #2952 added banner, vault and trial spawner arms to lan::chunks::entity_tag next to this branch's brushable and shelf arms; both stay.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 23:32:54 +01:00
Ludovic Marie 142388e1c1 Merge pull request #3361 from horizon-rust/launcher-mixins-0d6b103
🤖 Agent: MIXINS_REV bump to 0d6b103; launcher builds green on Linux, macOS and Windows at this head.
2026-10-28 23:31:50 +01:00
JacobRWebbandClaude Opus 5.5 a72f06cfab Test the moving piston's saved fields against vanilla's
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 17:28:37 -05:00
d3grandoandClaude Opus 5.5 e8896336da GUI: screens keep vanilla's int layout at any window size
Direct Connection, Add/Edit Server, Join a Friend, Connection Lost, the
death screen and the sign editor's Done button placed their widgets at
width / 2.0 - 100 and height / 4.0 + N in floats. Vanilla's Screen
width and height are ints, so `this.height / 4` drops the remainder;
Horizon kept it, and whenever the GUI height was not a multiple of 4 (a
maximized or tall window: 1009 / 4 = 253 GUI pixels) or the width was
odd, buttons, boxes and centred text landed on a half or quarter GUI
pixel. At scale 3 that is 1.5 device pixels: borders came out thinner on
one side and glyph rows uneven (#3362).

The positions now use int halves and quarters as DirectJoinServerScreen,
ManageServerScreen, DeathScreen and AbstractSignEditScreen do, and
centred text is width / 2 - font.width / 2 (GuiGraphicsExtractor
.centeredText).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 05:27:47 +07:00
JacobRWebbandClaude Opus 5.5 fab9c37413 Moving pistons save facing as a byte, as Direction.LEGACY_ID_CODEC does
PistonMovingBlockEntity.saveAdditional stores facing through
Direction.LEGACY_ID_CODEC, Codec.BYTE of get3DDataValue. The block-entity
canonicaliser rewrote it as an int too.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 17:27:45 -05:00
SenjinandClaude Opus 5.5 dc418c7623 HDR: Auto peak brightness from what the compositor reports
Peak Brightness 0 is Auto, the new default: the peak the system reports for
the display (on Wayland, the surface's preferred image description, in HDR
output's scale), else 1000. The slider's lowest step is Auto; 200..2000 stay
manual.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 23:23:15 +01:00
Ludovic Marie 93b7ddd09e Merge branch 'main' into launcher-mixins-0d6b103 2026-10-28 23:21:42 +01:00
Ludovic MarieandClaude Opus 5.5 6a221890bd Native mods: recipe tables, recipe book, place and give for native mods (hook points v2)
The item and recipe tables WebAssembly mods read through capability
`recipes` move to game/native_recipes.rs, built in every build (no `mods`
feature), as native types (NativeRecipes, NativeRecipe, NativeItem,
NativeRecipeStack, NativeRecipeKind). Native mods get helpers for them:
native_recipes, native_known_recipes, native_place_recipe and native_give,
refused as recipes.place and recipes.give are. WebAssembly mods read the
same tables, converted once to the runtime's types.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 23:19:12 +01:00
Ludovic Marie a7841ccb18 Merge remote-tracking branch 'origin/main' into feat/launcher-java-command 2026-10-28 23:18:24 +01:00
Ludovic Marie c9a103852c Merge pull request #3406 from horizon-rust/feat/launcher-any-version-download
🤖 Agent: launcher-java-1 verified this head (main merged in for #3402's Windows fix): PR checks green (verify, launcher) and the launcher build matrix green on Windows, macOS and Linux (dry run https://github.com/horizon-rust/horizon/actions/runs/40775800425). No CLI change, so docs/launcher-commands.md is unchanged.
2026-10-28 23:12:15 +01:00
Ludovic Marie a15faef940 Merge pull request #2768 from levzzz5154/fix/world-difficulty-survives-reopen
🤖 Agent: needs-proof check by Ludovic's AI agent (Claude), not by Ludovic himself. Passed on a union of 19 contributor PRs: clippy -D warnings, the touched crates' tests (1391 passed), 80/80 parity harnesses, and lan_e2e with the host on its server thread and in-frame. save round trip main -> PR -> main -> PR: difficulty Hard/Peaceful/Easy read back at each hop and in level.dat difficulty_settings, server thread and --in-frame. Queued for merge at 35581f52e.
2026-10-28 23:12:12 +01:00
Ludovic Marie 58cd73ee0b Merge pull request #2763 from ASOwnerYT/claude/issue-2746-fix-pr-f0eb69
🤖 Agent: needs-proof check by Ludovic's AI agent (Claude), not by Ludovic himself. Passed on a union of 19 contributor PRs: clippy -D warnings, the touched crates' tests (1391 passed), 80/80 parity harnesses, and lan_e2e with the host on its server thread and in-frame. changes test_music_parity.py (music harness PASS in the run). Queued for merge at 985ece811.
2026-10-28 23:12:08 +01:00
Ludovic Marie e2a3e92424 Merge pull request #3011 from feeldev12/feat/integrated-loads-async
🤖 Agent: needs-proof check by Ludovic's AI agent (Claude), not by Ludovic himself. Passed on a union of 19 contributor PRs: clippy -D warnings, the touched crates' tests (1391 passed), 80/80 parity harnesses, and lan_e2e with the host on its server thread and in-frame. every server-thread launch of this run (screenshots, save round trips) loaded through the new async start. Queued for merge at 356b69aea.
2026-10-28 23:12:04 +01:00
Ludovic Marie 6f8c7a0e5d Merge pull request #3012 from feeldev12/feat/connect-screen
🤖 Agent: needs-proof check by Ludovic's AI agent (Claude), not by Ludovic himself. Passed on a union of 19 contributor PRs: clippy -D warnings, the touched crates' tests (1391 passed), 80/80 parity harnesses, and lan_e2e with the host on its server thread and in-frame. screenshot: ConnectScreen with the status and a working Cancel back to the multiplayer list (main: flat screen, no Cancel). Queued for merge at 752fc2170.
2026-10-28 23:09:50 +01:00
Ludovic Marie 04cbda1b14 Merge pull request #2752 from iLostName/faster-server-connect
🤖 Agent: needs-proof check by Ludovic's AI agent (Claude), not by Ludovic himself. Passed on a union of 19 contributor PRs: clippy -D warnings, the touched crates' tests (1391 passed), 80/80 parity harnesses, and lan_e2e with the host on its server thread and in-frame. Queued for merge at de8dd66ac.
2026-10-28 23:09:46 +01:00
Ludovic Marie 2de8bbd84b Merge pull request #2998 from feeldev12/perf/server-loot-preload
🤖 Agent: needs-proof check by Ludovic's AI agent (Claude), not by Ludovic himself. Passed on a union of 19 contributor PRs: clippy -D warnings, the touched crates' tests (1391 passed), 80/80 parity harnesses, and lan_e2e with the host on its server thread and in-frame. Queued for merge at fdefcfeac.
2026-10-28 23:09:42 +01:00
Ludovic Marie a7177cfbd7 Merge pull request #3228 from iLostName/own-death-sound
🤖 Agent: needs-proof check by Ludovic's AI agent (Claude), not by Ludovic himself. Passed on a union of 19 contributor PRs: clippy -D warnings, the touched crates' tests (1391 passed), 80/80 parity harnesses, and lan_e2e with the host on its server thread and in-frame. the server sends no death sound to the dying guest, so it is not doubled. Queued for merge at 7b93b310a.
2026-10-28 23:09:38 +01:00
Ludovic Marie b558a845a0 Merge pull request #3066 from iLostName/item-pickup-one-sound
🤖 Agent: needs-proof check by Ludovic's AI agent (Claude), not by Ludovic himself. Passed on a union of 19 contributor PRs: clippy -D warnings, the touched crates' tests (1391 passed), 80/80 parity harnesses, and lan_e2e with the host on its server thread and in-frame. pickup sound pitch matches handleTakeItemEntity. Queued for merge at 0112a6fea.
2026-10-28 23:09:33 +01:00
Ludovic MarieandClaude Opus 5.5 7826732a5c Launcher: Play starts a Java instance's Minecraft
With a Java instance selected, Play builds its game off the window's
thread (`backend::java_game`: the owner gate, the version and Java
downloaded the first time, their lines in the log), then starts it as
the Horizon client starts. No account that owns the game opens the
sign-in screen, as for Horizon. Joining a friend and quick play stay
Horizon's.

The Play page names the instance's Minecraft version (a Game row in
place of Horizon's resources and Minecraft folder), needs no Horizon
version, and the sidebar shows the version. An old version's logged
session line never reaches the window.

The translations test also reads strings rustfmt moved to the line
after `tr(`/`trf(`.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 23:08:34 +01:00
Ludovic Marie 9f1a998403 Merge pull request #2833 from levzzz5154/fix/empty-compressed-packet
🤖 Agent: needs-proof check by Ludovic's AI agent (Claude), not by Ludovic himself. Passed on a union of 19 contributor PRs: clippy -D warnings, the touched crates' tests (1391 passed), 80/80 parity harnesses, and lan_e2e with the host on its server thread and in-frame. zero-length frame rejected as Varint21FrameDecoder does. Queued for merge at 65bca4ce9.
2026-10-28 23:07:10 +01:00
Ludovic Marie 527cd7225c Merge pull request #2816 from sirrayi/fix/malformed-network-input
🤖 Agent: needs-proof check by Ludovic's AI agent (Claude), not by Ludovic himself. Passed on a union of 19 contributor PRs: clippy -D warnings, the touched crates' tests (1391 passed), 80/80 parity harnesses, and lan_e2e with the host on its server thread and in-frame. Queued for merge at 2812cf201.
2026-10-28 23:07:06 +01:00
Ludovic Marie f64fdeaa75 Merge pull request #2813 from sirrayi/fix/guest-block-reach
🤖 Agent: needs-proof check by Ludovic's AI agent (Claude), not by Ludovic himself. Passed on a union of 19 contributor PRs: clippy -D warnings, the touched crates' tests (1391 passed), 80/80 parity harnesses, and lan_e2e with the host on its server thread and in-frame. reach checks match ServerGamePacketListenerImpl.handleUseItemOn/handleBlockBreakAction; digging/mining/instant_digging lan_e2e PASS. Queued for merge at 3be828d01.
2026-10-28 23:07:01 +01:00
Ludovic Marie a0c79c8b26 Merge pull request #2805 from ASOwnerYT/fix/2775-jukebox-offhand-eject
🤖 Agent: needs-proof check by Ludovic's AI agent (Claude), not by Ludovic himself. Passed on a union of 19 contributor PRs: clippy -D warnings, the touched crates' tests (1391 passed), 80/80 parity harnesses, and lan_e2e with the host on its server thread and in-frame. jukebox use read against JukeboxBlock.useItemOn/useWithoutItem. Queued for merge at b38fe43b0.
2026-10-28 23:06:57 +01:00
Ludovic Marie 0284d3bb16 Merge pull request #2952 from kacpherek/fix/lan-block-entity-data
🤖 Agent: needs-proof check by Ludovic's AI agent (Claude), not by Ludovic himself. Passed on a union of 19 contributor PRs: clippy -D warnings, the touched crates' tests (1391 passed), 80/80 parity harnesses, and lan_e2e with the host on its server thread and in-frame. screenshot: the single-player window (server thread) now shows banner patterns and the vault's display item (main: plain banner, empty vault). Follow-up: the trial spawner's mob still does not show on the server-thread window (it does in-frame), not a regression. Queued for merge at 1deb3bd36.
2026-10-28 23:06:54 +01:00
degepie 57a6f4e9aa Merge remote-tracking branch 'origin/main' into fix/goat-horn-instrument-sync
# Conflicts:
#	crates/horizon-client/src/item_sync.rs
#	crates/horizon-protocol/src/item.rs
2026-10-29 03:06:31 +05:00
degepie ac7e491f3b Merge remote-tracking branch 'origin/main' into fix/issue-3137-suspicious-stew
# Conflicts:
#	crates/horizon-protocol/src/item.rs
2026-10-29 03:04:30 +05:00
Ludovic Marie 72b4d8ecbb Merge pull request #3395 from horizon-rust/feat/launcher-java-instances
🤖 Agent: launcher-java-1 verified this head: PR checks green (verify, launcher) and the launcher build matrix green on Windows, macOS and Linux (dry run https://github.com/horizon-rust/horizon/actions/runs/40775337700); docs/launcher-commands.md is regenerated.
2026-10-28 23:02:35 +01:00
Ludovic Marie 1ed31e54eb Merge pull request #3062 from kacpherek/fix/shield-arm-and-banner
🤖 Agent: needs-proof check by Ludovic's AI agent (Claude), not by Ludovic himself. Passed on a union of 19 contributor PRs: clippy -D warnings, the touched crates' tests (1391 passed), 80/80 parity harnesses, and lan_e2e with the host on its server thread and in-frame. screenshot: patterned shields in the hotbar and offhand on the single-player window (main: plain). Queued for merge at 99008baad.
2026-10-28 23:00:42 +01:00
Ludovic Marie b3aaa04129 Merge pull request #3055 from kacpherek/fix/horse-feeding-particles-on-server
🤖 Agent: needs-proof check by Ludovic's AI agent (Claude), not by Ludovic himself. Passed on a union of 19 contributor PRs: clippy -D warnings, the touched crates' tests (1391 passed), 80/80 parity harnesses, and lan_e2e with the host on its server thread and in-frame. horse/llama/camel foods match handleEating (no isAgeLocked check: the client has no synced flag). Queued for merge at 9d32bebad.
2026-10-28 23:00:37 +01:00
Ludovic Marie bb38695695 Merge pull request #3052 from kacpherek/fix/hand-drops-after-use-on-server
🤖 Agent: needs-proof check by Ludovic's AI agent (Claude), not by Ludovic himself. Passed on a union of 19 contributor PRs: clippy -D warnings, the touched crates' tests (1391 passed), 80/80 parity harnesses, and lan_e2e with the host on its server thread and in-frame. Queued for merge at e24394ca3.
2026-10-28 23:00:33 +01:00
Ludovic Marie 6bda112225 Merge pull request #3050 from kacpherek/fix/disconnect-no-wait
🤖 Agent: needs-proof check by Ludovic's AI agent (Claude), not by Ludovic himself. Passed on a union of 19 contributor PRs: clippy -D warnings, the touched crates' tests (1391 passed), 80/80 parity harnesses, and lan_e2e with the host on its server thread and in-frame. Queued for merge at a5ee1e588.
2026-10-28 23:00:29 +01:00
Ludovic MarieandClaude Opus 5.5 d3559e2a5f Proceed past the safety notice in the F5 server list test
main's f5_reloads_the_server_list (4c4fcedf4) came after this branch: its first Multiplayer click now opens SafetyScreen.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 23:00:28 +01:00
Ludovic MarieandClaude Opus 5.5 c90893c29c Merge main into fix/multiplayer-safety-notice
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 23:00:18 +01:00
Ludovic MarieandClaude Opus 5.5 dcd733c7c1 Launcher: play a Java instance from the command line
`horizon-launcher play --instance <java instance>` starts its Minecraft:
Java Edition. Past the owner gate itself (no developer option), the
version is downloaded from Mojang the first time, the Java it needs is
the instance's own, one found here or Mojang's (downloaded), and the
game starts in the instance's folder as the account that owns it. With
no answer from the services, an owner plays with its last token.

Options only Horizon has (--version, --world, --creative, --join,
--server) are refused for Java instances; --arg passes through. The
window's Play still refuses them and names this command.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 22:59:59 +01:00
Ludovic Marie 77fa9fb89f Merge pull request #3388 from horizon-rust/fix/native-mods-mixins-cargo
🤖 Agent: verify green; launcher-only, one function. pr_ready green (launcher tests 228/228). Parity --changed: nothing selected (lan_integration is nightly-only, and the launcher is not in the LAN suite). Proof end to end with no system Rust: the launcher's toolchain built zoom on macOS (546 s) and Linux (729 s, launcher from this branch's dry-run 40767814775), and zoom's offscreen proof passed on both launcher-built games. Before the fix both stopped at E_METADATA.
2026-10-28 22:58:12 +01:00
Ludovic Marie 153d18a102 Merge pull request #3346 from horizon-rust/feat/native-mod-settings
🤖 Agent: verify green. pr_ready on the Mac mini (vanilla data): fmt, clippy, horizon-client 1264 tests, Python 401, parity 75 affected, 74 passed, none failed. Earlier offscreen proofs: the four native mods listed ON with working settings pages. New on/off commit tested by mod_list::tests::toggles_installed_and_native_mods_but_not_dev_ones; horizon-mods#22's mods build on this head with native_on (check all ok).
2026-10-28 22:55:57 +01:00
spiderandClaude Opus 5.5 e2b2b2ccd0 Players wear their capes: CapeLayer, the cloak's swing, and the elytra in the cape's texture
A player's cape texture was looked up and downloaded, then thrown away: nothing kept the image
and nothing drew it. Now:
- Cape downloads go into the skin slots as `capes/<sha1>` (SkinManager's cape cache), a 64x32
  texture in the top half of a 64x64 slot; an evicted one uploads again from memory or the disk
  cache. A larger cape is scaled to 64x32, the older 22x17 fits as it is.
- `PlayerSkin.cape` reaches the player models (other players and the local one), which load
  `player#cape` (PlayerCapeModel).
- CapeLayer draws it: no cape under an elytra's wings, moved out over a chestplate, hidden with
  the Cape skin-customisation option (the local player). PlayerCapeModel.setupAnim turns the cape
  by `ModelPart.rotateBy` with AvatarRenderer.extractCapeState's flap and leans, from
  ClientAvatarState's cloak (moveCloak: a quarter of the way each tick, snapping from 10 blocks),
  its bob and walked distance; other players tick theirs as RemotePlayer does.
- WingsLayer.getPlayerElytraTexture: a player with a cape wears it on the elytra.
- `HORIZON_OFFLINE_CAPE`, which the launcher sets for an offline name, is the local player's
  cape when its profile has none.

Other players' Cape option (Avatar.DATA_PLAYER_MODE_CUSTOMISATION) is left to #2971, which
tracks it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 22:55:56 +01:00
Ludovic MarieandClaude Opus 5.5 b85a9e8350 Merge main into fix/server-core-parity
main added the pack prompt and server dialogs to the screens that stay open when the window loses focus;
they move into pause_if_inactive with the rest of that check.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 22:54:25 +01:00
Ludovic MarieandClaude Opus 5.5 95ecc9d771 Merge main into fix/dyed-armor-colour
main's e7f5201c1 colours the armour stand's layers with its own layer_color; keep it there.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 22:54:04 +01:00
Ludovic Marie e470f3a0c9 Merge remote-tracking branch 'origin/main' into feat/launcher-any-version-download 2026-10-28 22:53:45 +01:00
Ludovic Marie f93c28a230 Merge pull request #2932 from RVZO6/fix/dig-down-jitter
🤖 Agent: needs-proof check by Ludovic's AI agent (Claude), not by Ludovic himself. Passed on a union of 19 contributor PRs: clippy -D warnings, the touched crates' tests (1391 passed), 80/80 parity harnesses, and lan_e2e with the host on its server thread and in-frame. Queued for merge at 32a0a11fe.
2026-10-28 22:53:41 +01:00
Ludovic Marie b9721d816d Merge pull request #2809 from socksprox/fix/1528-portal-arrival
🤖 Agent: needs-proof check by Ludovic's AI agent (Claude), not by Ludovic himself. Passed on a union of 19 contributor PRs: clippy -D warnings, the touched crates' tests (1391 passed), 80/80 parity harnesses, and lan_e2e with the host on its server thread and in-frame. Queued for merge at 9fcfdaa1e.
2026-10-28 22:53:37 +01:00
Ludovic Marie a49e72201a Merge branch 'feat/launcher-java-instances' into feat/launcher-java-play 2026-10-28 22:51:29 +01:00
Ludovic Marie 2034db1df6 Merge remote-tracking branch 'origin/main' into feat/launcher-java-instances 2026-10-28 22:50:15 +01:00
Ludovic MarieandClaude Opus 5.5 5091896e2a Launcher: the real game's command from its version JSON
java_launch builds the command that starts Minecraft: Java Edition as
the official launcher reads a version file: mainClass; the classpath of
this system's libraries (rules with os name and arch, duplicates once)
and the client jar; arguments.jvm / arguments.game with their rules
(every optional feature off) and ${...} placeholders, or the old
minecraftArguments line with -Djava.library.path and -cp; an old
version's natives unpacked without extract.exclude; virtual and
map_to_resources asset indexes laid out by name; Mojang's logging setup
(-Dlog4j.configurationFile) plus -Dlog4j2.formatMsgNoLookups=true;
-Xmx from the instance and the player's JVM arguments.

Nothing reaches the network; the next PR wires it to Play.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 22:50:02 +01:00
Ludovic Marie 6edc4e950d Merge pull request #3402 from horizon-rust/fix/launcher-java-test-windows
🤖 Agent: verified: launcher build matrix dry run 40773579704 green on Windows, macOS and Linux (java::tests::mojangs_runtimes_are_found_in_their_layout passes on Windows); PR checks green. Test-only fix for main's Windows build after #3386.
2026-10-28 22:49:19 +01:00
degepieandClaude Opus 5.5 99207f16b1 Suggest the crosshair entity's UUID for entity arguments (#3223)
EntityArgument.listSuggestions adds SharedSuggestionProvider.getSelectedEntities
to the player names unless the argument takes players only;
ClientSuggestionProvider.getSelectedEntities is the UUID of the entity under
the crosshair. The chat screen now fills Registries::selected_entities from
the nearest entity hit before any block.

For the UUID to name the entity back, two server-side gaps are closed:
- the entity tracker sent a made-up UUID (entity_uuid) in AddEntity; it now
  sends the entity's own UUID (ClientboundAddEntityPacket: entity.getUUID()),
  assigned on first tracking;
- a UUID in an entity argument also set playerName, which findEntities checks
  first among players, so `/tp <uuid> ...` found nothing. parseNameOrUUID
  sets entityUUID alone.

The chat_screen and command_parse oracles now return a selected entity from
getSelectedEntities, and the fixtures read it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
2026-10-29 02:49:02 +05:00
degepieandClaude Opus 5.5 e7f746782f Totem of undying: the item activation animation on screen
ClientPacketListener.handleEntityEvent 35 now calls
LocalPlayer.displayItemActivation(findTotem(player)) for the player's own
event. ItemActivation (40 ticks, two random offsets) ticks in
GameRenderer.tick and resets in GameRenderer.resetData.
ScreenEffectRenderer.renderItemActivationAnimation draws it in the hand
pass with a FIXED item at full brightness, ITEMS_3D lighting and no
camera transform or bob. Only F1 hides it.

Fixes #3142.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
2026-10-29 02:46:24 +05:00
Ludovic MarieandClaude Opus 5.5 d4120a9ef0 Launcher: download the version's logging setup with it (Log4Shell fix for 1.7-1.18)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 22:45:58 +01:00
Ludovic MarieandClaude Opus 5.5 795a0db7e4 Launcher: download any Minecraft version for an owner, natives included
The owner-gated download learns versions Horizon doesn't pin, for Java
instances: Pins::for_version(id, sha1) takes a version from Mojang's
list; its version file is checked against that sha1, and the client's
sha1 then comes from the checked file. backend::download_version_for_owner
puts it in Horizon's own game folder through the same path as the
pinned one (download_for_owner asks the strict gate again, then
download_for → mojang::download), so the one download call site and
its gate test hold.

Versions before 1.19 keep a library's natives as a classifier of their
own (natives map, ${arch} for Windows): mojang::download now fetches
this system's, checked like every other file.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 22:42:25 +01:00
Ludovic MarieandClaude Opus 5.5 75b8a176d4 Launcher tests: Mojang runtime fixture with Windows' java.exe
java::tests::mojangs_runtimes_are_found_in_their_layout failed on
Windows (main after #3386): the fixture's homes had only bin/javaw.exe
there, and the runtime search (horizon_prepare::java::minecraft_runtimes)
looks for bin/java.exe, so it found nothing. Mojang's Windows runtimes
ship both, so the fixture now does too. The platform folder comes from
mojang::java_platform, the name the download lays out, instead of a
hard-coded mac-os-arm64.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 22:35:49 +01:00
Ludovic MarieandClaude Opus 5.5 21a0656530 docs: launcher command reference for the Java instance commands
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 22:32:11 +01:00
Ludovic Marie 071580e716 Merge pull request #3400 from horizon-rust/launcher-commands-doc
🤖 Agent: docs-only fix for the launcher reference test (java list/install sections); all checks green at this head.
2026-10-28 22:30:19 +01:00
degepieandClaude Opus 5.5 cb1c3499bf Keep suspicious stew effects across the wire and list them in creative
A stew taken from the creative inventory gave no effect when eaten and
its tooltip listed none (#3137). The protocol read the
`suspicious_stew_effects` component past without decoding it, so the
creative client's `SetCreativeModeSlot` sent the server a bare stew and
the server's copy had nothing for `SuspiciousStewEffects.onConsume` to
apply. Decode and encode it (`Entry.STREAM_CODEC`: mob effect id,
duration) both ways, and add `SuspiciousStewEffects.addToTooltip`'s
creative-only effect lines (`PotionContents.addPotionTooltip`).

Fixes #3137.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
2026-10-29 02:29:33 +05:00
JacobRWebbandClaude Opus 5.5 4a094bee90 Pistons: moving blocks keep their tint, survive saves and leave no black ghost
- Moving blocks are tinted per face with the chunk mesher's block colours
  (MeshTables::block_tints), not the terrain particle tint, which is none
  for grass blocks: a pushed grass block was drawn gray.
- PistonMovingBlockEntity is saved with its chunk as vanilla's
  minecraft:piston block entity and loaded back into the level's moving
  pistons, so blocks caught mid-move finish it after a rejoin instead of
  leaving an invisible, solid moving_piston (#3310).
- A server client drops its moving piston when the server changes that
  block (LevelChunk.setBlockState removes the block entity), instead of
  animating it for five more death ticks over the placed block, black and
  z-fighting (#3002).
- A finished piston stays drawn until the chunk mesh shows its block, and
  no longer, so no frame shows neither.

Fixes #3310
Fixes #3002

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 16:29:10 -05:00
degepieandClaude Opus 5.5 66e1fc5079 Credit the curing player of a zombie villager on the server thread
ZombieVillager.mobInteract records the interacting player as the
conversion starter and finishConversion credits the player
level.getPlayerByUUID finds: the ZOMBIE_VILLAGER_CURED gossip and the
cured_zombie_villager trigger. Horizon used the server Game's own
level.player_uuid for both, so on the server thread (and for LAN guests)
the gossip went to a UUID nobody trades as and the player got no
discount and no Zombie Doctor (#3306).

The starter is now the acting player, and the cure credits the host or
the placed guest in this level with that UUID. lan_e2e gains a
cure-discount check: host and guest each cure one and pay 14 wheat
instead of 20.

Fixes #3306.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
2026-10-29 02:27:31 +05:00
Ludovic Marie ae01d872fb Merge remote-tracking branch 'origin/main' into feat/launcher-java-instances 2026-10-28 22:26:47 +01:00
Ludovic Marie d2f5a0d678 Merge pull request #3398 from horizon-rust/tests-ignore-port-probe
🤖 Agent: tests ignore T3's GET / port probe; account::tests 17/17 on the Mac mini (the_whole_chain failed before); merger waits for parity (online_mode).
2026-10-28 22:26:12 +01:00
Ludovic MarieandClaude Opus 5.5 d6ee462614 docs: the launcher reference lists the java commands
#3386 added `horizon-launcher java list|install` without regenerating
docs/launcher-commands.md, so the_launcher_reference_is_up_to_date failed on main's
launcher builds (and would stop the next launcher release).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 22:25:41 +01:00
Ludovic Marie 430e5e4c92 Merge pull request #3352 from horizon-rust/feat/native-mods-carry-settings
🤖 Agent: verify green. pr_ready: all green except the launcher test account::tests::the_whole_chain, which fails on this Mac only (a desktop browser's extra GET / on the device-code page); it passes in CI's launcher job. Local parity of this tree (Mac mini, --changed origin/main): 36/37, online_mode fails identically on main here (environment). Test the_native_mods_instance_gets_the_mods_settings_once passes.
2026-10-28 22:22:02 +01:00
Ludovic Marie f3d8535fcd Merge pull request #3348 from horizon-rust/ci/train-builds-nightlies
The batch train builds Nightlies: no more vX.Y.Z game releases
2026-10-28 22:15:22 +01:00
d3grandoandClaude Opus 5.5 8f00332979 Mod screens: a list's rows are cut at its frame
A mod screen's `list` drew one row more than its frame holds (the row a
scroll shows in part), uncut, so without scrolling the 7th row of a long
list sat below the frame, over the screen. As vanilla's
AbstractSelectionList, which draws its entries between enableScissor and
disableScissor, the rows (with their hover and selection marks) are now cut
at the frame's inside: the partly shown row is drawn in part. Fixes #3237.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 04:15:07 +07:00
Ludovic Marie b276d65b5f Merge branch 'main' into launcher-mixins-0d6b103 2026-10-28 22:13:02 +01:00
Ludovic MarieandClaude Opus 5.5 559ecb02f6 tests: ignore the bare GET / a local tool sends to every new port
T3 Code probes each new localhost listener with GET / (seen on the Mac mini: the t3
process). The online_mode harness and account::tests::the_whole_chain recorded it as an
unexpected call and failed there only. Neither fake service has a root, so it is ignored.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 22:11:32 +01:00
Ludovic Marie 11b8757bc7 Merge pull request #3386 from horizon-rust/feat/launcher-java-runtimes
🤖 Agent: verified: pr_ready --quick green on the Mac mini (233 launcher tests), CI launcher/verify/parity-scope green; java install 21 checked end to end in scratch homes (Mojang's runtime runs, real folders untouched).
2026-10-28 22:07:50 +01:00
Ludovic MarieandClaude Opus 5.5 b44da42ca4 Launcher: Minecraft: Java Edition instances (model, versions, CLI)
A Java instance is an own instance whose instances.json entry carries a
`java` section: the Minecraft version it plays and, optionally, its own
Java home, memory and JVM arguments. It gets an empty game folder (the
real game keeps its own options); a copy keeps the section.

- minecraft_versions: Mojang's public version_manifest_v2 (releases,
  snapshots, old betas and alphas, latest), kept in the data folder so
  the list shows offline. Only metadata: game files stay owner-gated.
- Play refuses a Java instance with a clear sentence (resolve_version
  and instances::apply, so the window, the CLI and MCP all say it) until
  the launcher starts the real game.
- CLI: instance create <name> --minecraft <id|latest|latest-snapshot>,
  instance versions [--snapshots] [--old], instance java <instance>
  [--home|--auto] [--memory] [--args]; instance list shows the version.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 22:04:34 +01:00
Ludovic Marie 9669600c38 Merge pull request #3323 from horizon-rust/feat/native-hooks-api-build
🤖 Agent: verify green; launcher-only (native_mods.rs, index native.rs). pr_ready green, launcher tests pass. Local parity of #3352's tree (which contains this PR): all affected harnesses pass except online_mode, which fails identically on main on this Mac (environment).
2026-10-28 21:58:51 +01:00
Ludovic Marie 528c322d16 Merge pull request #3321 from horizon-rust/feat/official-mods-turn-native
🤖 Agent: verify green. pr_ready green. Local parity (Mac mini, --changed origin/main): 36 of 37 pass. online_mode fails identically on main on this Mac (a stray GET / to its fake service, IndexError), so it is not this change. Registry test a_mod_may_turn_native_once and launcher test a_mod_that_turned_native_keeps_its_hmod_until_removed pass.
2026-10-28 21:58:46 +01:00
Ludovic MarieandClaude Opus 5.5 a9f8e64331 Native mods: horizon-mixins finds the launcher's cargo
`horizon-mixins apply` runs `cargo metadata`, but the launcher started it as a
plain command: on a computer with no Rust of its own every native build stopped
at E_METADATA. It now gets the launcher's toolchain like the build does.
Found by building zoom end to end with no system Rust, on macOS and Linux.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 21:50:20 +01:00
Ludovic MarieandClaude Opus 5.5 01d0f45c2d Native mods: turn one off in Options > Mods
A native mod's row now toggles too. Off writes `mod.<id>:false`, so the
launcher's next build leaves the mod out, and `native_on` tells the mod to rest
until then; on again writes `true`. The WebAssembly row of the same id is no
longer listed: the native row stands for the mod. Asked by levzzz5154.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 21:46:13 +01:00
Ludovic MarieandClaude Opus 5.5 228af35f2a Launcher: Java runtimes for Minecraft: Java Edition instances
The first piece of Java instances: which Java a version needs and where
it comes from, as other launchers do it.

- java::Need reads a version JSON's javaVersion (major + Mojang
  component); versions from before that field ran Java 8 (jre-legacy).
- Runtimes are found without starting them, from each Java home's
  release file: Mojang's that Horizon downloaded (minecraft/runtime/ in
  the data folder, the one preparing uses) and installed ones (JAVA_HOME,
  the PATH, the usual installer folders).
- pick: an instance's own Java home, else the exact major (Mojang's
  first, never a newer one by itself), else None = download Mojang's.
- install downloads Mojang's runtime through mojang::java_runtime
  (sha1-checked, resumable), refusing on Linux without glibc.
- CLI: horizon-launcher java [list|install <major|component>].

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 21:45:05 +01:00
Ludovic Marie abd1755f95 Merge pull request #2770 from socksprox/fix/2594-allay-travel-flying
🤖 Agent: read the diff at c216561da: `Allay.travel` -> `travelFlying(input, getSpeed())` via a new `travel_flying_speeds`, and `WaterAvoidingRandomFlyingGoal.getPosition` stroll targets. verify green, merges cleanly; the merger lands it once this head's parity run (queued) passes.
2026-10-28 21:42:59 +01:00
degepieandClaude Opus 5.5 c404ef5e2a Goat horns keep their instrument from the server
The client read InstrumentComponent past without keeping it, so every goat
horn a server sent arrived with no minecraft:instrument and its tooltip fell
back to "Ponder" (#3181). Decode the holder like ArmorTrim's: a reference
becomes Component::Instrument(registry id) and is named from the synced
minecraft:instrument registry in item_sync. A direct instrument stays Raw.
The LAN host and creative client send it back by the same id.

The item wire harness gains an "instrument registry" fixture, and its probe
treats Instrument like Trim (a byte-exact id, not a lossy type), so the
round trip is checked byte for byte.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
2026-10-29 01:14:59 +05:00
Ludovic MarieandClaude Opus 5.5 5a2d71d2ea launcher: build native mods with horizon-mixins 0d6b103
levzzz5154's update (optional public visibility for support modules); bundles that
pin their own mixins revision keep it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 21:09:24 +01:00
Hamza CoşkunandClaude Opus 5.5 0e2b244120 Send an armor stand's custom name and its visibility to its clients
The server never sent DATA_CUSTOM_NAME or DATA_CUSTOM_NAME_VISIBLE for an
armor stand, so hologram text didn't show for its clients: single player on
its server thread, LAN guests and dedicated servers. Both are sent now, and go
back to none / false when /data takes them away.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 22:57:42 +03:00
Hamza CoşkunandClaude Opus 5.5 835674f45d Draw an invisible armor stand's equipment, not just skip it
LivingEntityRenderer.submit gives an invisible stand no body render type but
still submits its layers: held items, the custom head and the armour. The
renderer skipped the whole stand instead, so floating heads, item displays and
armour on invisible stands never showed, in-frame and on any server that sends
the invisible flag (vanilla, Paper, Minestom). Only the wooden body is skipped now.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 22:57:42 +03:00
ThatCakeIsAPieandClaude Opus 5.5 85bfbc7d46 upgrade-datapack: docs, and checks against vanilla 26.2 and 26.3
- docs/datapack-upgrade.md: how to run `horizon-port-pack
  upgrade-datapack`, every rewrite and how it is checked, and what is not
  rewritten; linked from docs/README.md and the command's help.
- tools/datapack_upgrade_rosetta.py writes vanilla 26.2's own worldgen out
  as a pack, upgrades it and compares it with vanilla 26.3's file by file:
  registries whose format changed and whose content did not must come out
  identical.
- tools/datapack_upgrade_fidelity.py starts vanilla's 26.2 and 26.3
  dedicated servers on the same seed, the first with a pack and the second
  with its upgrade, force loads the same chunks and compares the saved
  blocks; without packs it measures what 26.3 itself changed.

Both need the 26.2 server jar the player downloaded; nothing is fetched.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 14:57:24 -05:00
ThatCakeIsAPieandClaude Opus 5.5 f983223354 port-pack: upgrade-datapack rewrites features and placements
The fourth part of the 26.2 to 26.3 data pack upgrade.

- Configured features move to `feature/` with `config` unwrapped, block
  state providers take their 26.3 names (a `simple_state_provider` is its
  state), and nested and inline placed features are upgraded with them.
  Feature types 26.3 replaced by compositions of other features are listed
  in the report and left as they are.
- Placement `random_offset` becomes `offset` with one provider per axis
  (x and z both from `xz_spread`).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 14:57:23 -05:00
ThatCakeIsAPieandClaude Opus 5.5 3d39bbdaf1 port-pack: upgrade-datapack rewrites noise settings, dimensions and presets
The third part of the 26.2 to 26.3 data pack upgrade: noise settings in
26.3's form, with the parts 26.2 hard-coded written out as data.

- The beardifier, which 26.2's NoiseChunk added, is added to the final
  density.
- The aquifer router fields move to `aquifers`, whose `exclusion` spells
  out 26.2's `isDeepDarkRegion` (erosion < -0.225 and depth > 0.9) from
  the pack's own erosion and depth; `surface_level` is the preliminary
  surface, and `chunk_surface_level` its per-chunk 16 x 1 interpolation.
- 26.2's ore veinifier becomes two `ore_vein` material rules (copper,
  iron) with OreVeinifier's heights, thresholds, edge roundoff, solidness,
  richness and gap, ahead of the surface rule: 26.2 placed veins while
  filling the noise and its surface pass only replaced the default block.
  The rule is written as a named `material_rule` under the settings' id.
- The climate functions are named, and the spawn target refers to them by
  id (`depth` and `offset` dropped, as vanilla did).
- `size_horizontal`/`size_vertical` are dropped: each `interpolated` holds
  its cell.
- Inline noise settings in dimensions and world presets get the same.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 14:57:23 -05:00
ThatCakeIsAPieandClaude Opus 5.5 2aca6fe06c port-pack: upgrade-datapack rewrites density functions
The second part of the 26.2 to 26.3 data pack upgrade: named density
functions in 26.3's form.

- Inputs are named `input`/`left`/`right`; `invert` is `reciprocal`,
  `y_clamped_gradient` a `gradient` on y, `shifted_noise` a `noise` with
  shifts, `end_islands` `end_outer_islands`, and `constant` its number.
- 26.2's `flat_cache` held each column's value at y = 0, so it becomes a
  `cache` around a `slice` at y = 0 when its input depends on y (through
  the pack's own functions); 26.2's other caches only memoise in 26.3.
- `interpolated` takes its cell from the noise settings whose router
  reaches it (`size_horizontal * 4`, `size_vertical * 4`), which 26.2 read
  from the settings, or vanilla's overworld cell when none does.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 14:57:23 -05:00
ThatCakeIsAPieandClaude Opus 5.5 598668e4bc Far terrain: stop decoding saved chunks for every distant column
SavedLodSampler::grid put saved chunks over every far tile of 16 blocks
a column or finer: for each column it opened the region file and
decoded the whole saved chunk, and each sampling thread kept only 128.
In a save with many thousands of chunks, the far view stalled for many
minutes after its first one.

- SaveDir keeps which chunks each region file holds (10 s), so columns
  over unsaved ground no longer open region files.
- Decoded saved chunks are shared by the sampling threads (1024, 30 s).
- With Reduced far detail, tiles coarser than 4 blocks a column (about
  1 km out and beyond) keep the generator's terrain, which saved chunks
  were made from; edits that far away show when their tiles refine on
  approach. Full keeps saved chunks up to 16 blocks a column, as before.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 14:57:22 -05:00
ThatCakeIsAPieandClaude Opus 5.5 a1a249e2f6 Far tiles: approximate only with Reduced far detail
The previous two commits trade the exact far tile for speed in wide
cells. They now apply only with Reduced far detail (FarDetail::Reduced,
which already "changes the picture"); Full keeps the exact tiles.

- GeneratorWorker::lod_grid takes `approximate`: one biome per 4x4
  columns, the coarser vertical lattice, and density only around the
  surface (with shared 2D planes) happen only when it is set. Without it
  the tile is sampled as before.
- LodSampler::set_approximate, and Lod::set_reduced_detail sets it for
  the sampling threads; tiles sampled after a change follow it.
- lod_grid_bench --approximate samples as Reduced does.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 14:57:21 -05:00
ThatCakeIsAPieandClaude Opus 5.5 eb5e0bb914 Far tiles: sample density only around the surface, share 2D planes
lod_grid sampled the density lattice over the whole build height. It now
finds each tile's surface band on a 64-block lattice made of the fine
lattice's points, then samples the fine lattice from 64 below the lowest
surface to 128 above the highest (and above sea level); below the band a
column is one span of the default block. Thin parts above an air sample
of the coarse lattice (floating islands, overhang lips) can be missed.

Most of a far tile's density work is 2D noise (y-fixed slices), which
the two passes would each compute: SamplerContext::with_planes keeps a
y-fixed slice's plane for its x/z lattice, so the band pass reuses the
coarse pass's. Opt-in, and only far sampling turns it on.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 14:57:21 -05:00
ThatCakeIsAPieandClaude Opus 5.5 d93c3cdd82 Far tiles: one biome per 4x4 columns, a coarser vertical lattice
Approximations for wide far cells, where they are hard to see at their
distance:

- One biome for 4x4 of a tile's columns, the first one asked (its
  surface's), in cells up to 64 blocks wide: far cells don't show a biome
  change a few blocks down, and climate changes over hundreds of blocks.
  Painting asks through the biome zoom, which strays into neighbouring
  quarts; per quart, those strays were most of a tile's climate samples.
- The interpolated density on a vertical lattice of 16 blocks for cells 32
  blocks wide or more, and of 32 from 128, instead of 8.

Distant tiles' biomes and some surface blocks differ from the per-quart
answers, and in the widest tiles the coarser lattice moves the surface.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 14:57:20 -05:00
ThatCakeIsAPieandClaude Opus 5.5 81b3803017 port-pack: upgrade-datapack, a 26.2 data pack's registries in 26.3's formats
Vanilla 26.3 refuses a world generation data pack written for 26.2: the
registries changed format. `horizon-port-pack upgrade-datapack <pack>
(<out> | --world <world>)` reads a pack (zip or folder) and writes it in
26.3's formats, with a report of what it rewrote, what has no faithful 26.3
form and which types it does not know. Each rewrite is the one vanilla's own
data went through between 26.2 and 26.3.

This first part handles the registries that need no context:
- noise parameters: `firstOctave`/`amplitudes` become `base_octave`,
  `octave_count`, `amplitude_modifiers` and the base amplitude of
  `NormalNoise.createParity`, which keeps 26.2's normalisation;
- block states: `{Name, Properties}` become `{id, properties}`, or the id;
- configured carvers move to `carver/` with `config` unwrapped and the cave
  carvers' hard-coded count, thickness and room scale written out;
- biome spawners and spawn probability become 26.3's gameplay attributes;
- structure spawn overrides and the dimension type's bed rule.

Density functions, noise settings and features follow in their own
changes. serde_json gains `float_roundtrip` so the numbers the upgrade
writes back are the ones it read.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 14:57:19 -05:00
ThatCakeIsAPieandClaude Opus 5.5 91ed783e8d Far tiles: resolve each biome quart once per tile
lod_grid paints eight depths of every far column through the surface
rule, and each depth asked the biome resolver again: a full climate
sample and parameter search per query, though neighbouring columns and
depths share quarts. The tile now keeps the answers per quart
(Biomes::Memo), for painting and for the columns' surface biome.

lod_grid_bench, vanilla seed 1234: surface heights, blocks and biomes
are the same at every level. At level 6 (64 blocks a column) some
painted blocks below the surface differ: the tile's cached sampler
context answers a repeated quart from what it sampled before, so the
order of queries mattered there.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 14:57:19 -05:00
ThatCakeIsAPieandClaude Opus 5.5 bc3a724c01 lod_grid_bench: far terrain sampling cost per tile level
Times GeneratorWorker::lod_grid as the client's far terrain calls it (16 x
16 columns a tile) for each tile level, with two checksums: everything the
tiles hold (an exact speedup keeps it) and the surface and water heights
alone (the terrain's shape, which an approximation of materials or biomes
keeps). --dump writes each column's height, block and biome.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 14:57:19 -05:00
Lyle ColeandClaude Opus 5.5 51244a64f6 Decoration: column scans, per-chunk heightmap reads, zoom-free uniform biomes
Three read paths in decoration:
- Speleothem clusters (and large dripstone) scan each column up and down
  for the floor and ceiling, up to 512 blocks, one region read per block.
  FeatureWorld::scan_column walks a chunk's column directly: an all-air
  section is tested once (the predicate is a pure state test), a mixed
  one is read without a section lookup per block. Same blocks, same stop.
- OreFeature's any_height_at_least read each column of its square through
  a region lookup and a checked heightmap read; now each chunk's part of
  the square is read from its heightmap in one pass (any column will do,
  so order is free).
- Placement biome checks ran the biome zoom (eight hashed corner
  distances) for every position. When the 2x2x2 quarts the zoom picks
  from lie in biome sections that each hold one biome, the same one, the
  answer is that biome whatever the zoom picks; per-section uniformity is
  worked out once per region.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 14:57:18 -05:00
Lyle ColeandClaude Opus 5.5 1d25065363 Decoration: cut per-region and per-shape-update setup
Three costs that came before any feature work:
- the biomes present around the chunk sorted every quart of the nine
  chunks' columns into a BTreeSet; a bitset gives the same ids in the same
  ascending order;
- every updateShape call built and dropped a whole Ticks (tick maps,
  deadline index) just to capture the ticks it schedules; capturing
  leaves the rest untouched, so the region keeps one;
- the nine stand-in chunks left in the proto chunks while the region
  holds their terrain were allocated at full height; one section does.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 14:57:18 -05:00
ThatCakeIsAPieandClaude Opus 5.5 22f0e30d07 Block reads: inline the region-to-palette path
A block read in decoration (and the surface stage's reads) went through
out-of-line calls: the region's chunk lookup, TerrainChunk::block,
TerrainBlocks::block and BlockSection::get, whose io::Result error path
kept it from inlining. They now inline into the feature code (the fast
build has no cross-crate LTO, so the core accessors need #[inline]), and
TerrainBlocks reads the section through an infallible accessor: its index
is a local index, always below 4096. #1790 takes the division out of
PackedStorage::read; this leaves storage alone.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 14:57:17 -05:00
Lyle ColeandClaude Opus 5.5 c8f484e040 Surface biomes: keep each quart cell's corner jitter and biomes
The surface stage's per-chunk cell cache only proved cells whose eight
quart corners hold one biome; a block in any other cell ran the full
eight-corner zoom mix and a stored lookup again. Now a mixed cell keeps
its corners' jitter (BiomeZoom::corner_jitter, one mix per cell) and
their eight biomes, so each block of the cell only redoes the distance
comparison (BiomeZoom::select_corner, same arithmetic and tie order); the
cell of the previous lookup is kept too, as columns walk down in y. A
test checks the split zoom against quart_at_block.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 14:57:16 -05:00
ThatCakeIsAPieandClaude Opus 5.5 d4a1ebaa33 Surface rules: compare biome ids, not names
Every solid block of every column asks the rule tree's biome conditions
for its biome. Each one cloned and dropped the biome's Arc<str> (an atomic
count all generator threads share) and looked its name up in a string set.
Queries that number their biomes (the generator's) now answer with the
index, and biome conditions keep a bit per index; others compare names as
before. Same answers: the generator's name -> id map is exact.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 14:57:16 -05:00
Lyle ColeandClaude Opus 5.5 5a75089f61 Biome stage: biome ids by bitset, names by a last-entry memo
Two lookups in the biome stage:
- the biome ids present around the chunk were gathered by inserting
  every quart of the nine neighbour chunks into a BTreeSet; a bitset
  gives the same ids in the same ascending order;
- each quart's biome name was looked up in the name -> id map. Names
  point into the biome source's parameter list and neighbouring quarts
  mostly resolve to the same entry, so the last entry's id is kept and
  the map is asked only when the name changes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 14:57:15 -05:00
Lyle ColeandClaude Opus 5.5 483ad5209c Biome stage: build free neighbour biome chunks before waiting
Each chunk needs the biome chunks of its 3x3 neighbourhood, built once by
whichever thread gets there first (per-chunk work lock). With several
workers on neighbouring chunks, a thread often blocked on a neighbour
another thread was building while other neighbours were still unbuilt.
The stage now first builds (or reads) every neighbour whose lock is free
(try_lock), then waits for the ones still being built. Each biome chunk
is still built exactly once, deterministically; only the order of the
work changes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 14:57:15 -05:00
Lyle ColeandClaude Opus 5.5 91369d11c5 Structure column checks: sample the column's climate in one volume
couldStructureExistInColumn (jigsaw projection, onTopOfChunkCenter,
temples, mansions) samples the column through createResolverForChunk:
one bulk climate volume of 1 x n x 1 quarts, then each quart from the
bottom. Horizon took one scalar climate sample per quart instead, and a
column with no valid biome walks all of them, so a few such start chunks
held most of the structure stage while neighbours waited on them. The
column is now one prepared volume through the biome stage's resolver
path, as vanilla does.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 14:57:14 -05:00
ThatCakeIsAPieandClaude Opus 5.5 82eeba7753 Structure checks: keep each worker's single noise-biome samples
Structure placement asks the biome of one quart at a time: several
structures test the same quarts of a start chunk, and column checks walk
whole columns when no quart matches. Each answer is a full climate sample
and parameter search. Workers now keep the answers, up to 65536 quarts;
the generator is seed-bound, so they cannot go stale.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 14:57:14 -05:00
ThatCakeIsAPieandClaude Opus 5.5 c6efb367b0 launcher: drop an unused import in the Windows make_link
9f1fa0a0a moved the mklink call into junction(), which imports
CommandExt itself; make_link kept its own copy, unused, so the
launcher's clippy with warnings denied fails on Windows.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 14:57:13 -05:00
degepieandClaude Opus 5.5 2acf1d7798 World list: show Hardcore Mode in red, and adventure and spectator by name
The Select World list built its info line from a creative flag only, so a
hardcore world read "Survival Mode" and adventure and spectator worlds read
"Survival Mode" too (#3298). LevelSummary.createInfo uses `gameMode.hardcore`
in CommonColors.RED (-65536) for a hardcore world, else `gameMode.` + the
GameType's name, then commands and the version. WorldEntry now carries the
level's game mode and hardcore flag (both already read from level.dat), and
`world_info` builds that line as styled text. The horizon-ctl JSON keeps its
`creative` / `mode` fields as before.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
2026-10-29 00:48:01 +05:00
Jose Gil c9bd057d70 feat(launcher): add Spanish language support 2026-10-28 15:43:54 -04:00
Ludovic MarieandClaude Opus 5.5 868256f1a8 The batch train builds Nightlies: no more vX.Y.Z game releases
Step 5 of Nightly only (Ludovic, 2026-10-28). release.yml keeps its triggers and its decision
(5 player-facing pull requests settled 5 minutes, or the oldest 6 hours, release-now,
skip-release, RELEASES_PAUSED, release-blocker, a commit where checks and parity-main passed),
counted since the commit the published Nightly was built on. When it decides, it starts
`nightly build` on that commit (`base`) instead of building vX.Y.Z:

- release_train: the last release of the game is the published `nightly`; one Nightly build at a
  time and at most 12 a day (nightly_set's cap), except for release-now and force (`urgent`).
- nightly-build.yml: an `urgent` input passes the cap; the notes say the launcher installs it.
- docs: release-flow (Game releases, Nightly only, Notes, Rolling back), versioning.

The last vX.Y.Z stays GitHub's latest for launchers and servers from before the switch.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 20:41:56 +01:00
Ludovic MarieandClaude Opus 5.5 1149c92cb8 Native mods: a settings page in Options > Mods
Native mods list themselves (native_mods) and give their settings page's
buttons (native_settings, native_settings_click); their keys show as Key
Binds rows and the player's choice is kept as the WebAssembly mods' keys
were (key.<id>.<name> in horizon-mods.txt). A native mod takes over the
WebAssembly mod of the same id, whose row says so.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 20:39:52 +01:00
Jose Gil 8339f6d12f fix(launcher): sync clipboard on Linux Wayland and X11 2026-10-28 15:34:15 -04:00
damnurmumandCodex 2ae5640704 Preserve styled item names and component shadow colors
Keep the original custom-name component as the sole stored value, deriving plain text for gameplay and rendering its styles in tooltips, held labels, and item links. Honor shadow_color in parsed text and HUD glyph passes. Extend the Java/Rust tooltip parity fixture with styled names; its fixture format builds on PR #1702 by RVZO6.

Co-Authored-By: Codex <[email protected]>
2026-10-28 22:31:12 +03:00
Ludovic MarieandClaude Opus 5.5 41266ae70b Native mods: the Native mods instance gets the mods' settings from the default home
Mods installed in the launcher's default home build into the Native mods
instance, which starts empty: a player moving from the WebAssembly Zoom,
Minimap, FPS Counter or Keystrokes to its native version lost their
settings there. The build now copies each mod's storage folder and key
lines from the default home into the instance, once: nothing the instance
already has is overwritten.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 20:25:34 +01:00
degepieandClaude Opus 5.5 de25583ee5 perf_bench: compile the driver mod with JAVA_HOME's javac
The vanilla runs start `$JAVA_HOME/bin/java`, but the driver mod was
built with whatever `javac` came first on PATH. With JAVA_HOME on a JDK 25
and an older default JDK on PATH (21 on Ubuntu 24.04, for example),
`javac --release 25` fails with "release version 25 not supported" before
any game starts. Use JAVA_HOME's javac when JAVA_HOME is set, as the java
lookups below already do; without it, PATH is used as before.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
2026-10-29 00:21:18 +05:00
Kacpherek 2f95ce431c Follow the furnace code to furnace.rs on main; answer Fox.getBreedOffspring 2026-10-28 20:15:39 +01:00
Kacpherek 0536ebbe98 Merge remote-tracking branch 'origin/main' into tools/port-ledger 2026-10-28 20:11:46 +01:00
Shreyas 0320e7d593 Piston head: destroy fitting base without drops in creative mode
In creative mode, playerWillDestroy on an extended piston head destroys
the base piston without dropping resources, matching vanilla Java Edition
PistonHeadBlock.playerWillDestroy.

Fixes #3317
2026-10-29 00:39:39 +05:30
damnurmumandCodex dd130f1902 Render player heads and atlas sprites in text components
Parse Minecraft object components into inline spans and draw player heads and atlas sprites with the existing HUD atlas. Resolve head profiles through the skin cache and preserve fallback text for plain output.

Co-Authored-By: Codex <[email protected]>
2026-10-28 22:07:28 +03:00
matydenandClaude Opus 5.5 8170b8742b Rabbits and wild ocelots run from players
Rabbits fell back to the generic goal list, and ocelots had no goals
of their own, so neither fled. Rabbit.registerGoals now registers its
three RabbitAvoidEntityGoals (players within 8, wolves within 10,
monsters within 4, never for the killer bunny) with panic at 2.2.
Ocelot.registerGoals plus reassessTrustingGoals gives ocelots
OcelotAvoidEntityGoal (players within 16), OcelotTemptGoal, the leap
and attack goals and their chicken and baby turtle targets.

Not ported yet: rabbit RaidGardenGoal and ClimbOnTopOfPowderSnowGoal,
and ocelot trust, so every ocelot is wild.

Refs #2780

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 21:05:31 +02:00
Kacpherek 59538c7dc1 Map the remaining vanilla methods: commands, network, packs, advancements and the last ten 2026-10-28 20:05:02 +01:00
Ludovic MarieandClaude Opus 5.5 4e804cd813 Native mods on the hook points build on any Horizon that has them
A .hnmod may name the native hook points' version it uses ("hooks": 1 in
horizon-native.json) instead of pinning a commit. The launcher builds such
mods on the commit the other mods pin, else on the installed version's,
once that commit's NATIVE_HOOKS_API (read from GitHub) matches; else, with
no pin, on the commit they were tested on. Its own copy of each such mod
is re-pinned to the build's commit for horizon-mixins. So Horizon's
official mods build together, on the player's own game, with no
re-release for each Nightly. The mods feature is passed only when the
source still has it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 19:46:58 +01:00
Shreyas 2ac85c50eb lan: register default size 1 for AbstractCubeMob metadata 2026-10-29 00:11:11 +05:30
Ludovic MarieandClaude Opus 5.5 e2f3d1a85a Registry: a WebAssembly mod's entry may turn native once
An official mod moving to native hook points switches its registry entry
from `mod` to `native-mod`. Review now allows that one change (never
back): the entry's old `.hmod` versions stay published and are still
checked as `.hmod`s, while the index lists only its `.hnmod` versions.

The launcher's update to the native version keeps the player's `.hmod`:
the game runs it until a native build has the mod, then skips it.
Removing the mod removes both files.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 19:15:38 +01:00
ThatCakeIsAPieandClaude Opus 5.5 24e316855e Create World: the Data Packs screen
The More tab's Data Packs button opens vanilla's PackSelectionScreen (the
resource pack screen's port, titled dataPack.title) on a new mcworld-
folder: its packs, with their icons, and the built-in one, re-listed when
the folder changes. Create New World copies the folder's packs into the
world's datapacks/ and writes datapacks.json with the selection, as
CreateWorldScreen.copyTempDataPackDirToNewWorld does. Cancel deletes the
folder. Worlds made without opening the screen are unchanged.

The pack screen parity harness also runs the screen under dataPack.title.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 12:50:12 -05:00
Hector Eccles e41443c64c fix: address review findings
- palette lookups only match live slots, not freed ones still on the free list (#3253)
2026-10-28 13:42:04 -04:00
Muhammad Asif 1a9040ad79 Swing the arm when opening containers
MultiPlayerGameMode.useItemOn swings on InteractionResult.SUCCESS
(SwingSource.CLIENT). The remote client only swung when the held item
was a block, so opening a chest, barrel, furnace, or similar storage
did not start the arm animation.
2026-10-28 22:33:23 +05:00
Kacpherek 2c10febff1 issue: read registry names, so a mooshroom report finds MushroomCow 2026-10-28 18:27:10 +01:00
RVZO6andCodex 61713b8359 Fix menu separator texture sampling
Use both rows of the separator textures when tiling headers and footers, including the create-world screen.

Co-Authored-By: Codex (GPT-6.1 Terra) <[email protected]>
2026-10-28 13:16:26 -04:00
Kacpherek a32e0ac4ec Add a vanilla-parity skill: start parity fixes from the port ledger 2026-10-28 18:09:19 +01:00
Hector EcclesandDeepSeek V4.1 Flash 22a4620643 Build beacon beam sections on server clients
A chunk's block entity data carries a beacon's levels and powers but not
its beam sections, so in the default server-thread path and on LAN or
multiplayer clients the sections stayed empty and no beam was drawn.
Tick beacons with ClientLevel.tickBlockEntities' beam pass, without the
effects, sounds and advancement the server owns.

Co-Authored-By: DeepSeek V4.1 Flash <[email protected]>
2026-10-28 12:54:44 -04:00
Hector EcclesandDeepSeek V4.1 Flash 8422225047 Keep beacon beams visible past the render distance
Remember each active beacon's beam sections when its chunk unloads, so
far terrain LOD keeps drawing the beam until the chunk loads again and
its block entity decides whether it is still there.

The cache only runs with far terrain on, and clears on world, dimension
or LOD changes.

Co-Authored-By: DeepSeek V4.1 Flash <[email protected]>
2026-10-28 12:54:44 -04:00
Kacpherek fd8f1a02a1 Map client screens, toasts, recipe book and world screens 2026-10-28 17:43:08 +01:00
Kacpherek a1cf544de7 map-check: accept full Class#method keys 2026-10-28 17:29:51 +01:00
Kacpherek 8d8737a266 Map client rendering, particles, sounds and resources 2026-10-28 17:29:16 +01:00
Kacpherek c045245e8e Map client packet handling, entity renderers and models, block renderers
Also let PORT_LEDGER_NO_MAP build without the verdict map, to measure the
automatic matches on their own.
2026-10-28 16:54:18 +01:00
Kacpherek 91358d8088 Map the rest of the game logic: classes with no Rust trace, then method batches 2026-10-28 16:26:00 +01:00
Hector EcclesandOpenCode d46bed5c36 perf: hash the world's integer bookkeeping maps with Fx
World.modified, inhabited, chest_openers and block_names used std SipHash
on small integer keys, and modified/inhabited rehash as they grow while an
area unloads. Use the existing crate::fast_hash maps; none of their
iterations is order-sensitive (save order comes from the batch list and
ChunkExtras::block_entity_order). chunks_with_block_entities takes the
names map by hasher so the faster map still fits.

BlockEntities::map stays std HashMap: crate::lan::chunks::body takes the
default-hasher map by type and lan/** is outside this change.

Co-Authored-By: OpenCode (DeepSeek V4.1 Flash) <[email protected]>
2026-10-28 11:07:24 -04:00
Hector EcclesandOpenCode 9b9d045150 perf: find section palette ids with a scan and a last-state cache
BlockSection kept a HashMap of state ids per section (24 tables and
allocations per proto chunk) and hashed on every worldgen write. Resident
palettes are almost always tiny, and the resident container already scans
them; use the same scan with a one-entry last lookup so a run of one state
is a compare. set/compact behaviour and the palette, counts and free list
are unchanged.

Co-Authored-By: OpenCode (DeepSeek V4.1 Flash) <[email protected]>
2026-10-28 11:07:24 -04:00
Hector EcclesandOpenCode 1e5cc9031f perf: share the multiply-shift index between paletted storage and PackedStorage
PackedStorage divided by its per-word width on every read and write, while
PalettedContainer already used an exact multiply-shift. Move MAGIC/locate/
mask into storage.rs and use them from both; only storages past 16,384
entries keep the division path.

Co-Authored-By: OpenCode (DeepSeek V4.1 Flash) <[email protected]>
2026-10-28 11:07:24 -04:00
Hector EcclesandOpenCode ee351caed8 perf: mark only the distinct sections a block write dirties
A write looped 27 block offsets through the dirty-section map; all but a
few hashed the same keys. Mark the exact section set instead: one key in
the interior, the adjacent section on a section face, a neighbour column
on a chunk border, and the diagonal where both borders meet (at most eight
keys). The mesher still sees the same 3x3x3 neighbourhood.

Co-Authored-By: OpenCode (DeepSeek V4.1 Flash) <[email protected]>
2026-10-28 11:07:24 -04:00
SolomonNumb1 9d7b4e8a2d tooltip fix 2026-10-28 20:03:12 +05:00
some-du6eandClaude Opus 5.5 f52af64e89 Spears: the jab animates in first and third person
A spear's left click is a STAB swing (SwingAnimationType 2), but every
swing was drawn as a WHACK. Port SpearAnimations.firstPersonAttack,
thirdPersonAttackHand and thirdPersonAttackItem, and keep a swing's type
for mobs and other players (from ClientboundSwingAnimationPacket) and
the local player's model.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 09:02:21 -06:00
Senjin 8ebcf6c13d WIP HDR: ask the compositor for the window's luminances (log only) 2026-10-28 16:00:54 +01:00
masa-masa-0314andZCode 60773a0e92 Dialog: send the show and clear packets the commands promise
/dialog show and /dialog clear ran, printed their feedback and told
nobody: the dialog state they set was never read, and nothing built
ClientboundShowDialogPacket or ClearDialogPacket. DialogCommand's own
ServerPlayer.openDialog/closeDialog send the packet to each target, so
the command now resolves the argument (a minecraft:dialog registry entry
by index, or the inline dialog as sent) and sends it to every target:
the host's own player opens it on the screen, LAN guests get the packet
through the hub.

Co-Authored-By: ZCode <[email protected]>
2026-10-28 22:21:00 +08:00
Claude 0291a5864f Look up players' teams by name for client-side pushes on servers
remote_pushes found teams through mob_teams, which keys entities by UUID
text and the local player by PLAYER_NAME. On a server, players are team
members by profile name (Player.getScoreboardName), so neither lookup hit
and every player pushed the local player, even on teams with
collisionRule never. The server applied no push and corrected the
position, so walking into players jittered (seen on Hypixel).

Use the profile name for the local player and the tab list name for other
players; other entities keep their UUID.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_018ksHJwFp11RfYk6S9xnyCf
2026-10-29 00:13:19 +10:00
Mik00000 08b9765278 Ensure mob effect IDs are namespaced so tipped arrows tick damage on living mobs 2026-10-28 16:09:27 +02:00
Kacpherek 969749da3d Map 13 game-logic batches and add map-check
map-check checks verdict-map files against the last build without
rebuilding, so several mappers can check their own rows at once.
2026-10-28 14:42:02 +01:00
Ludovic MarieandClaude Opus 5.5 d2e7a54082 rustfmt: wrap horizon-client's re-export of the render modules
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 14:32:21 +01:00
SenjinandClaude Opus 5.5 380d5ebe9f HDR: its settings get a screen of their own
Paper White and Peak Brightness sat among the display settings, where players
without an HDR display took them for settings of their own display. Display now
has only HDR Settings..., greyed where the display path offers no HDR output (the
renderer tells the settings when it opens its surface, `graphics::offer_hdr`).
It opens Graphics: HDR: the HDR switch, then its sliders, greyed while it is off.

Where HDR isn't offered, `horizon-graphics.txt`'s HDR settings are left as saved
and not used: the renderer already keeps HDR off there, and the screen no longer
opens, so a game folder copied from another computer, or a display swapped for
an SDR one, shows nothing that does nothing; an HDR display brings them back.

The screen leaves room for a calibration step and an in-game shortcut to it
later. horizon-ctl opens it as `options graphics-hdr`.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 13:58:55 +01:00
SlaVKsVolksandOpenAI Codex GPT-6.1 Terra 54360a74e3 fix(parity): normalize Windows dependency records for selection
Port the production path portability repair developed in Beyond onto original Horizon. Keep stored records and narrowed-root annotations intact; cover legacy local/shared records, changed probes and oracles, and reachability.

Co-Authored-By: OpenAI Codex GPT-6.1 Terra <[email protected]>

Co-Authored-By: OpenAI Codex GPT-6.1 Sol <[email protected]>
2026-10-28 09:58:22 -03:00
Kacpherek 5a8b9687e5 Document class homes, structural matches and the verdict map 2026-10-28 13:53:58 +01:00
Kacpherek 0f7036f9b1 Map the first three batches: animals, inventory screens, world generation 2026-10-28 13:53:58 +01:00
Kacpherek 12c503e154 Port ledger: class homes, structural matches and the verdict map
Give every vanilla class its Rust home files, weighed from all its matches.
In those files, match a method to the fn that touches the same fields and
reaches the ports of the same callees. Read tools/port_map/*.tsv, where a
person or agent records a verdict for what nothing else finds (port, inline,
missing, skip with a reason), and list the next unanswered batch with todo.
Count field-only constructors as trivial and fold initializer lambdas into
<clinit> and <init>.
2026-10-28 13:53:58 +01:00
Ludovic MarieandClaude Opus 5.5 a7efe597ea refactor: the client's world data, rendering and screens become horizon-client-render
80 modules of horizon-client's lib (chunks, meshing, light, streaming, gfx, the HUD,
menus, settings, resource packs, particles, block entity data) and its 8 WGSL shaders move
to crates/horizon-client-render with git mv. Nothing in them reaches gameplay. horizon-client
re-exports each under its old path, so horizon_client::gfx and the rest are unchanged.

Beyond the moves: ParsedModels, lod's Column::reduced/push and the spawner's Next,
Spawner.next and Selector.current become pub; Properties::for_tests sits behind a
test-support feature; crash.rs strips the new crate's path; tools and docs point at the
new paths, and three harnesses that hash every client source hash the new crate too.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 13:23:12 +01:00
Kacpherek 2dd963a539 Document the port ledger's new matching, gaps and commands 2026-10-28 13:16:02 +01:00
Kacpherek cd42938b88 Port ledger: bytecode calls, wider matching, drift, issue and check
Read the game jar's bytecode for exact call targets, constants and ids, and
resolve Rust calls through the receiver's type. Match more methods: same-named
fns in an impl or file named for the class (the impl type must fit the class),
and callees of known pairs linked through the call graph. Leave trivial methods
out of coverage.

New gap kind drift: a port that never names a sound, particle, item, block,
effect or game event its vanilla method uses, or lacks one of its rare numbers.
Seam gaps on the old in-frame path sort last; maths and geometry callees no
longer make seam or callee gaps.

New commands: issue (the classes, Rust code and gaps a bug report points at),
check (what a branch touches, gaps it opens and closes against its base), diff,
and a searchable offline report.html.
2026-10-28 13:16:01 +01:00
Ludovic Marie f70501aa42 Merge branch 'refactor/cut-biome-spawn-parsers' into refactor/horizon-client-render 2026-10-28 13:13:50 +01:00
Ludovic Marie d0a29a0f66 Merge branch 'refactor/furnace-cooking' into refactor/spawner-hive-traits 2026-10-28 13:13:03 +01:00
SenjinandClaude Opus 5.5 9c18b4e524 HDR: the log says where the HDR screenshot went
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 13:09:30 +01:00
SenjinandClaude Opus 5.5 70e3fe006d Screenshots are named in local time, and the log says where they went
screenshot_name worked the date and time out from the Unix clock by hand, in
UTC; vanilla's Util.getFilenameFormattedDateTime is local time, as Horizon's own
log is, so a screenshot taken at 12:28 in CET was named 11.28 (and near midnight
elsewhere, the day before). The log now also has the full path of a saved
screenshot; vanilla's chat line, which names only the file, is unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 13:07:27 +01:00
SenjinandClaude Opus 5.5 18c23a6df3 HDR: UI Brightness, and the HUD and menus never lift
Pixels counted as the world when they matched it darkened evenly (the vignette),
and grey text over the white sun matches that: menu text in front of the sun was
lifted to the peak. Once the world is kept for the last pass, the frame's alpha
is now zeroed; the hand, HUD and menus raise it where they draw (the vignettes
leave it), and after the hand a blend pass scales its alpha down to 1/255. The
last pass lifts nothing covered, keeps the hand at the world's brightness (it's
the player's, not UI), and scales the rest by UI Brightness.

UI Brightness (Display, 100% by default) is the HUD and menus as a share of paper
white, so pure white labels needn't sit at the world's white. At 100% the picture
is as before.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 12:57:35 +01:00
Shreyas 43f2497f1f Sulfur cubes: synchronize size metadata and adult egg spawns
Include sulfur cubes in AbstractCubeMob size metadata serialization and
remote data handling so adult cubes retain their size across the network.
Adult sulfur cubes spawned from spawn eggs now correctly render and act
as adults rather than defaulting to size 1 babies.

Vanilla reference: AbstractCubeMob.ID_SIZE, SulfurCube.setSpawnSize.

Fixes #3151
2026-10-28 17:13:45 +05:30
Stepan Bogomazov cd97b34460 Build the sunset fan once a frame instead of in every sky pixel
While the sunrise/sunset glow shows (about 11500-14500 and 22500-500),
sunrise_weight rebuilt the fan's 16 triangles for every pixel of the
fullscreen sky pass, with four sin/cos per triangle. The triangles depend
only on the glow's alpha and the sun's horizon, so they are now built on
the CPU and passed in Globals as normals; the shader's Moller-Trumbore
reduces to dot products, and only triangles whose rim normals change sign
along the ray are tested in full. The weights are unchanged (a test checks
them against the old per-pixel code).

Sky pass at 1920x1080, dawn: Intel UHD 730 10.2 ms -> 1.8 ms (0.6 ms by
day), RTX 3060 0.47 ms -> 0.27 ms.

Fixes #3143
2026-10-28 14:27:17 +03:00
Mik00000 445183a7df Display mob inside spawners and trial spawners (fixes #3107) 2026-10-28 13:22:28 +02:00
Shreyas 380d71d8a8 lists: fall back to offline resolution when services do not answer in tests 2026-10-28 16:31:23 +05:30
SenjinandClaude Opus 5.5 c7b02f8568 HDR: screenshots get an HDR10 copy, as a PNG tagged BT.2020 with PQ
F2 kept only the SDR frame. With HDR on it now also writes `…-hdr.png` beside
vanilla's screenshot: the HDR pass drawn once more into a 10-bit PQ target of its
own (so scRGB swapchains get one too), read back and saved as a 16-bit PNG with
the cICP chunk of PNG's third edition (BT.2020, PQ, full range) and cLLI (peak,
paper white). Each 10-bit value keeps its top bits below it, so 0 stays 0 and
1023 becomes 65535. Vanilla's screenshot is written as before.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 11:54:07 +01:00
Mik00000 85f53198ce Allow bows and crossbows to shoot spectral and tipped arrows (#3071) 2026-10-28 12:37:54 +02:00
SenjinandClaude Opus 5.5 5479b0362f HDR: a black level setting, for displays whose black isn't 0
HDR output told the display its content goes down to 0 nits, which only an OLED
shows. Black Level (Display, 0 by default) is the display's darkest black: the
last pass lifts the shadows onto it with ITU-R BT.2390's black level lift in PQ
(the peak stays, the lift fades out by the fourth power above black, so midtones
barely move and 100 nits and up don't), and the HDR metadata gives it as the
minimum luminance. At 0 the picture is exactly as before.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 11:32:28 +01:00
Shreyas 0543cf0345 Select the picked hotbar slot in singleplayer and LAN
Preserve the guest's updated hotbar selection when restoring the host
player state after an action. The previous restore sequence discarded
the guest's new selection while keeping the inventory changes.

The held-slot response now uses the selected slot produced by the pick,
so integrated-server and LAN players select the picked block or spawn
egg. Keep the host's selection independent of the guest's selection.

Vanilla reference: ServerGamePacketListenerImpl.tryPickItem,
Minecraft.pickBlockOrEntity, and ClientPacketListener.handleSetHeldSlot.

Fixes #3031
2026-10-28 16:00:22 +05:30
Kacpherek ef304f30c9 Document the port ledger 2026-10-28 11:23:50 +01:00
Kacpherek e36fd24008 Add the port ledger: vanilla methods next to the Rust functions that port them
tools/port_ledger.py indexes the decompiled sources and the Rust tree into .local/port-ledger/ledger.json and answers where/what/class/coverage queries both ways, with the gaps where a port isn't wired like vanilla (seam, client, class, callee, stale).
2026-10-28 11:23:50 +01:00
NetuosoDEVandOpenAI Codex 818a2ec52a Align the carried block between the enderman hands
Cancel item-model centering in the vanilla carried-block pose. Add a regression test using point coordinates captured from Minecraft 26.3 CarriedBlockLayer.submit, including a transformed entity root.

Implemented by OpenAI Codex on behalf of NetuosoDEV. NetuosoDEV tested the updated Linux binary in game and confirmed the fix works.

Co-Authored-By: OpenAI Codex <[email protected]>
2026-10-28 12:04:11 +02:00
Mik00000 13e441e235 Fix invisible item frames and custom head skins 2026-10-28 12:00:31 +02:00
Kacpherek e3fda9ce67 Keep golems built by a player from attacking players
A built iron golem was never marked player created, so it targeted its builder. Vanilla sets PlayerCreated on build (saved in NBT) and IronGolem.canAttack refuses players, which every target goal honours.
2026-10-28 10:43:10 +01:00
NetuosoDEVandOpenAI Codex f852985693 Fix enderman carried block textures and item tints
Render the carried block through the inventory item model pipeline so every quad uses its proper texture and tint source. Remove the single-sprite cube renderer and let the item pipeline center the model.

Implemented by OpenAI Codex on behalf of NetuosoDEV. NetuosoDEV confirmed the fix works in game.

Co-Authored-By: OpenAI Codex <[email protected]>
2026-10-28 11:40:04 +02:00
Kacpherek 03f3fb057d Show thrown eyes of ender in the hitbox view
F3+B drew every entity list but the eyes in flight, so a thrown eye of ender had no box. EntityType.EYE_OF_ENDER is 0.25 by 0.25 and never turned.
2026-10-28 10:35:17 +01:00
SenjinandClaude Opus 5.5 8502d86e24 tools/hdr_gallery.py: an HDR test gallery of every block that gives light
Writes a datapack into a world: hdr:gallery (one or two of each family) and
hdr:gallery_full (every block with a light level, in its brightest state), each
alone in dark stone tunnels, after hdr:setup holds the world at midnight with no
weather, mobs or random ticks. To judge and tune how light sources shine in HDR
output (#1441) under one standard; the block list comes from the prepared
resources, so it follows the pinned version.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 10:19:33 +01:00
SenjinandClaude Opus 5.5 41d3890a3c Revert "HDR: bright lifts from darker pixels"
Only a soul lantern needed it, and it lifted less for its light level, not for
its texture: that is better fixed where the lift is worked out.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 10:14:15 +01:00
malordinandClaude Sonnet 5.5 abdff5c89d Launcher: tell a prepared version without the language list by pack.mcmeta
`lacks_languages` counted the files in `assets/minecraft/lang` and took one or none as
"English alone". The client jar carries two there, `en_us.json` and `deprecated.json`,
so a real prepared version never counted as lacking and the languages #1594 copies from
the asset store were never added to one prepared before it, or by a preparer that could
not read the store. The test's fixture held `en_us.json` alone, which hid it.

The list the language screen offers, `pack.mcmeta`, is only in the asset index, so its
absence is what says the languages are missing. The fixture now has `deprecated.json`.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-28 13:50:26 +05:00
AlexanderandHermes Agent 3fe4834ceb Creative hotbar: say 'Saved' only when it was saved
hotbar::save's error was dropped and the action bar showed "Saved hotbar N"
anyway. Log the failure and skip the message, as saveHotbar does by setting
it inside the try.

Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
2026-10-28 11:39:17 +03:00
hashtheboardandClaude Opus 5.5 b78ff5f55e client: aim every crosshair target along one ray (VR groundwork)
Game::aim() returns the eyes and the view direction; pick, pick_fluid
and every *_hit go through it instead of rebuilding the pair. A tracked
controller can later stand in for it. No behaviour change.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 09:38:16 +01:00
AlexanderandHermes Agent be8b86d470 Scoreboard: report a failed write instead of dropping it
save_scoreboard wrote scoreboard.dat and world_border.dat with `let _ =`, so
a full disk or a read-only world lost objectives, scores and the border with
nothing in the log. Log the failure the way command storage and boss bars
already do.

Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
2026-10-28 11:37:19 +03:00
SenjinandClaude Opus 5.5 2f13cb3ac6 HDR: bright lifts from darker pixels
The bright tier's window goes to 0.04-0.28, from a round in the HDR gallery: a soul
lantern's glass (light level 10) now lifts, and the level-15 bright blocks still look
right.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 09:34:28 +01:00
AlexanderandHermes Agent f755d7fba0 Launcher: write the server list beside it and rename
servers.json was written in place, so an interrupted write left a truncated
list and every server vanished from the sidebar. Write servers.json.tmp and
rename it, as the launcher's other state files do.

Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
2026-10-28 11:18:41 +03:00
AlexanderandHermes Agent 9d9e1a97a8 rustfmt
Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
2026-10-28 10:58:47 +03:00
AlexanderandHermes Agent 1743bbc525 Tests: read the session lock after releasing it
LockFileEx refuses a read through another handle while the lock is held, so
reading session.lock between acquire and drop fails on Windows with error 33.
The exclusivity assertion stays; the bytes are read once the lock is released.

Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
2026-10-28 10:47:04 +03:00
Kacpherek cd2fea1f02 Throw projectiles with the thrower's known movement
Pearls, snowballs, eggs, splash potions, tridents, wind charges and arrows added the server's stored velocity of the player, which for a guest can hold knockback or a fall from ticks ago, so some throws dove. Projectile.shootFromRotation adds getKnownMovement: the move the client last sent.
2026-10-28 08:38:46 +01:00
AlexanderandHermes Agent b08e04cd8a Port publish: a body names the diff store with Posix separators
str(Path) keeps the host's separators, so a Windows run wrote
`~\code\horizon-parity\...` into the published body while every other line
of that body is the same on every machine. as_posix() makes it stable.

Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
2026-10-28 10:16:00 +03:00
Kacpherek 99008baad9 Hold items at the middle of a slim arm
A player with a slim skin held items at the wide arm's hand, half a pixel off the 3-pixel arm. PlayerModel.translateToHand shifts the arm by 0.5 toward the hand before the item's offset, as SkeletonModel does by 1.
2026-10-28 07:24:11 +01:00
Kacpherek e1bc2cbea3 Show banner patterns on shields sent by a server
The banner_patterns and base_color components were read past on the wire, so a stack from a server never carried them and its shield drew plain. Decode both (reference patterns; direct ones stay raw) and write them back, as vanilla's BannerPatternLayers and DyeColor stream codecs do.
2026-10-28 07:24:11 +01:00
hashtheboardandClaude Opus 5.5 c9a8361493 render: draw from a ViewSet of per-eye views (VR groundwork)
The renderer now builds its camera from `View` (rotation, lens, aspect,
tan(fov/2)) gathered in a `ViewSet`: one view on a flat screen, and the
view culling and back-face removal use. A headset can later supply one
view per eye without touching draw_frame's consumers.

No visible change: the products keep their order (projection * pose *
rotation), and a test pins the screen view to the old formula bit for bit.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 07:05:51 +01:00
iLostName 7b93b310a0 death: the player hears their own death sound on a server and in single player
LivingEntity.handleEntityEvent 3 plays it for the dying player; the server's goes to the others. Fixes #3021.
2026-10-28 06:52:29 +01:00
iLostName 0112a6feac item pickup: one sound per item, played by the client at the item
The server sent its own pickup sound beside TakeItemEntity, so single player heard two. Fixes #3029, fixes #2861.
2026-10-28 06:52:27 +01:00
SenjinandClaude Opus 5.5 f52e94cfb9 docs: glowing particles shine in HDR now; translucent blocks don't yet
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 06:41:34 +01:00
Kacpherek 9d32bebad3 Show the happy particle when feeding a baby horse on a server
AbstractHorse.handleEating draws it on the client's own copy of the mob; Horizon drew it only on the server's level, which a server's client never sees. Same for llamas and camels.
2026-10-28 06:26:23 +01:00
Kacpherek 345f64e47c Import the Sounds trait for the elytra sound 2026-10-28 06:23:27 +01:00
Kacpherek 8f84c2cd48 Play the elytra's flying sound while gliding
Horizon had no ElytraOnPlayerSoundInstance, so gliding and diving were silent. The loop starts when the player starts gliding, fades in after a second, and follows the player's whole speed, so a straight dive is the loudest.
2026-10-28 06:16:05 +01:00
Kacpherek e24394ca3a Drop the hand after placing or using an item on a server
The server-client use path never called LocalPlayer.itemUsed, so the held item stayed up after a placement. Vanilla drops it after a swinging useItemOn that spent the item (or in creative) and after every successful useItem.
2026-10-28 06:09:40 +01:00
Kacpherek a5ee1e588c Leave a server at once instead of waiting for the reader thread
On Windows a socket shutdown doesn't wake a read already waiting, so Disconnect froze the window until the server closed the connection. Vanilla closes the connection and moves on.
2026-10-28 05:59:37 +01:00
Kacpherek e0a48b3f66 Document the 2..128 render distance range 2026-10-28 05:56:49 +01:00
Kacpherek d35efa25ec Accept render distances up to 128 from the command line and horizon-ctl
--render-distance and horizon-ctl render-distance check against the same cap as the options.
2026-10-28 05:56:48 +01:00
Kacpherek b8b15b1682 Raise the render distance cap from 64 to 128 chunks
The options slider and options.txt now accept 2..128. 128 is where the server protocol's view distance (an i8) tops out.
2026-10-28 05:56:48 +01:00
Kacpherek 05f8ec650d Pass the safety notice in the direct connection test
Multiplayer now opens SafetyScreen first, so the test proceeds through it.
2026-10-28 05:45:51 +01:00
時穂andCodex 2702542c73 launcher: complete Chinese strings and strengthen translation checks
Fill the 17 missing Microsoft sign-in, resource setup and Nightly strings.
Scan Rust tokens for multiline translation calls and require shared catalog
keys in every language, including labels translated through widgets or constants.

Co-Authored-By: Codex <[email protected]>
2026-10-28 13:37:54 +09:00
SenjinandClaude Opus 5.5 113d694a87 HDR: very bright lifts over bright's window, a step above bright
Very bright's window (0.88 to 0.94) was tuned on the sun alone: on a block
only the hottest pixels reached it, and they blew out while the rest
stayed below bright. It now lifts over bright's window with the whole
headroom; the sun keeps its yellow corona near the horizon.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 05:22:02 +01:00
asownerytandClaude Opus 5.5 f1212a0e5a Beds: occupied while someone sleeps in them, and sleeping villagers are saved
LivingEntity.startSleeping sets the bed head's OCCUPIED and stopSleeping
clears it (the foot follows through updateShape):

- Villagers send MobEvent::BedOccupied when they lie down and get up
  (brain, hurt, bed gone, kicked out, death), and keep the bed they lie
  in as sleeping_pos.
- A player's bed is marked when it lies down (try_sleep) and freed in the
  level's tick once nobody sleeps in it any more, which covers waking, the
  Leave Bed button, death, a broken bed and a guest leaving; the server's
  last save (and closing an in-frame world) frees every player's bed, as
  PlayerList.removeAll does.
- AbstractBedBlock.useWithoutItem: an occupied bed wakes the villager in it
  (kickVillagerOutOfBed) or says "This bed is occupied".
- LivingEntity's sleeping_pos is saved and loaded, so a villager asleep at
  a save is still asleep in its occupied bed when the world opens again.
- The in-frame player gets up when its bed is gone (checkBedExists), and a
  client that leaves a server while asleep is no longer stuck in bed.

lan_e2e: bed-occupied and sleep-reload.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 14:24:58 +13:00
asownerytandClaude Opus 5.5 df4877a10b lan_e2e: sleep-villager, a bed wakes its villager and the sleeper's body shows
The guest uses a bed a villager has lain down in: the villager gets up and
the guest stays up, then the next use lies down. `status` reports whether
the world view draws the player's own body (`own_body`): yes in bed, in
first person too, and no once up again in first person.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 14:01:21 +13:00
asownerytandClaude Opus 5.5 cc575e2c6a Sleeping: villagers lie on the pillow, the player's body shows, beds wake villagers
- A sleeping villager is moved toward the foot by its standing eye height
  (LivingEntityRenderer's getEyeHeight(Pose.STANDING)), not by the 0.2 of
  its sleeping box, so its head lies on the pillow instead of past the bed.
- The player's own body is drawn while sleeping, in third person and in
  first person (LevelExtractor skips the camera entity unless it sleeps),
  lying the way the bed faces.
- Using a bed a villager sleeps in wakes the villager and keeps the player
  up (AbstractBedBlock.kickVillagerOutOfBed), instead of both sharing it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 14:01:21 +13:00
DanielGodriNeto e14d6f79b1 perf(assets): keep decoded block sprites on disk between launches 2026-10-27 20:38:51 -03:00
feeldev 752fc2170a fix(client): ConnectScreen as 26.3 draws it, with Cancel
Joining a server showed a flat dark fill with the status and the address.
ConnectScreen draws the menu background over the blurred panorama, the
status centred at height / 2 - 50, and a 200-wide Cancel at height / 4 +
132 that aborts the connection and goes back; Escape does nothing there
(shouldCloseOnEsc is false). Once logged in, the LevelLoadingScreen's
Loading terrain takes over until the player is placed.
2026-10-27 18:05:11 -05:00
feeldev 356b69aea7 fix(client): the window keeps drawing while single player's server starts
Minecraft.doWorldLoad shows the LevelLoadingScreen and keeps running the
client while its server prepares the world. Horizon's window waited on the
server thread's ready message instead, frozen (not responding on Windows)
for the whole load. The server's answer is now polled each frame from
resume_world_entry, the LevelLoadingScreen's title is drawn meanwhile,
and the window joins once the server is ready. Runs without a window and
the synchronous harness and control runs still wait for it, as before.
2026-10-27 18:02:04 -05:00
Ludovic MarieandClaude Opus 5.5 827bd7f609 Launcher: test which versions pruning deletes
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 23:32:54 +01:00
Ludovic MarieandClaude Opus 5.5 f049a08e48 packs: keep the available packs in one id order, as PackRepository does
PackRepository.discoverAvailable puts the packs of every source into a
single TreeMap by id, so the folder's file/ packs sort among the built-in
ones rather than after them.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 23:02:52 +01:00
Ludovic MarieandClaude Opus 5.5 3e930fc74b Launcher reference: the language setting lists zh
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 22:55:39 +01:00
feeldev fdefcfeac0 perf(client): read the loot tables while joining a server
On a server's world the loot tables were first read on the game thread by
the first block placed (StepContext's loot), a ~0.6 s freeze on a phone.
They are now read on a helper thread as the connection starts, like the
world screens' preload does for local worlds, and kept from then on.
2026-10-27 16:54:03 -05:00
Ludovic MarieandClaude Opus 5.5 04ec03745d Launcher Chinese: drop the coming-soon strings main removed
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 22:52:52 +01:00
Ludovic MarieandClaude Opus 5.5 6a6b5957eb Launcher Chinese: the nightly strings main added since
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 22:52:29 +01:00
Hector EcclesandDeepSeek V4.1 Flash a96f66ce1e Replay vanilla's small End island placement
The hash estimator placed islands with the right density but different
roots, so an estimate could vanish when the real chunk arrived. Reproduce
the feature's decoration seed, step-local index and placement modifiers
instead, run the feature on a no-op world to keep the random stream in
sync, and use those exact origins for the far cone. The per-cell biome
sampling is no longer needed; the feature's biome filter decides.

Co-Authored-By: DeepSeek V4.1 Flash <[email protected]>
2026-10-27 17:52:07 -04:00
Hector EcclesandDeepSeek V4.1 Flash b561f83220 Draw small End islands in LOD terrain
The small_end_islands biome's only feature is end_island_decorated, so
unexplored End void rendered empty beyond the refined rings. Carry the
cell biome for void direct samples and estimate the feature: rarity and
count from its placement modifiers, in-square positions, the 55..70
height range, and the vanilla end-stone cone.

Vanilla reference: EndIslandFeature (BasicFeature::EndIsland and
minecraft:end_island_decorated), already ported in horizon-world.

Co-Authored-By: DeepSeek V4.1 Flash <[email protected]>
2026-10-27 17:52:07 -04:00
Ludovic Marie 8c18d9429f Merge remote-tracking branch 'origin/main' into cg24/f2795 2026-10-27 22:50:42 +01:00
Ludovic MarieandClaude Opus 5.5 150a29974d Merge main into fix/burn-in-daylight-list
lan.rs mob_meta now builds all shared flags on main: its on-fire bit uses
is_on_fire, so a fire-immune mob never reports burning. game.rs keeps main's
charged-blaze flames with is_on_fire.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 22:43:53 +01:00
Ludovic MarieandClaude Opus 5.5 f5f5532c19 Merge main into fix/goat-horns
Kept the goat horn use next to main's bundle use in use_item.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 22:41:36 +01:00
Kacpherek 06fe3e8e69 Raise the bounce game event when a player bounces
A slime block or bed bounce made no vibration. Vanilla's Entity.restituteMovementAfterCollisions posts GameEvent.BOUNCE after bounceOn (Entity.java:891); the bounce motion event was dropped.
2026-10-27 22:40:48 +01:00
Kacpherek 158d99a528 Raise entity_mount and entity_dismount game events
Riding a vehicle and getting off it made no vibrations, so sculk sensors and wardens never heard it. Vanilla's startRiding and removeVehicle post entity_mount and entity_dismount at the vehicle (Entity.java:2533, 2573); derived here from the vehicle a rider had last tick.
2026-10-27 22:40:48 +01:00
Kacpherek 98da8f82b8 Enforce max_command_forks in /execute
/execute had no limit on how many sources its modifiers produce. Vanilla's BuildContexts ends the chain with command.forkLimit when the sources would reach the max_command_forks rule (silently after a forking modifier).
2026-10-27 22:40:48 +01:00
Kacpherek 465832b0e4 Honour global_sound_events for the wither, dragon and portal sounds
The wither spawn roar was a local sound at the wither, and none of the three global events checked the rule. Vanilla sends them as global level events (heard everywhere) only with global_sound_events on; without it they are plain level events the client has no sound for (ServerLevel.globalLevelEvent).
2026-10-27 22:40:48 +01:00
Kacpherek 0318c132eb Let projectiles break chorus flowers and decorated pots
Arrows, snowballs and the other impact projectiles never broke either block. Vanilla's onProjectileHit destroys them (a pot is cracked first) when projectiles_can_break_blocks is on and the shooter may break blocks (Projectile.mayBreak).
2026-10-27 22:40:48 +01:00
Ludovic MarieandClaude Opus 5.5 eff8ae41c6 Merge main into fix/death-advancement-messages-to-guests
Kept system_message_to next to main's overlay_message in lan/hub.rs.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 22:39:30 +01:00
Ludovic MarieandClaude Opus 5.5 4d0962e611 Merge main into fix/fog-lava-and-night-vision
Kept fog_night_vision next to main's level_flat/level_min_y and main's
flat-world dark disc.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 22:38:12 +01:00
Kacpherek cb5743dcc8 Format
cargo fmt.
2026-10-27 22:36:44 +01:00
Kacpherek 632179bda6 Format
cargo fmt.
2026-10-27 22:36:44 +01:00
Kacpherek eea99bb586 Fix clippy
Return the arrow hit result directly.
2026-10-27 22:36:18 +01:00
Kacpherek 1944dd45e4 Fix display light borrow and format
display_light took &self while text displays were borrowed mutably; it now takes the world. Also rustfmt.
2026-10-27 22:35:25 +01:00
Ludovic MarieandClaude Opus 5.5 09043cd199 trident: only the owner picks up its trident while the owner is in the level
ThrownTrident.playerTouch hands the touch to AbstractArrow only when the
player owns the trident or the owner is gone (getOwner() == null). Without
this check any guest standing on another player's landed or returning
trident took it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 22:34:28 +01:00
Kacpherek c5dead28a0 Draw the poppy an iron golem offers
IronGolemFlowerLayer draws a poppy at the right arm while getOfferFlowerTick() is above 0, with NO_OVERLAY; Horizon only ran the offer, never drew it.
2026-10-27 22:33:06 +01:00
Kacpherek 15099724ed Draw the item a fox holds in its mouth
FoxHeldItemLayer puts the main hand item at the head's position, rolled and turned by the render state, in the ground display context; Horizon drew nothing.
2026-10-27 22:33:06 +01:00
Ludovic MarieandClaude Opus 5.5 69bb386a6d Merge main into dyed-armor-colour
main sends a wolf armor's damage and components in the body slot; the
worn armour's stacks go beside them, and the body slot never takes the
helmet's stack.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 22:33:03 +01:00
Kacpherek 52a820e708 Match vanilla's shadow radius for armadillos, elder guardians, happy ghasts and shulkers
ArmadilloRenderer 0.4, ElderGuardianRenderer 1.2, HappyGhastRenderer 2.0 and ShulkerRenderer 0.0; Horizon used the 0.5 default for all four.
2026-10-27 22:32:59 +01:00
Kacpherek 85f2a9ad87 Draw an enderman's carried block as its baked model
CarriedBlockLayer submits the block's model on the unit cube. Horizon drew a cube of one face sprite, centred half a block off, so grass lost its sides and flowers became cubes.
2026-10-27 22:31:30 +01:00
Kacpherek 11b36e3867 Read and write saved NBT without the node cap
Chunks, entities, POI and .dat files were capped at 1M nodes, so a chunk with thousands of full containers would not load. Vanilla reads them with NbtAccounter.unlimitedHeap().
2026-10-27 22:31:27 +01:00
Kacpherek 382cc39713 Save and restore previousPlayerGameType
ServerPlayer.storeGameTypes writes the previous game mode (the switcher's return mode) next to playerGameType; Horizon never wrote it, so a fresh save dropped it.
2026-10-27 22:30:45 +01:00
Kacpherek 16d71b0a24 Cast a giant's shadow at 3 blocks
GiantMobRenderer passes 0.5 times its scale of 6 as the shadow radius; the default 0.5 was used.
2026-10-27 22:30:06 +01:00
Kacpherek f83310a891 Shake a skeleton while it freezes into a stray
Skeleton.isShaking is its freezing conversion, synced as DATA_STRAY_CONVERSION_ID; Horizon never sent or read it, so the shake never showed.
2026-10-27 22:30:06 +01:00
Kacpherek f351bcb06b Keep a map's banner and frame markers when it is saved
Horizon wrote empty banners and frames lists, so a vanilla map lost its markers on the first save. They are kept as read; a locked copy keeps the banner markers (MapItemSavedData.locked).
2026-10-27 22:29:58 +01:00
Kacpherek bbe656e4ce Use the monsters_burn window for sunlight burning
Undead burned whenever sky darkness was below 4, including the End and storms with a bright sky. Vanilla's Mob.isSunBurnTick requires the gameplay/monsters_burn attribute: overworld only, true from tick 23460 to 12542 (day.json).
2026-10-27 22:29:48 +01:00
Kacpherek 1861c82434 Make TNT, TNT minecarts and End crystal blasts honour the drop decay rules
They always used plain destruction. Vanilla's ExplosionInteraction.TNT picks decay or plain destruction from tnt_explosion_drop_decay, and BLOCK (End crystals, the dragon respawn spikes) from block_explosion_drop_decay (ServerLevel.getDestroyType, EndCrystal.java:102, DragonRespawnStage.java:64).
2026-10-27 22:29:48 +01:00
Ludovic MarieandClaude Opus 5.5 44448d941f Merge #2706 into #2927: the unchanged-block skip reads the column cache
#2706 lands first; with #2927's surface column cache the check reads the
cached state instead of decoding the section.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 22:29:15 +01:00
Kacpherek 8ba30576d1 Back up an unreadable player file and fall back to its .dat_old
A player file that failed to load was written over with no copy. Vanilla copies it to <uuid>_corrupted_<time>.dat and loads <uuid>.dat_old (PlayerDataStorage.load).
2026-10-27 22:29:07 +01:00
Kacpherek 8f26771053 Fall back to level.dat_old and keep an unreadable level.dat
An unreadable level.dat was treated as a new world and overwritten. Vanilla reads level.dat_old instead and moves the bad file to level.dat_corrupted_<time> (LevelStorageSource.restoreLevelDataFromOld).
2026-10-27 22:29:07 +01:00
Ludovic MarieandClaude Opus 5.5 2ee238e5f1 Merge #2931 into #2924: both add vanilla 26.3 options to options_ui.rs
The controls list and the mouse list sit side by side; #2931 lands
first.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 22:28:53 +01:00
Kacpherek 33921460cc Save the world border in the overworld's data folder
Horizon wrote data/minecraft/world_border.dat at the world root, which vanilla ignores. Vanilla's ServerLevel keeps it in each dimension's data folder; the old root file is still read as a fallback.
2026-10-27 22:28:04 +01:00
Ludovic Marie 50ebc9de60 Merge remote-tracking branch 'origin/main' into cg24/f2875
# Conflicts:
#	README.md
2026-10-27 22:27:57 +01:00
Ludovic MarieandClaude Opus 5.5 9fcfdaa1ea Portal arrivals: rustfmt, and name vanilla's portal ticket
resolve_arrivals keeps its own doc comment; the area an arrival keeps is
vanilla's TicketType.PORTAL, which has no ENTITY type.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 22:27:47 +01:00
Ludovic MarieandClaude Opus 5.5 3be828d012 Merge main into #2813: reach first, then spawn protection, then the border
handleBlockBreakAction refuses a dig out of reach before it looks at spawn
protection; ServerLevel.mayInteract (spawn protection, then the world
border) comes after.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 22:27:32 +01:00
feeldev d480ce24dd fix(client): book text runs and copies its click commands and shows its hover text
BookViewScreen.handleClickEvent: a run_command click closes the book
(closeContainerOnServer, then clickCommandAction with no screen after it)
and runs the command; copy_to_clipboard copies and leaves the book open;
suggest_command does nothing, as BookViewScreen has no insertText. The
page's words show their show_text hover event as a tooltip
(HoveredTextEffects.TOOLTIP_AND_CURSOR), as the chat does.
2026-10-27 16:27:30 -05:00
Ludovic MarieandClaude Opus 5.5 8237c15741 Merge bubble-column-entities (#2723) into flowing-water-mobs
Both add a LAN check at the same place.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 22:26:58 +01:00
Kacpherek 295871fcab Turn dirt to mud with a water bottle
Right-clicking a water bottle on mud-convertible blocks started a drink, only dispensers swapped. Vanilla (PotionItem.useOn) pours it out onto any side but the underside and sets mud.
2026-10-27 22:26:57 +01:00
Kacpherek 7711bfcdc5 Fail a dialog whose body or input cannot be read
Dialog.DIRECT_CODEC rejects the whole dialog for an unreadable body or input, a single option with no or two initial entries, or a text default longer than its limit. Horizon dropped the bad part and showed the rest.
2026-10-27 22:26:43 +01:00
Kacpherek 2ac08efcb2 Draw dialog sliders, dropdowns, item bodies and dialog lists
The dialog screen skipped number range and single option inputs, item bodies and the entries of a dialog_list, so a server menu showed only its Back button. Draw them as DialogScreen does: a slider and a cycle button, the item widget with its description, and one button per listed dialog (external title, opens it). The client now keeps the server's dialog tags to resolve a #tag list.
2026-10-27 22:26:43 +01:00
Kacpherek 559b83626f Open links from dialog buttons through the link confirmation
DialogScreen hands an open_url to Screen.clickUrlAction (Web Links, Prompt on Links, ConfirmLinkScreen over the screen the dialog leaves). A suggest_command has no screen to take text; change_page is logged as an error.
2026-10-27 22:26:43 +01:00
Kacpherek c2915208fd List built-in packs before folder packs in the pack screen
PackRepository keeps its sources in order (built-ins, then resourcepacks/), each sorted by id. Horizon sorted every id together.
2026-10-27 22:26:43 +01:00
Kacpherek 04bc30606d Take a texture's .mcmeta only from its own pack or a higher one
FallbackResourceManager ignores a .mcmeta from a pack below the one that supplies the file. Horizon used the top .mcmeta of any pack, so an animation could run on a pack's static PNG.
2026-10-27 22:26:43 +01:00
Kacpherek 6893bcff88 Cap server command suggestions at 1000
ServerCommandSuggestionsProvider keeps the first 1000 suggestions of a request; Horizon sent them all.
2026-10-27 22:26:43 +01:00
Kacpherek aae98d3db5 Send vanilla's error text for an invalid login name
There is no multiplayer.disconnect.invalid_name key. Vanilla's validState throws and Connection.exceptionCaught sends disconnect.genericReason with the exception.
2026-10-27 22:26:43 +01:00
Ludovic MarieandClaude Opus 5.5 6bb648db1e Controls: the three switches nothing reads yet show as unavailable
Operator Items Tab, Quit Shortcuts and Control-click are stored and saved
like vanilla's, but no code reads them yet: grey them out like the other
placeholders instead of offering a switch that does nothing.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 22:26:24 +01:00
Ludovic MarieandClaude Opus 5.5 b4de9dc23c Leave the current's push to main's
main pushes mobs with the fluid current since #2554 (kacpherek, in
update_fluids, with Entity.isPushedByFluid: fish, axolotls, turtles,
frogs and swimming drowned stay put); with both, a mob was pushed twice and
the water creatures were pushed too. The flow-push LAN check stays: it
covers main's push.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 22:26:24 +01:00
Ludovic MarieandClaude Opus 5.5 4a4cad6864 Merge main into flowing-water-mobs
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 22:26:24 +01:00
Kacpherek fece4c6a76 Run function commands at function-permission-level
Function lines parsed at whatever level the caller had, so a tick or load function did nothing in a world without cheats and an owner's function could run /stop. Vanilla compiles every function line at the library's permission (function-permission-level, 2 elsewhere).
2026-10-27 22:26:19 +01:00
Kacpherek a0b19a4820 Stop a dedicated server whose tick runs past max-tick-time
max-tick-time was read and never used. Vanilla's ServerWatchdog (DedicatedServer.initServer, when the value is above 0) logs and writes a crash report once a tick is that far past its due time, then exits with status 1.
2026-10-27 22:26:19 +01:00
Kacpherek 02b1d1f430 Pause on lost focus only after 500 ms without it
Horizon paused the moment the window lost focus. Vanilla's pauseIfInactive waits until focus has been missing for more than 500 ms, so a brief focus steal does not pause.
2026-10-27 22:26:19 +01:00
Ludovic MarieandClaude Opus 5.5 75c149afde Leave the bubble column push to main's
main pushes mobs in a bubble column since #2555 (kacpherek, Entity's
applyEffectsFromBlocks after the move); with both, a mob got the push
twice. The bubble-column LAN check stays: it covers main's push.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 22:26:17 +01:00
Kacpherek ae43424882 Place lily pads and frogspawn on the water aimed at
The aim ray skips water, so these items could not be placed on the surface. Vanilla (PlaceOnWaterBlockItem.use) uses a source-only fluid ray and places on the position above the block it meets.
2026-10-27 22:26:13 +01:00
Ludovic MarieandClaude Opus 5.5 6f785d1a28 Merge main into bubble-column-entities
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 22:26:11 +01:00
Ludovic MarieandClaude Opus 5.5 e1d825312f Read the flat world's horizon from WorldGen's preset
main's dark disc and void fog read WorldGen.flat, which is now the preset
(seen by sirrayi's review).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 22:25:52 +01:00
Ludovic MarieandClaude Opus 5.5 5ea61ed3d8 Merge main into world-types
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 22:25:45 +01:00
Kacpherek b910cdd257 Draw block and item display entities on clients
A client only had an arm for text displays. DisplayRenderer draws a block display's model or an item display's item under the same billboard and transformation, with the brightness override or the light at the entity.
2026-10-27 22:25:37 +01:00
Kacpherek 3934833369 Send text, block and item display data to clients
Summoned displays reached clients as bare entities, so a text display showed nothing. Vanilla syncs Display's entries 8 to 22 and each type's own (text 23 to 27, block state 23, item and context 23 and 24) from the saved compound.
2026-10-27 22:25:37 +01:00
Kacpherek d5ae9945fa Honour can_break and can_place_on in adventure mode
Adventure players could never place or break. Vanilla (Player.blockActionRestricted, ItemStack.useOn) allows it when the held item's can_break matches the block, or can_place_on matches the clicked one.
2026-10-27 22:25:25 +01:00
Ludovic MarieandClaude Opus 5.5 231b309634 Name the forced terrain ring where the protos are kept
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 22:24:48 +01:00
Ludovic MarieandClaude Opus 5.5 2466ccec98 Write and read the anger only for the NeutralMobs that keep it
anger_end_time and angry_at are NeutralMob's: here the wolf, iron golem,
polar bear, enderman and zombified piglin. The bee keeps its own anger and
a piglin's is a brain memory. Every other mob wrote anger_end_time: -1,
which vanilla never writes for them, and a bee's value from a vanilla save
was replaced by it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 22:24:33 +01:00
Ludovic MarieandClaude Opus 5.5 d3121cea67 Merge main into neutral-mob-anger
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 22:24:27 +01:00
Kacpherek c20247d708 Hurt mobs walking on magma blocks
Only the player took hot floor damage. Vanilla (MagmaBlock.stepOn) deals 1 hot_floor damage to any living entity on the block that is not sneaking.
2026-10-27 22:22:51 +01:00
Ludovic MarieandClaude Opus 5.5 f1a8cc6a0d Merge main into join-without-rescanning-registries
main moved hover_rows out of chat.rs and added UiState::new; the resource
id cache sits after it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 22:22:46 +01:00
Kacpherek 08947cd922 Burn mobs standing in a lit campfire
Only the player took campfire damage. Vanilla (CampfireBlock.entityInside) hurts any living entity in a lit campfire for 1, or 2 for a soul campfire.
2026-10-27 22:22:36 +01:00
Kacpherek 0e18c68b4d Hurt mobs with the lightning_bolt damage type
A strike hurt mobs as generic damage, which bypasses armour and wolf armour and loses the is_lightning loot condition (a struck turtle dropped no bowl). Vanilla (Entity.thunderHit) uses lightning_bolt.
2026-10-27 22:21:37 +01:00
Kacpherek 621473f60e Send the ominous item spawner to guests and draw its floating item
The spawner existed only on the server, so a client never saw the item grow and turn. Vanilla tracks it with DATA_ITEM; the client draws OminousItemSpawnerRenderer and runs tickClient's particles.
2026-10-27 22:21:29 +01:00
Kacpherek 1b0b0a8149 Hurt crowded mobs with cramming damage
The cramming roll was drawn but nothing hurt the mob. Vanilla (LivingEntity.pushEntities) deals 6 cramming damage, 1 in 4 ticks, above max_entity_cramming - 1 pushers who are not riding.
2026-10-27 22:21:18 +01:00
Kacpherek 2b2dc1929f Show tipped and spectral arrows to guests and clients
A server sent every arrow as a plain one. Vanilla sends a spectral arrow as its own type and a tipped arrow's ID_EFFECT_COLOR, and the client draws arrow_spectral.png and sheds the effect swirls (Arrow.tick) and sparkles (SpectralArrow.tick).
2026-10-27 22:20:33 +01:00
Kacpherek 052dac817a Drop a guest that misses a keep-alive for 15 seconds
The server waited 30 s for the answer. Vanilla disconnects with a timeout when the last keep-alive is still pending 15 s after it was sent; 30 s is only the read timeout.
2026-10-27 22:19:12 +01:00
Kacpherek ac22c4db04 Apply tipped and spectral arrow effects to the mobs they hit
Arrows built from a bow or crossbow carried no effect and mobs never got one. Vanilla (Arrow, SpectralArrow doPostHurtEffects) applies the pickup item's potion contents at its duration scale, or Glowing for 200 ticks.
2026-10-27 22:19:06 +01:00
Kacpherek e204d3c028 Drink a potion's custom effects at its duration scale
A stack's potion_contents lost custom_effects and custom_color on load, and drinking used scale 1. Vanilla (PotionContents.onConsume) applies the potion and custom effects at potion_duration_scale.
2026-10-27 22:18:00 +01:00
asownerytandClaude Opus 5.5 d321b656f6 Horses, donkeys, mules and llamas sync their rolled max health
finalizeSpawn's randomizeAttributes, setOffspringAttributes and a loaded
save wrote the rolled max health (and a horse's speed) straight into the
mob's stats instead of the attribute bases, so the server never sent them
and a rider's client drew the type's 53 health: 26 hearts. They are bases
now (AttributeInstance.setBaseValue), synced like vanilla's.

Fixes #2869

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 10:14:00 +13:00
Kacpherek 5dd3bf02d3 Tick thrown tridents on a server that has no player of its own
tick_tridents quit when there was no local player, so a server never moved, hit with, stuck or returned a trident. It now finds the owner and pickers among the level's players (guests), returns Loyalty tridents to the thrower and lets guests pick them up; the spawn packet carries the thrower's id, as Projectile.getAddEntityPacket does.
2026-10-27 22:08:18 +01:00
Kacpherek 7bfc737d81 Fire item_durability_changed when a player's item takes damage
Vanilla's ItemStack.applyDamage fires ITEM_DURABILITY_CHANGED for the owning player, with the stack as it was before the damage. Tools, weapons, armour, elytra, shield, mace, brush and the warped fungus on a stick now fire it, which lets ride_strider complete. The durability and delta ranges are not read; no advancement uses them.
2026-10-27 22:06:27 +01:00
Kacpherek 2e8c8e1dc7 Fire effects_changed when the player's effects change
Vanilla's ServerPlayer.onEffectAdded, onEffectUpdated and onEffectsRemoved fire EFFECTS_CHANGED; the axolotl is the source of the Regeneration it gives (Axolotl.applySupportingEffects). This lets all_effects, all_potions and kill_axolotl_target complete. Changes are seen once per tick, so one undone within the tick does not fire.
2026-10-27 22:06:27 +01:00
Kacpherek 727bd7f2dc Fire nether_travel when the player returns from the Nether
Vanilla's ServerPlayer keeps enteredNetherPosition when the player goes into the Nether and triggerDimensionChangeTriggers fires NETHER_TRAVEL with it on the way back to the Overworld. This lets fast_travel complete.
2026-10-27 22:05:42 +01:00
Kacpherek e54cf96271 Fire ride_entity_in_lava while riding a vehicle through lava
Vanilla's ServerPlayer.trackEnteredOrExitedLavaOnVehicle fires RIDE_ENTITY_IN_LAVA each tick the vehicle is in lava, measured from where the ride entered it. This lets ride_strider_in_overworld_lava complete.
2026-10-27 22:05:24 +01:00
Kacpherek 47bb5c668f Fire fall_after_explosion for a fall after a wind charge blast
Vanilla's ServerPlayer.onExplosionHit remembers the blast and trackStartFallingPosition fires FALL_AFTER_EXPLOSION when the player then falls. This lets who_needs_rockets complete.
2026-10-27 22:05:16 +01:00
Kacpherek a90e3bc36d Fire fall_from_height when a fall from high up ends
Vanilla's ServerPlayer.resetFallDistance fires FALL_FROM_HEIGHT with the position the fall started from (trackStartFallingPosition). This lets fall_from_world_height complete.
2026-10-27 22:05:06 +01:00
Kacpherek 3dc958611c Fire recipe_crafted when a crafting, stonecutter or smithing result is taken
Vanilla's ServerPlayer.triggerRecipeCrafted passes the recipe and the stacks that went in; ResultSlot, StonecutterMenu and SmithingMenu award it. This lets trim_with_any_armor_pattern and craft_decorated_pot_using_only_sherds complete, and smithing and stonecutter results now unlock their recipe too.
2026-10-27 22:02:48 +01:00
Kacpherek 33c15a6d79 Fire player_generates_container_loot when a loot roll is opened
Vanilla's RandomizableContainer.unpackLootTable fires it for chests, trapped chests and chest minecarts, and BrushableBlockEntity does for the first brush of suspicious sand or gravel. This lets loot_bastion and salvage_sherd complete; opening marks the roll unpacked so it fires once.
2026-10-27 22:02:31 +01:00
Kacpherek 305ffc4e39 Send End portal frame fill effects to nearby players 2026-10-27 22:02:24 +01:00
Kacpherek 42388d2512 Swap items with shelves and powered shelf chains 2026-10-27 22:02:24 +01:00
Kacpherek cac7fa95fd Show the End gateway spawn burst and sound 2026-10-27 22:02:24 +01:00
Kacpherek 2deaab2602 Render items stored in shelves 2026-10-27 22:02:24 +01:00
Kacpherek 1db835fa0c Apply arrow weapon knockback enchantments 2026-10-27 22:02:19 +01:00
Kacpherek 797482183c Show tutorial hints and track their progress 2026-10-27 22:02:18 +01:00
Kacpherek 60c5d9fefd Apply falling stalactite damage and landing sounds 2026-10-27 22:02:18 +01:00
Kacpherek ca28a0edad Show items revealed by brushing suspicious blocks 2026-10-27 22:02:18 +01:00
Kacpherek f6209003de Fire summoned_entity for built golems and the respawned dragon
Vanilla fires SUMMONED_ENTITY in CarvedPumpkinBlock.spawnGolemInWorld for players within 5 blocks of the new golem, and in EnderDragonFight.setRespawnStage(END) for the new dragon's fight players. This lets summon_iron_golem and respawn_dragon complete.
2026-10-27 22:02:13 +01:00
Kacpherek 736842d40f Unlock the Distortion Effects slider
The option was greyed out and always 100 percent. The slider now moves and saves screenEffectScale, which vanilla's Options.screenEffectScale scales camera shakes and the nausea effects by.
2026-10-27 22:01:26 +01:00
Kacpherek ba37fa75d6 Add the Directional Audio and Music Toast sound rows
Vanilla's SoundOptionsScreen has both next to Show Subtitles and Music Frequency. Nothing in Horizon's audio code reads them yet, so they are greyed out. Audio Device is left out: Horizon opens the default output and cannot switch it.
2026-10-27 22:01:14 +01:00
Kacpherek e9fbcfef78 Add the Online options page with Allow Server Listing
Vanilla's OnlineOptionsScreen has Allow Server Listing, which ClientInformation carries to servers, and the notification and presence options. Horizon has no friends list or Realms, so those three rows are greyed out.
2026-10-27 22:00:52 +01:00
Kacpherek 206b7acee4 Add the Skin Customisation options page and hide skin layers
The seven skin layers and Main Hand were saved but had no page. Vanilla's SkinCustomizationScreen lists them; the layers hide on the player model and reach servers in ClientInformation (Options.createInformation), and other players' layers follow Avatar.DATA_PLAYER_MODE_CUSTOMISATION.
2026-10-27 22:00:31 +01:00
Kacpherek 222dc28be9 Open the saddle and armour screen on a tame nautilus
Shift + use on a tame nautilus returned without opening anything. Vanilla AbstractNautilus.mobInteract opens NautilusInventoryScreen (saddle and body armour, no chest), and E while riding it opens the same screen. The mount menu now covers nautilus with its slot layout, texture, armour sprite and saddle sound.
2026-10-27 21:59:47 +01:00
Kacpherek 98d0c6d1a3 Show the server's Code of Conduct and wait for the answer
Horizon accepted every server's Code of Conduct without showing it. The text now opens on a screen with Acknowledge and Disconnect, and the server waits for the answer. The accepted text's hash is kept per server in servers.dat, as vanilla does (acceptedCodeOfConduct, String.hashCode).
2026-10-27 21:59:47 +01:00
Kacpherek 81be26d686 Accept flat_all_dimensions as a dedicated level-type
26.3's world presets include minecraft:flat_all_dimensions, so the dedicated server's level-type list accepts it. It still generates normal terrain.
2026-10-27 21:59:46 +01:00
Kacpherek 1a8d0ef420 Make unknown loot conditions and number providers agree
Unknown loot conditions fail and unknown number providers give 0 in both loot engines. The block engine used to pass conditions and give 1. location_check stays true there, as it has no world access at the offset.
2026-10-27 21:59:46 +01:00
Kacpherek 41f60562fd Add /datapack create
/datapack create <id> <description> fell into the disable branch and replied with an empty unknown-pack key. It now writes <world>/datapacks/<id>/ with a data folder and a pack.mcmeta, and replies with vanilla's success and failure keys (DataPackCommand.createPack).
2026-10-27 21:59:46 +01:00
Kacpherek 1deb3bd36c Send decorated pot sherds to LAN guests
Decorated pots showed no sherds for LAN guests. A pot is not a block entity here, so the hub and the chunk encoder read the saved tag for it and send the sherds, loot table and item (DecoratedPotBlockEntity.getUpdateTag, saveCustomOnly). Inserting into a pot marks it for update, and a client stores a pot update with the chunk.
2026-10-27 21:59:46 +01:00
Kacpherek afd40fdbfa Add the structure set's locate_offset to /locate
/locate adds the set's locate_offset (buried treasure's [9, 0, 9]) to the chunk corner, as StructurePlacement.getLocatePos does.
2026-10-27 21:59:46 +01:00
Kacpherek 511f2c42d8 Apply loot set_components so trial armour keeps its trim
Trial chamber equipment tables put a copper trim on chainmail and iron armour with set_components (SetComponentsFunction), which Horizon ignored. The trim now stays on the mob's worn armour, is saved with the mob and goes onto its drops.
2026-10-27 21:59:46 +01:00
Kacpherek 136b352e4f Give 15 vanilla damage types their own /damage kind
/damage fell back to the Generic kind for cramming, falling_anvil, falling_block, fireworks, generic_kill, lightning_bolt, out_of_world, outside_border, sonic_boom, spit, stalagmite, starve, sulfur_cube_hot, wind_charge and wither. Each now has its own kind and follows the vanilla damage type tags. Creaking, Warden and Wither ask "bypasses invulnerability" instead of "is Generic".
2026-10-27 21:59:46 +01:00
Kacpherek 2ad7b68710 Send the trial spawner mob to LAN guests
Trial spawners never showed their mob for LAN guests. The update tag now carries the spawn data the mob is drawn from, as TrialSpawnerStateData does. next_mob_spawns_at is left out: the client never reads it and it depends on the block state.
2026-10-27 21:59:46 +01:00
Kacpherek 2607d06f94 Use a saved world's preset for new chunks
New chunks always used minecraft:normal, so saved amplified, large-biome and single-biome worlds got normal terrain. The preset is now read from the world's overworld generator (noise settings and biome source), and far decoration uses it too.
2026-10-27 21:59:46 +01:00
Kacpherek ff1b01616f List every online player in /list
/list printed a fixed "1 of a max of 8" with only the host. It now lists the host of a single-player or LAN world first, then the placed guests in join order, with the real count and the hub's max, as ListPlayersCommand does. /list uuids shows each name with its UUID. A single-player owner is already a guest of the in-process server and is not listed twice.
2026-10-27 21:59:46 +01:00
Kacpherek 203fb2db89 Send the vault display item to LAN guests
Vaults never showed their display item for LAN guests. The update tag now carries the shared data that holds it (VaultBlockEntity.getUpdateTag). A guest's update keeps the turn the client has built up, since the tag does not carry it.
2026-10-27 21:59:46 +01:00
Kacpherek 2f7edd1524 Send banner patterns to LAN guests
Banners showed no pattern layers for LAN guests and the single-player window. The update tag now carries the patterns, the custom name and the components, as BannerBlockEntity.getUpdateTag (saveWithoutMetadata) does.
2026-10-27 21:59:46 +01:00
Kacpherek 732be56485 Launcher: show what installed versions take on disk
The Versions page measures each installed version's folder on a background thread and shows the size on its row and the total under the title, so players can see where 20 GB went.
2026-10-27 21:59:07 +01:00
Kacpherek ab955d66c4 Launcher: prune after each install and add a Settings switch
The window runs the same cleanup as the command line once an install or update is saved, and Settings gets a 'Delete old versions automatically' switch (on by default), translated to French and Russian.
2026-10-27 21:58:52 +01:00
Kacpherek 8fdcb37ddd Ask for a backup before upgrading a world that needs file fixes
Opening a world saved before DataVersion 4899 upgraded it in place with no prompt. Vanilla asks first (BackupStatus.FILE_FIXING_REQUIRED): Create Backup and Load, I Know What I'm Doing!, or Cancel. The backup reuses Make Backup, then the world opens, as EditWorldScreen does.
2026-10-27 21:56:00 +01:00
Kacpherek 694568a89c Show the multiplayer safety notice on the first Multiplayer click
Horizon opened the server list at once and never read skipMultiplayerWarning. Vanilla TitleScreen shows SafetyScreen first, with a do-not-show-again box saved to options.txt, unless that option is set.
2026-10-27 21:56:00 +01:00
Kacpherek e9b71c288a Launcher: delete old release versions after an install or update
Each version is about 790 MB and an update adds one. After an install the launcher now deletes every release version except the newest two, the selected one, the ones instances pin and any the game runs from. Turn it off with prune-old-versions, change the count with keep-versions.
2026-10-27 21:55:05 +01:00
AndriiandClaude Sonnet 5.5 db27568929 Merge upstream main into fix/sweep-attack-2764
Fold upstream's player sweep (Sweep, sweep_players) into mace::sweep_damage;
keep melee_damage in the dragon attack; keep enchanted_damage next to
enchant_damage_bonus.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-27 21:54:35 +01:00
Kacpherek 9c9ce0b435 Keep a server item's block entity data for the op warning
minecraft:block_entity_data stayed raw bytes after the item sync, so a command block or spawner from a server had no data for the op warning or a spawner's tooltip. It is read as TypedEntityData (the block entity type, then its data) and given the id the saved component carries.
2026-10-27 21:52:10 +01:00
Kacpherek af41d0325c Read a server item's container contents for its tooltip
minecraft:container stayed raw bytes after the item sync, so a shulker box from a server showed no items in its tooltip. It is now read as ItemContainerContents (an optional template per slot), the way the saved container is.
2026-10-27 21:52:10 +01:00
Kacpherek e21957efd7 Send a server's own components back for unedited creative stacks
The creative slot packet rebuilt every stack from the few components Horizon models, so moving an item with a container, unbreakable, lore or a kept raw component sent the server a stripped copy. A stack read from the wire now keeps its patch, and it goes back as it came while nothing but the count has changed.
2026-10-27 21:52:10 +01:00
Kacpherek 6790913e0b Send creative slot stacks with length-prefixed components
SetCreativeModeSlot uses ItemStack.OPTIONAL_UNTRUSTED_STREAM_CODEC, which puts each component value behind its byte length. Horizon wrote the plain codec, so a vanilla server read the first byte of every component as a length and kicked the player with a DecoderException.
2026-10-27 21:48:00 +01:00
asownerytandClaude Opus 5.5 deea1a5b83 entity_model: make the bat's animation states accessible before setting them
VanillaEntityAnimOracle.batHistory set Bat.flyAnimationState and
restAnimationState through getField(..).set without setAccessible, which
Java refuses for a final field on every JDK: the bat case failed with
IllegalAccessException and took the harness down with it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 09:45:11 +13:00
RVZO6andClaude Opus 5.5 32a0a11fed Keep a move behind a dig and its arm swing for the next tick
The hub hands a dig (handlePlayerAction) to the game after its pass, and
a move queued right behind it waited for the next tick so it was not
checked against the block the player had just broken. A creative break
sends the arm swing (Punch) between the dig and the move, so the move
was handled in the same pass: the block was still there, the move was
refused and the player sent back. Digging straight down, the player
sank into the block, was put back on top of it a tick later, then fell.

Any move left in the queue now waits, with what is queued before it,
as vanilla handles the packets in order. Traced with horizon-ctl in a
creative world: the player rose back after 6 to 7 of 8 breaks before,
after none now.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 15:47:14 -04:00
Hector Eccles 20d7d111e5 Merge branch 'ludovic111:main' into perf/terrain-rustgen-ports 2026-10-27 15:39:08 -04:00
Hector EcclesandOpenCode fb769e55e0 Worldgen: rustfmt the terrain performance changes
Co-Authored-By: OpenCode (DeepSeek V4.1 Flash) <[email protected]>
2026-10-27 15:25:06 -04:00
Hector EcclesandOpenCode 01d0170821 Worldgen: keep the skipped aquifer decisions' pending fluid flag
The fill no longer calls compute_substance for positive density or for air
above the aquifer's skip height. Both old paths cleared the aquifer's pending
fluid update; reproduce that side effect so the terrain-fill probe's
should_schedule_fluid_update matches vanilla (it caught the difference).

Co-Authored-By: OpenCode (DeepSeek V4.1 Flash) <[email protected]>
2026-10-27 15:01:42 -04:00
Ludovic MarieandClaude Opus 5.5 f6807fe7cd refactor: spawner, trial spawner and beehive logic as extension traits
Move-only step B8 of the horizon-client crate split. The spawner, trial
spawner and beehive data live in chunk::block_entity since the
BlockEntity cut; their behaviour needs mobs, so it stays above. Once the
data is in its own crate, Rust allows no inherent impl outside it, so
each impl block becomes an extension trait in the same module, with the
same method bodies:

- beehive: OccupantExt, BeehiveExt
- mobs::spawner: SpawnDataExt, SpawnerExt
- mobs::trial_spawner: TrialConfigExt, TrialSpawnerExt

The Default impls of Spawner, TrialConfig and TrialSpawner (plain field
values) move next to their structs, since a foreign trait impl must sit
with its type. Callers add one `use` for the trait.

No behaviour change.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 20:00:59 +01:00
Hector EcclesandOpenCode e71f98adde Worldgen: write positive-density fill runs through the section once
A positive-density run is exactly the default block, so the fill no longer
calls set_block_raw and the settled heightmap checks per block. One palette
lookup per section segment writes the run, its non-air count follows the air
blocks it replaced, and the heightmap is updated at the run's top only, as
rustgen's fill_initial_solid_cell does for uniform cells.

Noise 1.74 -> 1.57 ms/chunk one thread and 2.27 -> 2.10 at 13; terrain +4.4%
(min) and +1.2% (min) at 13. Block snapshots identical.

Co-Authored-By: OpenCode (DeepSeek V4.1 Flash) <[email protected]>
2026-10-27 14:39:52 -04:00
ElyTheCat 80df20837b Options: add Java 26.3 controls 2026-10-27 22:09:31 +03:30
masa-masa-0314andZCode 2092c5a2e8 Worlds: take the save's icon.png from the rendered frame
GameRenderer.tryTakeScreenshotIfNeeded saves a world icon the Select
World list never had: while a local world is on screen without one, the
frame is grabbed after the level and the hand (before the HUD), centre
cropped to a square, averaged down to 64x64 and written to the save's
icon.png once a second until it sticks. The list already drew the icon
when a save had one; nothing ever created it.

Co-Authored-By: ZCode <[email protected]>
2026-10-28 02:29:39 +08:00
Hector EcclesandOpenCode e319dd578f Worldgen: resolve each surface column once into a reused buffer
The surface walk read every column from paletted sections at least twice (the
top-down pass and the stone-depth look-behind). Column now keeps the resolved
ids for the read range, refilled into one buffer per chunk and updated by
set_block, as rustgen's SurfaceSystem does with read_column.

Surface 1.46 -> 1.41 ms/chunk one thread, 4.19 -> 3.89 at 13; terrain +1.4%
(min 2.6%) and +3.7% at 13. Block snapshots identical.

Co-Authored-By: OpenCode (DeepSeek V4.1 Flash) <[email protected]>
2026-10-27 14:20:16 -04:00
Hector EcclesandOpenCode 0f596d9e3c Worldgen: one material worker per carving pass
Every restored dirt block under a carved cell built a fresh
MaterialDensitySources and MaterialConditions, compiled the whole surface
rule and allocated its bindings. A carver now builds one worker per pass and
moves only the 1x1x1 bindings to the cell, as rustgen's CarvingContext does.

Carvers 0.44 -> 0.40 ms/chunk on one thread and 0.62 -> 0.49 at 13 threads;
terrain +0.6% one thread and +0.98% (median) at 13. Block snapshots identical.

Co-Authored-By: OpenCode (DeepSeek V4.1 Flash) <[email protected]>
2026-10-27 14:14:48 -04:00
Ludovic MarieandClaude Opus 5.5 3246732087 Read the inline instrument through horizon_client_core::scoreboard
#2215 moved items.rs into horizon-items, where scoreboard is not in the
crate root.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 19:13:32 +01:00
Ludovic MarieandClaude Opus 5.5 c215a48c91 Merge main into fix/goat-horns
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 19:11:21 +01:00
Hector EcclesandOpenCode 7671219f1a Worldgen: skip the aquifer for solid and high air blocks in the fill
The fill called compute_substance for every block although the aquifer only
does work for blocks below its skip height. Positive density always yields the
default block, and above both the skip height and the fluid level a
non-positive density is air, so the fill now decides both cases itself.

One-thread terrain 4.30 -> 4.08 s (noise 1.95 -> 1.75 ms/chunk); 13-thread
noise 2.51 -> 2.27 ms/chunk. Block snapshots are identical.

Co-Authored-By: OpenCode (DeepSeek V4.1 Flash) <[email protected]>
2026-10-27 14:04:59 -04:00
Hector EcclesandDeepSeek V4.1 Flash 783c25e9bc Draw chorus plants in End LOD terrain
Distant End terrain now estimates chorus plants from the biome's own
chorus_plant feature: the stem height and the age-5 tip follow the real
feature, while its placement modifiers give the density. Real chorus also
survives the refined, visited and saved far-column summaries, which
previously dropped every non-solid block.

Vanilla reference: ChorusPlantFeature and ChorusFlowerBlock.generatePlant,
already ported in horizon-world.

Co-Authored-By: DeepSeek V4.1 Flash <[email protected]>
2026-10-27 13:15:22 -04:00
Ludovic MarieandClaude Opus 5.5 6aabf3f618 Merge main into fix/goat-horns
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 18:11:43 +01:00
Senjin fbab7d29a5 Merge remote-tracking branch 'origin/main' into hdr-tiers
# Conflicts:
#	crates/horizon-client/src/entity.wgsl
2026-10-27 17:57:13 +01:00
SenjinandClaude Opus 5.5 be26c6db88 HDR: log what the display offers, and when HDR turns on or off
The log already names the adapter and the surface format; a report of HDR
trouble now also shows whether the surface offers HDR10 or scRGB, and the
output, swapchain format and nits it turned on with.

Part of #1441.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 16:57:42 +01:00
levzzz b901415c80 Merge remote-tracking branch 'upstream/main' into fix/forceload-rings 2026-10-27 20:32:05 +05:00
ElyTheCat c06a7d4511 Linux: OpenGL can present to the window
wgpu-hal's GLES backend takes its EGL display from the display handle the
wgpu instance was created with. Horizon created every instance without one
(`new_without_display_handle`), so on Linux EGL fell back to
`EGL_MESA_platform_surfaceless`: those configs cannot make a window surface,
the GL adapter reported every surface as incompatible (`gl not compatible with
provided surface`), and Auto never reached its OpenGL fallback.

Give the instance the window's X11 or Wayland display handle, and offer the
OpenGL choice in Options > Graphics on Linux (DirectX 12 stays Windows-only).

Tested on Intel HD 4000 with Mesa crocus (OpenGL 4.2), on X11 and Wayland:
graphicsApi:opengl, HORIZON_GRAPHICS_API=opengl and WGPU_BACKEND=gl each run
and present, and Auto falls back to OpenGL when Vulkan has no ICD.
2026-10-27 18:57:55 +03:30
levzzzandminimax-m3.1-flash da877fe1db Give a forced chunk vanilla's rings
A `/forceload`ed chunk holds a FORCED ticket at level 31, and vanilla's
level rises one per ring, so the chunk itself and the two rings around it
are level chunks with their light, the third ring has its features and the
fourth only its terrain. Horizon took the forced chunk's reach from the
caller's margin instead, so only the chunk itself ever finished and its
neighbours stayed proto chunks.

The reach now follows the ticket levels: the full ring is two chunks out,
the features ring three and the terrain ring four, and the protos of the
terrain ring are kept so the features ring can be decorated from them. A
decorated proto chunk is saved as `minecraft:initialize_light`, the status
vanilla gives a chunk that has its features but no light.

Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 20:18:42 +05:00
AndriiandClaude Sonnet 5.5 8e36842891 Merge upstream main into fix/sweep-attack-2764
Drops the player-victim sweep in favour of #2659 and uses the
enchantment's sweeping_damage_ratio and upstream's particle queue.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-27 16:04:16 +01:00
levzzzandminimax-m3.1-flash 7b841af095 Merge the dyed armour colour fix onto upstream/main
Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 20:01:00 +05:00
levzzzandminimax-m3.1-flash 033c89a1b2 A dyed leather armour keeps its colour
The worn armour was always tinted with the colour leather has when it is
undyed, so a dyed leather piece showed the undyed brown; the trim of
another player's armour was not drawn at all, because only the item id
of the worn pieces was kept.

The vanilla's EquipmentLayerRenderer tints a dyeable layer with the
stack's dye colour (DataComponents.DYED_COLOR) and the colour it has
when undyed otherwise, and draws the trim as a layer over the armour.
The worn pieces keep their stack now, so the dye colour and the trim
reach the renderer (#2774).

Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 20:00:53 +05:00
sirrayiandDevin 0580626784 Server: refuse digging and using blocks outside the world border
ServerLevel.mayInteract also requires WorldBorder.isWithinBounds(pos): START_DESTROY_BLOCK outside the border starts no dig, and UseItemOn uses nothing there (the block updates are still sent back). Part of #2743.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-27 14:58:38 +00:00
sirrayiandDevin 6143e258ab Release notes: a Contributors heading and a rule before the disclaimer
The "Thanks to ..." line read as part of the section before it (often Known issues), on GitHub and on playhorizon.dev/releases. It now has its own heading, and a horizontal rule sets the disclaimer apart. Fixes #2314.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-27 14:55:35 +00:00
Claude Codeandminimax-m3.1-flash 2dc154ff7f The structure pieces ask isSolidRender, not the collision cube
The legacy structure pieces asked "is the collision shape a full block"
for the blocks around them, when they choose a chest's facing
(StructurePiece.reorient) and when the mineshaft checks its floor. That
is a different set of blocks than vanilla's BlockState.isSolidRender:
glass, leaves, ice, a spawner and a barrier are full collision blocks
but do not draw as solid, and soul sand and mud draw as solid without
a full collision shape.

The pieces now ask the blocks' own facts, the flag the worldgen already
keeps for every state (BlockProperty::SolidRender, which the exporter
writes from state.isSolidRender()), the way the monster room's own
reorient does. The chest's facing is decided in one function that takes
the blocks around the chest, so the rule can be checked against
vanilla's.

Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 19:50:19 +05:00
levzzz f6e63d252d Merge remote-tracking branch 'upstream/main' into HEAD 2026-10-27 19:32:30 +05:00
AndriiandClaude Sonnet 5.5 4f13db42b0 Share the sweep arc options and the crit burst helper
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-27 15:32:04 +01:00
levzzzandminimax-m3.1-flash 84d204327f A neutral mob's anger survives the save and the reload
Vanilla keeps a neutral mob's anger in the entity tag: `NeutralMob.
addPersistentAngerSaveData` writes the `anger_end_time`, the absolute
game time the anger ends at, and the `angry_at` the target, and
`readPersistentAngerSaveData` reads them back, so a mob that is unloaded
and loaded again is still after whoever hurt it.

Horizon kept the anger as a countdown in memory only, so it was lost
with the chunk. The save now writes the anger end time and the target
with the vanilla's keys, and the load gives the mob back the anger that
is left of it and its target.

Fixes #2784

Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 19:26:34 +05:00
levzzzandminimax-m3.1-flash 65bca4ce98 net: skip a compressed frame that carries no packet
A server may send a compressed frame whose uncompressed length is 0 and
which holds no packet: `ConnReader::recv` read the packet id from the
empty body and failed with "needed 1 bytes, 0 left", which ended the
connection. Vanilla skips such a frame: its PacketDecoder only decodes a
packet from a readable buffer, so an empty frame produces no packet and
the connection stays up.

A frame with no bytes at all is a corrupted frame in vanilla ("Frame
length cannot be zero"), so the frame splitter now says that instead of
failing later with a missing byte.

Fixes #2739

Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 19:08:49 +05:00
AndriiandClaude Sonnet 5.5 574f3587ed Hit boats and carts with the full attack damage, sweep victims in section order
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-27 15:05:34 +01:00
iLostName 66b705cec3 sounds: list each namespace's sound files once instead of a lookup per sound 2026-10-27 14:52:56 +01:00
AndriiandClaude Sonnet 5.5 ee6b5bf585 Match Player.attack more closely: enchant bonus, magic crit, no frame sweep
The main hit takes getEnchantedDamage on the real attack damage, a dragon
part is never a critical hit, and the enchanted-hit burst is shown when the
enchantments add damage. Vehicles are hurt before the sweep and sounds.
Frames, paintings and cushions end the attack in skipAttackInteraction, so
they no longer sweep. Swept armor stands are knocked back.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-27 14:49:05 +01:00
sirrayiandDevin 690033b71b port-pack: keep pack-index file names and jar entries inside the output
A pack index's file name (the last /-part of an mrpack path, a CurseForge or modlist jar name) was joined onto jars/ unchecked: on Windows ..\..\x.dll or C:\x escaped it. Jar entries were checked for .. only between /. Refuse names with \, : or that are . or ..

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-27 13:45:29 +00:00
levzzzandminimax-m3.1-flash f9ca0a76cf The death message names the cause's item when it has a custom name
`DamageSource.getLocalizedDeathMessage` uses the `<id>.item` message when
the cause holds an item with a custom name, with the item's name as a
third argument: "Steve was shot by Zombie using My Sword". Horizon always
used the two-argument message, so the item was never named.

The mob's main hand item now keeps its custom name, and the death message
picks the message and its arguments from it.

Fixes #2802

Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 18:38:37 +05:00
sirrayiandDevin 4fea5fafe5 Dedicated server: apply server.properties rate-limit
rate-limit was parsed but never used. Average each player's packets a second as Connection.tickSecond does and kick past the limit with disconnect.exceeded_packet_rate, as RateKickingConnection does.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-27 13:36:52 +00:00
sirrayiandDevin 780abb9832 SNBT: bound nesting depth, keep nan/inf words as strings
Deeply nested [[[... in a command, function, command block or old world overflowed the stack and aborted the process; both SNBT parsers now stop at 512 levels. Bare words such as nan, infd or Infinity were parsed as floats by Rust's parser (and became JSON null in text components); only digits make a number.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-27 13:36:51 +00:00
sirrayiandDevin 2812cf201d Protocol: don't panic on malformed chunk sections, compressed frames or legacy pings
A server sending a palette wider than 64 bits divided by zero in storage_len (and 33-64 bits overflowed shifts); SimpleBitStorage allows at most 32. A non-canonical data-length VarInt in a compressed frame was misread. An empty first read in the legacy ping handler indexed past the end.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-27 13:36:51 +00:00
sirrayiandDevin 9bbc54bd4c Server: refuse block actions out of a player's reach
Breaking and using blocks (START/STOP_DESTROY_BLOCK, UseItemOn) were carried out for any position a client sent, so a modified client could dig or place anywhere in loaded chunks. Check isWithinBlockInteractionRange(pos, 1.0) and the hit point as vanilla's handleBlockBreakAction and handleUseItemOn do.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-27 13:36:50 +00:00
levzzzandminimax-m3.1-flash 1232745064 Keep a traveller that waits for an End gateway's exit
The traveller was dropped from the queue before the exit was resolved, so a
failure to resolve it lost the entity for good. The traveller now waits and
the failure is retried, with the reason in the log.

Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 18:34:07 +05:00
AndriiandClaude Sonnet 5.5 52c667f423 Sweep from stands, frames and dragon parts, and hurt other players
- start a sweep (and the attack sounds, with a living target's critical
  hit) when a punch lands on an armor stand, an item frame or a dragon part
- spare the entity the attack hit, and swing the arm for the stands and
  frames, which did not
- hurt the other players in the sweep box through the pvp rule and teams,
  for the host and for guests, with the 0.4 knockback; queued like critical
  hits until the hub is back in place

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-27 14:33:16 +01:00
levzzzandminimax-m3.1-flash 3382b666ce Add the audit that drives a cow and a stack through an End gateway
The script plays the End in a scratch home: it places a gateway beside the
spawn platform, puts a cow and a seven-diamond stack in it, and checks that
both leave it and land at the gateway's exit with their data, in the
server-thread and the in-frame singleplayer.

Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 18:27:43 +05:00
sleeperandClaude Opus 5.5 dcb7476d12 Keep a player entity's profile name after its list entry goes
A player entity keeps the profile it was created with
(RemotePlayer.getGameProfile), whose name is its scoreboard name, even
once ClientboundPlayerInfoRemovePacket drops it from the player list.
Servers rely on that to hide their NPCs (fake players) from the list
after spawning them on a team, as Hypixel SkyBlock does with
collisionRule never. Horizon read the name from the list, so an NPC
removed from it lost its team and pushed the local player again.

Remote::level_player_names keeps the name each player entity spawned
with; Game::remote_player_name and remote_player_names (teams, name tags,
glowing) read it, falling back to the list entry.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 13:23:38 +00:00
socksproxandDevin (SWE-2 High) 4fdc93257f Let portal arrivals tick in their destination without a player
An item or mob sent through a portal into a playerless dimension waited
forever: resolve_arrivals needed lit chunks, the level never streamed any,
and levels_to_tick skipped it entirely, so its Age stayed frozen until
someone followed and it was picked up intact (#1528). Vanilla's portal
move lands the entity under an ENTITY ticket, which keeps the chunk
entity-ticking while it ages and despawns on time.

Pending arrivals now add their landing chunk to the level's streaming
extra areas and keep their destination level in levels_to_tick, so it
lights, resolves, and ticks them just as if a player stood there.

Co-Authored-By: Devin (SWE-2 High) <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-27 14:21:24 +01:00
levzzzandminimax-m3.1-flash 826147d716 Merge upstream/main into fix/shield-blocking-pose
Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 18:14:14 +05:00
levzzzandminimax-m3.1-flash 0b4fa473e7 Test the shield's blocking pose and its use duration
A shield's use animation is the BLOCK one: AvatarRenderer.getArmPose
raises the arm for it and Item.getUseDuration gives it 72000 ticks, so
the arm stays up while it is used. The fix (kacpherek's #2310) sets the
blocking flag for the other players and gives the shield its use
duration; these tests pin both ends: the arm goes across the body while
the shield is used, and the shield's use duration is vanilla's 72000
ticks, not the 0 an item without an use animation gets (#2289).

Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 18:13:31 +05:00
sleeperandClaude Opus 5.5 045cabb7f2 Find other players' teams by profile name
Entity.getTeam looks a team member up by getScoreboardName, which is a
player's profile name (Player.getScoreboardName) and any other entity's
UUID. Game::mob_team, mob_teams and player_allies used the UUID for every
entity a server sent, players included, so another player's team was
never found. On a server that turns collisions off with a collisionRule
never team (Hypixel SkyBlock), the other players still pushed the local
player; the same lookup feeds allies, friendly fire, glowing colours and
the team checks explosions make.

Game::scoreboard_name now picks the profile name for another player (from
the player list, via Game::remote_player_name) and the UUID otherwise.

lan_e2e gains a team-push check: the host pushes the guest without a
team, and doesn't once the host's team has collisionRule never.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 13:10:03 +00:00
levzzzandminimax-m3.1-flash b85ce82160 Merge kacpherek's PR #2310: the shield goes up in first person and other players show the blocking pose
Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 17:48:33 +05:00
AndriiandClaude Sonnet 5.5 49ce3e6d5a Sweep in vanilla's order and against everything it hits
- play the sweep sound before the victims are hurt, then draw the arc
- hurt armor stands (not marker ones) in the sweep box through the same
  hit a direct punch gives them
- run the victims' damage through getEnchantedDamage with the real base
- sweep around a boat or a minecart that takes the hit, which only played
  the sound before

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-27 13:48:20 +01:00
Ludovic Marie 36ba5c65a6 Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	crates/horizon-client/src/item_tooltip.rs
2026-10-27 13:44:46 +01:00
AndriiandClaude Sonnet 5.5 3e0f4cf653 Sweep attacks hurt nearby mobs and show their arc
Player.doSweepAttack was only a particle and a sound that never fired: the
player's speed field is refreshed by the local movement step only, so it was
0 for the player a server simulates and isSweepAttack could not pass.

- read the movement speed attribute when checking for a sweep
- hurt the other mobs in the target's box inflated by (1, 0.25, 1) and under
  3 blocks away for 1 + sweeping ratio * damage, scaled by the charge, with
  0.4 knockback and the post-attack effects when the hurt lands
- send the arc as a LevelParticles packet so a server's clients draw it
- take its position from Mth's sine table

Fixes #2764

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-27 13:39:33 +01:00
asownerytandClaude Opus 5.5 b38fe43b09 Jukebox: keep the disc on the server thread; the off hand never ejects it
JukeboxBlock.useItemOn returns TRY_WITH_EMPTY_HAND when a disc is in, and
only the main hand goes on to useWithoutItem (popOutTheItem). The client now
predicts that the jukebox takes the click, so no off-hand UseItemOn follows
the insert, and the server ignores an off-hand turn on a full jukebox.

Fixes #2775

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 01:30:20 +13:00
1F10250001andClaude Opus 5.5 66969d8a93 launcher: Simplified Chinese
Adds Simplified Chinese (zh) beside English, French and Russian: the 160 strings
of launcher/lang, written for Horizon, and the language in Settings > Launcher,
the system pick (zh-Hans-CN and other zh locales) and `settings language zh`.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 21:29:17 +09:00
iLostName ea7df1d0dc registries: list the resources' recipe, loot table and template ids once, not at every join 2026-10-27 13:29:06 +01:00
levzzzandminimax-m3.1-flash 92c073de61 Merge upstream/main into fix/end-gateway-entities
Upstream now keeps a gateway's exit in the gateway's own block entity
(TheEndGatewayBlockEntity.setExitPosition), so the player travels to the
exit the gateway was given (#1517). The entity travel stays: the mobs and
the dropped items go through the same gateway, and the exit the first
traveller found is reused, so the player and the entities land in the same
place and only one island is built for a gateway.

Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 17:27:46 +05:00
levzzzandminimax-m3.1-flash 139de824aa Re-run the checks after the audit script fix
The pull request's last push did not start a run; this empty commit
starts one on the same tree.

Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 17:10:44 +05:00
Senjin fea8346f23 Merge remote-tracking branch 'origin/main' into hdr-f3 2026-10-27 13:10:29 +01:00
Senjin 523dd491c4 Merge remote-tracking branch 'origin/main' into hdr-tiers
# Conflicts:
#	crates/horizon-client/src/entity.wgsl
#	crates/horizon-client/src/gfx.rs
2026-10-27 13:07:04 +01:00
levzzzandminimax-m3.1-flash 35581f52e2 Keep the clippy rules happy in the difficulty test
The level is built with the struct update syntax instead of a field
assignment after Default::default(), which clippy's
field_reassign_with_default rejects.

Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 16:46:15 +05:00
socksproxandDevin (SWE-2 High) c216561daa Allays fly: their travel is travelFlying and their stroll hovers
An allay stood on the ground and fell when the block under it broke:
Mob.travel sent it down the ordinary air travel, which applies the
0.08 gravity attribute whenever noGravity is off. Vanilla's Allay.travel
is travelFlying(input, getSpeed()) — a travel with no gravity term —
so an allay hangs mid-air even before a goal moves it.

- Family::Allay routes to allay::travel, which calls travelFlying with
  the move control's current speed (LivingEntity.speed), like
  Allay.travel(input, getSpeed()); travel_flying is split so the three
  speeds can differ per mob.
- Stroll::flying picked a land position, so the stroll never produced
  a MoveTo mid-air. WaterAvoidingRandomFlyingGoal.getPosition tries
  HoverRandomPos (8, 7, pi/2, 3, 1) on the view direction, then
  AirAndWaterRandomPos (8, 4, -2); Stroll::flying now does the same.
  The wither registers the same vanilla goal and benefits equally.

Verified against the mob_ai probe: an allay spawned at y=-55 stays at
-55, then RandomStrollGoal fires and it drifts to air targets, never
falling — where it used to drop to the ground.

Fixes #2594.

Co-Authored-By: Devin (SWE-2 High) <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-27 12:40:16 +01:00
Ludovic MarieandClaude Opus 5.5 ed01a93df6 Merge main into fix/world-types
main's Superflat Customize screen (#2214) still set the removed WorldOptions.flat: it now picks WorldPreset::Flat,
and the agent's customize-flat state reads the preset. structure_mobs.rs's test passes the new preset argument
(minecraft:normal) to VanillaTerrain::for_dimension.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 12:39:11 +01:00
levzzzandminimax-m3.1-flash 1a56e74324 Fix the f-string in the end cargo audit script
The braces were one too many, so the file did not parse and the Python
tests failed on it. The command it builds is the same now: the item
stack {Item:{id:"minecraft:diamond",count:3}}.

Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 16:35:44 +05:00
asowneryt e36a480063 Merge remote-tracking branch 'upstream/main' into claude/2697-horse-make-mad-4431aa
# Conflicts:
#	crates/horizon-client/src/game/mounts.rs
2026-10-28 00:27:26 +13:00
levzzzandminimax-m3.1-flash 618d20bd08 The world's difficulty follows the level data
The difficulty is a property of the world: vanilla keeps it in the level
data (`PrimaryLevelData.getDifficulty`/`setDifficulty` over
`Data.difficulty_settings`), so it survives leaving and rejoining the
world and a downloaded world keeps the difficulty it was made with.

Horizon read the difficulty from the level data when the world opened,
but `/difficulty` only changed the game's own copy, so the level data
kept the old value and wrote it back: the world came back on normal.
The level data now follows the game, both when the command changes the
difficulty and when a LAN guest asks the host to change it.

Fixes #2571

Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 16:22:50 +05:00
asownerytandClaude Opus 5.5 985ece811b Stop the menu music when a world loads
Minecraft.setLevel -> updateLevelInEngines stops every sound
(SoundManager.stop), so the title-screen track ends on joining and the
music manager then waits the level's music delay. Horizon only stopped
sounds when leaving a world, so music.menu kept playing in it.

- A server's client (single player's server thread, LAN, multiplayer)
  stops every sound when it makes a new level: on Login and on a
  Respawn into another level. A dimension-changing Respawn also calls
  MusicManager.stopPlaying, as handleRespawn does.
- The in-frame path does the same when the opened world's first player
  is placed.
- Vanilla's Minecraft.player exists from handleLogin on; here it is made
  at the first teleport, so getSituationalMusic counts a logged-in client
  as in the world. Otherwise the menu's 0..10-tick replace delay would
  start the first game track almost at once.
- test_music_parity.py scripts the join: a menu-like track, stopall,
  game music, then a dimension change's stop.

Fixes #2746

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 00:18:31 +13:00
levzzzandminimax-m3.1-flash c734c72fc9 The world's difficulty survives leaving and rejoining it
Vanilla keeps the difficulty in the level data: `Data.difficulty_settings`
(`LevelSettings.DifficultySettings`) holds the name of the difficulty, the
hardcore flag and the lock. Horizon read and wrote `Data.Difficulty`, a
byte that 26.3 no longer has, so the world always opened on normal and
`/difficulty` never reached the file: the level data writes the
difficulty settings, and the game patched a key that was not there.

The difficulty now comes from and goes to `difficulty_settings`, and the
level data follows the game, so a world set to hard is hard when it is
opened again and a downloaded world keeps the difficulty it was made
with.

Fixes #2571

Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 16:16:40 +05:00
Claude Codeandminimax-m3.1-flash 25b84dfc2e Format the world preset test
rustfmt keeps the registry read on one line.

Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 16:11:14 +05:00
levzzzandminimax-m3.1-flash a2098c2ffa Let mobs and dropped items through End gateways
Only the player was checked for End gateways, so a cow or a dropped
stack standing in one stayed there for good. The entities now go
through a gateway the way the player does: the block their box swept
through takes them in (unless the gateway is cooling down, as
EndGatewayBlock.entityInside does), they travel on the next tick
(Entity.handlePortal, with the End gateway's transition time of 0) and
land where the gateway leads once its chunks have loaded, keeping their
data and counts.

Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 16:11:14 +05:00
Claude Codeandminimax-m3.1-flash d98dc8edc9 Read the test registry without the explicit deref
clippy's explicit_auto_deref asks for the LazyLock itself, which
derefs to the registry the generator data keeps.

Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 16:11:14 +05:00
Claude Codeandminimax-m3.1-flash bf05176692 Add the amplified, large-biome and single-biome world types
The create screen only offered Default and Superflat, so the amplified
world type of #1873 could not be chosen at all, and the generator
always built the overworld's noise settings.

The world-type button now cycles through the presets vanilla's
WorldPresetTags.NORMAL tag holds (normal, flat, large_biomes, amplified,
single_biome_surface), the chosen preset is written into the new world's
world_gen_settings.dat with the generator
worldgen/world_preset/<preset>.json has, and the generator is built from
that preset, so an amplified world gets the amplified noise settings.
A world opened again reads its preset back from its generator.

Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 16:11:14 +05:00
Ludovic MarieandClaude Opus 5.5 e5ca012ed8 Merge main into fix/server-mob-animation-sync-2
Brings main in so the branch has one merge base with it (GitHub saw a
conflict through the criss-cross bases; the merge itself is clean).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 12:08:06 +01:00
levzzzandminimax-m3.1-flash 4a9bb0ce17 Format the branch with the CI's rustfmt
The CI's rustfmt (stable) writes these signatures on one line; the local
toolchain's rustfmt split them, so the check job failed on the formatting
alone.

Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 16:06:37 +05:00
iLostName b042426180 lang: opening Options lists the languages without reading every file 2026-10-27 12:00:36 +01:00
levzzz cd5216e012 Merge remote-tracking branch 'upstream/main' into fix/flowing-water-mobs 2026-10-27 15:38:53 +05:00
levzzzandminimax-m3.1-flash 786c2482a1 Carry mobs along with a fluid current
A mob standing in flowing water was not carried along by it: the fluid machinery that computes a current (horizon-world's entity_fluid, the port of vanilla's EntityFluidInteraction) was wired to the player, the dropped items, the arrows and the primed TNT, but the mobs use a reduced fluid model of their own and never felt a current.

The mobs now run the same per-tick fluid update as the other bodies (Entity.updateFluidInteraction): the flow of the fluid they stand in is accumulated over the blocks of their box and added to their velocity, with the water scale 0.014 and the lava scale 0.007 (0.00233 without the fast lava dimension attribute), the current of a player being the average of the flow vectors and of any other body its direction.

The new flow-push LAN case puts a mob in a flowing water block and a second one in a pool of still sources, and checks that only the mob in the current is carried along.

The branch also drops the unreachable second "minecraft:rabbit" arm in crates/horizon-client/src/entity_nbt.rs, which clippy rejects upstream.

Fixes #2549

Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 15:38:51 +05:00
iLostName ee9d8a656f title: a world open in another instance shows the access failure toast instead of crashing 2026-10-27 11:28:35 +01:00
iLostName de8dd66ac2 connection: try a server's addresses side by side, 250 ms apart 2026-10-27 11:24:59 +01:00
iLostName 6fc2006447 dns: leave out scoped IPv6 name servers on Windows, as JNDI does 2026-10-27 11:24:59 +01:00
levzzzandminimax-m3.1-flash 0cac3a9153 Remove the duplicate rabbit arm in entity_nbt
The merge of #2585 left two "minecraft:rabbit" arms in the same match, so
the second one is unreachable and clippy's -D warnings rejects the build
(upstream's own checks job is red on it since 0e793be2a). The first arm
already writes RabbitType and MoreCarrotTicks, so the second one is dead
code; its note about the legacy id moves to the arm that runs.

Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 15:15:54 +05:00
asowneryt dae67a88cc Merge remote-tracking branch 'upstream/main' into claude/2697-horse-make-mad-4431aa
# Conflicts:
#	crates/horizon-client/src/game/mounts.rs
2026-10-27 22:58:40 +13:00
levzzz 9a84148376 Merge remote-tracking branch 'upstream/main' into fix/bubble-column-entities 2026-10-27 14:57:01 +05:00
levzzzandminimax-m3.1-flash 5678030da2 Carry mobs up and down with a bubble column
A mob standing in a bubble column was not moved by it: the block's
`entityInside` had no counterpart on the entity side, so only the local
player (whose motion code has it) and the arrows were affected. The mobs
now get the same push vanilla gives them in `BubbleColumnBlock.entityInside`
-> `Entity.onAboveBubbleColumn` / `Entity.onInsideBubbleColumn`: the body's
velocity along the vertical axis is raised by 0.1 (0.06 when the block above
the column is solid) and capped at 1.8 (0.7), or lowered by 0.03 and floored
at -0.9 (-0.3) for a column that drags down, and the fall distance is reset
inside a column.

The new `bubble-column` LAN case puts a mob in a soul sand column and a
second one in plain water, and checks that only the mob in the column is
carried up.

Fixes #2546

Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 14:57:01 +05:00
iLostName 758d66bf44 remote: titles, tab list, scoreboard and effects don't carry over to the next server 2026-10-27 10:09:12 +01:00
iLostName b201dafa75 server list: the player count's tooltip lists the status sample 2026-10-27 10:07:15 +01:00
iLostName 7c58bf004f options: the slider test expects saving on release 2026-10-27 10:06:32 +01:00
iLostName 4119284a4f options: sliders apply while dragged and save on release 2026-10-27 09:55:35 +01:00
FusselTVandClaude Opus 5.5 8401cde519 Dedicated server: nothing kept around the spawn while nobody is on
While nobody was online, the dedicated server behaved as if a player
stood at the world spawn: at startup it streamed a view-distance area
there and placed a stand-in player, and afterwards it kept that area
loaded, generated and ticked. Vanilla only loads the chunks its spawn
spiral tests (MinecraftServer.setInitialSpawn: getChunk(FULL), whose
3x3 is decorated in one generation task), has no spawn ticket, and finds
a player's spawn when one joins. In a pre-generated world the startup
area decorated in another order made ore blobs and a mineshaft start's
saved flags near spawn differ from vanilla's.

A streaming radius below 0 now means no area around the centre. The
dedicated server only finds the world spawn, loading each tested chunk
as an extra area of radius -1 ranked as one FULL task (x-outer,
z-inner), and keeps the radius at -1 while no guest is online.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 09:36:12 +01:00
SenjinandClaude Opus 5.5 957a3c6622 HDR: show the output on the F3 screen
While the HDR setting is on, F3 shows the swapchain HDR presents to, with
the paper white and peak ("HDR: HDR10 (PQ), 200/500 nits"), or that the
display offers none. With the setting off nothing is added, so the screen
stays as vanilla's.

Part of #1441.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 09:33:26 +01:00
SenjinandClaude Opus 5.5 58f2ea700e HDR: light sources shine by the kind of light, not its level
Light level says how far a source lights its surroundings, not how bright
it looks: a candle's flame is as bright as a torch's. Light sources now
get a tier (mesher::hdr_tier): very bright (the sun), bright (glowstone,
campfires, the soul fires), incandescent (flames, lava), low (redstone
torch, magma, a candle's wick) or none (glow lichen). Unlisted ones go by
their level. The tier rides in the emission bits as the level did, and
each has its own share of the headroom and its own lift window
(gfx/hdr.rs TIERS), tuned by eye on an HDR1000 OLED.

Flames, lava drops and obsidian tears now shine too: their tier rides in
the particle vertex's overlay bits, and with HDR on a second draw of the
entities writes only the frame's alpha for them, so their colour still
blends as before.

HORIZON_HDR_DEBUG=1 now greys what isn't lifted and tints what is, so a
lift shows on purple and red light too.

Part of #1441.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 09:07:23 +01:00
Senjin 9da81f9293 Merge remote-tracking branch 'origin/main' into hdr-tiers 2026-10-27 09:01:54 +01:00
asownerytandClaude Opus 5.5 1758b147b8 Untamed horses rear at a held item instead of taking the rider (#2697)
Horse, AbstractChestedHorse (donkeys, mules, llamas) and ZombieHorse.mobInteract:
with something other than food in hand, an untamed one is made mad (makeMad:
standIfPossible and the angry sound) and the player is not seated; an untamed
skeleton horse ignores the click (SkeletonHorse.mobInteract). The shared
equine::held_item decides it for the game and the mob_ai probe.

makeMad goes through makeSound (0.8 volume, the voice pitch from the mob's
random), with the baby horse's and the zombie horse's own angry sounds.
Donkeys and mules rear too (canPerformRearing is false only for llamas), a
rearing or eating horse runs no goals (AbstractHorse.isImmobile), and
RandomStandGoal plays the ambient sound, as vanilla's getAmbientStandSound.

On a server thread, a horse that bucks (RunAroundLikeCrazyGoal) now throws
off the server player riding it, not only the window's own player.

mob_ai harness: `interact` uses an item on a mob (Mob.mobInteract), with
sword scenarios for horses, donkeys, mules, zombie and skeleton horses and
llamas, foals included.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 20:36:10 +13:00
levzzz a3489d33b4 Merge remote-tracking branch 'upstream/main' into fix/root-vehicle-restore 2026-10-27 10:53:42 +05:00
Ludovic Marie 9bb3fc077e Merge #2570 2026-10-27 06:43:30 +01:00
Ludovic MarieandClaude Opus 5.5 2aa9056259 Structure mobs test: pass the world preset for_dimension now takes
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 06:43:30 +01:00
Ludovic Marie 8117e2767f Merge main 2026-10-27 06:43:23 +01:00
Ludovic MarieandClaude Opus 5.5 bc5ec6fb46 Structure mobs test: pass the world preset #2570 added to for_dimension
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 06:42:39 +01:00
Ludovic Marie e5ebeddb90 Merge #2585
# Conflicts:
#	crates/horizon-client/src/entity_nbt.rs
2026-10-27 06:42:33 +01:00
Ludovic Marie d0b42fb88e Merge #2570 2026-10-27 06:42:19 +01:00
Ludovic Marie bca77d1ac4 Merge origin/main 2026-10-27 06:42:18 +01:00
Ludovic MarieandClaude Opus 5.5 815c09b1f8 Merge #2214 (world presets in the create-world screen)
The superflat screen and the agent's state read the world preset that
#2214 adds instead of the old flat flag; drop a stray parenthesis in
the horizon-prepare hint.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 06:40:03 +01:00
FusselTVandClaude Opus 5.5 3788dff6a1 Worldgen: the surface step skips writes that change nothing
Most of the Nether's surface rule ends in netherrack, written over the
netherrack the noise step placed: every solid block paid for a section
write, two state table reads, four heightmap updates and a fluid check
for nothing. Column::set_block now leaves a block that already holds the
state (a fluid is still marked for post-processing, as every write marks
it).

chunkgen_bench, 576 chunks, 1 thread: Nether surface 1.81 -> 1.52 ms per
chunk; the terrain snapshots of seeds 12345 and 1 (Nether and Overworld)
are byte for byte the same.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 03:54:40 +01:00
Claude Codeandminimax-m3.1-flash 0104eeaf33 Format the world preset test
rustfmt keeps the registry read on one line.

Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 05:06:02 +05:00
Joansitoh 9458e6e99e Launcher: every text field uses the shared LineEdit
Join a friend, the mods search and the Servers fields (create, players,
console, MOTD, port, token, email) had their own String and their own
key handling, so none of them could move the caret, select or undo.
They use LineEdit now, and type_key and clip, which only they used, go.
2026-10-27 01:00:11 +01:00
levzzz 9c16d40db1 Merge remote-tracking branch 'upstream/main' into fix/root-vehicle-restore 2026-10-27 04:54:43 +05:00
Claude Codeandminimax-m3.1-flash 729a224eef Read the test registry without the explicit deref
clippy's explicit_auto_deref asks for the LazyLock itself, which
derefs to the registry the generator data keeps.

Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 04:52:31 +05:00
Claude Code 730184c64f Riding a horse survives a save and a reopen
The player's file now names the vehicle it rides, the way vanilla's
ServerPlayer.saveParentVehicle does, and the ride comes back on the next
login in both single player's modes.

- The player compound holds a "RootVehicle" compound whose "Attach" is the
  UUID of the vehicle at the root of the rider chain (Entity.getRootVehicle).
  Horizon keeps the vehicle in the world, so the file names it instead of
  holding a second copy of it: the horse and its equipment stay in the
  chunk's entity file, and nothing is duplicated.
- The single player's own player writes the vehicle it rides, and a server's
  guests write theirs; on the next login the vehicle is looked up by its
  UUID among the entities in the world, and the player rides it again.
- A guest whose file names a vehicle gets back on it once the vehicle is
  loaded, as ServerPlayer.loadAndSpawnParentVehicle does.

Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 04:40:43 +05:00
FusselTVandClaude Opus 5.5 22054eee90 Worldgen: chunk generation spawns its animals as vanilla, ticking or not
NaturalSpawner.spawnMobsForChunkGeneration runs in the generation
pipeline (spawnOriginalMobs), with the WorldGenRegion's random for the
spawn rules and finalizeSpawn. Horizon ran it from the game tick for
chunks that were fresh and lit, so:

- a world generated with ticks frozen, as a seed generator does, got no
  animals at all, and a chunk saved before a tick saw it lost them;
- the rules and finalizeSpawn drew from other randoms, and the reduced
  spawning::finalize gave no variants, babies or group data;
- the spawn list came from the biome's noise cell, not the zoomed biome
  (fullResolutionBiomes) vanilla reads;
- members of a pack were kept apart, though a WorldGenRegion has no
  entities in the way.

A chunk now gets its animals as soon as it is lit, whatever the tick
state. The positions come from the same selection, the rules and the
finalize from the chunk's region random, and each mob is made with its
constructor's state, finalized like a structure mob
(Reason::ChunkGeneration, group data per pack) and loaded from the NBT
generation would save. The natural spawn oracle's generation mode sees
no entities, as a WorldGenRegion.

Goats and frogs keep their long jump and ram cooldowns under vanilla's
memory names (long_jump_cooling_down, ram_cooldown_ticks), and rabbits
save the attack damage attribute Rabbit.setVariant makes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 00:37:18 +01:00
Claude Codeandminimax-m3.1-flash 52f0e38d41 Add the amplified, large-biome and single-biome world types
The create screen only offered Default and Superflat, so the amplified
world type of #1873 could not be chosen at all, and the generator
always built the overworld's noise settings.

The world-type button now cycles through the presets vanilla's
WorldPresetTags.NORMAL tag holds (normal, flat, large_biomes, amplified,
single_biome_surface), the chosen preset is written into the new world's
world_gen_settings.dat with the generator
worldgen/world_preset/<preset>.json has, and the generator is built from
that preset, so an amplified world gets the amplified noise settings.
A world opened again reads its preset back from its generator.

Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 04:20:01 +05:00
FusselTVandClaude Opus 5.5 1a80673d50 Worldgen: structure mobs save the fields vanilla's chunk keeps
ProtoChunk.addEntity saves a structure's entity as soon as it is placed,
and the chunk loads it again when it becomes full, so vanilla keeps what
each entity's own save writes. Horizon kept the template's NBT with its
model on top, and its model left out some fields:

- the keys in vanilla's templates that no entity reads (food_level,
  lastRestock, CatType, OwnerUUID, ...) are dropped;
- villagers save VillagerDataFinalized and their age (AgeableMob writes
  Age, ForcedAge and AgeLocked whenever canBeABaby, not only for
  creatures; never for a frog, a parrot or a zombie nautilus), and the
  age is read back for the same mobs (a baby villager or hoglin no
  longer grows up on load);
- iron golems and the other neutral mobs save anger_end_time;
- zombies, drowned and piglins save the movement speed attribute that
  their setBaby creates on load;
- a drowned with a nautilus shell keeps drop_chances (offhand 2.0), and
  saved drop chances are read back;
- the rider of a new chicken jockey is saved as it was when the chicken
  was added, before its equipment and attributes, as vanilla's chunk
  saves the chicken with its passenger at that point.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 23:12:29 +01:00
Kacpherek 75267d2fc6 Read inline goat horn instruments as well as registry ids 2026-10-26 21:43:33 +01:00
Kacpherek 4e96037c78 Never show fire-immune mobs as on fire 2026-10-26 20:42:58 +01:00
Kacpherek 9b88df32d3 Make goat horns play their sound and show the horn pose
A goat horn did nothing when used. It now plays its instrument at the
player, is held up for the instrument's duration, goes on cooldown, posts
the instrument_play game event and counts as used. Other players hear it,
see the arm raised to the head, and the tooltip names the call.
2026-10-26 19:54:15 +01:00
Kacpherek 5b066b1b78 Send the host's death and advancement messages to guests and follow team visibility 2026-10-26 19:53:09 +01:00
Kacpherek 36ebd0c545 Follow vanilla's lava fog for Fire Resistance and spectators, and brighten the fog colour with Night Vision 2026-10-26 19:34:12 +01:00
Kacpherek 4eb95c4d3e Follow the burn in daylight tag for mobs that burn in the sun 2026-10-26 18:53:26 +01:00
1269 changed files with 158513 additions and 30248 deletions

No files matched your search

+76
View File
@@ -0,0 +1,76 @@
---
name: vanilla-parity
description: Fix a Horizon bug or gap so it behaves 1:1 like vanilla 26.3, starting from the port ledger (tools/port_ledger.py) instead of grepping. Use for any issue, Discord report or PR that makes Horizon match vanilla: "X doesn't work like in Java", a missing sound, particle, animation, mob behaviour, block rule, screen or packet.
---
# Vanilla parity with the port ledger
The port ledger pairs every vanilla method with the Rust fn that ports it (or says it is missing, or needs
no port) and lists where a port isn't wired like vanilla. Ask it first; grep only for what it can't answer.
Full reference: `docs/port-ledger.md`.
## 0. Have a fresh ledger
```sh
python3 tools/port_ledger.py build # ~20 s; again after pulling main
```
It needs the vanilla jar (the launcher's `versions/26.3/26.3.jar`, or `--jar`) and/or the lab's decompiled
sources (`python3 tools/lab.py decompile`, or `--sources DIR`). If `build` says it has no vanilla code, run
the lab first; don't guess vanilla from memory.
## 1. From the report to the code
```sh
python3 tools/port_ledger.py issue 3236 # a GitHub issue number
python3 tools/port_ledger.py issue "mooshroom stays red after lightning" # or the report's text
```
It ranks the vanilla classes the report names and lists, per class, its Rust home files, where each method is
ported, and the known gaps (`missing` rows from the verdict map, seam/client/drift gaps). Often the cause is
already written there ("Type is not saved"). Then drill in:
```sh
python3 tools/port_ledger.py where MushroomCow.readAdditionalSaveData # ports, callers, callees, gaps
python3 tools/port_ledger.py class MushroomCow # every method: where, or missing
python3 tools/port_ledger.py what crates/horizon-client/src/mobs/farm.rs:120 # which vanilla method a fn ports
```
A ledger answer is a lead, not a verdict: about 1 in 12 map rows is wrong. Read the vanilla method (path and
line are in the output) and the Rust fn before changing anything.
## 2. Port it the vanilla way
- Translate the vanilla method, don't reinvent it: same order of operations, same constants, the same random
draws in the same order (`horizon_core::random`), Java `Math` through `horizon_core::jmath`.
- Open the fn's doc with the vanilla method it ports: `` /// `MushroomCow.readAdditionalSaveData`: ... ``.
That one line is what makes the next agent find it.
- Fix the path players run: single player is a client of the integrated server, so server logic lives in
`game/lan*.rs` and `src/lan/`, and what only the client's copy shows (particles, local sounds, hand
animation) must be reached from `game/remote*.rs`. A fix on the old in-frame path alone fixes nothing.
- Look at the neighbours the ledger lists for the same class: a `client` or `seam` gap next to your fix is
usually the same bug on the other path.
## 3. Before the PR
```sh
python3 tools/port_ledger.py check --base origin/main
```
It lists the ports your branch touches with their open gaps, citations to add, stale citations, and (against
the base) the gaps the branch closes and opens. Close what belongs to your fix, cite what you ported, leave
no new stale citation. If you ported a method the verdict map called `missing`, change its row in
`tools/port_map/*.tsv` to `port` with the fn (`file.rs::Owner::fn`) and run
`python3 tools/port_ledger.py map-check tools/port_map/<file>.tsv`.
Then the usual: the harness for the area (`docs/architecture.md`), `cargo check`/tests per `AGENTS.md`, and
a small PR that says which vanilla method it now matches.
## Finding work
```sh
python3 tools/port_ledger.py gaps --kind seam # ported on two paths, wired on one: the best leads
python3 tools/port_ledger.py gaps --kind client # client-only effects the server path never reaches
python3 tools/port_ledger.py gaps --kind drift # a port missing a sound, particle, item or constant
rg -n "player:" tools/port_map/*.tsv # missing behaviour a player would see
```
+14
View File
@@ -1,7 +1,21 @@
<!-- Opened by an AI agent? First line: "> 🤖 Opened by <your GitHub login>'s AI agent (<tool, model>), not by <your GitHub login> personally." Use your own login, not someone else's. -->
## What and why
<!-- What this changes, and why. Link the issue it fixes ("Fixes #123"). -->
## Kind
<!-- Tick one: it sets the pull request's label (CONTRIBUTING.md "Pull requests"). -->
- [ ] parity: Horizon now does what vanilla 26.3 does
- [ ] bug: a crash or a breakage
- [ ] perf: faster, or less memory
- [ ] feature: something new for players (listed under New in the release notes)
- [ ] extra: Horizon-only, off by default
- [ ] tooling: tools, CI, tests, harnesses
- [ ] docs
## Vanilla reference
<!-- The vanilla classes and methods this follows (tools/rea_vanilla.py), and the harness that checks it.
+6 -4
View File
@@ -16,12 +16,14 @@ jobs:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
BRANCH: ${{ github.event.pull_request.head.ref }}
SHA: ${{ github.event.pull_request.head.sha }}
HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }}
run: |
# Every head the pull request had, not only its last: a run-parity label added on an older head left that
# run queued after the merge (#3104, 2026-10-29). Its branch in its own repository names its runs (another
# fork can have a branch of the same name).
for status in queued in_progress waiting pending requested; do
gh run list -R "$REPO" --branch "$BRANCH" --status "$status" --limit 100 \
--json databaseId,event,headSha \
-q ".[] | select(.event == \"pull_request\" and .headSha == \"$SHA\") | .databaseId"
gh api -X GET "repos/$REPO/actions/runs" -f branch="$BRANCH" -f event=pull_request -f status="$status" \
-f per_page=100 --jq ".workflow_runs[] | select(.head_repository.full_name == \"$HEAD_REPO\") | .id"
done | sort -u | while read -r id; do
gh run cancel -R "$REPO" "$id" || true
done
+5 -6
View File
@@ -51,7 +51,7 @@ jobs:
if: steps.rust.outputs.required != 'false'
run: |
sudo apt-get update && sudo apt-get install -y --no-install-recommends libasound2-dev libudev-dev
rustup toolchain install stable --profile minimal --component rustfmt --component clippy --target wasm32-unknown-unknown
rustup toolchain install stable --profile minimal --component rustfmt --component clippy
# Dependencies and the workspace's own build, saved from main's runs (pull requests read main's cache).
- uses: Swatinem/rust-cache@v2
if: steps.rust.outputs.required != 'false'
@@ -62,8 +62,7 @@ jobs:
if: steps.rust.outputs.required != 'false'
run: |
cargo +stable fmt --all -- --check
# With the game's code mods (on in every release build, off by default).
cargo +stable clippy --workspace --all-targets --features horizon-client/mods -- -D warnings
cargo +stable clippy --workspace --all-targets -- -D warnings
# The server build (released as horizon-engine): the game without sound or controllers.
cargo +stable clippy -p horizon-client --all-targets --no-default-features -- -D warnings
# Libraries, binaries and integration tests, then doc tests: not the ~275 examples, which clippy above already
@@ -71,11 +70,11 @@ jobs:
- name: Test Rust core and voxel kernel correctness
if: steps.rust.outputs.required != 'false'
run: |
cargo +stable test --workspace --lib --bins --tests --features horizon-client/mods,horizon-mods/pack
cargo +stable test --workspace --lib --bins --tests
# The full content-root tests (#[ignore]d: minutes each on a laptop). The same build as above; they need the
# prepared vanilla data, so without it they pass at once.
cargo +stable test --workspace --lib --features horizon-client/mods,horizon-mods/pack mod_content::tests:: -- --ignored
cargo +stable test --workspace --doc --features horizon-client/mods,horizon-mods/pack
cargo +stable test --workspace --lib mod_content::tests:: -- --ignored
cargo +stable test --workspace --doc
# The launcher is its own workspace (launcher/Cargo.toml), so verify's cargo runs never build it. A pull request that
# changes it or a crate it builds is checked here, and so is every push to main: two pull requests can each pass
# alone and break it together (#2059 and the registry ratings did, 2026-10-26).
+76
View File
@@ -0,0 +1,76 @@
name: claim command
# `/claim` and `/unclaim` comments: anyone takes an issue before working on it, and a daily run releases the claims
# older than 5 days that no open pull request links (CONTRIBUTING.md "Claim before you start",
# tools/ci/claim_command.py).
#
# Why no pull request's code runs here, though the token can write:
# - `issue_comment` and `schedule` always run this file from main, whoever comments and wherever.
# - It checks out main's script and list only, never a pull request; it reads threads through the API.
# - The comment's text and author reach the script as env vars, never in a shell line. The token stays in the step
# and can only write issues (assignees, comments, reactions).
on:
issue_comment:
types: [created]
schedule:
- cron: "23 5 * * *"
workflow_dispatch:
permissions:
contents: read
jobs:
command:
# Every other comment skips here, before a runner starts. Pull requests are answered by the script.
if: >-
github.event_name == 'issue_comment' && github.event.comment.user.type != 'Bot' && (
startsWith(github.event.comment.body, '/claim') ||
startsWith(github.event.comment.body, '/unclaim') ||
startsWith(github.event.comment.body, '🤖 Agent: /claim') ||
startsWith(github.event.comment.body, '🤖 Agent: /unclaim'))
# One command per issue at a time, so two `/claim`s can't both find it free. On the job, not the workflow:
# the skipped runs of ordinary comments must not cancel a waiting one.
concurrency:
group: claim-${{ github.event.issue.number }}
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
issues: write
steps:
- uses: actions/checkout@v4
with:
ref: main
persist-credentials: false
sparse-checkout: |
.github/trusted-reviewers.txt
tools/ci/nightly_set.py
tools/ci/claim_command.py
sparse-checkout-cone-mode: false
- name: Run the command
env:
GH_TOKEN: ${{ github.token }}
BODY: ${{ github.event.comment.body }}
AUTHOR: ${{ github.event.comment.user.login }}
ISSUE: ${{ github.event.issue.number }}
COMMENT_ID: ${{ github.event.comment.id }}
REPO: ${{ github.repository }}
run: python3 tools/ci/claim_command.py command
release:
if: github.event_name != 'issue_comment' && github.repository == 'horizon-rust/horizon'
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
issues: write
steps:
- uses: actions/checkout@v4
with:
ref: main
persist-credentials: false
sparse-checkout: |
tools/ci/nightly_set.py
tools/ci/claim_command.py
sparse-checkout-cone-mode: false
- name: Release stale claims
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
run: python3 tools/ci/claim_command.py release
+8 -9
View File
@@ -37,9 +37,10 @@ permissions:
jobs:
exporters:
# This runner holds the maintainer's own pinned Minecraft jars. Only our classes leave it. An untrusted build
# goes to the Mac mini runner that already runs pull requests' parity jobs (macOS + horizon-game:
# ludovics-mac-mini), never the release runner (MAC_RUNNER: ludovics-mac-mini-release) nor parity-main's.
runs-on: ${{ inputs.trusted && vars.MAC_RUNNER || fromJSON('["self-hosted", "macOS", "horizon-game"]') }}
# goes to the Mac mini's nightly runner (macOS + horizon-nightly: ludovics-mac-mini-nightly, beside the one that
# runs pull requests' parity jobs, so a nightly never waits behind their queue), never the release runner
# (MAC_RUNNER: ludovics-mac-mini-release) nor parity-main's.
runs-on: ${{ inputs.trusted && vars.MAC_RUNNER || fromJSON('["self-hosted", "macOS", "horizon-nightly"]') }}
# Three full preparations (two isolated, one batched) besides the compile.
timeout-minutes: 60
steps:
@@ -103,7 +104,7 @@ jobs:
mac: true
dir: horizon-macos-arm64
asset: horizon-macos-arm64.zip
runs-on: ${{ matrix.mac && (inputs.trusted && vars.MAC_RUNNER || fromJSON('["self-hosted", "macOS", "horizon-game"]')) || matrix.os }}
runs-on: ${{ matrix.mac && (inputs.trusted && vars.MAC_RUNNER || fromJSON('["self-hosted", "macOS", "horizon-nightly"]')) || matrix.os }}
# Notarizing the zip can take a few minutes.
timeout-minutes: ${{ matrix.mac && 60 || 40 }}
env:
@@ -137,8 +138,7 @@ jobs:
if: runner.os == 'Linux'
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends libasound2-dev libudev-dev
- name: Build the client, horizon-ctl, horizon-prepare and horizon-server
# `horizon-client/mods`: every released game runs code mods (the feature is off in dev builds).
run: cargo build --release --locked --features horizon-client/mods -p horizon-client --bin horizon-client --bin horizon-ctl -p horizon-prepare --bin horizon-prepare -p horizon-server --bin horizon-server
run: cargo build --release --locked -p horizon-client --bin horizon-client --bin horizon-ctl -p horizon-prepare --bin horizon-prepare -p horizon-server --bin horizon-server
- name: Every program the release folder takes was built
run: |
for binary in horizon-client horizon-ctl horizon-prepare horizon-server; do
@@ -266,9 +266,8 @@ jobs:
run: |
set -euo pipefail
# After the client is packaged: target/release/horizon-client is rebuilt without the audio and
# controllers features, so it needs neither libasound nor libudev (minimal server images lack them),
# and with code mods like the client.
cargo build --release --locked -p horizon-client --no-default-features --features mods --bin horizon-client
# controllers features, so it needs neither libasound nor libudev (minimal server images lack them).
cargo build --release --locked -p horizon-client --no-default-features --bin horizon-client
cargo run --locked -p xtask -- package-server target/release/horizon-client "dist/${{ matrix.server_dir }}"
folder="dist/${{ matrix.server_dir }}"
python3 tools/check_imports.py --server "$folder/horizon-engine" "$folder/horizon-server" "$folder/horizon-prepare"
+15 -8
View File
@@ -1,11 +1,12 @@
name: nightly build
run-name: nightly build${{ inputs.request && format(' (request {0})', inputs.request) || '' }}
# The opt-in Nightly channel (docs/release-flow.md "Nightly"; Ludovic, 2026-10-27). Once a night, and when a trusted
# The Nightly, the only build Horizon ships since 2026-10-28 (docs/release-flow.md "Nightly"; Ludovic). When the batch
# train decides (release.yml, with the commit as `base`), once a night, and when a trusted
# reviewer asks (`/nightly 2640 2642`, nightly-command.yml), it builds main plus the pull requests the reviewers
# picked, merged in order (tools/ci/nightly_set.py), or main alone with none picked. Main is its newest commit where
# checks and parity-main passed (or the `base` input), as for a release, and no Nightly is published while a
# release-blocker is open. It publishes the result as the `nightly` pre-release. Launchers install it only for players
# who turn Nightly on. The previous nightly stays a week as nightly-<date>.
# release-blocker is open. It publishes the result as the `nightly` pre-release, which every launcher and
# `horizon-server update` install. The previous nightly stays a week as nightly-<date>.
#
# Why the signing key never meets the pull requests' code:
# - `pick` merges the picked heads onto main with `git merge-tree --write-tree`: git objects only, no working tree,
@@ -60,6 +61,10 @@ on:
description: The commit of main to build on (default main's newest with green checks and parity-main)
type: string
default: ''
urgent:
description: Past the day's cap of 12 builds (the train, for a release-now pull request or a forced run)
type: boolean
default: false
permissions:
contents: read
concurrency:
@@ -102,6 +107,7 @@ jobs:
NIGHTLY_ISSUE: ${{ inputs.issue }}
NIGHTLY_PRS: ${{ inputs.prs }}
NIGHTLY_BASE: ${{ inputs.base }}
NIGHTLY_URGENT: ${{ inputs.urgent }}
run: |
python3 tools/ci/nightly.py pick ${FORCE:+--force} | tee -a "$GITHUB_STEP_SUMMARY"
{ echo; cat nightly-notes.md; } >> "$GITHUB_STEP_SUMMARY"
@@ -223,12 +229,13 @@ jobs:
set -euo pipefail
python3 tools/ci/nightly.py rotate
signature=(); [[ -f dist/SHA256SUMS.sig ]] && signature=(dist/SHA256SUMS.sig)
# A pre-release, never GitHub's latest: launchers on the release channel never see it.
# A pre-release, never GitHub's latest: the latest stays the last vX.Y.Z, which launchers and servers from
# before the Nightly-only switch keep (they don't read Nightlies).
gh release create nightly --prerelease --latest=false --target "$SHA" \
--title "Nightly $DATE · ${SHA:0:7}" --notes-file dist/nightly-notes.md \
"dist/horizon-linux-x86_64.tar.gz#Nightly game files: Linux (horizon-linux-x86_64.tar.gz)" \
"dist/horizon-windows-x64.zip#Nightly game files: Windows (horizon-windows-x64.zip)" \
"dist/horizon-macos-arm64.zip#Nightly game files: macOS (horizon-macos-arm64.zip)" \
"dist/horizon-linux-x86_64.tar.gz#Game files the launcher installs (Nightly): Linux (horizon-linux-x86_64.tar.gz)" \
"dist/horizon-windows-x64.zip#Game files the launcher installs (Nightly): Windows (horizon-windows-x64.zip)" \
"dist/horizon-macos-arm64.zip#Game files the launcher installs (Nightly): macOS (horizon-macos-arm64.zip)" \
"dist/horizon-server-linux-x86_64.tar.gz#Nightly dedicated server for Linux (horizon-server-linux-x86_64.tar.gz)" \
"dist/horizon-source.tar.gz#Source of this Nightly, commit ${SHA:0:7} (horizon-source.tar.gz; kept for good as the tag $SOURCE_TAG)" \
dist/SHA256SUMS "${signature[@]}" dist/smoke-test.json
@@ -264,7 +271,7 @@ jobs:
elif [[ "$PUBLISHED" == success && "$DRY_RUN" == true ]]; then
text="the dry run built and signed $NAME; nothing was published. $RUN"
elif [[ "$PUBLISHED" == success ]]; then
text="published $NAME: https://github.com/$GITHUB_REPOSITORY/releases/tag/nightly. Launchers with Nightly on install it at their next start."
text="published $NAME: https://github.com/$GITHUB_REPOSITORY/releases/tag/nightly. Launchers install it at their next check."
else
text="the build failed, nothing was published: $RUN"
fi
+41
View File
@@ -0,0 +1,41 @@
name: overlap note
# One comment on a pull request naming the other open pull requests that close the same issue or change the same
# functions, edited on every push (tools/ci/overlap_note.py, issue #3859).
#
# Why no pull request's code runs here, though `pull_request_target` gives a token that can write:
# - `pull_request_target` runs this file and the script from main, never the pull request's copy.
# - The checkout is main's (`ref: main`), blobless, with the script alone on disk. The pull requests' heads are fetched
# as git objects and only diffed, never checked out; git runs with hooks, fsmonitor, external diff and textconv off.
# - The rest comes from the API. The token writes the comment only.
on:
pull_request_target:
types: [opened, reopened, synchronize, ready_for_review]
permissions:
contents: read
jobs:
note:
if: github.event.pull_request.base.ref == 'main'
# A newer push replaces the note being computed.
concurrency:
group: overlap-note-${{ github.event.pull_request.number }}
cancel-in-progress: true
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v4
with:
ref: main
fetch-depth: 0
filter: blob:none
persist-credentials: false
sparse-checkout: |
tools/ci/overlap_note.py
sparse-checkout-cone-mode: false
- name: Note
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
run: python3 tools/ci/overlap_note.py event
+36
View File
@@ -0,0 +1,36 @@
name: pr labels
# A pull request's kind label (the box ticked in the template) and area labels (the paths it changes):
# tools/ci/pr_labels.py, CONTRIBUTING.md "Pull requests".
#
# Why no pull request's code runs here, though `pull_request_target` gives a token that can write to fork PRs:
# - `pull_request_target` runs this file and the script from main, never the pull request's copy.
# - It checks out main's script only (`ref: main`), never the pull request's head or merge commit.
# - The script reads the event file (the description is data in JSON, never in a shell line) and the API's list of
# changed file names; it runs, builds and imports nothing of theirs. The token writes labels only.
on:
pull_request_target:
types: [opened, reopened, synchronize, edited, ready_for_review]
permissions:
contents: read
jobs:
label:
# Title-only edits change nothing: skip them before a runner starts.
if: github.event.action != 'edited' || github.event.changes.body != null
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v4
with:
ref: main
persist-credentials: false
sparse-checkout: |
tools/ci/pr_labels.py
sparse-checkout-cone-mode: false
- name: Label
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
run: python3 tools/ci/pr_labels.py event
+27 -139
View File
@@ -1,22 +1,15 @@
name: release
# Automatic game releases (docs/release-flow.md). After every green `checks` run on main, every 10 minutes (so a
# release waiting for merges to settle goes out without another merge) and by hand, tools/release_train.py decides,
# for a commit whose checks and parity-main (the vanilla harnesses on main) both passed:
# a release when 5 pull requests merged since the last one changed what players run and the newest merged 5 minutes
# ago, or when the oldest has waited 6 hours, or at once for a pull request labelled release-now.
# RELEASES_PAUSED=1 (repository variable) stops them; so does an open issue labelled release-blocker, forced runs too.
# Builds Linux (an older Ubuntu, so the binary runs on any recent distribution and on NixOS through nix-ld) and
# Windows on GitHub, macOS on the Mac mini's runner (MAC_RUNNER=macos-latest sends it to GitHub), then
# publishes vX.Y.Z on the release line in .github/release-line.txt (docs/versioning.md; without it, the dated
# vYYYY.MM.DD[.N] from before 0.2) with notes from the merged pull requests, as the release and GitHub's latest: one
# channel, which launchers, `horizon-server update` and install.sh follow at once. So it is published only after the
# checks that used to gate a promotion pass in this run: the Mac mini's smoke test drew a world, and no release-blocker
# is open (tools/release_train.py gate); otherwise nothing is published. macOS: with the repository variable
# MACOS_SIGN_IDENTITY set, the programs are signed with
# Developer ID and the hardened runtime and the zip is notarized, from the runner's signing keychain
# (packaging/macos/README.md); unset (forks), ad hoc.
# gh workflow run release --ref <branch> # dry run: builds and notes, publishes nothing
# gh workflow run release -f dry_run=false -f force=true # on main: release now, whatever the rules say
# Automatic game releases (docs/release-flow.md). Horizon ships only the Nightly since 2026-10-28 (Ludovic): this
# workflow is the batch train that decides when a new Nightly goes out, and `nightly build` (nightly-build.yml) builds,
# signs and publishes it. After every green `checks` run on main, every 10 minutes (so a batch waiting for merges to
# settle goes out without another merge) and by hand, tools/release_train.py decides, for a commit whose checks and
# parity-main (the vanilla harnesses on main) both passed: a Nightly when 5 pull requests merged since the published
# Nightly changed what players run and the newest merged 5 minutes ago, or when the oldest has waited 6 hours, or at
# once for a pull request labelled release-now. RELEASES_PAUSED=1 (repository variable) stops it; so does an open issue
# labelled release-blocker, forced runs too (nightly-build.yml checks again before publishing). One Nightly build at a
# time, at most 12 a day (release-now and force=true pass the cap). No `vX.Y.Z` game release is made any more.
# gh workflow run release --ref <branch> # dry run: decides, starts nothing
# gh workflow run release -f dry_run=false -f force=true # on main: a Nightly now, whatever the rules say
on:
workflow_run:
# A game release ships a commit both passed on (parity-main runs on batches: the newest commit it passed on).
@@ -28,17 +21,19 @@ on:
workflow_dispatch:
inputs:
dry_run:
description: Build everything and write the notes, but publish nothing
description: Decide only; start no Nightly build
type: boolean
default: true
force:
description: Release even without player-facing changes, before merges settle or while paused
description: A Nightly even without player-facing changes, before merges settle, while paused or past the day's cap
type: boolean
default: false
permissions:
contents: read
# The open release-blocker issues (decide).
issues: read
# The checks, parity-main and nightly build runs (decide).
actions: read
concurrency:
# One release at a time. A hand-started dry run publishes nothing, so it can't race a real release: it gets a
# group of its own. In the shared group, a release queued behind it replaced it as the pending run and GitHub
@@ -54,9 +49,8 @@ jobs:
timeout-minutes: 10
outputs:
release: ${{ steps.decide.outputs.release }}
tag: ${{ steps.decide.outputs.tag }}
version: ${{ steps.decide.outputs.version }}
head: ${{ steps.decide.outputs.head }}
urgent: ${{ steps.decide.outputs.urgent }}
dry_run: ${{ steps.mode.outputs.dry_run }}
steps:
- uses: actions/checkout@v4
@@ -76,127 +70,21 @@ jobs:
run: |
python3 tools/release_train.py decide --kind game ${HEAD_SHA:+--head "$HEAD_SHA"} ${FORCE:+--force} | tee decision.json
echo "### $(jq -r '.reason' decision.json)" >> "$GITHUB_STEP_SUMMARY"
# The version the programs are built with (docs/versioning.md): the tag without its v.
tag=$(jq -r '.tag' decision.json)
echo "version=${tag#v}" >> "$GITHUB_OUTPUT"
# A dry run builds whatever the decision, to test the builds.
if [[ "${{ steps.mode.outputs.dry_run }}" == true ]]; then echo "release=true" >> "$GITHUB_OUTPUT"; fi
build:
nightly:
# The Nightly is the release: nightly-build.yml builds the decided commit plus the reviewers' picks, signs it for
# the Nightly channel and publishes it once its smoke test passed and no release-blocker is open.
needs: decide
if: needs.decide.outputs.release == 'true'
# Every platform's archive, and macOS's smoke test (game-build.yml).
uses: ./.github/workflows/game-build.yml
with:
ref: ${{ needs.decide.outputs.head }}
version: ${{ needs.decide.outputs.version }}
trusted: true # main: the release runner, the release cache, no shared sccache
sign:
needs: [decide, build]
if: needs.decide.outputs.release == 'true' && needs.decide.outputs.dry_run != 'true'
runs-on: ubuntu-latest
timeout-minutes: 10
# The only job that holds the release signing key, a secret of the `release` environment, which only main's real
# releases enter (dry runs sign with a throwaway key). It runs no code from the repository but
# tools/ci/sign_release.sh, which calls only the runner's sha256sum and OpenSSL (docs/release-flow.md).
environment: ${{ needs.decide.outputs.dry_run != 'true' && 'release' || '' }}
steps:
- uses: actions/checkout@v4
with:
sparse-checkout: tools/ci/sign_release.sh
sparse-checkout-cone-mode: false
persist-credentials: false
- uses: actions/download-artifact@v4
with:
path: dist
pattern: horizon-*
merge-multiple: true
- name: Sign the checksums (SHA256SUMS, SHA256SUMS.sig)
env:
RELEASE_SIGNING_KEY: ${{ secrets.RELEASE_SIGNING_KEY }}
RELEASE_SIGNING_KEY_NEXT: ${{ secrets.RELEASE_SIGNING_KEY_NEXT }}
EPHEMERAL: ${{ needs.decide.outputs.dry_run == 'true' && 'yes' || '' }}
TAG: ${{ needs.decide.outputs.tag }}
run: |
bash tools/ci/sign_release.sh "$TAG" dist/horizon-linux-x86_64.tar.gz dist/horizon-windows-x64.zip \
dist/horizon-macos-arm64.zip dist/horizon-server-linux-x86_64.tar.gz
- uses: actions/upload-artifact@v4
with:
name: signature
path: dist/SHA256SUMS*
if-no-files-found: error
retention-days: 3
- uses: actions/upload-artifact@v4
if: needs.decide.outputs.dry_run == 'true'
with:
name: ephemeral.pub
path: dist/ephemeral.pub
retention-days: 3
publish:
needs: [decide, build, sign]
runs-on: ubuntu-latest
timeout-minutes: 15
timeout-minutes: 5
permissions:
contents: write
issues: read
actions: write # starts `nightly build`
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Notes
env:
- env:
GH_TOKEN: ${{ github.token }}
HEAD_SHA: ${{ needs.decide.outputs.head }}
URGENT: ${{ needs.decide.outputs.urgent }}
run: |
python3 tools/release_train.py notes --kind game --tag "${{ needs.decide.outputs.tag }}" \
--head "${{ needs.decide.outputs.head }}" --out notes > /dev/null
{ echo "## $(cat notes/title.txt)"; echo; cat notes/body.md; } >> "$GITHUB_STEP_SUMMARY"
- uses: actions/download-artifact@v4
with:
path: dist
merge-multiple: true
- name: The signature, checked as launchers check it
env:
TAG: ${{ needs.decide.outputs.tag }}
EPHEMERAL: ${{ needs.decide.outputs.dry_run == 'true' && 'yes' || '' }}
run: |
key=(); [[ -n "$EPHEMERAL" ]] && key=(--key "$(cat dist/ephemeral.pub)")
cargo run --locked --quiet -p horizon-release-sign -- verify --tag "$TAG" --publishing "${key[@]}" dist
- name: Ready to publish (the smoke test passed, no release-blocker is open)
# Right before publishing: an issue labelled while the builds ran still holds this release. A dry run only
# reports what a real run would do.
env:
GH_TOKEN: ${{ github.token }}
DRY_RUN: ${{ needs.decide.outputs.dry_run }}
run: |
if ! python3 tools/release_train.py gate --kind game --smoke dist/smoke-test.json --blockers; then
[[ "$DRY_RUN" == true ]] || { echo "::error::Nothing published"; exit 1; }
echo "::warning::A real run would publish nothing"
fi
- name: Publish ${{ needs.decide.outputs.tag }}
if: needs.decide.outputs.dry_run != 'true'
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ needs.decide.outputs.tag }}
run: |
signature=(); [[ -f dist/SHA256SUMS.sig ]] && signature=(dist/SHA256SUMS.sig)
# The release and the latest at once: there is one channel (docs/release-flow.md). Each archive gets a
# label (file#label), which GitHub's page shows instead of the file name, so nobody takes the game's
# files for the download (#1647). Launchers, servers and install.sh match the name, which stays.
gh release create "$TAG" --target "${{ needs.decide.outputs.head }}" --title "$(cat notes/title.txt)" \
--notes-file notes/body.md --latest \
"dist/horizon-linux-x86_64.tar.gz#Game files the launcher installs: Linux (horizon-linux-x86_64.tar.gz)" \
"dist/horizon-windows-x64.zip#Game files the launcher installs: Windows (horizon-windows-x64.zip)" \
"dist/horizon-macos-arm64.zip#Game files the launcher installs: macOS (horizon-macos-arm64.zip)" \
"dist/horizon-server-linux-x86_64.tar.gz#Dedicated server for Linux (horizon-server-linux-x86_64.tar.gz)" \
dist/SHA256SUMS "${signature[@]}" dist/smoke-test.json
- name: Dry run, the release that would be published
if: needs.decide.outputs.dry_run == 'true'
run: |
echo "Would publish ${{ needs.decide.outputs.tag }} at ${{ needs.decide.outputs.head }} as the release and latest: $(cat notes/title.txt)"
ls -l dist
- uses: actions/upload-artifact@v4
with:
name: release-notes
path: notes/
retention-days: 3
gh workflow run nightly-build.yml --repo "$GITHUB_REPOSITORY" --ref main -f base="$HEAD_SHA" -f urgent="$URGENT"
echo "### Started \`nightly build\` on ${HEAD_SHA:0:9}" >> "$GITHUB_STEP_SUMMARY"
+6 -2
View File
@@ -5,11 +5,15 @@ name: run-parity
# skipped `verify`, and GitHub counts a skipped required check as passed.
on:
pull_request:
types: [labeled]
# A push starts a run only to cancel the old head's (see concurrency): the job runs on the label alone.
types: [labeled, synchronize]
permissions:
contents: read
concurrency:
group: run-parity-${{ github.event.pull_request.number }}
# The run-parity label and pushes share one group per pull request, so the newest run cancels the one before: a
# pushed pull request's run tested a head the merge would no longer land. Any other label gets a group of its own:
# in the shared one, adding needs-proof cancelled the parity run queued seconds before (run 40766180150).
group: run-parity-${{ github.event.pull_request.number }}${{ github.event.action == 'labeled' && github.event.label.name != 'run-parity' && format('-{0}', github.run_id) || '' }}
cancel-in-progress: true
env:
# Test runs never reach the telemetry server: no live-count ping, no uploads (docs/telemetry.md).
+29 -21
View File
@@ -13,13 +13,15 @@ Feature → files → harness for the most-changed areas: [docs/architecture.md]
`XDG_CONFIG_HOME`, plus `HORIZON_LAUNCHER_NO_SELF_UPDATE=1` and `HORIZON_NO_AUDIO=1`, and, for the
launcher, `HORIZON_LAUNCHER_TRASH`, `HORIZON_LAUNCHER_CLIPBOARD` and `HORIZON_LAUNCHER_DESKTOP` pointing into the scratch home. Never write to
`~/.local/share/{applications,horizon,horizon-launcher}` or `~/.local/bin`.
- **Mark what an agent posts.** A PR body starts with a line naming whose agent opened it and with what.
Ludovic's agents write `> 🤖 Opened by Ludovic's AI agent (Claude), not by Ludovic himself.`, and a
contributor's agent names its own person and tool (issues: "Filed by", reviews: "Review by"). Every
comment starts with `🤖 Agent:`. Never write as Ludovic or as anyone else.
- **Mark what an agent posts.** A PR body starts with a line naming the person whose agent opened it and
the tool: `> 🤖 Opened by <login>'s AI agent (<tool, model>), not by <login> personally.`, where `<login>`
is the GitHub account the PR is opened from (`gh api user --jq .login`). Issues say "Filed by", reviews
"Review by". Every comment starts with `🤖 Agent:`. Write only as the person you work for: the lines on
this page that name Ludovic are for his own agents (account `ludovic111`), never for anyone else's.
- **Attribution.** Commits end with a `Co-Authored-By:` line for the agent that wrote them, and PR bodies
with that agent's own "Generated with" line. Ludovic's agents use
`Co-Authored-By: Claude <model> <[email protected]>` and
with that agent's own "Generated with" line. Example, Ludovic's agents (Claude Code on `ludovic111`):
first line `> 🤖 Opened by Ludovic's AI agent (Claude), not by Ludovic himself.`, commits
`Co-Authored-By: Claude <model> <[email protected]>`, last line
`🤖 Generated with [Claude Code](https://claude.com/claude-code)`.
- Everything players run is Rust. Python/Java only for dev tools and harness drivers in `tools/`.
- Horizon-only features follow [docs/design-system.md](docs/design-system.md) and are off by default where
@@ -28,6 +30,8 @@ Feature → files → harness for the most-changed areas: [docs/architecture.md]
change), none for a fix. Agents never add `release-now`: the maintainer decides it. Merges ship in batches of 5, or after
6 hours ([docs/release-flow.md](docs/release-flow.md)).
- Contributors first: `gh pr list --state open --search <area>` before you start; never duplicate theirs.
- **Claim before you start**: comment `/claim` on the issue (no issue yet: open one, then claim it). An assigned
issue is taken: pick another. `/unclaim` when you stop; a claim with no open PR linking it is released after 5 days.
## Crates
@@ -35,11 +39,12 @@ Game workspace (root `Cargo.toml`); `launcher/` and `crates/horizon-telemetry-se
| Crate | Owns |
|---|---|
| `horizon-client` | The game: a lib (`src/lib.rs`, ~170 flat modules) and the bin (`src/main.rs` → `game.rs` + `game/`), plus `horizon-ctl` (`src/bin/horizon-ctl/`). Also the dedicated server (`--dedicated`) |
| `horizon-client` | The game: a lib (`src/lib.rs`, ~100 flat modules: gameplay, networking, title screens) and the bin (`src/main.rs` → `game.rs` + `game/`), plus `horizon-ctl` (`src/bin/horizon-ctl/`). Also the dedicated server (`--dedicated`) |
| `horizon-items` | Blocks and items below gameplay: `blocks.rs` (block facts), `items.rs`, `item_data.rs` (components), `enchant.rs`, `cooldowns.rs`, `bundle.rs`, `item_nbt.rs`. Re-exported as `horizon_client::items`... |
| `horizon-command` | Command parsing (Brigadier): the vanilla tree, argument types, selectors, suggestions, the tree's network form. Re-exported as `horizon_client::command` |
| `horizon-client-core` | The client lib's bottom layer: `text.rs`, `lang.rs`, `snbt.rs`, `nbt_path.rs`, `component.rs`, `scoreboard.rs`, `bossbar.rs`, `gamerules.rs`, `block_entity_nbt.rs`, registry/particle/sound tables. Re-exported under the old paths (`horizon_client::text`...) |
| `horizon-client-input` | Key bindings and key names, toggle keys, mouse options, controller settings and glyphs, the clipboard, window options (`keybinds.rs`, `gamepad.rs`, `mouse.rs`, `window.rs`...). Re-exported under the old paths |
| `horizon-client-render` | The client's world data, rendering and screens (chunks, meshing, light, streaming, `gfx`, HUD, menus, settings): ~80 modules that horizon-client re-exports under the same paths. Nothing in it reaches gameplay |
| `horizon-world` | Vanilla world data with no gameplay loop: NBT, regions, chunks, `datafix/`, `worldgen/`, `level/` (server-side block behaviour, ticks, redstone), entity physics, items, effects, food, damage |
| `horizon-core` | Simulation primitives: Java `Random`s, `jmath` (HotSpot-exact `Math`), positions, noise, density, paletted storage, `version.rs` (the pinned version) |
| `horizon-protocol` | Network protocol 777: codecs, framing, encryption, `packets/{handshake,status,login,configuration,play}.rs`, `entity_data.rs` |
@@ -47,8 +52,9 @@ Game workspace (root `Cargo.toml`); `launcher/` and `crates/horizon-telemetry-se
| `horizon-prepare` | Builds `.local/horizon-client/26.3` from the player's own Minecraft install (runs the Java exporters) |
| `horizon-server` | `horizon-server`: `server.toml`, CLI, supervisor of the headless game ([docs/hosting.md](docs/hosting.md)) |
| `horizon-server-files` | `server.properties`, eula, ops/whitelist/bans, usercache |
| `horizon-anticheat` | The dedicated server's anticheat, off by default: `[anticheat]` settings, violation levels, the checks' rules; the hub runs them (`lan/anticheat.rs`) ([docs/anticheat.md](docs/anticheat.md)) |
| `horizon-friends`, `horizon-relay` | Play with Friends: invite codes, encrypted tunnel, the relay ([docs/play-with-friends.md](docs/play-with-friends.md)) |
| `horizon-mods`, `horizon-mod-sdk`, `cargo-horizon-mod` | Sandboxed WebAssembly mods: runtime, SDK + WIT, `cargo horizon-mod` ([docs/mods.md](docs/mods.md)) |
| `horizon-mods`, `cargo-horizon-mod` | Content mods (`.hmod`: manifest, content fence, no code), the registry index and `.hpack`, and `cargo horizon-mod` (`new`, `build`, `run`, `check`, `registry`) ([docs/mods.md](docs/mods.md)). Code mods are native, built by the launcher ([docs/native-mods.md](docs/native-mods.md)) |
| `horizon-port-pack` | Porting Java modpacks ([docs/modpacks.md](docs/modpacks.md)) |
| `horizon-shader-api` | Horizon's own WGSL shader packs ([docs/api/shaders.md](docs/api/shaders.md)): `horizon-shaders.toml`, the `.hshader` package; shared by the game, the launcher and the registry |
| `horizon-shaders` | Shader packs (the `shaders/` folder format), rebuilt clean-room from a written spec of the format; no wgpu |
@@ -68,12 +74,12 @@ reads `README.md`, `launcher/links.json`, `docs/api/`, `roadmap/roadmap.json` an
python3 tools/pr_ready.py # the self-check: fmt, clippy -D warnings, touched crates' tests, harnesses; paste its block in the PR
python3 tools/pr_ready.py --quick # same without harnesses
cargo test -p horizon-client --lib --bins --tests # skips the 104 examples (pr_ready does this); `--doc` separately
cargo test -p horizon-client --features horizon-client/mods --lib mod_content::tests:: -- --ignored # the 5 slow content-root tests, #[ignore]d; pr_ready runs them when mod content, assets, blocks or items change
cargo test -p horizon-client --lib mod_content::tests:: -- --ignored # the 5 slow content-root tests, #[ignore]d; pr_ready runs them when mod content, assets, blocks or items change
cargo build --profile fast -p horizon-client # optimized, quick to link: use it to play or run lan_e2e
python3 tools/parity.py --changed origin/main # the harnesses your branch can affect (CI's parity job)
python3 tools/parity.py --changed-files paths.txt --list # which harnesses cover these files, without running them
python3 tools/parity.py mob_ai sculk # named harnesses
python3 tools/lan_e2e.py --check chest # two real games over LAN (server-thread host); --in-frame compares the fallback
python3 tools/lan_e2e.py --check chest # two real games over LAN (server-thread host)
python3 tools/agent_control_e2e.py # horizon-ctl / MCP end to end
```
@@ -88,9 +94,6 @@ python3 tools/agent_control_e2e.py # horizon-ctl / MCP end to end
- What a change reruns (of 329 harnesses, measured 2026-10-23): a file of the game binary (`game.rs`, `game/`)
~25, a file of the client lib ~103, of `horizon-world` ~103–109, of `horizon-core` ~119. Plan for it.
- Rust integration tests: `crates/horizon-client/tests/` (`lan_bot.rs`, `generation_saves.rs`, `streaming_meshes.rs`).
- **macOS:** `cargo build/test --workspace` may fail to link the two wasm guest cdylibs (undefined
`horizon:mods/events` symbols) until #1446 lands: add `--exclude coords-hud --exclude predator-missile --exclude horizon-mods-test-guest --exclude horizon-mods-test-ui-guest`.
Their real check is `cargo test -p horizon-mods` (builds them for `wasm32-unknown-unknown`).
- Linux needs `libasound2-dev libudev-dev`; `--no-default-features` drops audio and controllers (the server build).
- Run and drive the game (starts the `horizon-client` beside `horizon-ctl`; other arguments go to the game):
`target/fast/horizon-ctl --socket $PWD/ctl.sock launch --world W --creative --offscreen --wait --resources $PWD/.local/horizon-client/26.3`,
@@ -99,6 +102,9 @@ python3 tools/agent_control_e2e.py # horizon-ctl / MCP end to end
## Vanilla reference
- Start parity work from the port ledger, not grep: `python3 tools/port_ledger.py issue N` (or `where
Class.method`, `what file:line`), and `check --base origin/main` before the PR. How:
[.claude/skills/vanilla-parity/SKILL.md](.claude/skills/vanilla-parity/SKILL.md), [docs/port-ledger.md](docs/port-ledger.md).
- Decompiled sources: `.local/minecraft/26.3/sources/{client,server}/` (`python3 tools/lab.py decompile`).
- Bytecode queries (callers through lambdas, every overload):
`python3 tools/rea_vanilla.py method net.minecraft.world.level.block.DispenserBlock dispenseFrom`
@@ -110,7 +116,8 @@ python3 tools/agent_control_e2e.py # horizon-ctl / MCP end to end
## horizon-client: where things live
`L:` = lib (`crates/horizon-client/src/`), `G:` = bin (`crates/horizon-client/src/game/`). The lib can't see
`L:` = lib (`crates/horizon-client/src/`, or `crates/horizon-client-render/src/` for the modules that crate holds, under
the same `horizon_client::<module>` paths), `G:` = bin (`crates/horizon-client/src/game/`). The lib can't see
`Game`; bin code reaches the lib as `horizon_client::<module>`.
| Subsystem | Files |
@@ -129,7 +136,7 @@ python3 tools/agent_control_e2e.py # horizon-ctl / MCP end to end
| Multiplayer client | L: `net.rs` (network thread); G: `remote.rs` (`fn handle_packet`, `fn tick_remote`), `remote_*.rs` (mob data, objects, projectiles, vehicles, stations) |
| LAN host / server side | L: `lan/` (`conn.rs` handshake + login threads, `hub.rs` + `hub/` play state, `chunks.rs`, `entity.rs`, `tracking.rs`, `guest.rs`, `playerdata.rs`); G: `lan.rs` (`LanHost`), `lan_actions.rs` (`fn as_guest`, `fn tick_guests`), `lan_*.rs` per feature, `lan_entities.rs` (tracked entities) |
| Integrated server | G: `integrated.rs` (the server thread), `levels.rs` (one level per dimension, `fn tick_server`), `server_travel.rs` |
| Dedicated server | G: `dedicated.rs`, `dedicated/status.rs`; crates `horizon-server-files`, `horizon-server` |
| Dedicated server | G: `dedicated.rs`, `dedicated/status.rs`; crates `horizon-server-files`, `horizon-server`, `horizon-anticheat` (L: `lan/anticheat.rs`) |
| Save and load | L: `save.rs` (level.dat, regions, playerdata), `entity_nbt.rs`, `options_file.rs`; `horizon-client-core/src/block_entity_nbt.rs`; G: `saving.rs`, `persist.rs` (entities); `crates/horizon-world/src/{region.rs,nbt.rs,datafix/}` |
| Input | game.rs `fn window_event`, `fn key_pressed`, `fn play_mouse`; crate `horizon-client-input` (`keybinds.rs`, `keymapping.rs`, `key_names.rs`, `mouse.rs`, `input.rs`, `interaction_input.rs`, `gamepad.rs`, `clipboard.rs`); L: `osk.rs`; G: `pad.rs`, `pad_menu.rs` |
| Audio | L: `audio.rs` (mixer), `sound_engine.rs`, `sound_events.rs`, `music.rs`, `ambience.rs`; G: `music.rs`, `subtitle_hud.rs` |
@@ -162,17 +169,18 @@ python3 tools/agent_control_e2e.py # horizon-ctl / MCP end to end
"Server thread" with one level per dimension; the window joins it as a client through memory (`remote`),
exactly as a LAN guest or a dedicated server's player does. This is what players run. Open
`[server-thread]` issues list the gaps left.
2. **In-frame (`--in-frame`, `--no-server-thread` or `HORIZON_SERVER_THREAD=0`):** the window's `Game` ticks
the world itself, one level that follows the player (`fn enter_dimension`). Deterministic rule fixtures still use this path; player-facing integration uses the default server thread.
2. **In-frame (`--in-frame`, `--no-server-thread` or `HORIZON_SERVER_THREAD=0`): DEPRECATED, never use it, it
will be removed soon.** The window's `Game` ticks the world itself, one level that follows the player
(`fn enter_dimension`). Compatibility does not matter: don't add features or fixes for it, don't write new
tests against it, and move code off it when you touch it. The deterministic rule fixtures still run on it
until they are ported.
LAN guests and dedicated servers always use the server code, so a fix there serves both. Test both paths
when you touch player-facing gameplay:
LAN guests and dedicated servers always use the server code, so a fix there serves both. Test the server
thread when you touch player-facing gameplay:
```sh
cargo run --profile fast -p horizon-client -- --resources .local/horizon-client/26.3 --world T # server thread
cargo run --profile fast -p horizon-client -- --resources .local/horizon-client/26.3 --world T --in-frame # in-frame
python3 tools/lan_e2e.py --check <name> # host on its server thread, guest is a server client
python3 tools/lan_e2e.py --in-frame --check <name> # comparison (portals, levels, distance, lava need server-thread mode)
python3 tools/parity.py dedicated_server server_entities server_player_damage
```
+20 -3
View File
@@ -74,6 +74,19 @@ including examples. The dependency scope includes optional, platform, build and
Tests run library, binary, integration and doc tests without building unrelated example executables. Shared Cargo
configuration and toolchain changes select the whole workspace; CI also keeps its workspace-wide checks.
## Claim before you start
Work is tracked in GitHub issues, and an issue that is assigned is taken (so is one with the `in progress` label:
claimed on Discord with Kira's `/claim`): pick another, or ask its holder in the thread. Before you start, comment
**`/claim`** on the issue; you are assigned at once, no role needed. No issue for your work yet? Open one (a line is
enough), then claim it. `/unclaim` gives it back.
A claim with no open pull request linking the issue (`Fixes #123` in the description, or a draft) is released after
5 days, with a comment: open a draft early. You can hold 5 open issues at a time. Agents claim the same way.
When your pull request changes the same functions as another open one, or closes the same issue, a bot comment names
it (edited on every push, never repeated): talk to its author in the thread before you both finish the same work.
## Pull requests
- **Small.** One change per pull request, under about 500 lines of code where you can; split bigger work into
@@ -82,8 +95,12 @@ configuration and toolchain changes select the whole workspace; CI also keeps it
can see.
- **Tick "Allow edits by maintainers".** When something small is left (a lint, a rebase, a nit) we push the fixup
ourselves instead of sending it back to you; only design questions come back.
- **Labels** (we set them if you can't): `feature` when players get something new (it appears under "New" in the
release notes), `skip-release` when players see no change (tooling, docs, tests), nothing for a fix.
- **Tick its kind** in the template: `parity` (Horizon now does what vanilla does), `bug` (a crash or breakage),
`perf`, `feature` (new for players), `extra` (Horizon-only, off by default), `tooling` or `docs`. A workflow sets
that label, and area labels (`launcher`, `mobs`, `worldgen`, `rendering`...) from the paths you change.
`label:parity` is the list of real progress toward vanilla.
- **Release labels** (we set them if you can't): `feature` also puts it under "New" in the release notes,
`skip-release` when players see no change (tooling, docs, tests), nothing for a fix.
`release-now` is for crash fixes that shouldn't wait. pr_ready suggests one.
Who reviews and merges: the Horizon Team, on CI for ordinary pull requests; risky ones (saves, network, world
@@ -102,4 +119,4 @@ new pushes, we add it again.
## Talk to us
[Discord](https://discord.gg/HcTeMZxe5E) for questions and ideas, [issues](https://github.com/horizon-rust/horizon/issues)
for bugs. If you want to take something on, say so in its issue so nobody else starts the same work.
for bugs. To take something on, `/claim` its issue ([above](#claim-before-you-start)).
Generated
+86 -813
View File
File diff suppressed because it is too large. Load diff
+2 -13
View File
@@ -9,9 +9,11 @@ members = [
"crates/horizon-client",
"crates/horizon-client-core",
"crates/horizon-client-input",
"crates/horizon-client-render",
"crates/horizon-command",
"crates/horizon-items",
"crates/horizon-server-files",
"crates/horizon-anticheat",
"crates/horizon-server",
"crates/horizon-bots",
"crates/horizon-assets",
@@ -22,26 +24,13 @@ members = [
"crates/horizon-bench",
"crates/horizon-friends",
"crates/horizon-relay",
"crates/horizon-mod-sdk",
"crates/horizon-mods",
"crates/horizon-mods/tests/guest",
"crates/horizon-mods/tests/ui_guest",
"crates/cargo-horizon-mod",
"crates/horizon-port-pack",
"crates/horizon-shaders",
"crates/horizon-shader-api",
"crates/horizon-release-sign",
"crates/horizon-mod-sdk/examples/coords-hud",
"crates/horizon-mod-sdk/examples/predator-missile",
"crates/horizon-mod-sdk/examples/welcome",
"crates/horizon-mod-sdk/examples/beacons",
"mods/official/fps-overlay",
"mods/official/keystrokes",
"mods/official/far-view",
"mods/official/minimap",
"mods/official/zoom",
"xtask",
"mods/official/recipes",
]
resolver = "2"
+15 -7
View File
@@ -1,5 +1,5 @@
<p align="center">
<img src="docs/media/banner.webp" alt="Horizon: Minecraft: Java Edition 26.3, rewritten in Rust and checked against vanilla by 281 differential harnesses" width="100%">
<img src="docs/media/banner.webp" alt="Horizon: Minecraft: Java Edition 26.3, rewritten in Rust and checked against vanilla by more than 300 differential harnesses" width="100%">
</p>
<p align="center">
@@ -7,6 +7,7 @@
<img src="https://img.shields.io/badge/Minecraft-Java%2026.3-5D9C3F" alt="Minecraft Java 26.3">
<img src="https://img.shields.io/badge/Rust-stable-B7410E?logo=rust&logoColor=white" alt="Rust">
<img src="https://img.shields.io/badge/vanilla%20parity-100%25-F0795A" alt="Vanilla parity 100%">
<a href="#project-status"><img src="https://img.shields.io/badge/player--visible%20gaps-833%20open-F0795A" alt="Player-visible gaps open"></a>
<img src="https://img.shields.io/badge/platform-Linux%20%7C%20macOS%20%7C%20Windows-2A2470" alt="Linux, macOS and Windows">
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-3B2F7E" alt="MIT license"></a>
</p>
@@ -23,15 +24,15 @@
# Horizon
Horizon is Minecraft: Java Edition 26.3 rewritten in Rust: a native game that reads the same worlds, follows the same rules and generates the same terrain as the Java game, and is checked against it by 281 differential harnesses. You play it through a small launcher, on the files from your own copy of Minecraft. It is free, open source and unofficial.
Horizon is Minecraft: Java Edition 26.3 rewritten in Rust: a native game that reads the same worlds, follows the same rules and generates the same terrain as the Java game, and is checked against it by more than 300 differential harnesses. You play it through a small launcher, on the files from your own copy of Minecraft. It is free, open source and unofficial.
> Not an official Minecraft product. Not approved by or associated with Mojang or Microsoft. Horizon contains no Minecraft code, assets or data: you need your own legally obtained copy of Minecraft: Java Edition 26.3.
## Download
Horizon is installed through **the Horizon launcher**. Get it from [playhorizon.dev](https://playhorizon.dev) (the Download button picks your system) or from [the launcher releases](https://github.com/horizon-rust/horizon/releases?q=launcher-v&expanded=true), for Linux x86-64, macOS on Apple Silicon or Windows x64. The launcher installs and updates the game, prepares its files from your copy of Minecraft (your own install, or downloaded from Mojang for your account), and updates itself.
Horizon is installed through **the Horizon launcher**. Get it from [playhorizon.dev](https://playhorizon.dev) (the Download button picks your system) or from [the launcher releases](https://github.com/horizon-rust/horizon/releases?q=launcher-v&expanded=true), for Linux x86-64, macOS on Apple Silicon or Windows x64. The launcher installs the game and keeps it up to date with each new build (Horizon ships on one channel, the Nightly), prepares its files from your copy of Minecraft (your own install, or downloaded from Mojang for your account), and updates itself.
**You need** a Microsoft account that owns Minecraft: Java Edition (the launcher signs in with it, as other launchers do; see [Microsoft account](docs/microsoft-account.md)) and a GPU with Vulkan on Linux and Windows, or any Apple Silicon Mac. Nothing else: with no Minecraft on your computer, Play downloads 26.3 and the Java it needs from Mojang for your account, as the official launcher does, then prepares it once. An install you already have (the official launcher, Prism, MultiMC and other launchers all work) is used instead. Step by step, with macOS's first-open prompt and the first launch: [Install and first launch](https://playhorizon.dev/docs/install).
**You need** a Microsoft account that owns Minecraft: Java Edition (the launcher signs in with it, as other launchers do; see [Microsoft account](docs/microsoft-account.md)) and a GPU with Vulkan or OpenGL 3.3 on Linux, Vulkan or DirectX 12 on Windows, or any Apple Silicon Mac. Nothing else: with no Minecraft on your computer, Play downloads 26.3 and the Java it needs from Mojang for your account, as the official launcher does, then prepares it once. An install you already have (the official launcher, Prism, MultiMC and other launchers all work) is used instead. Step by step, with macOS's first-open prompt and the first launch: [Install and first launch](https://playhorizon.dev/docs/install).
<table>
<tr>
@@ -46,9 +47,9 @@ The [documentation](https://playhorizon.dev/docs/) covers each of these.
- **The game:** singleplayer with vanilla's world generation, mobs, villagers, raids, redstone, every command, the HUD and the recipe book, on saves in vanilla's format. Worlds from 1.8 to 26.2 are upgraded as vanilla does. [Status table](#project-status).
- **Horizon's own extras:** new menus (vanilla's are one setting away), graphics presets, distant terrain up to 1024 chunks, controller support, optional zoom, minimap and more, all off by default where they would change the vanilla look. See [Settings](https://playhorizon.dev/docs/settings).
- **Resource packs and data packs:** resource packs work, and data packs load functions, predicates and item modifiers.
- **Resource packs and data packs:** resource packs work, and data packs load functions, predicates, item modifiers and world generation, taller levels included (vanilla's biomes for now).
- **Playing with others:** invite friends from the pause menu's Friends button with a code, Open to LAN, and joining servers, online-mode ones with the Microsoft account the launcher signs in with. See [Multiplayer](https://playhorizon.dev/docs/multiplayer).
- **Mods:** Horizon has its own mod system (Rust compiled to WebAssembly). Java mod jars do not run. See [Mods and data packs](https://playhorizon.dev/docs/mods).
- **Mods:** content mods add blocks, items and data, and native mods change the game's own Rust code, built by the launcher on your copy, with no Rust to install. Horizon's own (Zoom, FPS Counter, Keystrokes, Minimap, Recipes and Coordinates HUD) are native mods. Java mod jars do not run in Horizon. See [Mods and data packs](https://playhorizon.dev/docs/mods).
Screenshots, captured in the Rust client itself with vanilla assets and no mods or shaders:
@@ -127,6 +128,8 @@ It runs rustfmt, clippy with warnings denied, the tests of the crates you touche
## Project status
**Player-visible gaps: 833 open** of the 833 the [port ledger](tools/port_ledger.py) found, reading every vanilla 26.3 method against Horizon's code: vanilla behaviour a player would notice that Horizon doesn't have yet. By area: Screens, HUD and client packets [197](https://github.com/horizon-rust/horizon/issues/3518); Entity and block rendering, models [129](https://github.com/horizon-rust/horizon/issues/3519); Animals [110](https://github.com/horizon-rust/horizon/issues/3512); Server and players on the server [106](https://github.com/horizon-rust/horizon/issues/3520); Blocks, redstone and fluids [86](https://github.com/horizon-rust/horizon/issues/3516); Entities, projectiles, vehicles and decorations [64](https://github.com/horizon-rust/horizon/issues/3514); Monsters and bosses [53](https://github.com/horizon-rust/horizon/issues/3513); Mob AI, villagers and raids [35](https://github.com/horizon-rust/horizon/issues/3521); World, world generation and saves [32](https://github.com/horizon-rust/horizon/issues/3517); Items, enchantments and inventories [21](https://github.com/horizon-rust/horizon/issues/3515). This number goes down as gaps close (`tools/parity_gaps.py`); the harness results below say that what is tested matches.
The table below is the project's own audit of each subsystem. Every subsystem now matches vanilla in every harness that compares it. 100% means 100% of what the harnesses compare, not proof that nothing else differs. The remaining known differences are listed below.
### Java parity: 100%
@@ -139,6 +142,11 @@ average; `tests/test_parity.py` keeps it in sync with this table. Last audited
rerun whenever one of its inputs changed, and the Mac mini now runs every one of
them too (Rust uses Java's own float maths on each platform).
The harnesses measure behaviour. A second measure is method by method: the [port ledger](tools/port_ledger.py)
lists every method of vanilla 26.3 next to the Rust functions that port it, with a verdict for each one
(ported, folded into another function, missing, or needing no port), and the untested ports. Run
`python3 tools/port_ledger.py build`, then `coverage`, with the vanilla data in `.local/`.
| Subsystem | Score | Main gaps |
| --- | ---: | --- |
| Window, input and game loop | 1 | None: the game loop, options, camera, auto-jump, toggle sprint/sneak, local input and the frame limiter match vanilla |
@@ -158,7 +166,7 @@ them too (Rust uses Java's own float maths on each platform).
### Known issues
Skins served over https show the default skin unless already cached; pack icons show the unknown-pack texture in game; the creative hotbar keys (C/X) aren't wired to the hotbar loader; item tooltips lack attribute lines.
Skins served over https show the default skin unless already cached; pack icons show the unknown-pack texture in game; item tooltips lack attribute lines.
## Licence
+374 -438
View File
File diff suppressed because it is too large. Load diff
+39
View File
@@ -19,3 +19,42 @@ by The Pixelify Sans Project Authors, licensed under the SIL Open Font License 1
[Pixelarticons](https://github.com/halfmage/pixelarticons) by Gerrit Halfmann, under the MIT
license (`crates/horizon-theme/icons/pixel/LICENSE-pixelarticons.txt`). `star.svg` is Horizon's own, drawn on
the same grid.
## AMD FidelityFX SDK (FSR 3.1 frame generation)
`crates/horizon-client-render/src/gfx/frame_gen/optical_flow.wgsl` is a port to WGSL of the
optical flow shaders of the [AMD FidelityFX SDK](https://github.com/GPUOpen-LibrariesAndSDKs/FidelityFX-SDK)
1.1.4 (`sdk/include/FidelityFX/gpu/opticalflow`, at commit
[c6efa6bf7f20](https://github.com/GPUOpen-LibrariesAndSDKs/FidelityFX-SDK/tree/c6efa6bf7f2027b3ec94f28578bb5965eabb9e55)),
and `optical_flow.rs` follows the SDK's `sdk/src/components/opticalflow/ffx_opticalflow.cpp`
(its passes, their order and their dispatch sizes). Copyright (C) 2024 Advanced Micro Devices,
Inc., under the MIT license, whose text heads both files. AMD and FidelityFX are AMD's
trademarks; Horizon is not affiliated with AMD.
## Mod loader logos
The launcher shows the logo of the mod loader an instance plays with.
- `launcher/assets/logos/quilt.png` is the Quilt mini icon (`brand/128png/quilt_mini_icon_transparent.png`)
from [QuiltMC/art](https://github.com/QuiltMC/art), by the Quilt Project, dedicated to the public
domain under [CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/).
- `launcher/assets/logos/neoforge.png` is NeoForged's icon (`art/neoforged_icon_16x16.png`) from
[neoforged/.github](https://github.com/neoforged/.github/tree/main/art), © the NeoForged team,
created by Ridanisaurus, under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/). It is
unmodified (the launcher scales it up by whole pixels).
- Fabric's and Forge's icons are not shipped: neither publishes a license for its logo. The launcher
fetches them from their own sites when an instance needs them
([fabric-loader's icon](https://github.com/FabricMC/fabric-loader/blob/master/src/main/resources/assets/fabricloader/icon.png),
[files.minecraftforge.net's](https://files.minecraftforge.net/)).
Each logo belongs to its project. Showing it says which loader an instance runs; it does not mean
the project endorses Horizon.
## Sign in with Microsoft
The launcher's sign-in buttons follow Microsoft's published branding guidelines for apps that sign in
with Microsoft accounts
([Sign in with Microsoft branding](https://learn.microsoft.com/en-us/entra/identity-platform/howto-add-branding-in-apps)).
The four-square Microsoft symbol in `launcher/src/microsoft.rs` reproduces the squares, positions and
colours of Microsoft's official `ms-symbollockup_mssymbol_19.svg` from that page, unmodified. Microsoft
and its logo are trademarks of the Microsoft group of companies; Horizon is not endorsed by Microsoft.
@@ -0,0 +1,773 @@
{
"format": "horizon-benchmark-report",
"version": 1,
"kind": "game",
"created": "2026-10-29T01:13:01Z",
"app": {
"name": "horizon-client",
"version": "v0.2.97",
"commit": null
},
"hardware": {
"os": "windows",
"arch": "x86_64",
"cpu_threads": 16,
"os_version": "Microsoft Windows [Version 10.0.26200.8037]",
"cpu": "AMD Ryzen 7 9800X3D 8-Core Processor",
"ram_mb": 95943,
"gpu": {
"name": "NVIDIA GeForce RTX 5080",
"vendor": "nvidia",
"driver": "NVIDIA 617.42",
"backend": "vulkan",
"kind": "discrete"
}
},
"conditions": {
"scenario": "perf_bench:village,forest,ocean,mountain",
"seed": "1234",
"render_distance": 16,
"simulation_distance": 12,
"vsync": false,
"offscreen": false,
"repeats": 3,
"warmup_s": 2.0,
"window": [
1280,
720
]
},
"metrics": {
"startup.title_ms": {
"n": 3,
"mean": 3158.208,
"stdev": 215.77,
"min": 3023.046,
"max": 3407.049,
"ci95": 536.046
},
"startup.world_ms": {
"n": 3,
"mean": 7170.164,
"stdev": 557.958,
"min": 6703.242,
"max": 7788.08,
"ci95": 1386.157
},
"chunks.village.load_ms": {
"n": 3,
"mean": 1694.647,
"stdev": 41.285,
"min": 1653.122,
"max": 1735.687,
"ci95": 102.566
},
"chunks.village.meshed_count": {
"n": 3,
"mean": 23544.0,
"stdev": 47.127,
"min": 23495.0,
"max": 23589.0,
"ci95": 117.081
},
"fps.village.avg_fps": {
"n": 3,
"mean": 1711.652,
"stdev": 77.412,
"min": 1623.403,
"max": 1768.093,
"ci95": 192.317
},
"fps.village.low_1pct_fps": {
"n": 3,
"mean": 593.407,
"stdev": 32.279,
"min": 571.485,
"max": 630.473,
"ci95": 80.191
},
"fps.village.low_01pct_fps": {
"n": 3,
"mean": 358.473,
"stdev": 27.832,
"min": 326.35,
"max": 375.353,
"ci95": 69.143
},
"memory.village.gpu_mb": {
"n": 3,
"mean": 631.813,
"stdev": 8.381,
"min": 626.803,
"max": 641.489,
"ci95": 20.822
},
"chunks.forest.load_ms": {
"n": 3,
"mean": 2521.363,
"stdev": 291.624,
"min": 2336.881,
"max": 2857.57,
"ci95": 724.493
},
"chunks.forest.meshed_count": {
"n": 3,
"mean": 27346.667,
"stdev": 57.934,
"min": 27306.0,
"max": 27413.0,
"ci95": 143.927
},
"fps.forest.avg_fps": {
"n": 3,
"mean": 2423.783,
"stdev": 55.746,
"min": 2386.353,
"max": 2487.851,
"ci95": 138.493
},
"fps.forest.low_1pct_fps": {
"n": 3,
"mean": 709.165,
"stdev": 66.309,
"min": 638.666,
"max": 770.286,
"ci95": 164.734
},
"fps.forest.low_01pct_fps": {
"n": 3,
"mean": 401.039,
"stdev": 40.413,
"min": 354.472,
"max": 426.929,
"ci95": 100.399
},
"memory.forest.gpu_mb": {
"n": 3,
"mean": 705.008,
"stdev": 6.062,
"min": 698.466,
"max": 710.435,
"ci95": 15.06
},
"chunks.ocean.load_ms": {
"n": 3,
"mean": 2094.909,
"stdev": 521.225,
"min": 1758.024,
"max": 2695.274,
"ci95": 1294.9
},
"chunks.ocean.meshed_count": {
"n": 3,
"mean": 24312.667,
"stdev": 261.934,
"min": 24116.0,
"max": 24610.0,
"ci95": 650.732
},
"fps.ocean.avg_fps": {
"n": 3,
"mean": 2201.125,
"stdev": 73.595,
"min": 2153.6,
"max": 2285.897,
"ci95": 182.834
},
"fps.ocean.low_1pct_fps": {
"n": 3,
"mean": 553.968,
"stdev": 100.955,
"min": 445.599,
"max": 645.355,
"ci95": 250.806
},
"fps.ocean.low_01pct_fps": {
"n": 3,
"mean": 325.078,
"stdev": 131.154,
"min": 177.4,
"max": 427.981,
"ci95": 325.831
},
"memory.ocean.gpu_mb": {
"n": 3,
"mean": 652.944,
"stdev": 5.177,
"min": 647.51,
"max": 657.818,
"ci95": 12.862
},
"chunks.mountain.load_ms": {
"n": 3,
"mean": 3304.58,
"stdev": 134.041,
"min": 3155.996,
"max": 3416.409,
"ci95": 333.003
},
"chunks.mountain.meshed_count": {
"n": 3,
"mean": 26591.0,
"stdev": 57.472,
"min": 26525.0,
"max": 26630.0,
"ci95": 142.779
},
"fps.mountain.avg_fps": {
"n": 3,
"mean": 1961.086,
"stdev": 39.725,
"min": 1921.241,
"max": 2000.69,
"ci95": 98.69
},
"fps.mountain.low_1pct_fps": {
"n": 3,
"mean": 536.973,
"stdev": 38.828,
"min": 495.61,
"max": 572.635,
"ci95": 96.461
},
"fps.mountain.low_01pct_fps": {
"n": 3,
"mean": 296.586,
"stdev": 30.236,
"min": 275.859,
"max": 331.281,
"ci95": 75.118
},
"memory.mountain.gpu_mb": {
"n": 3,
"mean": 896.941,
"stdev": 5.177,
"min": 891.506,
"max": 901.814,
"ci95": 12.862
},
"memory.rss_peak_mb": {
"n": 3,
"mean": 1458.578,
"stdev": 3.229,
"min": 1455.32,
"max": 1461.777,
"ci95": 8.022
},
"memory.rss_steady_mb": {
"n": 3,
"mean": 1236.28,
"stdev": 4.869,
"min": 1230.701,
"max": 1239.672,
"ci95": 12.096
}
},
"frames": [
{
"phase": "village",
"repeat": 0,
"frames": 21218,
"seconds": 12.0,
"avg_fps": 1768.093,
"low_1pct_fps": 630.473,
"low_01pct_fps": 375.353,
"p50_ms": 0.537,
"p95_ms": 0.846,
"p99_ms": 1.222,
"p999_ms": 2.081,
"max_ms": 6.256,
"cpu_p50_ms": 0.38,
"gpu_p50_ms": 0.104,
"histogram": [
20695,
499,
22,
1,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 1,
"frames": 19482,
"seconds": 12.001,
"avg_fps": 1623.403,
"low_1pct_fps": 578.262,
"low_01pct_fps": 373.717,
"p50_ms": 0.586,
"p95_ms": 0.95,
"p99_ms": 1.357,
"p999_ms": 2.158,
"max_ms": 7.35,
"cpu_p50_ms": 0.445,
"gpu_p50_ms": 0.104,
"histogram": [
18729,
713,
38,
1,
0,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 2,
"frames": 20922,
"seconds": 12.0,
"avg_fps": 1743.46,
"low_1pct_fps": 571.485,
"low_01pct_fps": 326.35,
"p50_ms": 0.526,
"p95_ms": 0.883,
"p99_ms": 1.313,
"p999_ms": 2.14,
"max_ms": 12.027,
"cpu_p50_ms": 0.379,
"gpu_p50_ms": 0.104,
"histogram": [
20308,
582,
28,
2,
1,
0,
0,
0,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 0,
"frames": 29855,
"seconds": 12.0,
"avg_fps": 2487.851,
"low_1pct_fps": 770.286,
"low_01pct_fps": 421.717,
"p50_ms": 0.365,
"p95_ms": 0.619,
"p99_ms": 0.948,
"p999_ms": 1.717,
"max_ms": 9.446,
"cpu_p50_ms": 0.232,
"gpu_p50_ms": 0.157,
"histogram": [
29607,
233,
13,
0,
0,
1,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 1,
"frames": 28766,
"seconds": 12.0,
"avg_fps": 2397.145,
"low_1pct_fps": 718.542,
"low_01pct_fps": 426.929,
"p50_ms": 0.374,
"p95_ms": 0.662,
"p99_ms": 1.016,
"p999_ms": 1.858,
"max_ms": 6.379,
"cpu_p50_ms": 0.239,
"gpu_p50_ms": 0.157,
"histogram": [
28457,
293,
14,
0,
0,
1,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 2,
"frames": 28637,
"seconds": 12.0,
"avg_fps": 2386.353,
"low_1pct_fps": 638.666,
"low_01pct_fps": 354.472,
"p50_ms": 0.373,
"p95_ms": 0.704,
"p99_ms": 1.159,
"p999_ms": 1.952,
"max_ms": 14.388,
"cpu_p50_ms": 0.233,
"gpu_p50_ms": 0.157,
"histogram": [
28198,
415,
18,
5,
0,
0,
0,
0,
0,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 0,
"frames": 25968,
"seconds": 12.001,
"avg_fps": 2163.877,
"low_1pct_fps": 570.949,
"low_01pct_fps": 369.854,
"p50_ms": 0.408,
"p95_ms": 0.767,
"p99_ms": 1.339,
"p999_ms": 2.38,
"max_ms": 3.329,
"cpu_p50_ms": 0.271,
"gpu_p50_ms": 0.141,
"histogram": [
25383,
534,
47,
4,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 1,
"frames": 25843,
"seconds": 12.0,
"avg_fps": 2153.6,
"low_1pct_fps": 445.599,
"low_01pct_fps": 177.4,
"p50_ms": 0.413,
"p95_ms": 0.725,
"p99_ms": 1.309,
"p999_ms": 3.451,
"max_ms": 12.071,
"cpu_p50_ms": 0.275,
"gpu_p50_ms": 0.142,
"histogram": [
25375,
373,
56,
22,
1,
7,
3,
5,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 2,
"frames": 27431,
"seconds": 12.0,
"avg_fps": 2285.897,
"low_1pct_fps": 645.355,
"low_01pct_fps": 427.981,
"p50_ms": 0.388,
"p95_ms": 0.693,
"p99_ms": 1.163,
"p999_ms": 2.041,
"max_ms": 3.068,
"cpu_p50_ms": 0.254,
"gpu_p50_ms": 0.141,
"histogram": [
27036,
363,
31,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 0,
"frames": 23536,
"seconds": 12.0,
"avg_fps": 1961.326,
"low_1pct_fps": 542.675,
"low_01pct_fps": 331.281,
"p50_ms": 0.436,
"p95_ms": 0.856,
"p99_ms": 1.425,
"p999_ms": 2.22,
"max_ms": 9.458,
"cpu_p50_ms": 0.296,
"gpu_p50_ms": 0.14,
"histogram": [
22819,
671,
40,
5,
0,
0,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 1,
"frames": 23056,
"seconds": 12.001,
"avg_fps": 1921.241,
"low_1pct_fps": 495.61,
"low_01pct_fps": 282.617,
"p50_ms": 0.44,
"p95_ms": 0.911,
"p99_ms": 1.516,
"p999_ms": 2.543,
"max_ms": 14.342,
"cpu_p50_ms": 0.299,
"gpu_p50_ms": 0.141,
"histogram": [
22198,
783,
65,
7,
0,
2,
0,
0,
0,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 2,
"frames": 24009,
"seconds": 12.0,
"avg_fps": 2000.69,
"low_1pct_fps": 572.635,
"low_01pct_fps": 275.859,
"p50_ms": 0.436,
"p95_ms": 0.819,
"p99_ms": 1.274,
"p999_ms": 2.085,
"max_ms": 14.003,
"cpu_p50_ms": 0.297,
"gpu_p50_ms": 0.14,
"histogram": [
23421,
560,
22,
0,
1,
0,
2,
2,
0,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
}
],
"label": "horizon-win-9800x3d-5080"
}
@@ -0,0 +1,773 @@
{
"format": "horizon-benchmark-report",
"version": 1,
"kind": "game",
"created": "2026-10-29T18:55:59Z",
"app": {
"name": "horizon-client",
"version": "v0.2.97",
"commit": null
},
"hardware": {
"os": "windows",
"arch": "x86_64",
"cpu_threads": 16,
"os_version": "Microsoft Windows [Version 10.0.26200.8037]",
"cpu": "AMD Ryzen 7 9800X3D 8-Core Processor",
"ram_mb": 95943,
"gpu": {
"name": "NVIDIA GeForce RTX 5080",
"vendor": "nvidia",
"driver": "NVIDIA 617.42",
"backend": "vulkan",
"kind": "discrete"
}
},
"conditions": {
"scenario": "perf_bench:village,forest,ocean,mountain",
"seed": "1234",
"render_distance": 32,
"simulation_distance": 12,
"vsync": false,
"offscreen": false,
"repeats": 3,
"warmup_s": 2.0,
"window": [
1280,
720
]
},
"metrics": {
"startup.title_ms": {
"n": 3,
"mean": 2897.812,
"stdev": 82.455,
"min": 2844.677,
"max": 2992.8,
"ci95": 204.846
},
"startup.world_ms": {
"n": 3,
"mean": 10503.454,
"stdev": 493.592,
"min": 9934.491,
"max": 10816.992,
"ci95": 1226.251
},
"chunks.village.load_ms": {
"n": 3,
"mean": 3717.47,
"stdev": 1330.649,
"min": 2942.872,
"max": 5253.954,
"ci95": 3305.783
},
"chunks.village.meshed_count": {
"n": 3,
"mean": 47525.333,
"stdev": 1369.122,
"min": 46133.0,
"max": 48870.0,
"ci95": 3401.363
},
"fps.village.avg_fps": {
"n": 3,
"mean": 1163.541,
"stdev": 3.485,
"min": 1159.52,
"max": 1165.693,
"ci95": 8.658
},
"fps.village.low_1pct_fps": {
"n": 3,
"mean": 415.992,
"stdev": 15.742,
"min": 399.062,
"max": 430.188,
"ci95": 39.108
},
"fps.village.low_01pct_fps": {
"n": 3,
"mean": 215.549,
"stdev": 34.678,
"min": 177.857,
"max": 246.101,
"ci95": 86.151
},
"memory.village.gpu_mb": {
"n": 3,
"mean": 1833.432,
"stdev": 4.415,
"min": 1828.334,
"max": 1835.997,
"ci95": 10.969
},
"chunks.forest.load_ms": {
"n": 3,
"mean": 5455.049,
"stdev": 593.897,
"min": 4973.659,
"max": 6118.723,
"ci95": 1475.44
},
"chunks.forest.meshed_count": {
"n": 3,
"mean": 82758.0,
"stdev": 413.547,
"min": 82487.0,
"max": 83234.0,
"ci95": 1027.39
},
"fps.forest.avg_fps": {
"n": 3,
"mean": 1389.46,
"stdev": 28.128,
"min": 1371.096,
"max": 1421.843,
"ci95": 69.88
},
"fps.forest.low_1pct_fps": {
"n": 3,
"mean": 504.377,
"stdev": 13.251,
"min": 492.444,
"max": 518.637,
"ci95": 32.919
},
"fps.forest.low_01pct_fps": {
"n": 3,
"mean": 329.682,
"stdev": 47.451,
"min": 294.872,
"max": 383.732,
"ci95": 117.885
},
"memory.forest.gpu_mb": {
"n": 3,
"mean": 1985.175,
"stdev": 6.354,
"min": 1978.907,
"max": 1991.612,
"ci95": 15.786
},
"chunks.ocean.load_ms": {
"n": 3,
"mean": 4504.145,
"stdev": 745.393,
"min": 3650.589,
"max": 5026.8,
"ci95": 1851.809
},
"chunks.ocean.meshed_count": {
"n": 3,
"mean": 74927.333,
"stdev": 399.375,
"min": 74476.0,
"max": 75235.0,
"ci95": 992.182
},
"fps.ocean.avg_fps": {
"n": 3,
"mean": 1188.639,
"stdev": 10.807,
"min": 1177.005,
"max": 1198.365,
"ci95": 26.849
},
"fps.ocean.low_1pct_fps": {
"n": 3,
"mean": 154.773,
"stdev": 3.712,
"min": 151.38,
"max": 158.738,
"ci95": 9.223
},
"fps.ocean.low_01pct_fps": {
"n": 3,
"mean": 67.809,
"stdev": 3.032,
"min": 65.903,
"max": 71.305,
"ci95": 7.532
},
"memory.ocean.gpu_mb": {
"n": 3,
"mean": 2084.845,
"stdev": 9.674,
"min": 2074.009,
"max": 2092.615,
"ci95": 24.034
},
"chunks.mountain.load_ms": {
"n": 3,
"mean": 9048.431,
"stdev": 344.33,
"min": 8821.41,
"max": 9444.624,
"ci95": 855.432
},
"chunks.mountain.meshed_count": {
"n": 3,
"mean": 89659.667,
"stdev": 87.123,
"min": 89561.0,
"max": 89726.0,
"ci95": 216.442
},
"fps.mountain.avg_fps": {
"n": 3,
"mean": 1085.847,
"stdev": 13.819,
"min": 1070.378,
"max": 1096.974,
"ci95": 34.332
},
"fps.mountain.low_1pct_fps": {
"n": 3,
"mean": 257.7,
"stdev": 31.437,
"min": 224.366,
"max": 286.814,
"ci95": 78.101
},
"fps.mountain.low_01pct_fps": {
"n": 3,
"mean": 107.783,
"stdev": 14.883,
"min": 91.541,
"max": 120.768,
"ci95": 36.975
},
"memory.mountain.gpu_mb": {
"n": 3,
"mean": 2708.833,
"stdev": 9.674,
"min": 2697.997,
"max": 2716.603,
"ci95": 24.034
},
"memory.rss_peak_mb": {
"n": 3,
"mean": 2044.895,
"stdev": 10.562,
"min": 2032.875,
"max": 2052.691,
"ci95": 26.238
},
"memory.rss_steady_mb": {
"n": 3,
"mean": 1730.107,
"stdev": 14.764,
"min": 1717.52,
"max": 1746.357,
"ci95": 36.678
}
},
"frames": [
{
"phase": "village",
"repeat": 0,
"frames": 13986,
"seconds": 12.001,
"avg_fps": 1165.41,
"low_1pct_fps": 399.062,
"low_01pct_fps": 177.857,
"p50_ms": 0.822,
"p95_ms": 1.21,
"p99_ms": 1.691,
"p999_ms": 3.499,
"max_ms": 13.089,
"cpu_p50_ms": 0.665,
"gpu_p50_ms": 0.193,
"histogram": [
12380,
1527,
58,
13,
2,
2,
1,
2,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 1,
"frames": 13915,
"seconds": 12.001,
"avg_fps": 1159.52,
"low_1pct_fps": 430.188,
"low_01pct_fps": 246.101,
"p50_ms": 0.809,
"p95_ms": 1.212,
"p99_ms": 1.702,
"p999_ms": 3.291,
"max_ms": 6.894,
"cpu_p50_ms": 0.648,
"gpu_p50_ms": 0.192,
"histogram": [
11996,
1839,
59,
17,
2,
1,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 2,
"frames": 13989,
"seconds": 12.001,
"avg_fps": 1165.693,
"low_1pct_fps": 418.725,
"low_01pct_fps": 222.688,
"p50_ms": 0.816,
"p95_ms": 1.22,
"p99_ms": 1.692,
"p999_ms": 3.264,
"max_ms": 7.781,
"cpu_p50_ms": 0.669,
"gpu_p50_ms": 0.193,
"histogram": [
12321,
1578,
72,
11,
2,
4,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 0,
"frames": 17063,
"seconds": 12.001,
"avg_fps": 1421.843,
"low_1pct_fps": 502.049,
"low_01pct_fps": 294.872,
"p50_ms": 0.666,
"p95_ms": 0.935,
"p99_ms": 1.549,
"p999_ms": 2.692,
"max_ms": 6.831,
"cpu_p50_ms": 0.533,
"gpu_p50_ms": 0.362,
"histogram": [
16417,
596,
40,
7,
2,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 1,
"frames": 16454,
"seconds": 12.001,
"avg_fps": 1371.096,
"low_1pct_fps": 492.444,
"low_01pct_fps": 310.443,
"p50_ms": 0.688,
"p95_ms": 0.979,
"p99_ms": 1.556,
"p999_ms": 2.693,
"max_ms": 4.596,
"cpu_p50_ms": 0.553,
"gpu_p50_ms": 0.36,
"histogram": [
15692,
697,
57,
7,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 2,
"frames": 16506,
"seconds": 12.001,
"avg_fps": 1375.442,
"low_1pct_fps": 518.637,
"low_01pct_fps": 383.732,
"p50_ms": 0.675,
"p95_ms": 1.017,
"p99_ms": 1.605,
"p999_ms": 2.369,
"max_ms": 3.201,
"cpu_p50_ms": 0.54,
"gpu_p50_ms": 0.364,
"histogram": [
15628,
833,
44,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 0,
"frames": 14381,
"seconds": 12.001,
"avg_fps": 1198.365,
"low_1pct_fps": 154.2,
"low_01pct_fps": 66.219,
"p50_ms": 0.731,
"p95_ms": 1.023,
"p99_ms": 3.003,
"p999_ms": 12.478,
"max_ms": 18.863,
"cpu_p50_ms": 0.596,
"gpu_p50_ms": 0.315,
"histogram": [
13586,
546,
105,
60,
13,
13,
8,
16,
8,
11,
5,
7,
3,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 1,
"frames": 14287,
"seconds": 12.0,
"avg_fps": 1190.548,
"low_1pct_fps": 158.738,
"low_01pct_fps": 71.305,
"p50_ms": 0.741,
"p95_ms": 1.041,
"p99_ms": 2.9,
"p999_ms": 12.0,
"max_ms": 17.23,
"cpu_p50_ms": 0.602,
"gpu_p50_ms": 0.315,
"histogram": [
13450,
591,
109,
57,
13,
8,
10,
11,
14,
10,
7,
6,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 2,
"frames": 14124,
"seconds": 12.0,
"avg_fps": 1177.005,
"low_1pct_fps": 151.38,
"low_01pct_fps": 65.903,
"p50_ms": 0.739,
"p95_ms": 1.096,
"p99_ms": 2.928,
"p999_ms": 11.884,
"max_ms": 19.375,
"cpu_p50_ms": 0.604,
"gpu_p50_ms": 0.314,
"histogram": [
13052,
822,
114,
54,
16,
8,
4,
14,
11,
15,
3,
8,
3,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 0,
"frames": 12845,
"seconds": 12.0,
"avg_fps": 1070.378,
"low_1pct_fps": 224.366,
"low_01pct_fps": 91.541,
"p50_ms": 0.825,
"p95_ms": 1.362,
"p99_ms": 2.325,
"p999_ms": 9.621,
"max_ms": 13.309,
"cpu_p50_ms": 0.689,
"gpu_p50_ms": 0.307,
"histogram": [
10937,
1699,
143,
25,
5,
6,
4,
8,
8,
8,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 1,
"frames": 13083,
"seconds": 12.001,
"avg_fps": 1090.188,
"low_1pct_fps": 261.921,
"low_01pct_fps": 111.039,
"p50_ms": 0.816,
"p95_ms": 1.335,
"p99_ms": 2.14,
"p999_ms": 8.222,
"max_ms": 11.95,
"cpu_p50_ms": 0.681,
"gpu_p50_ms": 0.306,
"histogram": [
11105,
1812,
112,
21,
5,
5,
5,
6,
11,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 2,
"frames": 13164,
"seconds": 12.0,
"avg_fps": 1096.974,
"low_1pct_fps": 286.814,
"low_01pct_fps": 120.768,
"p50_ms": 0.822,
"p95_ms": 1.307,
"p99_ms": 1.989,
"p999_ms": 7.701,
"max_ms": 9.429,
"cpu_p50_ms": 0.687,
"gpu_p50_ms": 0.307,
"histogram": [
11280,
1758,
82,
13,
5,
5,
6,
8,
7,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
}
],
"label": "horizon-win-9800x3d-5080"
}
@@ -0,0 +1,773 @@
{
"format": "horizon-benchmark-report",
"version": 1,
"kind": "game",
"created": "2026-10-29T18:37:03Z",
"app": {
"name": "horizon-client",
"version": "v0.2.97",
"commit": null
},
"hardware": {
"os": "windows",
"arch": "x86_64",
"cpu_threads": 16,
"os_version": "Microsoft Windows [Version 10.0.26200.8037]",
"cpu": "AMD Ryzen 7 9800X3D 8-Core Processor",
"ram_mb": 95943,
"gpu": {
"name": "NVIDIA GeForce RTX 5080",
"vendor": "nvidia",
"driver": "NVIDIA 617.42",
"backend": "vulkan",
"kind": "discrete"
}
},
"conditions": {
"scenario": "perf_bench:village,forest,ocean,mountain",
"seed": "1234",
"render_distance": 8,
"simulation_distance": 12,
"vsync": false,
"offscreen": false,
"repeats": 3,
"warmup_s": 2.0,
"window": [
1280,
720
]
},
"metrics": {
"startup.title_ms": {
"n": 3,
"mean": 2950.343,
"stdev": 162.732,
"min": 2822.925,
"max": 3133.657,
"ci95": 404.282
},
"startup.world_ms": {
"n": 3,
"mean": 6135.1,
"stdev": 863.831,
"min": 5547.861,
"max": 7126.98,
"ci95": 2146.05
},
"chunks.village.load_ms": {
"n": 3,
"mean": 985.519,
"stdev": 28.184,
"min": 956.244,
"max": 1012.469,
"ci95": 70.019
},
"chunks.village.meshed_count": {
"n": 3,
"mean": 6908.0,
"stdev": 9.849,
"min": 6897.0,
"max": 6916.0,
"ci95": 24.468
},
"fps.village.avg_fps": {
"n": 3,
"mean": 2941.608,
"stdev": 79.283,
"min": 2850.416,
"max": 2994.188,
"ci95": 196.965
},
"fps.village.low_1pct_fps": {
"n": 3,
"mean": 1271.318,
"stdev": 58.797,
"min": 1203.758,
"max": 1310.917,
"ci95": 146.072
},
"fps.village.low_01pct_fps": {
"n": 3,
"mean": 839.261,
"stdev": 37.982,
"min": 795.415,
"max": 862.067,
"ci95": 94.36
},
"memory.village.gpu_mb": {
"n": 3,
"mean": 363.984,
"stdev": 0.926,
"min": 363.449,
"max": 365.053,
"ci95": 2.299
},
"chunks.forest.load_ms": {
"n": 3,
"mean": 1350.752,
"stdev": 293.128,
"min": 1106.764,
"max": 1675.913,
"ci95": 728.23
},
"chunks.forest.meshed_count": {
"n": 3,
"mean": 8066.0,
"stdev": 108.885,
"min": 7986.0,
"max": 8190.0,
"ci95": 270.508
},
"fps.forest.avg_fps": {
"n": 3,
"mean": 3494.693,
"stdev": 110.997,
"min": 3389.894,
"max": 3610.993,
"ci95": 275.755
},
"fps.forest.low_1pct_fps": {
"n": 3,
"mean": 1295.14,
"stdev": 159.638,
"min": 1148.468,
"max": 1465.171,
"ci95": 396.596
},
"fps.forest.low_01pct_fps": {
"n": 3,
"mean": 859.326,
"stdev": 119.845,
"min": 721.994,
"max": 942.743,
"ci95": 297.734
},
"memory.forest.gpu_mb": {
"n": 3,
"mean": 381.189,
"stdev": 5.222,
"min": 376.423,
"max": 386.77,
"ci95": 12.972
},
"chunks.ocean.load_ms": {
"n": 3,
"mean": 1123.386,
"stdev": 604.175,
"min": 734.302,
"max": 1819.414,
"ci95": 1500.976
},
"chunks.ocean.meshed_count": {
"n": 3,
"mean": 6613.0,
"stdev": 135.632,
"min": 6523.0,
"max": 6769.0,
"ci95": 336.955
},
"fps.ocean.avg_fps": {
"n": 3,
"mean": 3346.003,
"stdev": 59.312,
"min": 3303.535,
"max": 3413.77,
"ci95": 147.352
},
"fps.ocean.low_1pct_fps": {
"n": 3,
"mean": 1357.991,
"stdev": 95.364,
"min": 1248.887,
"max": 1425.451,
"ci95": 236.917
},
"fps.ocean.low_01pct_fps": {
"n": 3,
"mean": 911.822,
"stdev": 94.717,
"min": 802.528,
"max": 969.989,
"ci95": 235.308
},
"memory.ocean.gpu_mb": {
"n": 3,
"mean": 391.636,
"stdev": 5.435,
"min": 387.424,
"max": 397.771,
"ci95": 13.502
},
"chunks.mountain.load_ms": {
"n": 3,
"mean": 1588.954,
"stdev": 458.924,
"min": 1309.548,
"max": 2118.606,
"ci95": 1140.123
},
"chunks.mountain.meshed_count": {
"n": 3,
"mean": 7887.667,
"stdev": 41.041,
"min": 7862.0,
"max": 7935.0,
"ci95": 101.959
},
"fps.mountain.avg_fps": {
"n": 3,
"mean": 3375.623,
"stdev": 26.82,
"min": 3352.309,
"max": 3404.933,
"ci95": 66.629
},
"fps.mountain.low_1pct_fps": {
"n": 3,
"mean": 1383.745,
"stdev": 67.628,
"min": 1324.071,
"max": 1457.203,
"ci95": 168.01
},
"fps.mountain.low_01pct_fps": {
"n": 3,
"mean": 850.213,
"stdev": 103.149,
"min": 790.305,
"max": 969.318,
"ci95": 256.256
},
"memory.mountain.gpu_mb": {
"n": 3,
"mean": 448.64,
"stdev": 5.435,
"min": 444.428,
"max": 454.775,
"ci95": 13.502
},
"memory.rss_peak_mb": {
"n": 3,
"mean": 1171.629,
"stdev": 0.337,
"min": 1171.398,
"max": 1172.016,
"ci95": 0.837
},
"memory.rss_steady_mb": {
"n": 3,
"mean": 1082.078,
"stdev": 12.494,
"min": 1069.117,
"max": 1094.047,
"ci95": 31.04
}
},
"frames": [
{
"phase": "village",
"repeat": 0,
"frames": 35764,
"seconds": 12.0,
"avg_fps": 2980.22,
"low_1pct_fps": 1310.917,
"low_01pct_fps": 860.301,
"p50_ms": 0.317,
"p95_ms": 0.433,
"p99_ms": 0.633,
"p999_ms": 0.937,
"max_ms": 2.807,
"cpu_p50_ms": 0.192,
"gpu_p50_ms": 0.063,
"histogram": [
35741,
22,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 1,
"frames": 35931,
"seconds": 12.0,
"avg_fps": 2994.188,
"low_1pct_fps": 1299.278,
"low_01pct_fps": 795.415,
"p50_ms": 0.318,
"p95_ms": 0.428,
"p99_ms": 0.62,
"p999_ms": 0.943,
"max_ms": 5.797,
"cpu_p50_ms": 0.191,
"gpu_p50_ms": 0.063,
"histogram": [
35906,
23,
1,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 2,
"frames": 34206,
"seconds": 12.0,
"avg_fps": 2850.416,
"low_1pct_fps": 1203.758,
"low_01pct_fps": 862.067,
"p50_ms": 0.326,
"p95_ms": 0.508,
"p99_ms": 0.705,
"p999_ms": 1.025,
"max_ms": 1.398,
"cpu_p50_ms": 0.196,
"gpu_p50_ms": 0.063,
"histogram": [
34167,
39,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 0,
"frames": 41800,
"seconds": 12.0,
"avg_fps": 3483.193,
"low_1pct_fps": 1271.782,
"low_01pct_fps": 913.242,
"p50_ms": 0.264,
"p95_ms": 0.415,
"p99_ms": 0.657,
"p999_ms": 0.935,
"max_ms": 1.482,
"cpu_p50_ms": 0.144,
"gpu_p50_ms": 0.075,
"histogram": [
41768,
32,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 1,
"frames": 43333,
"seconds": 12.0,
"avg_fps": 3610.993,
"low_1pct_fps": 1465.171,
"low_01pct_fps": 942.743,
"p50_ms": 0.263,
"p95_ms": 0.355,
"p99_ms": 0.561,
"p999_ms": 0.859,
"max_ms": 1.694,
"cpu_p50_ms": 0.143,
"gpu_p50_ms": 0.074,
"histogram": [
43310,
23,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 2,
"frames": 40679,
"seconds": 12.0,
"avg_fps": 3389.894,
"low_1pct_fps": 1148.468,
"low_01pct_fps": 721.994,
"p50_ms": 0.267,
"p95_ms": 0.446,
"p99_ms": 0.67,
"p999_ms": 1.183,
"max_ms": 2.434,
"cpu_p50_ms": 0.146,
"gpu_p50_ms": 0.076,
"histogram": [
40590,
87,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 0,
"frames": 39849,
"seconds": 12.0,
"avg_fps": 3320.704,
"low_1pct_fps": 1399.634,
"low_01pct_fps": 969.989,
"p50_ms": 0.281,
"p95_ms": 0.404,
"p99_ms": 0.601,
"p999_ms": 0.875,
"max_ms": 1.494,
"cpu_p50_ms": 0.159,
"gpu_p50_ms": 0.069,
"histogram": [
39831,
18,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 1,
"frames": 40966,
"seconds": 12.0,
"avg_fps": 3413.77,
"low_1pct_fps": 1425.451,
"low_01pct_fps": 962.948,
"p50_ms": 0.278,
"p95_ms": 0.372,
"p99_ms": 0.581,
"p999_ms": 0.857,
"max_ms": 1.576,
"cpu_p50_ms": 0.157,
"gpu_p50_ms": 0.069,
"histogram": [
40947,
19,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 2,
"frames": 39643,
"seconds": 12.0,
"avg_fps": 3303.535,
"low_1pct_fps": 1248.887,
"low_01pct_fps": 802.528,
"p50_ms": 0.28,
"p95_ms": 0.413,
"p99_ms": 0.642,
"p999_ms": 1.087,
"max_ms": 2.215,
"cpu_p50_ms": 0.158,
"gpu_p50_ms": 0.069,
"histogram": [
39582,
60,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 0,
"frames": 40436,
"seconds": 12.0,
"avg_fps": 3369.626,
"low_1pct_fps": 1369.962,
"low_01pct_fps": 790.305,
"p50_ms": 0.279,
"p95_ms": 0.403,
"p99_ms": 0.604,
"p999_ms": 0.812,
"max_ms": 10.313,
"cpu_p50_ms": 0.159,
"gpu_p50_ms": 0.069,
"histogram": [
40425,
8,
1,
1,
0,
0,
0,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 1,
"frames": 40860,
"seconds": 12.0,
"avg_fps": 3404.933,
"low_1pct_fps": 1457.203,
"low_01pct_fps": 969.318,
"p50_ms": 0.278,
"p95_ms": 0.389,
"p99_ms": 0.574,
"p999_ms": 0.814,
"max_ms": 2.714,
"cpu_p50_ms": 0.158,
"gpu_p50_ms": 0.069,
"histogram": [
40847,
11,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 2,
"frames": 40229,
"seconds": 12.0,
"avg_fps": 3352.309,
"low_1pct_fps": 1324.071,
"low_01pct_fps": 791.016,
"p50_ms": 0.28,
"p95_ms": 0.402,
"p99_ms": 0.617,
"p999_ms": 0.968,
"max_ms": 3.746,
"cpu_p50_ms": 0.159,
"gpu_p50_ms": 0.069,
"histogram": [
40192,
35,
1,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
}
],
"label": "horizon-win-9800x3d-5080"
}
@@ -0,0 +1,706 @@
{
"format": "horizon-benchmark-report",
"version": 1,
"kind": "game",
"created": "2026-10-29T01:25:53Z",
"app": {
"name": "minecraft",
"version": "26.3",
"mods": [
{
"name": "fabric-loader",
"version": "0.19.5"
},
{
"name": "sodium",
"version": "mc26.3-0.9.2-fabric"
},
{
"name": "lithium",
"version": "mc26.3-0.26.2-fabric"
},
{
"name": "ferrite-core",
"version": "9.0.0-fabric"
},
{
"name": "immediatelyfast",
"version": "1.17.1+26.3-fabric"
}
]
},
"hardware": {
"os": "windows",
"arch": "x86_64",
"cpu_threads": 16,
"os_version": "Microsoft Windows [Version 10.0.26200.8037]",
"cpu": "AMD Ryzen 7 9800X3D 8-Core Processor",
"ram_mb": 95943,
"gpu": {
"name": "NVIDIA GeForce RTX 5080/PCIe/SSE2",
"vendor": "NVIDIA Corporation",
"driver": "3.3.0 NVIDIA 617.42",
"backend": "opengl",
"kind": ""
}
},
"conditions": {
"scenario": "perf_bench:village,forest,ocean,mountain",
"seed": "1234",
"render_distance": 16,
"simulation_distance": 12,
"vsync": false,
"offscreen": false,
"repeats": 3,
"warmup_s": 2.0,
"window": [
1280,
720
]
},
"metrics": {
"startup.title_ms": {
"n": 3,
"mean": 10777.756,
"stdev": 129.67,
"min": 10636.92,
"max": 10892.2,
"ci95": 322.144
},
"startup.world_ms": {
"n": 3,
"mean": 23477.423,
"stdev": 346.614,
"min": 23130.92,
"max": 23824.148,
"ci95": 861.106
},
"chunks.village.load_ms": {
"n": 3,
"mean": 6007.033,
"stdev": 104.227,
"min": 5891.431,
"max": 6093.825,
"ci95": 258.936
},
"fps.village.avg_fps": {
"n": 3,
"mean": 1282.11,
"stdev": 65.207,
"min": 1228.13,
"max": 1354.56,
"ci95": 161.997
},
"fps.village.low_1pct_fps": {
"n": 3,
"mean": 343.465,
"stdev": 9.96,
"min": 335.223,
"max": 354.532,
"ci95": 24.744
},
"fps.village.low_01pct_fps": {
"n": 3,
"mean": 131.669,
"stdev": 4.692,
"min": 126.427,
"max": 135.476,
"ci95": 11.656
},
"chunks.forest.load_ms": {
"n": 3,
"mean": 8669.311,
"stdev": 434.39,
"min": 8280.854,
"max": 9138.35,
"ci95": 1079.173
},
"fps.forest.avg_fps": {
"n": 3,
"mean": 1745.757,
"stdev": 13.014,
"min": 1734.41,
"max": 1759.963,
"ci95": 32.332
},
"fps.forest.low_1pct_fps": {
"n": 3,
"mean": 460.958,
"stdev": 10.859,
"min": 449.051,
"max": 470.316,
"ci95": 26.978
},
"fps.forest.low_01pct_fps": {
"n": 3,
"mean": 178.955,
"stdev": 3.777,
"min": 175.213,
"max": 182.766,
"ci95": 9.383
},
"chunks.ocean.load_ms": {
"n": 3,
"mean": 7056.368,
"stdev": 117.329,
"min": 6986.912,
"max": 7191.833,
"ci95": 291.485
},
"fps.ocean.avg_fps": {
"n": 3,
"mean": 1815.056,
"stdev": 47.875,
"min": 1759.788,
"max": 1843.745,
"ci95": 118.937
},
"fps.ocean.low_1pct_fps": {
"n": 3,
"mean": 514.215,
"stdev": 57.205,
"min": 452.445,
"max": 565.368,
"ci95": 142.117
},
"fps.ocean.low_01pct_fps": {
"n": 3,
"mean": 191.842,
"stdev": 23.217,
"min": 175.546,
"max": 218.426,
"ci95": 57.68
},
"chunks.mountain.load_ms": {
"n": 3,
"mean": 14094.582,
"stdev": 399.624,
"min": 13641.534,
"max": 14397.0,
"ci95": 992.801
},
"fps.mountain.avg_fps": {
"n": 3,
"mean": 1499.768,
"stdev": 22.197,
"min": 1477.269,
"max": 1521.651,
"ci95": 55.146
},
"fps.mountain.low_1pct_fps": {
"n": 3,
"mean": 408.466,
"stdev": 16.114,
"min": 395.097,
"max": 426.358,
"ci95": 40.032
},
"fps.mountain.low_01pct_fps": {
"n": 3,
"mean": 139.743,
"stdev": 3.969,
"min": 136.465,
"max": 144.156,
"ci95": 9.86
},
"memory.rss_peak_mb": {
"n": 3,
"mean": 2495.607,
"stdev": 75.339,
"min": 2412.738,
"max": 2559.965,
"ci95": 187.166
},
"memory.rss_steady_mb": {
"n": 3,
"mean": 2326.611,
"stdev": 15.702,
"min": 2314.98,
"max": 2344.473,
"ci95": 39.01
}
},
"frames": [
{
"phase": "village",
"repeat": 0,
"frames": 14738,
"seconds": 12.0,
"avg_fps": 1228.13,
"low_1pct_fps": 335.223,
"low_01pct_fps": 133.103,
"p50_ms": 0.722,
"p95_ms": 1.384,
"p99_ms": 2.043,
"p999_ms": 5.224,
"max_ms": 11.459,
"histogram": [
12159,
2421,
128,
14,
1,
5,
2,
3,
4,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 1,
"frames": 16255,
"seconds": 12.0,
"avg_fps": 1354.56,
"low_1pct_fps": 354.532,
"low_01pct_fps": 135.476,
"p50_ms": 0.672,
"p95_ms": 1.136,
"p99_ms": 1.841,
"p999_ms": 4.913,
"max_ms": 16.94,
"histogram": [
14841,
1298,
86,
13,
2,
5,
4,
1,
3,
1,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 2,
"frames": 15164,
"seconds": 12.0,
"avg_fps": 1263.64,
"low_1pct_fps": 340.639,
"low_01pct_fps": 126.427,
"p50_ms": 0.708,
"p95_ms": 1.299,
"p99_ms": 1.895,
"p999_ms": 5.173,
"max_ms": 16.748,
"histogram": [
13026,
2014,
94,
13,
1,
3,
3,
6,
2,
1,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 0,
"frames": 20915,
"seconds": 12.0,
"avg_fps": 1742.897,
"low_1pct_fps": 470.316,
"low_01pct_fps": 182.766,
"p50_ms": 0.5,
"p95_ms": 0.936,
"p99_ms": 1.437,
"p999_ms": 2.77,
"max_ms": 13.414,
"histogram": [
20067,
788,
44,
4,
0,
4,
3,
3,
1,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 1,
"frames": 21120,
"seconds": 12.0,
"avg_fps": 1759.963,
"low_1pct_fps": 449.051,
"low_01pct_fps": 178.887,
"p50_ms": 0.49,
"p95_ms": 0.901,
"p99_ms": 1.531,
"p999_ms": 3.005,
"max_ms": 12.907,
"histogram": [
20414,
644,
40,
10,
1,
3,
2,
2,
2,
1,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 2,
"frames": 20813,
"seconds": 12.0,
"avg_fps": 1734.41,
"low_1pct_fps": 463.506,
"low_01pct_fps": 175.213,
"p50_ms": 0.496,
"p95_ms": 0.924,
"p99_ms": 1.414,
"p999_ms": 2.733,
"max_ms": 16.832,
"histogram": [
20049,
707,
41,
5,
0,
1,
5,
2,
2,
0,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 0,
"frames": 22100,
"seconds": 12.0,
"avg_fps": 1841.634,
"low_1pct_fps": 565.368,
"low_01pct_fps": 218.426,
"p50_ms": 0.483,
"p95_ms": 0.835,
"p99_ms": 1.209,
"p999_ms": 2.082,
"max_ms": 14.627,
"histogram": [
21540,
532,
18,
1,
1,
1,
2,
2,
1,
1,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 1,
"frames": 21118,
"seconds": 12.0,
"avg_fps": 1759.788,
"low_1pct_fps": 452.445,
"low_01pct_fps": 175.546,
"p50_ms": 0.489,
"p95_ms": 0.915,
"p99_ms": 1.532,
"p999_ms": 2.811,
"max_ms": 11.846,
"histogram": [
20308,
755,
36,
9,
0,
2,
0,
3,
2,
3,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 2,
"frames": 22125,
"seconds": 12.0,
"avg_fps": 1843.745,
"low_1pct_fps": 524.832,
"low_01pct_fps": 181.555,
"p50_ms": 0.477,
"p95_ms": 0.855,
"p99_ms": 1.241,
"p999_ms": 2.236,
"max_ms": 12.478,
"histogram": [
21487,
608,
18,
1,
1,
1,
2,
2,
1,
2,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 0,
"frames": 18005,
"seconds": 12.0,
"avg_fps": 1500.383,
"low_1pct_fps": 403.944,
"low_01pct_fps": 136.465,
"p50_ms": 0.591,
"p95_ms": 1.058,
"p99_ms": 1.592,
"p999_ms": 3.186,
"max_ms": 12.608,
"histogram": [
16864,
1084,
32,
8,
0,
5,
3,
5,
1,
2,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 1,
"frames": 17728,
"seconds": 12.001,
"avg_fps": 1477.269,
"low_1pct_fps": 395.097,
"low_01pct_fps": 138.609,
"p50_ms": 0.598,
"p95_ms": 1.042,
"p99_ms": 1.561,
"p999_ms": 5.033,
"max_ms": 11.462,
"histogram": [
16612,
1051,
39,
7,
1,
6,
5,
3,
1,
3,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 2,
"frames": 18260,
"seconds": 12.0,
"avg_fps": 1521.651,
"low_1pct_fps": 426.358,
"low_01pct_fps": 144.156,
"p50_ms": 0.585,
"p95_ms": 1.015,
"p99_ms": 1.458,
"p999_ms": 3.609,
"max_ms": 11.945,
"histogram": [
17267,
943,
26,
6,
5,
2,
3,
3,
2,
3,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
}
],
"label": "vanilla-mods-win-9800x3d-5080"
}
@@ -0,0 +1,706 @@
{
"format": "horizon-benchmark-report",
"version": 1,
"kind": "game",
"created": "2026-10-29T19:27:56Z",
"app": {
"name": "minecraft",
"version": "26.3",
"mods": [
{
"name": "fabric-loader",
"version": "0.19.5"
},
{
"name": "sodium",
"version": "mc26.3-0.9.2-fabric"
},
{
"name": "lithium",
"version": "mc26.3-0.26.2-fabric"
},
{
"name": "ferrite-core",
"version": "9.0.0-fabric"
},
{
"name": "immediatelyfast",
"version": "1.17.1+26.3-fabric"
}
]
},
"hardware": {
"os": "windows",
"arch": "x86_64",
"cpu_threads": 16,
"os_version": "Microsoft Windows [Version 10.0.26200.8037]",
"cpu": "AMD Ryzen 7 9800X3D 8-Core Processor",
"ram_mb": 95943,
"gpu": {
"name": "NVIDIA GeForce RTX 5080/PCIe/SSE2",
"vendor": "NVIDIA Corporation",
"driver": "3.3.0 NVIDIA 617.42",
"backend": "opengl",
"kind": ""
}
},
"conditions": {
"scenario": "perf_bench:village,forest,ocean,mountain",
"seed": "1234",
"render_distance": 32,
"simulation_distance": 12,
"vsync": false,
"offscreen": false,
"repeats": 3,
"warmup_s": 2.0,
"window": [
1280,
720
]
},
"metrics": {
"startup.title_ms": {
"n": 3,
"mean": 11038.922,
"stdev": 118.058,
"min": 10935.424,
"max": 11167.506,
"ci95": 293.295
},
"startup.world_ms": {
"n": 3,
"mean": 39248.588,
"stdev": 479.755,
"min": 38964.834,
"max": 39802.506,
"ci95": 1191.875
},
"chunks.village.load_ms": {
"n": 3,
"mean": 11176.953,
"stdev": 356.144,
"min": 10836.65,
"max": 11547.064,
"ci95": 884.781
},
"fps.village.avg_fps": {
"n": 3,
"mean": 925.977,
"stdev": 6.727,
"min": 918.832,
"max": 932.189,
"ci95": 16.713
},
"fps.village.low_1pct_fps": {
"n": 3,
"mean": 250.092,
"stdev": 1.868,
"min": 248.431,
"max": 252.114,
"ci95": 4.64
},
"fps.village.low_01pct_fps": {
"n": 3,
"mean": 92.692,
"stdev": 4.466,
"min": 88.423,
"max": 97.332,
"ci95": 11.095
},
"chunks.forest.load_ms": {
"n": 3,
"mean": 17335.638,
"stdev": 687.588,
"min": 16544.168,
"max": 17785.772,
"ci95": 1708.201
},
"fps.forest.avg_fps": {
"n": 3,
"mean": 929.034,
"stdev": 3.11,
"min": 926.32,
"max": 932.428,
"ci95": 7.727
},
"fps.forest.low_1pct_fps": {
"n": 3,
"mean": 312.69,
"stdev": 16.386,
"min": 297.667,
"max": 330.163,
"ci95": 40.708
},
"fps.forest.low_01pct_fps": {
"n": 3,
"mean": 129.331,
"stdev": 12.786,
"min": 115.486,
"max": 140.694,
"ci95": 31.765
},
"chunks.ocean.load_ms": {
"n": 3,
"mean": 12601.063,
"stdev": 148.042,
"min": 12430.182,
"max": 12690.506,
"ci95": 367.785
},
"fps.ocean.avg_fps": {
"n": 3,
"mean": 1016.598,
"stdev": 5.481,
"min": 1010.482,
"max": 1021.068,
"ci95": 13.618
},
"fps.ocean.low_1pct_fps": {
"n": 3,
"mean": 325.361,
"stdev": 15.112,
"min": 315.682,
"max": 342.774,
"ci95": 37.543
},
"fps.ocean.low_01pct_fps": {
"n": 3,
"mean": 117.042,
"stdev": 11.095,
"min": 107.807,
"max": 129.349,
"ci95": 27.563
},
"chunks.mountain.load_ms": {
"n": 3,
"mean": 38513.008,
"stdev": 958.119,
"min": 37447.333,
"max": 39303.226,
"ci95": 2380.292
},
"fps.mountain.avg_fps": {
"n": 3,
"mean": 644.782,
"stdev": 3.94,
"min": 640.866,
"max": 648.745,
"ci95": 9.788
},
"fps.mountain.low_1pct_fps": {
"n": 3,
"mean": 166.029,
"stdev": 4.758,
"min": 162.326,
"max": 171.395,
"ci95": 11.819
},
"fps.mountain.low_01pct_fps": {
"n": 3,
"mean": 86.231,
"stdev": 8.587,
"min": 76.632,
"max": 93.184,
"ci95": 21.334
},
"memory.rss_peak_mb": {
"n": 3,
"mean": 2964.911,
"stdev": 8.27,
"min": 2959.813,
"max": 2974.453,
"ci95": 20.545
},
"memory.rss_steady_mb": {
"n": 3,
"mean": 2894.541,
"stdev": 12.847,
"min": 2879.928,
"max": 2904.059,
"ci95": 31.917
}
},
"frames": [
{
"phase": "village",
"repeat": 0,
"frames": 11123,
"seconds": 12.0,
"avg_fps": 926.911,
"low_1pct_fps": 249.732,
"low_01pct_fps": 92.322,
"p50_ms": 0.989,
"p95_ms": 1.492,
"p99_ms": 2.33,
"p999_ms": 7.409,
"max_ms": 15.258,
"histogram": [
6224,
4701,
147,
28,
3,
2,
5,
3,
3,
3,
1,
3,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 1,
"frames": 11187,
"seconds": 12.001,
"avg_fps": 932.189,
"low_1pct_fps": 252.114,
"low_01pct_fps": 88.423,
"p50_ms": 0.984,
"p95_ms": 1.496,
"p99_ms": 2.328,
"p999_ms": 7.858,
"max_ms": 16.843,
"histogram": [
6518,
4486,
138,
20,
9,
1,
1,
6,
1,
2,
3,
1,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 2,
"frames": 11026,
"seconds": 12.0,
"avg_fps": 918.832,
"low_1pct_fps": 248.431,
"low_01pct_fps": 97.332,
"p50_ms": 0.989,
"p95_ms": 1.524,
"p99_ms": 2.37,
"p999_ms": 7.446,
"max_ms": 16.573,
"histogram": [
6075,
4740,
162,
23,
5,
2,
4,
7,
4,
1,
1,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 0,
"frames": 11117,
"seconds": 12.001,
"avg_fps": 926.32,
"low_1pct_fps": 310.239,
"low_01pct_fps": 131.814,
"p50_ms": 1.011,
"p95_ms": 1.34,
"p99_ms": 2.045,
"p999_ms": 4.711,
"max_ms": 14.077,
"histogram": [
3849,
7150,
78,
26,
5,
1,
3,
2,
1,
0,
1,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 1,
"frames": 11190,
"seconds": 12.001,
"avg_fps": 932.428,
"low_1pct_fps": 330.163,
"low_01pct_fps": 140.694,
"p50_ms": 1.008,
"p95_ms": 1.307,
"p99_ms": 1.869,
"p999_ms": 4.341,
"max_ms": 13.489,
"histogram": [
4273,
6822,
60,
20,
7,
0,
3,
3,
0,
1,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 2,
"frames": 11141,
"seconds": 12.001,
"avg_fps": 928.354,
"low_1pct_fps": 297.667,
"low_01pct_fps": 115.486,
"p50_ms": 1.005,
"p95_ms": 1.352,
"p99_ms": 2.054,
"p999_ms": 5.292,
"max_ms": 14.339,
"histogram": [
4901,
6118,
83,
23,
3,
2,
4,
3,
0,
1,
2,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 0,
"frames": 12126,
"seconds": 12.0,
"avg_fps": 1010.482,
"low_1pct_fps": 317.626,
"low_01pct_fps": 113.971,
"p50_ms": 0.933,
"p95_ms": 1.23,
"p99_ms": 1.935,
"p999_ms": 4.393,
"max_ms": 14.824,
"histogram": [
9183,
2830,
82,
17,
3,
1,
2,
3,
0,
1,
3,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 1,
"frames": 12219,
"seconds": 12.0,
"avg_fps": 1018.243,
"low_1pct_fps": 342.774,
"low_01pct_fps": 129.349,
"p50_ms": 0.929,
"p95_ms": 1.222,
"p99_ms": 1.867,
"p999_ms": 4.329,
"max_ms": 10.33,
"histogram": [
9421,
2710,
60,
15,
3,
0,
0,
5,
3,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 2,
"frames": 12253,
"seconds": 12.0,
"avg_fps": 1021.068,
"low_1pct_fps": 315.682,
"low_01pct_fps": 107.807,
"p50_ms": 0.923,
"p95_ms": 1.225,
"p99_ms": 1.969,
"p999_ms": 5.235,
"max_ms": 19.524,
"histogram": [
9231,
2904,
92,
10,
2,
5,
0,
4,
1,
2,
0,
1,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 0,
"frames": 7691,
"seconds": 12.001,
"avg_fps": 640.866,
"low_1pct_fps": 164.367,
"low_01pct_fps": 93.184,
"p50_ms": 1.454,
"p95_ms": 1.95,
"p99_ms": 3.4,
"p999_ms": 9.754,
"max_ms": 14.995,
"histogram": [
36,
7331,
233,
26,
17,
13,
20,
3,
6,
5,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 1,
"frames": 7737,
"seconds": 12.0,
"avg_fps": 644.736,
"low_1pct_fps": 162.326,
"low_01pct_fps": 76.632,
"p50_ms": 1.454,
"p95_ms": 1.905,
"p99_ms": 3.676,
"p999_ms": 9.926,
"max_ms": 18.459,
"histogram": [
46,
7399,
199,
24,
21,
24,
10,
6,
1,
1,
4,
1,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 2,
"frames": 7785,
"seconds": 12.0,
"avg_fps": 648.745,
"low_1pct_fps": 171.395,
"low_01pct_fps": 88.876,
"p50_ms": 1.439,
"p95_ms": 1.886,
"p99_ms": 3.562,
"p999_ms": 10.342,
"max_ms": 12.927,
"histogram": [
57,
7432,
186,
44,
23,
20,
9,
3,
3,
7,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
}
],
"label": "vanilla-mods-win-9800x3d-5080"
}
@@ -0,0 +1,706 @@
{
"format": "horizon-benchmark-report",
"version": 1,
"kind": "game",
"created": "2026-10-29T18:49:40Z",
"app": {
"name": "minecraft",
"version": "26.3",
"mods": [
{
"name": "fabric-loader",
"version": "0.19.5"
},
{
"name": "sodium",
"version": "mc26.3-0.9.2-fabric"
},
{
"name": "lithium",
"version": "mc26.3-0.26.2-fabric"
},
{
"name": "ferrite-core",
"version": "9.0.0-fabric"
},
{
"name": "immediatelyfast",
"version": "1.17.1+26.3-fabric"
}
]
},
"hardware": {
"os": "windows",
"arch": "x86_64",
"cpu_threads": 16,
"os_version": "Microsoft Windows [Version 10.0.26200.8037]",
"cpu": "AMD Ryzen 7 9800X3D 8-Core Processor",
"ram_mb": 95943,
"gpu": {
"name": "NVIDIA GeForce RTX 5080/PCIe/SSE2",
"vendor": "NVIDIA Corporation",
"driver": "3.3.0 NVIDIA 617.42",
"backend": "opengl",
"kind": ""
}
},
"conditions": {
"scenario": "perf_bench:village,forest,ocean,mountain",
"seed": "1234",
"render_distance": 8,
"simulation_distance": 12,
"vsync": false,
"offscreen": false,
"repeats": 3,
"warmup_s": 2.0,
"window": [
1280,
720
]
},
"metrics": {
"startup.title_ms": {
"n": 3,
"mean": 11089.831,
"stdev": 46.336,
"min": 11036.36,
"max": 11118.192,
"ci95": 115.114
},
"startup.world_ms": {
"n": 3,
"mean": 17643.831,
"stdev": 47.672,
"min": 17589.192,
"max": 17676.942,
"ci95": 118.433
},
"chunks.village.load_ms": {
"n": 3,
"mean": 3603.773,
"stdev": 161.342,
"min": 3488.106,
"max": 3788.086,
"ci95": 400.828
},
"fps.village.avg_fps": {
"n": 3,
"mean": 2103.54,
"stdev": 40.465,
"min": 2078.464,
"max": 2150.222,
"ci95": 100.529
},
"fps.village.low_1pct_fps": {
"n": 3,
"mean": 577.326,
"stdev": 33.693,
"min": 557.613,
"max": 616.23,
"ci95": 83.704
},
"fps.village.low_01pct_fps": {
"n": 3,
"mean": 195.53,
"stdev": 18.899,
"min": 181.495,
"max": 217.019,
"ci95": 46.951
},
"chunks.forest.load_ms": {
"n": 3,
"mean": 3282.612,
"stdev": 48.569,
"min": 3233.16,
"max": 3330.246,
"ci95": 120.661
},
"fps.forest.avg_fps": {
"n": 3,
"mean": 2424.739,
"stdev": 26.074,
"min": 2394.789,
"max": 2442.383,
"ci95": 64.777
},
"fps.forest.low_1pct_fps": {
"n": 3,
"mean": 732.987,
"stdev": 11.926,
"min": 724.144,
"max": 746.55,
"ci95": 29.627
},
"fps.forest.low_01pct_fps": {
"n": 3,
"mean": 249.926,
"stdev": 8.826,
"min": 241.293,
"max": 258.934,
"ci95": 21.928
},
"chunks.ocean.load_ms": {
"n": 3,
"mean": 2705.954,
"stdev": 173.487,
"min": 2538.486,
"max": 2884.891,
"ci95": 431.0
},
"fps.ocean.avg_fps": {
"n": 3,
"mean": 2431.99,
"stdev": 33.071,
"min": 2394.02,
"max": 2454.493,
"ci95": 82.159
},
"fps.ocean.low_1pct_fps": {
"n": 3,
"mean": 764.065,
"stdev": 14.782,
"min": 747.287,
"max": 775.168,
"ci95": 36.723
},
"fps.ocean.low_01pct_fps": {
"n": 3,
"mean": 261.954,
"stdev": 14.724,
"min": 252.457,
"max": 278.916,
"ci95": 36.58
},
"chunks.mountain.load_ms": {
"n": 3,
"mean": 4982.865,
"stdev": 115.523,
"min": 4849.837,
"max": 5057.932,
"ci95": 286.997
},
"fps.mountain.avg_fps": {
"n": 3,
"mean": 2538.05,
"stdev": 67.405,
"min": 2462.815,
"max": 2592.938,
"ci95": 167.458
},
"fps.mountain.low_1pct_fps": {
"n": 3,
"mean": 760.16,
"stdev": 13.503,
"min": 746.001,
"max": 772.895,
"ci95": 33.547
},
"fps.mountain.low_01pct_fps": {
"n": 3,
"mean": 240.897,
"stdev": 17.872,
"min": 224.391,
"max": 259.878,
"ci95": 44.401
},
"memory.rss_peak_mb": {
"n": 3,
"mean": 2052.982,
"stdev": 57.756,
"min": 1992.574,
"max": 2107.66,
"ci95": 143.487
},
"memory.rss_steady_mb": {
"n": 3,
"mean": 1903.921,
"stdev": 62.254,
"min": 1838.307,
"max": 1962.156,
"ci95": 154.659
}
},
"frames": [
{
"phase": "village",
"repeat": 0,
"frames": 25803,
"seconds": 12.0,
"avg_fps": 2150.222,
"low_1pct_fps": 616.23,
"low_01pct_fps": 217.019,
"p50_ms": 0.433,
"p95_ms": 0.642,
"p99_ms": 1.04,
"p999_ms": 2.307,
"max_ms": 7.466,
"histogram": [
25503,
269,
8,
6,
7,
6,
2,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 1,
"frames": 24942,
"seconds": 12.0,
"avg_fps": 2078.464,
"low_1pct_fps": 557.613,
"low_01pct_fps": 181.495,
"p50_ms": 0.45,
"p95_ms": 0.665,
"p99_ms": 1.1,
"p999_ms": 2.305,
"max_ms": 9.819,
"histogram": [
24615,
288,
17,
2,
4,
9,
2,
3,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 2,
"frames": 24984,
"seconds": 12.0,
"avg_fps": 2081.934,
"low_1pct_fps": 558.135,
"low_01pct_fps": 188.076,
"p50_ms": 0.444,
"p95_ms": 0.682,
"p99_ms": 1.101,
"p999_ms": 2.412,
"max_ms": 11.567,
"histogram": [
24645,
308,
9,
3,
5,
5,
7,
1,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 0,
"frames": 29245,
"seconds": 12.0,
"avg_fps": 2437.044,
"low_1pct_fps": 728.266,
"low_01pct_fps": 249.55,
"p50_ms": 0.38,
"p95_ms": 0.555,
"p99_ms": 0.892,
"p999_ms": 1.851,
"max_ms": 6.973,
"histogram": [
29066,
154,
4,
6,
7,
6,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 1,
"frames": 28738,
"seconds": 12.0,
"avg_fps": 2394.789,
"low_1pct_fps": 724.144,
"low_01pct_fps": 241.293,
"p50_ms": 0.384,
"p95_ms": 0.571,
"p99_ms": 0.907,
"p999_ms": 1.833,
"max_ms": 8.35,
"histogram": [
28573,
140,
6,
3,
6,
5,
3,
1,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 2,
"frames": 29309,
"seconds": 12.0,
"avg_fps": 2442.383,
"low_1pct_fps": 746.55,
"low_01pct_fps": 258.934,
"p50_ms": 0.378,
"p95_ms": 0.566,
"p99_ms": 0.876,
"p999_ms": 1.744,
"max_ms": 10.291,
"histogram": [
29138,
148,
5,
6,
5,
2,
4,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 0,
"frames": 29454,
"seconds": 12.0,
"avg_fps": 2454.493,
"low_1pct_fps": 769.741,
"low_01pct_fps": 252.457,
"p50_ms": 0.378,
"p95_ms": 0.55,
"p99_ms": 0.853,
"p999_ms": 1.471,
"max_ms": 6.629,
"histogram": [
29323,
110,
2,
2,
7,
7,
3,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 1,
"frames": 28729,
"seconds": 12.0,
"avg_fps": 2394.02,
"low_1pct_fps": 747.287,
"low_01pct_fps": 254.49,
"p50_ms": 0.383,
"p95_ms": 0.579,
"p99_ms": 0.896,
"p999_ms": 1.56,
"max_ms": 7.367,
"histogram": [
28567,
141,
3,
5,
4,
4,
4,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 2,
"frames": 29370,
"seconds": 12.0,
"avg_fps": 2447.458,
"low_1pct_fps": 775.168,
"low_01pct_fps": 278.916,
"p50_ms": 0.376,
"p95_ms": 0.559,
"p99_ms": 0.887,
"p999_ms": 1.567,
"max_ms": 9.65,
"histogram": [
29209,
141,
3,
9,
0,
5,
1,
1,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 0,
"frames": 31116,
"seconds": 12.0,
"avg_fps": 2592.938,
"low_1pct_fps": 761.584,
"low_01pct_fps": 224.391,
"p50_ms": 0.356,
"p95_ms": 0.511,
"p99_ms": 0.767,
"p999_ms": 1.843,
"max_ms": 8.478,
"histogram": [
31011,
76,
4,
3,
13,
4,
3,
1,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 1,
"frames": 29554,
"seconds": 12.0,
"avg_fps": 2462.815,
"low_1pct_fps": 746.001,
"low_01pct_fps": 238.423,
"p50_ms": 0.366,
"p95_ms": 0.579,
"p99_ms": 0.849,
"p999_ms": 1.75,
"max_ms": 7.607,
"histogram": [
29430,
101,
2,
3,
9,
4,
3,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 2,
"frames": 30701,
"seconds": 12.0,
"avg_fps": 2558.396,
"low_1pct_fps": 772.895,
"low_01pct_fps": 259.878,
"p50_ms": 0.357,
"p95_ms": 0.537,
"p99_ms": 0.81,
"p999_ms": 1.702,
"max_ms": 6.682,
"histogram": [
30568,
112,
1,
6,
5,
3,
6,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
}
],
"label": "vanilla-mods-win-9800x3d-5080"
}
@@ -0,0 +1,690 @@
{
"format": "horizon-benchmark-report",
"version": 1,
"kind": "game",
"created": "2026-10-29T01:19:23Z",
"app": {
"name": "minecraft",
"version": "26.3",
"mods": [
{
"name": "fabric-loader",
"version": "0.19.5"
}
]
},
"hardware": {
"os": "windows",
"arch": "x86_64",
"cpu_threads": 16,
"os_version": "Microsoft Windows [Version 10.0.26200.8037]",
"cpu": "AMD Ryzen 7 9800X3D 8-Core Processor",
"ram_mb": 95943,
"gpu": {
"name": "NVIDIA GeForce RTX 5080/PCIe/SSE2",
"vendor": "NVIDIA Corporation",
"driver": "3.3.0 NVIDIA 617.42",
"backend": "opengl",
"kind": ""
}
},
"conditions": {
"scenario": "perf_bench:village,forest,ocean,mountain",
"seed": "1234",
"render_distance": 16,
"simulation_distance": 12,
"vsync": false,
"offscreen": false,
"repeats": 3,
"warmup_s": 2.0,
"window": [
1280,
720
]
},
"metrics": {
"startup.title_ms": {
"n": 3,
"mean": 7681.834,
"stdev": 189.48,
"min": 7538.777,
"max": 7896.728,
"ci95": 470.732
},
"startup.world_ms": {
"n": 3,
"mean": 19898.834,
"stdev": 192.981,
"min": 19695.728,
"max": 20079.777,
"ci95": 479.431
},
"chunks.village.load_ms": {
"n": 3,
"mean": 6127.947,
"stdev": 26.756,
"min": 6097.19,
"max": 6145.854,
"ci95": 66.471
},
"fps.village.avg_fps": {
"n": 3,
"mean": 1218.486,
"stdev": 26.337,
"min": 1188.13,
"max": 1235.248,
"ci95": 65.43
},
"fps.village.low_1pct_fps": {
"n": 3,
"mean": 318.468,
"stdev": 6.292,
"min": 313.2,
"max": 325.435,
"ci95": 15.632
},
"fps.village.low_01pct_fps": {
"n": 3,
"mean": 126.59,
"stdev": 1.844,
"min": 124.744,
"max": 128.431,
"ci95": 4.58
},
"chunks.forest.load_ms": {
"n": 3,
"mean": 8548.814,
"stdev": 152.302,
"min": 8380.912,
"max": 8678.071,
"ci95": 378.37
},
"fps.forest.avg_fps": {
"n": 3,
"mean": 1912.359,
"stdev": 130.238,
"min": 1791.386,
"max": 2050.215,
"ci95": 323.555
},
"fps.forest.low_1pct_fps": {
"n": 3,
"mean": 401.907,
"stdev": 35.911,
"min": 379.849,
"max": 443.345,
"ci95": 89.216
},
"fps.forest.low_01pct_fps": {
"n": 3,
"mean": 149.26,
"stdev": 9.983,
"min": 139.809,
"max": 159.701,
"ci95": 24.801
},
"chunks.ocean.load_ms": {
"n": 3,
"mean": 6953.478,
"stdev": 276.636,
"min": 6635.649,
"max": 7140.072,
"ci95": 687.258
},
"fps.ocean.avg_fps": {
"n": 3,
"mean": 1860.512,
"stdev": 116.512,
"min": 1726.149,
"max": 1933.597,
"ci95": 289.454
},
"fps.ocean.low_1pct_fps": {
"n": 3,
"mean": 446.336,
"stdev": 56.993,
"min": 382.26,
"max": 491.369,
"ci95": 141.589
},
"fps.ocean.low_01pct_fps": {
"n": 3,
"mean": 164.934,
"stdev": 11.839,
"min": 152.493,
"max": 176.062,
"ci95": 29.413
},
"chunks.mountain.load_ms": {
"n": 3,
"mean": 15211.386,
"stdev": 256.348,
"min": 14938.008,
"max": 15446.375,
"ci95": 636.856
},
"fps.mountain.avg_fps": {
"n": 3,
"mean": 1653.223,
"stdev": 61.705,
"min": 1584.652,
"max": 1704.273,
"ci95": 153.297
},
"fps.mountain.low_1pct_fps": {
"n": 3,
"mean": 331.929,
"stdev": 10.858,
"min": 319.394,
"max": 338.396,
"ci95": 26.974
},
"fps.mountain.low_01pct_fps": {
"n": 3,
"mean": 113.436,
"stdev": 3.255,
"min": 110.869,
"max": 117.097,
"ci95": 8.086
},
"memory.rss_peak_mb": {
"n": 3,
"mean": 2540.129,
"stdev": 152.057,
"min": 2373.152,
"max": 2670.633,
"ci95": 377.761
},
"memory.rss_steady_mb": {
"n": 3,
"mean": 2299.514,
"stdev": 81.451,
"min": 2207.129,
"max": 2360.973,
"ci95": 202.352
}
},
"frames": [
{
"phase": "village",
"repeat": 0,
"frames": 14785,
"seconds": 12.0,
"avg_fps": 1232.081,
"low_1pct_fps": 325.435,
"low_01pct_fps": 128.431,
"p50_ms": 0.742,
"p95_ms": 1.265,
"p99_ms": 2.032,
"p999_ms": 5.653,
"max_ms": 12.791,
"histogram": [
12845,
1784,
127,
7,
1,
8,
4,
3,
5,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 1,
"frames": 14258,
"seconds": 12.0,
"avg_fps": 1188.13,
"low_1pct_fps": 313.2,
"low_01pct_fps": 126.595,
"p50_ms": 0.757,
"p95_ms": 1.349,
"p99_ms": 2.12,
"p999_ms": 5.541,
"max_ms": 13.998,
"histogram": [
11994,
2076,
157,
9,
2,
8,
2,
4,
5,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 2,
"frames": 14823,
"seconds": 12.0,
"avg_fps": 1235.248,
"low_1pct_fps": 316.768,
"low_01pct_fps": 124.744,
"p50_ms": 0.744,
"p95_ms": 1.199,
"p99_ms": 2.047,
"p999_ms": 5.628,
"max_ms": 15.519,
"histogram": [
13214,
1453,
118,
16,
2,
8,
6,
3,
0,
1,
0,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 0,
"frames": 21497,
"seconds": 12.0,
"avg_fps": 1791.386,
"low_1pct_fps": 382.527,
"low_01pct_fps": 139.809,
"p50_ms": 0.474,
"p95_ms": 0.931,
"p99_ms": 1.646,
"p999_ms": 5.274,
"max_ms": 13.459,
"histogram": [
20598,
799,
66,
9,
3,
8,
8,
2,
2,
0,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 1,
"frames": 24603,
"seconds": 12.0,
"avg_fps": 2050.215,
"low_1pct_fps": 443.345,
"low_01pct_fps": 159.701,
"p50_ms": 0.426,
"p95_ms": 0.752,
"p99_ms": 1.442,
"p999_ms": 3.134,
"max_ms": 13.282,
"histogram": [
24041,
487,
48,
6,
5,
6,
2,
3,
3,
1,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 2,
"frames": 22746,
"seconds": 12.0,
"avg_fps": 1895.476,
"low_1pct_fps": 379.849,
"low_01pct_fps": 148.269,
"p50_ms": 0.456,
"p95_ms": 0.829,
"p99_ms": 1.561,
"p999_ms": 4.176,
"max_ms": 10.671,
"histogram": [
22092,
523,
91,
17,
4,
7,
3,
4,
4,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 0,
"frames": 23062,
"seconds": 12.0,
"avg_fps": 1921.791,
"low_1pct_fps": 465.379,
"low_01pct_fps": 166.247,
"p50_ms": 0.463,
"p95_ms": 0.809,
"p99_ms": 1.45,
"p999_ms": 2.398,
"max_ms": 11.808,
"histogram": [
22454,
561,
27,
3,
2,
6,
0,
4,
3,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 1,
"frames": 23204,
"seconds": 12.0,
"avg_fps": 1933.597,
"low_1pct_fps": 491.369,
"low_01pct_fps": 176.062,
"p50_ms": 0.466,
"p95_ms": 0.766,
"p99_ms": 1.406,
"p999_ms": 2.229,
"max_ms": 15.102,
"histogram": [
22699,
469,
19,
4,
1,
1,
4,
2,
2,
2,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 2,
"frames": 20714,
"seconds": 12.0,
"avg_fps": 1726.149,
"low_1pct_fps": 382.26,
"low_01pct_fps": 152.493,
"p50_ms": 0.5,
"p95_ms": 0.956,
"p99_ms": 1.742,
"p999_ms": 3.852,
"max_ms": 12.888,
"histogram": [
19814,
787,
83,
11,
2,
7,
3,
5,
0,
1,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 0,
"frames": 20049,
"seconds": 12.0,
"avg_fps": 1670.745,
"low_1pct_fps": 338.396,
"low_01pct_fps": 117.097,
"p50_ms": 0.502,
"p95_ms": 0.949,
"p99_ms": 1.673,
"p999_ms": 6.456,
"max_ms": 16.791,
"histogram": [
19187,
753,
70,
8,
1,
6,
12,
4,
3,
4,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 1,
"frames": 19016,
"seconds": 12.0,
"avg_fps": 1584.652,
"low_1pct_fps": 337.998,
"low_01pct_fps": 112.343,
"p50_ms": 0.531,
"p95_ms": 1.082,
"p99_ms": 1.717,
"p999_ms": 5.939,
"max_ms": 12.958,
"histogram": [
17849,
1069,
63,
7,
3,
6,
2,
7,
4,
5,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 2,
"frames": 20452,
"seconds": 12.0,
"avg_fps": 1704.273,
"low_1pct_fps": 319.394,
"low_01pct_fps": 110.869,
"p50_ms": 0.496,
"p95_ms": 0.911,
"p99_ms": 1.706,
"p999_ms": 5.938,
"max_ms": 24.863,
"histogram": [
19694,
626,
82,
14,
3,
14,
5,
4,
5,
3,
1,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
}
],
"label": "vanilla-win-9800x3d-5080"
}
@@ -0,0 +1,690 @@
{
"format": "horizon-benchmark-report",
"version": 1,
"kind": "game",
"created": "2026-10-29T19:06:49Z",
"app": {
"name": "minecraft",
"version": "26.3",
"mods": [
{
"name": "fabric-loader",
"version": "0.19.5"
}
]
},
"hardware": {
"os": "windows",
"arch": "x86_64",
"cpu_threads": 16,
"os_version": "Microsoft Windows [Version 10.0.26200.8037]",
"cpu": "AMD Ryzen 7 9800X3D 8-Core Processor",
"ram_mb": 95943,
"gpu": {
"name": "NVIDIA GeForce RTX 5080/PCIe/SSE2",
"vendor": "NVIDIA Corporation",
"driver": "3.3.0 NVIDIA 617.42",
"backend": "opengl",
"kind": ""
}
},
"conditions": {
"scenario": "perf_bench:village,forest,ocean,mountain",
"seed": "1234",
"render_distance": 32,
"simulation_distance": 12,
"vsync": false,
"offscreen": false,
"repeats": 3,
"warmup_s": 2.0,
"window": [
1280,
720
]
},
"metrics": {
"startup.title_ms": {
"n": 3,
"mean": 7518.874,
"stdev": 42.968,
"min": 7469.532,
"max": 7548.049,
"ci95": 106.748
},
"startup.world_ms": {
"n": 3,
"mean": 35424.207,
"stdev": 144.711,
"min": 35318.049,
"max": 35589.041,
"ci95": 359.511
},
"chunks.village.load_ms": {
"n": 3,
"mean": 11146.828,
"stdev": 49.683,
"min": 11094.798,
"max": 11193.773,
"ci95": 123.43
},
"fps.village.avg_fps": {
"n": 3,
"mean": 882.053,
"stdev": 19.433,
"min": 866.126,
"max": 903.706,
"ci95": 48.279
},
"fps.village.low_1pct_fps": {
"n": 3,
"mean": 183.995,
"stdev": 4.924,
"min": 178.512,
"max": 188.041,
"ci95": 12.234
},
"fps.village.low_01pct_fps": {
"n": 3,
"mean": 83.193,
"stdev": 6.797,
"min": 77.734,
"max": 90.806,
"ci95": 16.886
},
"chunks.forest.load_ms": {
"n": 3,
"mean": 18184.002,
"stdev": 266.879,
"min": 17880.667,
"max": 18382.735,
"ci95": 663.017
},
"fps.forest.avg_fps": {
"n": 3,
"mean": 1202.907,
"stdev": 20.426,
"min": 1179.9,
"max": 1218.906,
"ci95": 50.744
},
"fps.forest.low_1pct_fps": {
"n": 3,
"mean": 228.139,
"stdev": 5.166,
"min": 222.394,
"max": 232.401,
"ci95": 12.833
},
"fps.forest.low_01pct_fps": {
"n": 3,
"mean": 107.956,
"stdev": 4.675,
"min": 105.185,
"max": 113.354,
"ci95": 11.614
},
"chunks.ocean.load_ms": {
"n": 3,
"mean": 13146.632,
"stdev": 507.943,
"min": 12572.118,
"max": 13536.145,
"ci95": 1261.902
},
"fps.ocean.avg_fps": {
"n": 3,
"mean": 1223.169,
"stdev": 13.11,
"min": 1208.837,
"max": 1234.556,
"ci95": 32.569
},
"fps.ocean.low_1pct_fps": {
"n": 3,
"mean": 236.27,
"stdev": 6.77,
"min": 228.686,
"max": 241.702,
"ci95": 16.818
},
"fps.ocean.low_01pct_fps": {
"n": 3,
"mean": 99.089,
"stdev": 8.354,
"min": 91.897,
"max": 108.253,
"ci95": 20.755
},
"chunks.mountain.load_ms": {
"n": 3,
"mean": 41846.641,
"stdev": 661.649,
"min": 41435.666,
"max": 42609.895,
"ci95": 1643.76
},
"fps.mountain.avg_fps": {
"n": 3,
"mean": 815.008,
"stdev": 24.644,
"min": 797.565,
"max": 843.201,
"ci95": 61.224
},
"fps.mountain.low_1pct_fps": {
"n": 3,
"mean": 55.371,
"stdev": 5.128,
"min": 50.389,
"max": 60.633,
"ci95": 12.739
},
"fps.mountain.low_01pct_fps": {
"n": 3,
"mean": 7.966,
"stdev": 1.103,
"min": 6.848,
"max": 9.053,
"ci95": 2.74
},
"memory.rss_peak_mb": {
"n": 3,
"mean": 2892.492,
"stdev": 29.54,
"min": 2859.574,
"max": 2916.691,
"ci95": 73.387
},
"memory.rss_steady_mb": {
"n": 3,
"mean": 2799.159,
"stdev": 12.256,
"min": 2786.234,
"max": 2810.613,
"ci95": 30.447
}
},
"frames": [
{
"phase": "village",
"repeat": 0,
"frames": 10845,
"seconds": 12.001,
"avg_fps": 903.706,
"low_1pct_fps": 185.432,
"low_01pct_fps": 81.039,
"p50_ms": 0.98,
"p95_ms": 1.708,
"p99_ms": 3.082,
"p999_ms": 9.304,
"max_ms": 15.487,
"histogram": [
6030,
4403,
297,
49,
25,
16,
8,
2,
6,
2,
4,
3,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 1,
"frames": 10516,
"seconds": 12.0,
"avg_fps": 876.328,
"low_1pct_fps": 188.041,
"low_01pct_fps": 90.806,
"p50_ms": 1.012,
"p95_ms": 1.806,
"p99_ms": 3.354,
"p999_ms": 8.714,
"max_ms": 14.39,
"histogram": [
4927,
5158,
292,
70,
33,
13,
6,
5,
6,
2,
3,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 2,
"frames": 10394,
"seconds": 12.001,
"avg_fps": 866.126,
"low_1pct_fps": 178.512,
"low_01pct_fps": 77.734,
"p50_ms": 1.026,
"p95_ms": 1.789,
"p99_ms": 3.399,
"p999_ms": 10.6,
"max_ms": 15.389,
"histogram": [
4437,
5552,
273,
65,
18,
28,
8,
1,
1,
3,
5,
3,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 0,
"frames": 14519,
"seconds": 12.0,
"avg_fps": 1209.916,
"low_1pct_fps": 232.401,
"low_01pct_fps": 105.185,
"p50_ms": 0.726,
"p95_ms": 1.306,
"p99_ms": 2.25,
"p999_ms": 7.033,
"max_ms": 13.303,
"histogram": [
13121,
1197,
103,
36,
24,
19,
4,
3,
8,
2,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 1,
"frames": 14159,
"seconds": 12.0,
"avg_fps": 1179.9,
"low_1pct_fps": 222.394,
"low_01pct_fps": 105.33,
"p50_ms": 0.732,
"p95_ms": 1.366,
"p99_ms": 2.499,
"p999_ms": 8.061,
"max_ms": 11.797,
"histogram": [
12501,
1356,
195,
45,
25,
14,
6,
3,
9,
5,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 2,
"frames": 14627,
"seconds": 12.0,
"avg_fps": 1218.906,
"low_1pct_fps": 229.622,
"low_01pct_fps": 113.354,
"p50_ms": 0.713,
"p95_ms": 1.292,
"p99_ms": 2.317,
"p999_ms": 7.136,
"max_ms": 12.041,
"histogram": [
13194,
1224,
97,
47,
27,
21,
2,
6,
6,
2,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 0,
"frames": 14714,
"seconds": 12.001,
"avg_fps": 1226.113,
"low_1pct_fps": 238.423,
"low_01pct_fps": 97.117,
"p50_ms": 0.722,
"p95_ms": 1.223,
"p99_ms": 2.243,
"p999_ms": 7.032,
"max_ms": 13.21,
"histogram": [
13490,
1027,
115,
29,
18,
13,
7,
3,
3,
5,
4,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 1,
"frames": 14507,
"seconds": 12.001,
"avg_fps": 1208.837,
"low_1pct_fps": 228.686,
"low_01pct_fps": 91.897,
"p50_ms": 0.719,
"p95_ms": 1.256,
"p99_ms": 2.354,
"p999_ms": 7.473,
"max_ms": 15.432,
"histogram": [
13124,
1112,
180,
35,
23,
9,
8,
4,
2,
6,
2,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 2,
"frames": 14815,
"seconds": 12.0,
"avg_fps": 1234.556,
"low_1pct_fps": 241.702,
"low_01pct_fps": 108.253,
"p50_ms": 0.721,
"p95_ms": 1.2,
"p99_ms": 2.197,
"p999_ms": 6.689,
"max_ms": 12.791,
"histogram": [
13565,
1055,
107,
29,
23,
15,
8,
3,
4,
5,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 0,
"frames": 10119,
"seconds": 12.001,
"avg_fps": 843.201,
"low_1pct_fps": 60.633,
"low_01pct_fps": 9.053,
"p50_ms": 0.891,
"p95_ms": 1.845,
"p99_ms": 3.91,
"p999_ms": 8.209,
"max_ms": 316.409,
"histogram": [
6956,
2747,
240,
83,
45,
22,
8,
8,
5,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
4,
0,
0
]
},
{
"phase": "mountain",
"repeat": 1,
"frames": 9693,
"seconds": 12.052,
"avg_fps": 804.258,
"low_1pct_fps": 50.389,
"low_01pct_fps": 6.848,
"p50_ms": 0.906,
"p95_ms": 1.924,
"p99_ms": 4.157,
"p999_ms": 8.578,
"max_ms": 297.695,
"histogram": [
6578,
2678,
248,
94,
45,
18,
16,
5,
4,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
5,
0,
0
]
},
{
"phase": "mountain",
"repeat": 2,
"frames": 9571,
"seconds": 12.0,
"avg_fps": 797.565,
"low_1pct_fps": 55.09,
"low_01pct_fps": 7.997,
"p50_ms": 0.929,
"p95_ms": 1.944,
"p99_ms": 4.495,
"p999_ms": 7.764,
"max_ms": 321.994,
"histogram": [
5954,
3172,
230,
103,
39,
34,
19,
11,
4,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
4,
0,
0
]
}
],
"label": "vanilla-win-9800x3d-5080"
}
@@ -0,0 +1,690 @@
{
"format": "horizon-benchmark-report",
"version": 1,
"kind": "game",
"created": "2026-10-29T18:43:17Z",
"app": {
"name": "minecraft",
"version": "26.3",
"mods": [
{
"name": "fabric-loader",
"version": "0.19.5"
}
]
},
"hardware": {
"os": "windows",
"arch": "x86_64",
"cpu_threads": 16,
"os_version": "Microsoft Windows [Version 10.0.26200.8037]",
"cpu": "AMD Ryzen 7 9800X3D 8-Core Processor",
"ram_mb": 95943,
"gpu": {
"name": "NVIDIA GeForce RTX 5080/PCIe/SSE2",
"vendor": "NVIDIA Corporation",
"driver": "3.3.0 NVIDIA 617.42",
"backend": "opengl",
"kind": ""
}
},
"conditions": {
"scenario": "perf_bench:village,forest,ocean,mountain",
"seed": "1234",
"render_distance": 8,
"simulation_distance": 12,
"vsync": false,
"offscreen": false,
"repeats": 3,
"warmup_s": 2.0,
"window": [
1280,
720
]
},
"metrics": {
"startup.title_ms": {
"n": 3,
"mean": 7676.526,
"stdev": 305.763,
"min": 7426.024,
"max": 8017.247,
"ci95": 759.617
},
"startup.world_ms": {
"n": 3,
"mean": 13862.526,
"stdev": 284.577,
"min": 13569.024,
"max": 14137.247,
"ci95": 706.985
},
"chunks.village.load_ms": {
"n": 3,
"mean": 3808.014,
"stdev": 61.275,
"min": 3737.989,
"max": 3851.799,
"ci95": 152.227
},
"fps.village.avg_fps": {
"n": 3,
"mean": 2216.576,
"stdev": 45.247,
"min": 2166.285,
"max": 2253.985,
"ci95": 112.409
},
"fps.village.low_1pct_fps": {
"n": 3,
"mean": 545.307,
"stdev": 14.876,
"min": 528.15,
"max": 554.607,
"ci95": 36.957
},
"fps.village.low_01pct_fps": {
"n": 3,
"mean": 215.412,
"stdev": 5.692,
"min": 209.522,
"max": 220.882,
"ci95": 14.14
},
"chunks.forest.load_ms": {
"n": 3,
"mean": 3212.958,
"stdev": 151.722,
"min": 3076.655,
"max": 3376.428,
"ci95": 376.928
},
"fps.forest.avg_fps": {
"n": 3,
"mean": 2456.542,
"stdev": 336.369,
"min": 2068.162,
"max": 2654.549,
"ci95": 835.654
},
"fps.forest.low_1pct_fps": {
"n": 3,
"mean": 624.759,
"stdev": 90.0,
"min": 521.303,
"max": 685.008,
"ci95": 223.59
},
"fps.forest.low_01pct_fps": {
"n": 3,
"mean": 252.469,
"stdev": 21.044,
"min": 229.228,
"max": 270.233,
"ci95": 52.28
},
"chunks.ocean.load_ms": {
"n": 3,
"mean": 2983.236,
"stdev": 168.416,
"min": 2788.777,
"max": 3082.249,
"ci95": 418.403
},
"fps.ocean.avg_fps": {
"n": 3,
"mean": 2469.831,
"stdev": 333.258,
"min": 2085.121,
"max": 2669.9,
"ci95": 827.925
},
"fps.ocean.low_1pct_fps": {
"n": 3,
"mean": 688.684,
"stdev": 96.943,
"min": 576.803,
"max": 747.759,
"ci95": 240.839
},
"fps.ocean.low_01pct_fps": {
"n": 3,
"mean": 286.444,
"stdev": 21.36,
"min": 262.183,
"max": 302.422,
"ci95": 53.066
},
"chunks.mountain.load_ms": {
"n": 3,
"mean": 6079.869,
"stdev": 719.864,
"min": 5542.196,
"max": 6897.69,
"ci95": 1788.385
},
"fps.mountain.avg_fps": {
"n": 3,
"mean": 2605.12,
"stdev": 326.424,
"min": 2228.824,
"max": 2812.075,
"ci95": 810.948
},
"fps.mountain.low_1pct_fps": {
"n": 3,
"mean": 699.63,
"stdev": 87.846,
"min": 599.44,
"max": 763.45,
"ci95": 218.239
},
"fps.mountain.low_01pct_fps": {
"n": 3,
"mean": 264.461,
"stdev": 28.459,
"min": 239.602,
"max": 295.503,
"ci95": 70.701
},
"memory.rss_peak_mb": {
"n": 3,
"mean": 2120.426,
"stdev": 116.406,
"min": 2046.93,
"max": 2254.637,
"ci95": 289.191
},
"memory.rss_steady_mb": {
"n": 3,
"mean": 1912.642,
"stdev": 38.925,
"min": 1889.766,
"max": 1957.586,
"ci95": 96.702
}
},
"frames": [
{
"phase": "village",
"repeat": 0,
"frames": 27048,
"seconds": 12.0,
"avg_fps": 2253.985,
"low_1pct_fps": 553.164,
"low_01pct_fps": 215.833,
"p50_ms": 0.412,
"p95_ms": 0.617,
"p99_ms": 1.187,
"p999_ms": 2.238,
"max_ms": 9.792,
"histogram": [
26716,
296,
15,
4,
7,
5,
2,
2,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 1,
"frames": 26754,
"seconds": 12.0,
"avg_fps": 2229.458,
"low_1pct_fps": 554.607,
"low_01pct_fps": 220.882,
"p50_ms": 0.417,
"p95_ms": 0.626,
"p99_ms": 1.212,
"p999_ms": 2.448,
"max_ms": 8.388,
"histogram": [
26423,
293,
16,
9,
4,
5,
1,
1,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 2,
"frames": 25996,
"seconds": 12.0,
"avg_fps": 2166.285,
"low_1pct_fps": 528.15,
"low_01pct_fps": 209.522,
"p50_ms": 0.43,
"p95_ms": 0.662,
"p99_ms": 1.308,
"p999_ms": 2.477,
"max_ms": 11.033,
"histogram": [
25615,
338,
23,
5,
5,
5,
2,
2,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 0,
"frames": 31855,
"seconds": 12.0,
"avg_fps": 2654.549,
"low_1pct_fps": 667.966,
"low_01pct_fps": 257.945,
"p50_ms": 0.341,
"p95_ms": 0.519,
"p99_ms": 0.932,
"p999_ms": 1.761,
"max_ms": 10.944,
"histogram": [
31558,
275,
6,
2,
7,
0,
4,
2,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 1,
"frames": 31763,
"seconds": 12.0,
"avg_fps": 2646.916,
"low_1pct_fps": 685.008,
"low_01pct_fps": 270.233,
"p50_ms": 0.344,
"p95_ms": 0.52,
"p99_ms": 0.94,
"p999_ms": 1.685,
"max_ms": 11.406,
"histogram": [
31474,
272,
3,
4,
2,
2,
1,
3,
1,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 2,
"frames": 24818,
"seconds": 12.0,
"avg_fps": 2068.162,
"low_1pct_fps": 521.303,
"low_01pct_fps": 229.228,
"p50_ms": 0.439,
"p95_ms": 0.767,
"p99_ms": 1.284,
"p999_ms": 2.351,
"max_ms": 11.468,
"histogram": [
24356,
410,
36,
4,
4,
3,
2,
2,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 0,
"frames": 32039,
"seconds": 12.0,
"avg_fps": 2669.9,
"low_1pct_fps": 741.491,
"low_01pct_fps": 294.727,
"p50_ms": 0.345,
"p95_ms": 0.501,
"p99_ms": 0.86,
"p999_ms": 1.431,
"max_ms": 9.257,
"histogram": [
31771,
250,
3,
2,
4,
6,
1,
1,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 1,
"frames": 31854,
"seconds": 12.0,
"avg_fps": 2654.471,
"low_1pct_fps": 747.759,
"low_01pct_fps": 302.422,
"p50_ms": 0.347,
"p95_ms": 0.503,
"p99_ms": 0.853,
"p999_ms": 1.404,
"max_ms": 13.925,
"histogram": [
31580,
261,
3,
2,
0,
1,
3,
3,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 2,
"frames": 25022,
"seconds": 12.0,
"avg_fps": 2085.121,
"low_1pct_fps": 576.803,
"low_01pct_fps": 262.183,
"p50_ms": 0.428,
"p95_ms": 0.758,
"p99_ms": 1.214,
"p999_ms": 2.109,
"max_ms": 9.206,
"histogram": [
24600,
390,
20,
3,
2,
3,
2,
1,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 0,
"frames": 33745,
"seconds": 12.0,
"avg_fps": 2812.075,
"low_1pct_fps": 736.0,
"low_01pct_fps": 258.279,
"p50_ms": 0.327,
"p95_ms": 0.471,
"p99_ms": 0.808,
"p999_ms": 1.689,
"max_ms": 10.07,
"histogram": [
33516,
205,
7,
0,
7,
4,
3,
1,
1,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 1,
"frames": 33294,
"seconds": 12.0,
"avg_fps": 2774.461,
"low_1pct_fps": 763.45,
"low_01pct_fps": 295.503,
"p50_ms": 0.334,
"p95_ms": 0.471,
"p99_ms": 0.813,
"p999_ms": 1.63,
"max_ms": 8.512,
"histogram": [
33056,
221,
3,
5,
1,
2,
3,
2,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 2,
"frames": 26747,
"seconds": 12.0,
"avg_fps": 2228.824,
"low_1pct_fps": 599.44,
"low_01pct_fps": 239.602,
"p50_ms": 0.403,
"p95_ms": 0.678,
"p99_ms": 1.107,
"p999_ms": 2.054,
"max_ms": 9.178,
"histogram": [
26418,
298,
13,
6,
2,
8,
0,
1,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
}
],
"label": "vanilla-win-9800x3d-5080"
}
@@ -0,0 +1,106 @@
# Horizon v0.2.97 vs vanilla 26.3 on Windows 11: Ryzen 7 9800X3D + RTX 5080 (2026-10-29, RD 8 and 32 rerun 2026-10-30)
The first `tools/perf_bench.py` run on Windows ([docs/benchmark.md](../../docs/benchmark.md)): Horizon, vanilla 26.3 and
vanilla with performance mods, render distances 8, 16 and 32, 1280x720, three games each, every game from a fresh
world. The reports are the `*-win-9800x3d-5080-rd*.json` files next to this page.
```sh
python tools/perf_bench.py --table horizon-win-9800x3d-5080 vanilla-win-9800x3d-5080 vanilla-mods-win-9800x3d-5080
```
## Setup
- **Machine:** AMD Ryzen 7 9800X3D (8 cores, 16 threads), 94 GB RAM (DDR5, EXPO off), NVIDIA GeForce RTX 5080
(driver 617.42), Windows 11 Pro 26200, Balanced power plan, desktop. One 1920x1080 120 Hz display. The game windows
were on the desktop of an unlocked session.
- **Horizon:** release v0.2.97 as the launcher installs it (`--build %APPDATA%\horizon-launcher\versions\v0.2.97`,
`--resources` its `.local\horizon-client\26.3`), Vulkan.
- **Vanilla:** the 26.3 client jar with Fabric Loader 0.19.5 and the driver mod, Java 25.0.5 (Temurin), `-Xmx2G`
with the launcher's G1 flags and the manifest's `-XX:StackShadowPages=32`, OpenGL 3.3.
- **Vanilla + mods:** the same with Sodium mc26.3-0.9.2, Lithium mc26.3-0.26.2, FerriteCore 9.0.0 and
ImmediatelyFast 1.17.1 (`tools/benchmark-deps/mods.lock.json`).
- **Commands:** `python tools/perf_bench.py --game <game> --label <game>-win-9800x3d-5080 --distances <d>
--sizes 1280x720 --repeats 3` (plus `--build` and `--resources` for Horizon), with the games taking turns at
each distance.
- **Memory** is the working set on Windows (what Task Manager shows), sampled from outside the process. Horizon's
GPU figure is its own Vulkan allocator total. `nvidia-smi` gives no per-process figure under WDDM, so vanilla has
none.
## Load
Windows has no load average, so the reports have no `load_avg_1m`. Every game ran on an otherwise idle machine:
- **RD 8 and RD 32** (rerun on 2026-10-30, after the first pass had builds running alongside): a script started each
game only once no `rustc`, `cargo`, `link` (or other compiler) process and no other game (`horizon-client`, `java`,
`javaw`, Minecraft) was running, and the machine's CPU load had stayed under 15 % in every 5-second sample for
30 seconds. During each game it sampled every 2 seconds the machine's CPU load and the CPU of everything outside the
benchmark's own processes. A game during which a compiler or another game ran, or other processes used over 15 % of
the machine for 10 seconds, was discarded and run again: two of the 20 games were (vanilla + mods at RD 32, a
build started during one, background processes during the other).
- **RD 16** (2026-10-29): no `rustc`, `cargo` or `link` process at any point, the load logged every 5 seconds.
| Game | RD | Mean / max CPU load (machine, the game included) | Other processes' CPU, mean / max | Compiler processes |
|---|---|---|---|---|
| Horizon | 8 | 18 % / 49 % | 6 % / 12 % | 0 |
| Vanilla | 8 | 28 % / 75 % | 7 % / 20 % | 0 |
| Vanilla + mods | 8 | 30 % / 81 % | 7 % / 20 % | 0 |
| Horizon | 16 | 29 % / 80 % | – | 0 |
| Vanilla | 16 | 41 % / 91 % | – | 0 |
| Vanilla + mods | 16 | 51 % / 90 % | – | 0 |
| Horizon | 32 | 36 % / 100 % | 5 % / 15 % | 0 |
| Vanilla | 32 | 39 % / 73 % | 6 % / 18 % | 0 |
| Vanilla + mods | 32 | 42 % / 88 % | 6 % / 21 % | 0 |
"Other processes" is the machine's load minus the benchmark's own processes (the game and `perf_bench.py`), in
2-second samples. Most of it is the system's share of the game's own drawing (the graphics driver and the
compositor), which no process of the benchmark is charged for; the desktop apps left open (a browser, chat apps)
briefly reached 5 % at most. RD 16's load was logged from outside every 5 seconds, so it has no such split, and its mean includes the time between
games. Compared with the first pass, the idle RD 8 runs give Horizon about 10 % higher averages and about 50 % higher
1% lows, and vanilla up to 10 % higher averages.
## Results
Mean of 3 games ± 95% confidence interval. FPS over the 12-second flight at 16 blocks/s (average / 1% low /
0.1% low):
| RD | scene | Horizon v0.2.97 | Vanilla 26.3 | Vanilla + mods |
| --- | --- | --- | --- | --- |
| 8 | forest | 3495 ±276 / 1295 ±397 / 859 ±298 | 2457 ±836 / 625 ±224 / 252 ±52 | 2425 ±65 / 733 ±30 / 250 ±22 |
| 8 | mountain | 3376 ±67 / 1384 ±168 / 850 ±256 | 2605 ±811 / 700 ±218 / 264 ±71 | 2538 ±167 / 760 ±34 / 241 ±44 |
| 8 | ocean | 3346 ±147 / 1358 ±237 / 912 ±235 | 2470 ±828 / 689 ±241 / 286 ±53 | 2432 ±82 / 764 ±37 / 262 ±37 |
| 8 | village | 2942 ±197 / 1271 ±146 / 839 ±94 | 2217 ±112 / 545 ±37 / 215 ±14 | 2104 ±101 / 577 ±84 / 196 ±47 |
| 16 | forest | 2424 ±138 / 709 ±165 / 401 ±100 | 1912 ±324 / 402 ±89 / 149 ±25 | 1746 ±32 / 461 ±27 / 179 ±9 |
| 16 | mountain | 1961 ±99 / 537 ±96 / 297 ±75 | 1653 ±153 / 332 ±27 / 113 ±8 | 1500 ±55 / 408 ±40 / 140 ±10 |
| 16 | ocean | 2201 ±183 / 554 ±251 / 325 ±326 | 1861 ±289 / 446 ±142 / 165 ±29 | 1815 ±119 / 514 ±142 / 192 ±58 |
| 16 | village | 1712 ±192 / 593 ±80 / 358 ±69 | 1218 ±65 / 318 ±16 / 127 ±5 | 1282 ±162 / 343 ±25 / 132 ±12 |
| 32 | forest | 1389 ±70 / 504 ±33 / 330 ±118 | 1203 ±51 / 228 ±13 / 108 ±12 | 929 ±8 / 313 ±41 / 129 ±32 |
| 32 | mountain | 1086 ±34 / 258 ±78 / 108 ±37 | 815 ±61 / 55 ±13 / 8 ±3 | 645 ±10 / 166 ±12 / 86 ±21 |
| 32 | ocean | 1189 ±27 / 155 ±9 / 68 ±8 | 1223 ±33 / 236 ±17 / 99 ±21 | 1017 ±14 / 325 ±38 / 117 ±28 |
| 32 | village | 1164 ±9 / 416 ±39 / 216 ±86 | 882 ±48 / 184 ±12 / 83 ±17 | 926 ±17 / 250 ±5 / 93 ±11 |
Startup, loading and memory (means of 3 games):
| RD | | Horizon v0.2.97 | Vanilla 26.3 | Vanilla + mods |
| --- | --- | --- | --- | --- |
| 8 | start to title / in-world | 3.0 s / 6.1 s | 7.7 s / 13.9 s | 11.1 s / 17.6 s |
| 8 | mean scene load | 1.3 s | 4.0 s | 3.6 s |
| 8 | working set, steady / peak | 1082 / 1172 MB | 1913 / 2120 MB | 1904 / 2053 MB |
| 16 | start to title / in-world | 3.2 s / 7.2 s | 7.7 s / 19.9 s | 10.8 s / 23.5 s |
| 16 | mean scene load | 2.4 s | 9.2 s | 9.0 s |
| 16 | working set, steady / peak | 1236 / 1459 MB | 2300 / 2540 MB | 2327 / 2496 MB |
| 32 | start to title / in-world | 2.9 s / 10.5 s | 7.5 s / 35.4 s | 11.0 s / 39.2 s |
| 32 | mean scene load | 5.7 s | 21.1 s | 19.9 s |
| 32 | working set, steady / peak | 1730 / 2045 MB | 2799 / 2892 MB | 2895 / 2965 MB |
## What it shows
- At RD 8 and 16, Horizon's average FPS is 18–42 % above vanilla's in every scene. Its 1% lows are 1.2–2.3 times
vanilla's and its 0.1% lows 2.0–3.9 times. On this CPU and GPU the performance mods leave vanilla's averages
level or slightly lower, and raise its 1% lows by 6–23 %.
- At RD 32, Horizon's average is 15–33 % above vanilla's over the forest, the mountain and the village, with 1% lows
2.2–4.7 times vanilla's. Over the ocean its average is level with vanilla's, but its lows are lower than vanilla's
and the modded client's (155 vs 236 and 325 FPS 1% low, 68 vs 99 and 117 FPS 0.1% low), on an idle machine too.
That scene is worth a closer look.
- Horizon reaches the title screen 2.4–2.6 times faster than vanilla (3.4–3.8 times faster than the modded client)
and loads a scene's chunks 3–4 times faster. Its steady working set is 38–46 % smaller.
- Each game uses its own graphics API: Horizon Vulkan, vanilla OpenGL. That difference is part of what's compared.
+2 -2
View File
@@ -4,10 +4,10 @@ version = "0.1.0"
edition.workspace = true
license.workspace = true
rust-version = "1.86"
description = "cargo horizon-mod new/build/run/check: make Horizon mods in Rust (docs/mods.md)"
description = "cargo horizon-mod new/build/run/check: make Horizon content mods (docs/mods.md)"
[dependencies]
horizon-mods = { path = "../horizon-mods", default-features = false, features = ["pack", "runtime", "index"] }
horizon-mods = { path = "../horizon-mods", default-features = false, features = ["index"] }
serde_json = "1.0.151"
[lints]
+18 -167
View File
@@ -1,35 +1,19 @@
//! `cargo horizon-mod check <file>`: what the registry checks of a submission,
//! run anywhere: its kind's own checks (docs/api/mod-index.md), its signature
//! when given a key, and for a mod a trial run in the sandbox: it must start,
//! survive ticks, frames, a chat line and each of its commands (a server mod:
//! a player joining, chatting, typing them and leaving), and import no more
//! than its capabilities.
//! when given a key, and for a `.hmod` that it is content only.
use horizon_mods::{
Clock, GameMode, Mods, Output, Package, Player, ServerPlayer, Snapshot, Status,
Package,
index::{self, Facts, Kind},
manifest::component_imports,
};
use serde_json::{Value, json};
use std::path::Path;
/// How long the trial run plays: two seconds of game.
const TICKS: u64 = 40;
const FRAMES_PER_TICK: u64 = 3;
pub struct Report {
pub kind: Kind,
pub facts: Facts,
pub sha256: String,
pub size: u64,
pub signed: bool,
/// A mod has code to run (a content mod may have none).
pub code: bool,
/// A mod's component imports.
pub imports: Vec<String>,
/// A mod's commands, each run once.
pub commands: Vec<String>,
/// What the mod showed during the trial run.
pub output: Vec<String>,
}
impl Report {
@@ -53,10 +37,6 @@ impl Report {
"sha256": self.sha256,
"size": self.size,
"signed": self.signed,
"code": self.code,
"imports": self.imports,
"commands": self.commands,
"output": self.output,
})
}
@@ -100,18 +80,8 @@ impl Report {
if self.kind == Kind::NativeMod {
out += " native source: unsandboxed; not executed by the registry check. Compile and test against the exact pinned Horizon and Mixins revisions before review.\n";
}
if self.kind == Kind::Mod && !self.code {
if self.kind == Kind::Mod {
out += " no code: content only, nothing to run\n";
} else if self.kind == Kind::Mod {
let commands = if self.commands.is_empty() {
String::new()
} else {
format!(", /{}", self.commands.join(", /"))
};
out += &format!(
" trial run: started, {TICKS} ticks, {} frames, a chat line{commands}: ok\n",
TICKS * FRAMES_PER_TICK
);
}
out
}
@@ -153,145 +123,26 @@ pub fn check(
(None, None) => false,
_ => return Err("--key and --signature go together".into()),
};
let mut report = Report {
if kind == Kind::Mod {
content_only(path)?;
}
Ok(Report {
kind,
facts,
sha256: index::sha256_file(path)?,
size,
signed,
code: false,
imports: Vec::new(),
commands: Vec::new(),
output: Vec::new(),
};
if report.kind == Kind::Mod {
trial_run(path, &mut report)?;
}
Ok(report)
})
}
/// Runs a mod in the sandbox, as the game does, from a scratch copy so its
/// storage lands in a folder removed afterwards. A content mod has no code to
/// run: `inspect` already read its content as the game does.
fn trial_run(path: &Path, report: &mut Report) -> Result<(), String> {
let package = Package::read(path)?;
let Some(wasm) = package.wasm.as_deref() else {
return Ok(());
};
report.code = true;
report.imports = component_imports(wasm)?;
package
.manifest
.check_imports(report.imports.iter().map(String::as_str))?;
let dir = std::env::temp_dir().join(format!("horizon-mod-check-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?;
let copy = dir.join("mod.hmod");
let result = std::fs::copy(path, &copy)
.map_err(|e| e.to_string())
.and_then(|_| play(&copy, report));
let _ = std::fs::remove_dir_all(&dir);
result
}
fn play(path: &Path, report: &mut Report) -> Result<(), String> {
let mut mods = Mods::load(&[path.to_path_buf()], |_| true, |_| false);
let failed = |mods: &Mods| match &mods.entries()[0].status {
Status::Failed(reason) => Some(reason.clone()),
_ => None,
};
if let Some(reason) = failed(&mods) {
return Err(format!("it didn't start: {reason}"));
}
let client: Vec<String> = mods.commands().map(str::to_owned).collect();
let server: Vec<String> = mods.server_commands().map(|c| c.name.clone()).collect();
report.commands = [client.clone(), server.clone()].concat();
// A server mod plays with one player on, who joins, chats and leaves.
let steve = ServerPlayer {
id: "00000000-0000-4000-8000-000000000001".into(),
name: "Steve".into(),
x: 0.5,
y: 64.0,
z: 0.5,
dimension: "minecraft:overworld".into(),
game_mode: GameMode::Survival,
health: 20.0,
};
let snapshot = |t: u64| Snapshot {
player: Some(Player {
x: 0.5 + t as f64 * 0.1,
y: 64.0,
z: 0.5,
yaw: 90.0,
pitch: 0.0,
health: 20.0,
food: 20,
dimension: "minecraft:overworld".into(),
}),
clock: Some(Clock {
game_time: t,
day_time: 1000 + t,
}),
players: vec![steve.clone()],
// The GUI of a 1280 x 720 window at scale 3.
gui_size: (427.0, 240.0),
..Snapshot::default()
};
mods.set_snapshot(snapshot(0));
mods.player_joined(&steve);
for t in 0..TICKS {
mods.set_snapshot(snapshot(t));
mods.tick(t);
mods.server_tick(t);
for _ in 0..FRAMES_PER_TICK {
mods.frame(1.0 / 60.0);
}
}
mods.chat("<Steve> hello");
mods.player_chat(&steve, "hello");
for name in &client {
mods.command(name);
}
for name in &server {
mods.server_command(&steve, name);
}
mods.set_snapshot(Snapshot::default());
mods.player_left(&steve);
for out in mods.drain() {
report.output.push(match out {
Output::Chat { text, .. } => format!("chat: {text}"),
Output::Tell { text, .. } => format!("tell: {text}"),
Output::Broadcast { text, .. } => format!("broadcast: {text}"),
Output::Feedback { text, error } | Output::Reply { text, error, .. } => {
format!("{}: {text}", if error { "error" } else { "feedback" })
}
Output::Command { line } => format!("command: /{line}"),
Output::QuietCommands { lines, .. } => {
format!("{} quiet command(s)", lines.len())
}
Output::Stopped { reason, .. } => return Err(format!("it stopped: {reason}")),
Output::SetBlocks {
dimension, blocks, ..
} => {
format!("set {} block(s) in {dimension}", blocks.len())
}
Output::OpenScreen { .. } => "opened a screen".to_string(),
Output::CloseScreen { .. } => "closed its screen".to_string(),
Output::PlaceRecipe { book_id, all } => {
format!("placed recipe {book_id}{}", if all { " (all)" } else { "" })
}
Output::Give { item, count, .. } => format!("gave {count} {item}"),
Output::Shake { strength, seconds } => {
format!("shook the camera: {strength:.2} for {seconds:.1} s")
}
});
}
let shapes = mods.hud_shapes().count();
if shapes > 0 {
report.output.push(format!("drew {shapes} HUD shape(s)"));
}
match failed(&mods) {
Some(reason) => Err(format!("it stopped: {reason}")),
/// A mod is content only: WebAssembly code mods don't run any more, code mods are native
/// (`.hnmod`, docs/native-mods.md). `inspect` already read its content as the game does.
fn content_only(path: &Path) -> Result<(), String> {
match Package::read(path)?.wasm {
Some(_) => Err(
"it has WebAssembly code, which Horizon doesn't run any more: make it a native mod (docs/native-mods.md)"
.into(),
),
None => Ok(()),
}
}
@@ -316,7 +167,7 @@ mod tests {
.text()
.contains("cozy 1.0.0 (modpack, format 1, for 26.3)")
);
// The fixture mod's code is a stand-in: its manifest reads, its run fails.
// A mod with WebAssembly code is refused: code mods are native.
let hmod = fixture("coords_hud-0.1.0.hmod");
let key = std::fs::read_to_string(fixture("test.pub")).unwrap();
let sig = std::fs::read_to_string(fixture("coords_hud-0.1.0.hmod.minisig")).unwrap();
@@ -324,7 +175,7 @@ mod tests {
let e = check(&hmod, Kind::Mod, Some(key), Some(&sig))
.err()
.unwrap();
assert!(e.contains("didn't start"), "{e}");
assert!(e.contains("native mod"), "{e}");
let e = check(
&hmod,
Kind::Mod,
+38 -196
View File
@@ -1,32 +1,23 @@
//! `cargo horizon-mod`: make Horizon mods in Rust (`docs/mods.md`).
//! `cargo horizon-mod`: make Horizon content mods (`docs/modpacks.md`): blocks,
//! items, data and assets, no code. Code mods are native (docs/native-mods.md).
//!
//! - `new <path> [--template code|content] [--name "Name"] [--sdk-path <dir>]`:
//! a mod from a template: a Rust crate, or a content mod (no code).
//! - `build [--debug]`: the `.hmod` in `target/horizon-mod/`, with the mod's
//! `assets/` and `data/`; a folder without `Cargo.toml` is content only.
//! - `new <path> [--name "Name"]`: a content mod from the template.
//! - `build`: the `.hmod` in `target/horizon-mod/`, with the mod's `assets/`
//! and `data/`.
//! - `run [--game <horizon-client>] [-- <game args>]`: build, then play with
//! the mod; run `build` again and the game reloads it.
//! - `test [--debug] [--hmod <file>] [<name>]`: build, then run the mod's
//! `tests/*.toml` scripts in the sandbox, with no game.
//! the mod.
//! - `check <file> [--json]`: what the registry checks of a submission
//! (`.hmod`, `.hpack`, resource pack `.zip`), with a trial run for a mod.
//! (`.hmod`, `.hnmod`, `.hpack`, resource pack `.zip`, `.hshader`).
//! - `registry build|review|check|list`: the registry repository's CI.
use horizon_mods::{
Capability, Content, Manifest, Package,
manifest::{MANIFEST, component_imports, componentize},
testing::{Script, TestGame},
};
use horizon_mods::{Capability, Content, Manifest, Package, manifest::MANIFEST};
mod check;
mod registry;
use std::{
path::{Path, PathBuf},
process::{Command, ExitCode, Stdio},
process::{Command, ExitCode},
};
const TARGET: &str = "wasm32-unknown-unknown";
const SDK_GIT: &str = "{ git = \"https://github.com/horizon-rust/horizon\" }";
fn main() -> ExitCode {
// Cargo runs `cargo-horizon-mod horizon-mod <args>`.
let mut args: Vec<String> = std::env::args().skip(1).collect();
@@ -35,14 +26,13 @@ fn main() -> ExitCode {
}
let result = match args.first().map(String::as_str) {
Some("new") => new(&args[1..]),
Some("build") => build(!args.iter().any(|a| a == "--debug")).map(|p| {
Some("build") => build().map(|p| {
println!("Built {}", p.display());
}),
Some("run") => run(&args[1..]),
Some("test") => test(&args[1..]),
Some("check") => check(&args[1..]),
Some("registry") => registry::main(&args[1..]),
_ => Err("usage: cargo horizon-mod new <path> [--template code|content] | build [--debug] | run [--game <horizon-client>] [-- <game args>] | test [--debug] [--hmod <file>] [<name>] | check <file> [--kind <kind>] [--key <public key> --signature <file.minisig>] [--json] | registry build|review|check|list".into()),
_ => Err("usage: cargo horizon-mod new <path> [--name \"Name\"] | build | run [--game <horizon-client>] [-- <game args>] | check <file> [--kind <kind>] [--key <public key> --signature <file.minisig>] [--json] | registry build|review|check|list".into()),
};
match result {
Ok(()) => ExitCode::SUCCESS,
@@ -83,31 +73,9 @@ fn new(args: &[String]) -> Result<(), String> {
})
.collect();
let name = flag(args, "--name").map_or_else(|| base.replace(['-', '_'], " "), str::to_string);
let sdk = match flag(args, "--sdk-path") {
Some(p) => {
let p = std::fs::canonicalize(p).map_err(|e| format!("--sdk-path {p}: {e}"))?;
format!("{{ path = {:?} }}", p.display().to_string())
}
None => SDK_GIT.to_string(),
};
let fill = |t: &str| {
t.replace("{crate}", &id.replace('_', "-"))
.replace("{id}", &id)
.replace("{name}", &name)
.replace("{sdk}", &sdk)
};
let code = [
("Cargo.toml", include_str!("../template/Cargo.toml.in")),
(MANIFEST, include_str!("../template/horizon-mod.toml")),
("src/lib.rs", include_str!("../template/src/lib.rs")),
(".gitignore", include_str!("../template/gitignore")),
(
"tests/hello.toml",
include_str!("../template/tests/hello.toml"),
),
];
let fill = |t: &str| t.replace("{id}", &id).replace("{name}", &name);
// A block copying a vanilla one, with its model, name, loot, recipe and tag.
let content = [
let files = [
(
MANIFEST,
include_str!("../template/content/horizon-mod.toml"),
@@ -146,12 +114,12 @@ fn new(args: &[String]) -> Result<(), String> {
),
(".gitignore", include_str!("../template/gitignore")),
];
let files: &[(&str, &str)] = match flag(args, "--template").unwrap_or("code") {
"code" => &code,
"content" => &content,
other => return Err(format!("no template {other}: code or content")),
};
for &(file, text) in files {
if flag(args, "--template").is_some_and(|t| t != "content") {
return Err(
"only content mods are made here: code mods are native (docs/native-mods.md)".into(),
);
}
for (file, text) in files {
let file = fill(file);
let file = file.as_str();
let to = dir.join(file);
@@ -169,29 +137,24 @@ fn new(args: &[String]) -> Result<(), String> {
Ok(())
}
/// Builds the mod in the current folder and packs `target/horizon-mod/<id>.hmod`:
/// the manifest, the crate's component (none without a `Cargo.toml`: a
/// content mod) and the files in `assets/` and `data/`.
fn build(release: bool) -> Result<PathBuf, String> {
build_in(Path::new("."), release)
/// Packs the content mod in the current folder into `target/horizon-mod/<id>.hmod`: the
/// manifest and the files in `assets/` and `data/`.
fn build() -> Result<PathBuf, String> {
build_in(Path::new("."))
}
fn build_in(root: &Path, release: bool) -> Result<PathBuf, String> {
fn build_in(root: &Path) -> Result<PathBuf, String> {
let manifest_text = std::fs::read_to_string(root.join(MANIFEST))
.map_err(|e| format!("{MANIFEST}: {e} (run this in a mod's folder)"))?;
let manifest = Manifest::parse(&manifest_text)?;
let (wasm, target_dir) = if root.join("Cargo.toml").exists() {
let (wasm, target) = compile(root, release)?;
// The game refuses a component importing capabilities the manifest lacks.
manifest.check_imports(component_imports(&wasm)?.iter().map(String::as_str))?;
(Some(wasm), target)
} else if manifest.capabilities.contains(&Capability::Registries) {
(None, root.join("target"))
} else {
if root.join("Cargo.toml").exists() {
return Err("a Cargo.toml: code mods are native now (docs/native-mods.md)".into());
}
if !manifest.capabilities.contains(&Capability::Registries) {
return Err(format!(
"no Cargo.toml: only a content mod (capability registries in {MANIFEST}) has no code"
"a content mod has capability registries in {MANIFEST}"
));
};
}
// The same checks as the game's, and the files in the order it lists them.
let content = Content::read(root, &manifest)?;
let mut reader = content.reader()?;
@@ -200,53 +163,17 @@ fn build_in(root: &Path, release: bool) -> Result<PathBuf, String> {
files.push((name.as_str(), reader.read(name)?));
}
let files: Vec<(&str, &[u8])> = files.iter().map(|(n, b)| (*n, b.as_slice())).collect();
let dir = target_dir.join("horizon-mod");
let dir = root.join("target/horizon-mod");
std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?;
let path = dir.join(format!("{}.hmod", manifest.id));
// Write then rename, so a running game never reads half a file.
let partial = path.with_extension("hmod.partial");
let bytes = Package::write_files(&manifest_text, wasm.as_deref(), &files)?;
let bytes = Package::write_files(&manifest_text, None, &files)?;
std::fs::write(&partial, bytes).map_err(|e| e.to_string())?;
std::fs::rename(&partial, &path).map_err(|e| e.to_string())?;
Ok(path)
}
/// Builds the crate for WebAssembly: its component, and Cargo's target folder.
fn compile(root: &Path, release: bool) -> Result<(Vec<u8>, PathBuf), String> {
let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".into());
let mut cmd = Command::new(cargo);
cmd.current_dir(root).args([
"build",
"--lib",
"--target",
TARGET,
"--message-format=json-render-diagnostics",
]);
if release {
cmd.arg("--release");
}
let out = cmd
.stderr(Stdio::inherit())
.output()
.map_err(|e| e.to_string())?;
if !out.status.success() {
return Err(format!(
"cargo build failed (is the target installed? rustup target add {TARGET})"
));
}
let core = String::from_utf8_lossy(&out.stdout)
.lines()
.filter_map(|l| serde_json::from_str::<serde_json::Value>(l).ok())
.filter(|m| m["reason"] == "compiler-artifact")
.flat_map(|m| m["filenames"].as_array().cloned().unwrap_or_default())
.filter_map(|f| f.as_str().map(PathBuf::from))
.rfind(|f| f.extension().is_some_and(|e| e == "wasm"))
.ok_or("the build made no .wasm: is the crate a cdylib?")?;
let wasm = componentize(&std::fs::read(&core).map_err(|e| e.to_string())?)?;
let target = core.ancestors().nth(3).ok_or("unexpected target folder")?;
Ok((wasm, target.to_path_buf()))
}
/// `check <file>`: the registry's checks of a submission, printed; fails
/// when one does.
fn check(args: &[String]) -> Result<(), String> {
@@ -278,74 +205,14 @@ fn check(args: &[String]) -> Result<(), String> {
Ok(())
}
/// `test`: every `tests/*.toml` script (those whose name holds `<name>`)
/// against the built mod, or the `.hmod` given with `--hmod`.
fn test(args: &[String]) -> Result<(), String> {
let hmod = match flag(args, "--hmod") {
Some(p) => PathBuf::from(p),
None => build(!args.iter().any(|a| a == "--debug"))?,
};
let filter = args
.iter()
.enumerate()
.find(|(i, a)| !a.starts_with("--") && (*i == 0 || args[i - 1] != "--hmod"))
.map(|(_, a)| a.as_str());
let mut scripts: Vec<PathBuf> = std::fs::read_dir("tests")
.map_err(|e| {
format!(
"tests/: {e} (a mod's tests are tests/<name>.toml: docs/mods.md, \"Testing a mod\")"
)
})?
.filter_map(|e| Some(e.ok()?.path()))
.filter(|p| p.extension().is_some_and(|e| e == "toml"))
.filter(|p| filter.is_none_or(|f| p.to_string_lossy().contains(f)))
.collect();
scripts.sort();
if scripts.is_empty() {
return Err("no tests/*.toml to run".into());
}
println!(
"running {} scripts against {}",
scripts.len(),
hmod.display()
);
let mut failed = 0;
for path in &scripts {
let result = std::fs::read_to_string(path)
.map_err(|e| e.to_string())
.and_then(|text| Script::parse(&text))
.and_then(|script| {
let mut game = TestGame::load(&hmod)?;
script.run(&mut game)?;
Ok(script.steps.len())
});
match result {
Ok(steps) => println!("test {} ... ok ({steps} steps)", path.display()),
Err(e) => {
failed += 1;
println!("test {} ... FAILED\n {e}", path.display());
}
}
}
let passed = scripts.len() - failed;
if failed > 0 {
return Err(format!("{failed} failed, {passed} passed"));
}
println!("test result: ok. {passed} passed");
Ok(())
}
fn run(args: &[String]) -> Result<(), String> {
let path = build(true)?;
let path = build()?;
let game = flag(args, "--game")
.map(str::to_string)
.or_else(|| std::env::var("HORIZON_CLIENT").ok())
.unwrap_or_else(|| "horizon-client".into());
let extra = args.iter().skip_while(|a| *a != "--").skip(1);
println!(
"Starting {game} with {} (rebuild with `cargo horizon-mod build` to reload it)",
path.display()
);
println!("Starting {game} with {}", path.display());
let status = Command::new(&game)
.arg("--dev-mod")
.arg(&path)
@@ -364,28 +231,6 @@ fn run(args: &[String]) -> Result<(), String> {
mod tests {
use super::*;
#[test]
fn the_template_is_a_valid_mod() {
let dir = std::env::temp_dir().join(format!("horizon-mod-new-{}", std::process::id()));
let path = dir.join("My-Mod");
new(&[path.display().to_string(), "--name".into(), "My Mod".into()]).unwrap();
let manifest =
Manifest::parse(&std::fs::read_to_string(path.join(MANIFEST)).unwrap()).unwrap();
assert_eq!(
(manifest.id.as_str(), manifest.name.as_str()),
("my_mod", "My Mod")
);
let cargo = std::fs::read_to_string(path.join("Cargo.toml")).unwrap();
assert!(cargo.contains("name = \"my-mod\"") && cargo.contains(SDK_GIT));
let script = std::fs::read_to_string(path.join("tests/hello.toml")).unwrap();
assert_eq!(Script::parse(&script).unwrap().steps.len(), 3);
assert!(
new(&[path.display().to_string()]).is_err(),
"never overwrites"
);
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn the_content_template_builds_and_checks_with_no_code() {
let dir = std::env::temp_dir().join(format!("horizon-mod-content-{}", std::process::id()));
@@ -397,7 +242,7 @@ mod tests {
];
new(&new_args).unwrap();
assert!(!path.join("Cargo.toml").exists());
let hmod = build_in(&path, true).unwrap();
let hmod = build_in(&path).unwrap();
assert_eq!(hmod, path.join("target/horizon-mod/gems.hmod"));
let package = Package::read(&hmod).unwrap();
assert!(package.wasm.is_none());
@@ -409,17 +254,14 @@ mod tests {
);
assert_eq!(content.files.len(), 8, "{:?}", content.files);
let report = check::check(&hmod, check::kind_of(&hmod).unwrap(), None, None).unwrap();
assert!(!report.code);
assert!(
report.text().contains("no code: content only"),
"{}",
report.text()
);
// Without registries, a mod needs code.
let manifest = std::fs::read_to_string(path.join(MANIFEST)).unwrap();
let manifest = manifest.replace("[\"registries\"]", "[]");
std::fs::write(path.join(MANIFEST), manifest).unwrap();
assert!(build_in(&path, true).unwrap_err().contains("no Cargo.toml"));
// Code is native now.
std::fs::write(path.join("Cargo.toml"), "").unwrap();
assert!(build_in(&path).unwrap_err().contains("native"));
assert!(
new(&[
dir.join("x").display().to_string(),
+71 -1
View File
@@ -13,7 +13,9 @@ const SITE: &str = "https://horizon-rust.github.io/horizon-mods/";
const USAGE: &str = "usage: cargo horizon-mod registry build <checkout> --files <dir> --out <index.json> [--release <url>] [--site <url>] [--generated <time>]
cargo horizon-mod registry review <base checkout> <head checkout> --author <login> [--json]
cargo horizon-mod registry check <base checkout> <head checkout> --files <dir>
cargo horizon-mod registry list <checkout>";
cargo horizon-mod registry list <checkout>
cargo horizon-mod registry release <checkout> <id> <file> --tag <tag> --published <YYYY-MM-DD> [--notes <file>] [--signature <file>] [--game <version>]
cargo horizon-mod registry page <checkout> <id> <horizon-registry.toml>";
fn flag<'a>(args: &'a [String], name: &str) -> Option<&'a str> {
super::flag(args, name)
@@ -154,6 +156,74 @@ pub fn main(args: &[String]) -> Result<(), String> {
}
Ok(())
}
// A release of a project's repository, downloaded: the checks of a submission, then the
// version as the index lists it (JSON on stdout), for the registry's release sync.
Some("release") => {
let [checkout, id, file] = pos[..] else {
return Err(USAGE.into());
};
let (tag, published) = (
flag(rest, "--tag").ok_or(USAGE)?,
flag(rest, "--published").ok_or(USAGE)?,
);
let read = |name: &str| {
flag(rest, name)
.map(|p| std::fs::read_to_string(p).map_err(|e| format!("{p}: {e}")))
.transpose()
};
let (notes, signature) = (read("--notes")?, read("--signature")?);
let entry = entry_of(checkout, id)?;
let path = Path::new(file);
let cache = registry::Cache::read(Path::new(checkout), id)?;
let entry = entry.resolved(&cache.page);
let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
crate::check::check(
path,
entry.kind.clone(),
entry.key.as_deref(),
signature.as_deref(),
)?;
let version = registry::release_version(
&entry,
&registry::Release {
tag,
published,
notes: notes.as_deref().unwrap_or(""),
asset: flag(rest, "--name").unwrap_or(name),
game: flag(rest, "--game").unwrap_or("26.3"),
signature: signature.as_deref(),
},
path,
)?;
println!(
"{}",
serde_json::to_string(&version).map_err(|e| e.to_string())?
);
Ok(())
}
// The page file of a project's repository, checked against its entry: the page as JSON.
Some("page") => {
let [checkout, id, file] = pos[..] else {
return Err(USAGE.into());
};
let entry = entry_of(checkout, id)?;
let text = std::fs::read_to_string(file).map_err(|e| format!("{file}: {e}"))?;
let page = registry::page(&entry, &text)?;
println!(
"{}",
serde_json::to_string(&page).map_err(|e| e.to_string())?
);
Ok(())
}
_ => Err(USAGE.into()),
}
}
/// `mods/<id>.toml` of the checkout, which must name a repository.
fn entry_of(checkout: &str, id: &str) -> Result<registry::Entry, String> {
let (entries, _) = registry::load(Path::new(checkout)).map_err(lines)?;
entries
.into_iter()
.find(|e| e.id == id && e.repo.is_some())
.ok_or_else(|| format!("{id}: no entry with a repo"))
}
@@ -1,14 +0,0 @@
[package]
name = "{crate}"
version = "0.1.0"
edition = "2024"
[lib]
crate-type = ["cdylib"]
[dependencies]
horizon-mod-sdk = {sdk}
[profile.release]
opt-level = "s"
lto = true
@@ -1,12 +0,0 @@
id = "{id}"
name = "{name}"
version = "0.1.0"
description = ""
authors = []
# The Horizon mod API the SDK targets.
api = "0.1"
# client: runs in the player's game.
side = "client"
# What the mod may do, shown to players before they turn it on:
# game-read (position, health, time), chat (show messages), hud (draw text).
capabilities = ["chat"]
@@ -1,19 +0,0 @@
use horizon_mod_sdk::{Mod, Registration, chat};
struct MyMod {
greeted: u32,
}
impl Mod for MyMod {
fn init() -> Result<(Self, Registration), String> {
Ok((MyMod { greeted: 0 }, Registration::new().command("hello")))
}
fn command(&mut self, _name: &str, args: &str) -> Result<Option<String>, String> {
self.greeted += 1;
chat::show(&format!("Hello {args}! ({} so far)", self.greeted));
Ok(None)
}
}
horizon_mod_sdk::export_mod!(MyMod);
@@ -1,14 +0,0 @@
# `cargo horizon-mod test` runs every tests/*.toml against the built mod, with no game:
# steps happen in order, and `expect` checks what the mod showed during the step.
# Every key: docs/mods.md, "Testing a mod".
[[step]]
expect.commands = ["hello"]
[[step]]
command = "hello world"
expect.chat = ["Hello world! (1 so far)"]
[[step]]
command = "hello again"
expect.chat = ["Hello again! (2 so far)"]
+14
View File
@@ -0,0 +1,14 @@
[package]
name = "horizon-anticheat"
version = "0.1.0"
edition.workspace = true
license.workspace = true
rust-version = "1.85"
description = "The dedicated server's anticheat: its [anticheat] settings, violation levels and the checks' rules, with no game state (docs/anticheat.md)"
[dependencies]
serde = { version = "1", features = ["derive"] }
toml = "1.1"
[lints]
workspace = true
+303
View File
@@ -0,0 +1,303 @@
//! The dedicated server's anticheat (docs/anticheat.md), without the game: the `[anticheat]` section of
//! `server.toml`, each player's violation levels, and the rules of the checks. The server's hub
//! (`horizon_client::lan`) feeds it what players send and carries out what it decides.
//!
//! It is off unless the owner turns it on. A check that fails *flags* the player: its violation level
//! (VL) rises by the flag's weight and falls back by `decay` a second. From `setback` on, the move or
//! action is cancelled (the player is sent back where the server last had them); from `alert` on,
//! online operators and the console are told; from `kick` on, the player is disconnected. Nothing bans.
//! Any threshold at 0 turns that action off.
//!
//! The checks are written from vanilla 26.3's own rules and from what cheats are publicly described to
//! do, never from another anticheat's code.
use serde::{Deserialize, Serialize};
pub mod timer;
/// What one check's violation level leads to.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Actions {
pub setback: f32,
pub alert: f32,
pub kick: f32,
/// Violation level lost each second.
pub decay: f32,
}
macro_rules! checks {
($(
$(#[$doc:meta])*
$field:ident: $Check:ident => $Config:ident [$setback:expr, $alert:expr, $kick:expr, $decay:expr] {
$( $(#[$xdoc:meta])* $extra:ident: $ty:ty = $default:expr, )*
}
)*) => {
/// Every check, in the order of the players' violation levels.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Check { $($Check,)* }
impl Check {
pub const ALL: &[Check] = &[$(Check::$Check,)*];
/// Its name in `server.toml` and in alerts.
pub fn name(self) -> &'static str {
match self { $(Check::$Check => stringify!($field),)* }
}
}
$(
$(#[$doc])*
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct $Config {
pub enabled: bool,
pub setback: f32,
pub alert: f32,
pub kick: f32,
pub decay: f32,
$( $(#[$xdoc])* pub $extra: $ty, )*
}
impl Default for $Config {
fn default() -> Self {
Self {
enabled: true,
setback: $setback,
alert: $alert,
kick: $kick,
decay: $decay,
$($extra: $default,)*
}
}
}
)*
/// `server.toml`'s `[anticheat]`: off by default.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct Config {
pub enabled: bool,
/// Tell online operators of an alert in their chat (the console and the log always are).
pub alerts: bool,
/// The permission level a player needs to see alerts.
pub alert_level: u8,
/// Every flag, one line each, relative to the server's folder; empty: no file.
pub log: String,
$(pub $field: $Config,)*
}
impl Default for Config {
fn default() -> Self {
Self {
enabled: false,
alerts: true,
alert_level: 2,
log: "logs/anticheat.log".into(),
$($field: $Config::default(),)*
}
}
}
impl Config {
/// Whether `check` runs: the anticheat and the check both on.
pub fn runs(&self, check: Check) -> bool {
self.enabled && match check { $(Check::$Check => self.$field.enabled,)* }
}
pub fn actions(&self, check: Check) -> Actions {
match check {
$(Check::$Check => {
let c = &self.$field;
Actions { setback: c.setback, alert: c.alert, kick: c.kick, decay: c.decay }
})*
}
}
}
};
}
checks! {
/// Timer: more client ticks than the server's clock allows (a sped-up game). Vanilla's client ends
/// every tick with ServerboundClientTickEndPacket, at the server's tick rate; each one spends a tick's
/// time from a balance that wall-clock time fills, capped so a pause can't be saved up for a burst.
timer: Timer => TimerConfig [2.0, 5.0, 20.0, 0.02] {
/// How far ahead of the clock, in milliseconds, a client may get before it is flagged.
tolerance_ms: f64 = 300.0,
/// The most time, in milliseconds, a quiet connection banks for the ticks that follow (a lag
/// spike's backlog arrives at once).
max_lag_ms: f64 = 1000.0,
}
/// Ground: a move that says it ends on the ground with nothing there to stand on: no-fall (claiming
/// the ground to skip fall damage) and walking on water. The claim is always dropped, so vanilla's
/// fall damage follows; from `setback` on the player is also sent back.
ground: Ground => GroundConfig [5.0, 15.0, 60.0, 2.0] {}
}
/// How many checks there are.
pub const CHECKS: usize = Check::ALL.len();
impl Config {
/// The `[anticheat]` section of a whole `server.toml`; the default (off) when it has none.
pub fn from_server_toml(text: &str) -> Result<Self, String> {
#[derive(Deserialize)]
struct File {
#[serde(default)]
anticheat: Config,
}
// Other sections are the supervisor's to check: only this one is read here.
let mut table: toml::Table = toml::from_str(text).map_err(|e| e.to_string())?;
let mut only = toml::Table::new();
if let Some(section) = table.remove("anticheat") {
only.insert("anticheat".into(), section);
}
let file: File = only
.try_into()
.map_err(|e: toml::de::Error| format!("[anticheat]: {e}"))?;
file.anticheat.check()?;
Ok(file.anticheat)
}
/// Thresholds can't be negative.
pub fn check(&self) -> Result<(), String> {
for &c in Check::ALL {
let a = self.actions(c);
if [a.setback, a.alert, a.kick, a.decay]
.iter()
.any(|v| !v.is_finite() || *v < 0.0)
{
return Err(format!(
"anticheat.{}: setback, alert, kick and decay are numbers from 0 up",
c.name()
));
}
}
Ok(())
}
}
/// What a flag leads to.
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct Verdict {
/// The check's violation level after the flag.
pub vl: f32,
/// Cancel the move or action.
pub setback: bool,
/// Tell operators and the console (at most once a second per check).
pub alert: bool,
pub kick: bool,
}
/// One player's anticheat state. Clean players cost nothing: levels only change when a check flags.
#[derive(Clone, Debug, Default)]
pub struct Player {
vl: [f32; CHECKS],
/// The server tick of each check's last flag (its decay runs from there).
flagged: [u64; CHECKS],
/// The server tick of each check's last alert.
alerted: [Option<u64>; CHECKS],
pub timer: timer::Balance,
/// The server tick and height where the ground last held the player.
pub ground: Option<(u64, f64)>,
}
impl Player {
/// The check's violation level at `tick`, with its decay since the last flag.
pub fn vl(&self, config: &Config, check: Check, tick: u64) -> f32 {
let i = check as usize;
let seconds = tick.saturating_sub(self.flagged[i]) as f32 / 20.0;
(self.vl[i] - config.actions(check).decay * seconds).max(0.0)
}
/// `check` failed with this weight at server tick `tick`.
pub fn flag(&mut self, config: &Config, check: Check, weight: f32, tick: u64) -> Verdict {
let i = check as usize;
let a = config.actions(check);
let vl = self.vl(config, check, tick) + weight;
self.vl[i] = vl;
self.flagged[i] = tick;
let reached = |threshold: f32| threshold > 0.0 && vl >= threshold;
let alert = reached(a.alert) && self.alerted[i].is_none_or(|t| tick >= t + 20);
if alert {
self.alerted[i] = Some(tick);
}
Verdict {
vl,
setback: reached(a.setback),
alert,
kick: reached(a.kick),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn on() -> Config {
Config {
enabled: true,
..Config::default()
}
}
#[test]
fn off_by_default_and_without_a_section() {
assert!(!Config::default().enabled);
let c = Config::from_server_toml("[server]\nport = 25565\n").unwrap();
assert_eq!(c, Config::default());
assert!(!c.runs(Check::Timer));
assert!(!c.runs(Check::Ground));
}
#[test]
fn reads_its_section() {
let text = "[server]\nport = 1\n[anticheat]\nenabled = true\n[anticheat.timer]\nkick = 0\ntolerance_ms = 500\n";
let c = Config::from_server_toml(text).unwrap();
assert!(c.runs(Check::Timer));
assert_eq!(c.timer.kick, 0.0);
assert_eq!(c.timer.tolerance_ms, 500.0);
assert_eq!(c.timer.alert, TimerConfig::default().alert);
let off = "[anticheat]\nenabled = true\n[anticheat.timer]\nenabled = false\n";
assert!(!Config::from_server_toml(off).unwrap().runs(Check::Timer));
}
#[test]
fn refuses_unknown_keys_and_negative_levels() {
assert!(Config::from_server_toml("[anticheat]\nenable = true\n").is_err());
assert!(Config::from_server_toml("[anticheat.timer]\nkick = -1\n").is_err());
}
#[test]
fn levels_rise_decay_and_act_in_order() {
let mut c = on();
c.timer = TimerConfig {
setback: 2.0,
alert: 3.0,
kick: 5.0,
decay: 1.0,
..TimerConfig::default()
};
let mut p = Player::default();
let v = p.flag(&c, Check::Timer, 1.0, 100);
assert_eq!((v.setback, v.alert, v.kick), (false, false, false));
let v = p.flag(&c, Check::Timer, 1.0, 100);
assert_eq!((v.vl, v.setback, v.alert), (2.0, true, false));
let v = p.flag(&c, Check::Timer, 1.0, 100);
assert!(v.alert && !v.kick);
// At most one alert a second.
assert!(!p.flag(&c, Check::Timer, 0.5, 110).alert);
// 3 at tick 110 (half a level gone, half added); two seconds later two more are gone.
assert_eq!(p.vl(&c, Check::Timer, 150), 1.0);
assert_eq!(p.vl(&c, Check::Timer, 10_000), 0.0);
let v = p.flag(&c, Check::Timer, 5.0, 10_000);
assert!(v.kick && v.alert);
}
#[test]
fn zero_turns_an_action_off() {
let mut c = on();
c.timer.kick = 0.0;
let mut p = Player::default();
assert!(!p.flag(&c, Check::Timer, 1000.0, 1).kick);
}
}
+111
View File
@@ -0,0 +1,111 @@
//! The timer check's balance: how far a client's ticks are behind (positive) or ahead (negative) of the
//! server's clock, in milliseconds.
//!
//! The server adds the wall-clock time of each of its ticks, and every ServerboundClientTickEndPacket
//! takes away the length of one tick at the server's tick rate (vanilla's client ticks at that rate,
//! `TickRateManager.millisecondsPerTick`). The balance is capped at `max_lag_ms` *after* the tick's
//! packets are counted: a slow server tick that handles a second of packets at once also added that
//! second, and a quiet connection banks at most `max_lag_ms` for the burst that ends a lag spike.
use crate::TimerConfig;
#[derive(Clone, Debug, Default)]
pub struct Balance {
pub ms: f64,
}
impl Balance {
/// The start of a server tick that came `elapsed_ms` after the previous one.
pub fn elapse(&mut self, elapsed_ms: f64, config: &TimerConfig) {
self.ms = self.ms.min(config.max_lag_ms) + elapsed_ms;
}
/// One client tick ended, at a tick rate of `tick_rate` a second. Some(how far ahead, in ms) when the
/// client is further ahead than the tolerance; the balance starts again from zero then, so a
/// sustained speed-up is flagged again each time it gets that far ahead.
pub fn tick_end(&mut self, tick_rate: f32, config: &TimerConfig) -> Option<f64> {
self.ms -= 1000.0 / f64::from(tick_rate.max(1.0));
if self.ms < -config.tolerance_ms {
let ahead = -self.ms;
self.ms = 0.0;
return Some(ahead);
}
None
}
/// The tick rate changed or the player arrived: start again.
pub fn reset(&mut self) {
self.ms = 0.0;
}
}
#[cfg(test)]
mod tests {
use super::*;
/// `seconds` of server ticks of `server_ms` each, with `per_tick` client ticks handled in each.
fn run(b: &mut Balance, server_ms: f64, per_tick: f64, seconds: f64) -> usize {
let c = TimerConfig::default();
let mut owed = 0.0;
let mut flags = 0;
for _ in 0..(seconds * 1000.0 / server_ms) as usize {
b.elapse(server_ms, &c);
owed += per_tick;
while owed >= 1.0 {
owed -= 1.0;
flags += usize::from(b.tick_end(20.0, &c).is_some());
}
}
flags
}
#[test]
fn a_vanilla_client_is_never_flagged() {
let mut b = Balance::default();
assert_eq!(run(&mut b, 50.0, 1.0, 600.0), 0);
// A server that runs at 10 ticks a second handles two client ticks in each.
assert_eq!(run(&mut b, 100.0, 2.0, 600.0), 0);
}
#[test]
fn a_slow_server_tick_with_its_backlog_is_not_flagged() {
let (mut b, c) = (Balance::default(), TimerConfig::default());
b.elapse(3000.0, &c);
for _ in 0..60 {
assert_eq!(b.tick_end(20.0, &c), None);
}
}
#[test]
fn a_short_network_stall_is_not_flagged() {
let (mut b, c) = (Balance::default(), TimerConfig::default());
// 800 ms with nothing arriving, then the 16 ticks sent meanwhile.
for _ in 0..16 {
b.elapse(50.0, &c);
}
b.elapse(50.0, &c);
for _ in 0..17 {
assert_eq!(b.tick_end(20.0, &c), None);
}
}
#[test]
fn a_sped_up_client_is_flagged_again_and_again() {
let mut b = Balance::default();
// 10 % faster: 5 ms ahead each tick, 300 ms after 3 s, so close to 20 flags a minute.
let flags = run(&mut b, 50.0, 1.1, 60.0);
assert!((15..=20).contains(&flags), "{flags}");
// Twice as fast: a flag every 7 ticks.
assert!(run(&mut Balance::default(), 50.0, 2.0, 10.0) >= 25);
}
#[test]
fn a_pause_cant_be_saved_for_a_burst() {
let (mut b, c) = (Balance::default(), TimerConfig::default());
for _ in 0..200 {
b.elapse(50.0, &c);
}
let flagged = (0..60).any(|_| b.tick_end(20.0, &c).is_some());
assert!(flagged);
}
}
+352 -13
View File
@@ -21,18 +21,19 @@ use horizon_protocol::{
connection::{ConnWriter, Connection},
crypto::offline_uuid,
ids,
item::Stack,
packets::{
Empty, configuration,
handshake::{self, Intention},
login::{self, ServerHello},
play,
play, recipes,
},
};
use serde_json::json;
use std::{
collections::HashSet,
sync::{
Arc, Mutex,
Arc, LazyLock, Mutex,
atomic::{AtomicBool, AtomicU64, Ordering},
},
thread,
@@ -44,29 +45,75 @@ horizon-bots: load-test bots for a Minecraft Java Edition 26.3 server (offline m
--server HOST:PORT default 127.0.0.1:25565
--bots N default 10
--behaviour B idle, walk, explore or mixed (60 % walk, 20 % explore, 20 % idle); default mixed
--behaviour B idle, walk, explore or mixed (60 % walk, 20 % explore, 20 % idle); default mixed.
stand: stays on the ground where the server puts it (/tp), for a survival test.
One abusive client, to test a server's limits (run it beside a normal crowd):
flood-move (--flood moves a tick, each 64 to 256 blocks away, ignoring chunks),
flood-acks (--flood chunk batch acks asking for 64 chunks a tick, while exploring),
flood-packets (--flood rotations a tick),
flood-chunks (one move a tick, 9.9 blocks straight ahead: as fast as the
moved-too-quickly check allows, never waiting for chunks),
and --flood a tick of packets that cost the server work:
flood-use (place and break stone around itself), flood-click (inventory
clicks with a stale state), flood-suggest (command suggestions, every
other one 30 000 characters long), flood-book (100 pages of 1024 characters
into a writable book), flood-sign (four 384-character lines on the block
below), flood-recipe (recipe book placements)
--flood N packets a tick for the flood behaviours; default 100
--attack NAME attack the player NAME twice a second, from wherever the bot is (a reach test);
the report gives each bot's last health
--attack-after S start attacking S seconds after joining; default 5
--no-read stop reading from the server once spawned (a client that lets the
server's replies pile up)
--join-every SECONDS default 1
--seconds N how long to run after the first bot joins; default 60
--view-distance N what the bots ask for; default 10
--seed N default 12345
--explore-speed B blocks a second an explore bot flies; default 22 (as fast as chunks come)
--name PREFIX bot names: PREFIX0, PREFIX1...; default Bot
--brand B the client brand sent in configuration (minecraft:brand); default none
--height H blocks above spawn the bots fly at (walk, explore, idle); default 24
--ground claim the ground in every move (with --height 0 on a flat world: walking
on it; higher, or over water: a no-fall or jesus cheat, to test an anticheat)
--timer F cheat, to test an anticheat: tick F times as fast as vanilla's client (1.5: 30 Hz)
--report FILE a JSON summary at the end (- prints it after the per-second lines)
";
#[derive(Clone, Copy, Debug, PartialEq)]
enum Behaviour {
Idle,
Stand,
Walk,
Explore,
FloodMove,
FloodAcks,
FloodPackets,
FloodChunks,
FloodUse,
FloodClick,
FloodSuggest,
FloodBook,
FloodSign,
FloodRecipe,
}
impl Behaviour {
fn name(self) -> &'static str {
match self {
Self::Idle => "idle",
Self::Stand => "stand",
Self::Walk => "walk",
Self::Explore => "explore",
Self::FloodMove => "flood-move",
Self::FloodAcks => "flood-acks",
Self::FloodPackets => "flood-packets",
Self::FloodChunks => "flood-chunks",
Self::FloodUse => "flood-use",
Self::FloodClick => "flood-click",
Self::FloodSuggest => "flood-suggest",
Self::FloodBook => "flood-book",
Self::FloodSign => "flood-sign",
Self::FloodRecipe => "flood-recipe",
}
}
}
@@ -81,8 +128,19 @@ struct Options {
view_distance: i8,
seed: u64,
explore_speed: f64,
/// The player to attack, and how long after joining to start.
attack: Option<String>,
attack_after: f64,
flood: usize,
no_read: bool,
height: f64,
ground: bool,
name: String,
/// The `minecraft:brand` to send, if any.
brand: Option<String>,
report: Option<String>,
/// How many times as fast as vanilla's 20 Hz the bots tick (a timer cheat above 1).
timer: f64,
}
fn options() -> Result<Options, String> {
@@ -96,8 +154,16 @@ fn options() -> Result<Options, String> {
view_distance: 10,
seed: 12345,
explore_speed: 22.0,
attack: None,
attack_after: 5.0,
flood: 100,
no_read: false,
height: 24.0,
ground: false,
name: "Bot".into(),
report: None,
brand: None,
timer: 1.0,
};
let mut args = std::env::args().skip(1);
while let Some(arg) = args.next() {
@@ -105,6 +171,14 @@ fn options() -> Result<Options, String> {
print!("{HELP}");
std::process::exit(0);
}
if arg == "--no-read" {
o.no_read = true;
continue;
}
if arg == "--ground" {
o.ground = true;
continue;
}
let value = args.next().ok_or(format!("{arg} needs a value"))?;
let number = |what: &str| format!("{arg} needs {what}, not {value:?}");
match arg.as_str() {
@@ -122,14 +196,42 @@ fn options() -> Result<Options, String> {
"--explore-speed" => {
o.explore_speed = value.parse().map_err(|_| number("blocks a second"))?;
}
"--flood" => o.flood = value.parse().map_err(|_| number("packets a tick"))?,
"--attack" => o.attack = Some(value),
"--attack-after" => o.attack_after = value.parse().map_err(|_| number("seconds"))?,
"--name" => o.name = value,
"--report" => o.report = Some(value),
"--brand" => o.brand = Some(value),
"--height" => o.height = value.parse().map_err(|_| number("blocks"))?,
"--timer" => {
o.timer = value.parse().map_err(|_| number("a speed, like 1.5"))?;
if !(o.timer >= 0.1 && o.timer <= 20.0) {
return Err(format!("--timer {value}: use 0.1 to 20"));
}
}
_ => return Err(format!("unknown option {arg}; --help lists them")),
}
}
if !["idle", "walk", "explore", "mixed"].contains(&o.behaviour.as_str()) {
const BEHAVIOURS: [&str; 15] = [
"idle",
"walk",
"explore",
"mixed",
"stand",
"flood-move",
"flood-acks",
"flood-packets",
"flood-chunks",
"flood-use",
"flood-click",
"flood-suggest",
"flood-book",
"flood-sign",
"flood-recipe",
];
if !BEHAVIOURS.contains(&o.behaviour.as_str()) {
return Err(format!(
"--behaviour {}: use idle, walk, explore or mixed",
"--behaviour {}: use idle, walk, explore, mixed or a flood behaviour (--help lists them)",
o.behaviour
));
}
@@ -156,6 +258,13 @@ struct Bot {
teleport: Mutex<Option<[f64; 3]>>,
error: Mutex<Option<String>>,
joined_after_ms: AtomicU64,
/// The bot stopped reading (--no-read).
deaf: AtomicBool,
/// `--attack`: the target's UUID and, once AddEntity showed it, its entity id.
target: Mutex<(Option<u128>, Option<i32>)>,
attacks: AtomicU64,
/// The last SetHealth.
health: Mutex<Option<f32>>,
}
/// A small deterministic generator (SplitMix64), so every run moves the bots the same way.
@@ -185,6 +294,89 @@ fn raw(id: i32, write: impl FnOnce(&mut Writer)) -> Vec<u8> {
w.data
}
/// Item ids of the pinned version (26.3's item registry).
const STONE: i32 = 1;
const WRITABLE_BOOK: i32 = 1371;
/// The `i`th packet of a costly flood this tick, the bot at `pos`; none for other behaviours.
fn costly(
behaviour: Behaviour,
i: usize,
sequence: i32,
pos: [f64; 3],
random: &mut Random,
) -> Option<Vec<u8>> {
use ids::play::serverbound as sb;
static LONG_COMMAND: LazyLock<String> =
LazyLock::new(|| format!("/msg {}", "x".repeat(30_000)));
static BOOK: LazyLock<Vec<u8>> = LazyLock::new(|| {
play::Serverbound::EditBook(play::EditBook {
slot: 0,
pages: vec!["a".repeat(1024); 100],
title: None,
})
.encode()
});
let block = pos.map(|v| v.floor() as i32);
let packet = match behaviour {
Behaviour::FloodUse => {
// A block within reach: place stone there, or break what is there.
let at = [0, 1, 2].map(|k| block[k] + (random.unit() * 7.0) as i32 - 3);
if i % 2 == 0 {
play::Serverbound::UseItemOn(play::UseItemOn {
hand: 0,
pos: at,
face: 1,
cursor: [0.5, 1.0, 0.5],
inside: false,
world_border: false,
sequence,
})
} else {
play::Serverbound::PlayerAction(play::PlayerAction {
action: 0,
pos: at,
direction: 1,
sequence,
})
}
}
Behaviour::FloodClick => play::Serverbound::ContainerClick(play::ContainerClick {
container: 0,
state: 0,
slot: 36 + (i % 9) as i16,
button: 0,
input: 0,
changed: Vec::new(),
carried: None,
}),
Behaviour::FloodSuggest => {
let text = if i % 2 == 0 {
"/"
} else {
LONG_COMMAND.as_str()
};
return Some(raw(sb::COMMAND_SUGGESTION, |w| {
w.varint(sequence);
w.string(text);
}));
}
Behaviour::FloodBook => return Some(BOOK.clone()),
Behaviour::FloodSign => play::Serverbound::SignUpdate(play::SignUpdate {
pos: [block[0], block[1] - 1, block[2]],
lines: std::array::from_fn(|_| "a".repeat(384)),
front: true,
}),
Behaviour::FloodRecipe => play::Serverbound::PlaceRecipe(recipes::PlaceRecipe {
container: 0,
recipe: (i % 2042) as i32,
use_max: true,
}),
_ => return None,
};
Some(packet.encode())
}
fn err(e: impl std::fmt::Display) -> String {
e.to_string()
}
@@ -252,6 +444,19 @@ fn join(o: &Options, name: &str) -> Result<Connection, String> {
.encode(),
)
.map_err(err)?;
// ClientBrandRetriever: vanilla's client says "vanilla", Horizon's "horizon".
if let Some(brand) = &o.brand {
let mut w = Writer::new();
w.string(brand);
c.send(
&configuration::Serverbound::CustomPayload(configuration::CustomPayload {
channel: "minecraft:brand".into(),
data: w.data,
})
.encode(),
)
.map_err(err)?;
}
loop {
let (id, body) = c.recv().map_err(err)?;
let reply = match id {
@@ -279,8 +484,9 @@ fn join(o: &Options, name: &str) -> Result<Connection, String> {
payload: None,
})
}
configuration::Clientbound::Disconnect(_) => {
return Err("disconnected during configuration".into());
configuration::Clientbound::Disconnect(p) => {
let reason = horizon_protocol::text::plain(&p.reason, &Default::default());
return Err(format!("disconnected during configuration: {reason}"));
}
configuration::Clientbound::FinishConfiguration(_) => {
c.send(&configuration::Serverbound::FinishConfiguration(Empty).encode())
@@ -339,6 +545,12 @@ fn read(
let mut batch = BatchSize::new();
let mut teleports = 0u64;
loop {
if bot.deaf.load(Ordering::Relaxed) {
while bot.online.load(Ordering::SeqCst) {
thread::sleep(Duration::from_millis(100));
}
return;
}
let (id, body) = match reader.recv() {
Ok(packet) => packet,
Err(e) => {
@@ -379,6 +591,21 @@ fn read(
}
Ok(())
}
cb::ADD_ENTITY => {
if let Ok(play::Clientbound::AddEntity(p)) = play::Clientbound::decode(id, &body) {
let mut target = lock(&bot.target);
if target.0 == Some(p.uuid) {
target.1 = Some(p.id);
}
}
Ok(())
}
cb::SET_HEALTH => {
if let Ok(play::Clientbound::SetHealth(p)) = play::Clientbound::decode(id, &body) {
*lock(&bot.health) = Some(p.health);
}
Ok(())
}
cb::CHUNK_BATCH_START => {
batch.started = Instant::now();
Ok(())
@@ -466,6 +693,7 @@ fn run_bot(
};
let (reader, writer) = connection.split();
let writer = Arc::new(Mutex::new(writer));
lock(&bot.target).0 = o.attack.as_deref().map(offline_uuid);
bot.online.store(true, Ordering::SeqCst);
{
let (writer, bot) = (writer.clone(), bot.clone());
@@ -489,12 +717,31 @@ fn run_bot(
.store(asked.elapsed().as_millis() as u64, Ordering::SeqCst);
let send = |body: Vec<u8>| lock(&writer).send(&body).is_ok();
send(raw(sb::PLAYER_LOADED, |_| {}));
bot.deaf.store(o.no_read, Ordering::Relaxed);
// What the costly floods need in hand (creative: the server takes the bot's word for it).
let tool = match behaviour {
Behaviour::FloodUse => Some(Stack::new(STONE, 64)),
Behaviour::FloodBook => Some(Stack::new(WRITABLE_BOOK, 1)),
_ => None,
};
if tool.is_some() {
send(
play::Serverbound::SetCreativeModeSlot(play::SetCreativeModeSlot {
slot: 36,
item: tool,
})
.encode(),
);
send(play::Serverbound::SetCarriedItem(play::SetCarriedItem { slot: 0 }).encode());
}
let mut sequence = 0;
let mut random = Random(o.seed ^ (index as u64).wrapping_mul(0x2545_F491_4F6C_DD1D));
let mut pos = spawn;
let cruise = spawn[1] + 24.0;
let cruise = spawn[1] + o.height;
let heading = random.unit() * std::f64::consts::TAU;
let mut target = pos;
let mut next = Instant::now();
let (joined, mut tick) = (Instant::now(), 0u64);
loop {
if lock(&until).is_some_and(|u| Instant::now() >= u) || !bot.online.load(Ordering::SeqCst) {
break;
@@ -504,13 +751,46 @@ fn run_bot(
pos = at;
}
let here = ((pos[0].floor() as i32) >> 4, (pos[2].floor() as i32) >> 4);
if !lock(&bot.held).contains(&here) {
// The abusive client doesn't wait for its chunk, as a real client must.
if behaviour == Behaviour::Stand {
// Where the server put it, on the ground.
} else if behaviour == Behaviour::FloodMove {
// The last of these moves goes out below, with the tick's end.
pos[1] = cruise;
for _ in 1..o.flood.max(1) {
let angle = random.unit() * std::f64::consts::TAU;
let reach = 64.0 + random.unit() * 192.0;
pos[0] += angle.cos() * reach;
pos[2] += angle.sin() * reach;
send(
play::Serverbound::MovePlayerPos(play::MovePlayerPos {
x: pos[0],
y: pos[1],
z: pos[2],
flags: 0,
})
.encode(),
);
}
let angle = random.unit() * std::f64::consts::TAU;
pos[0] += angle.cos() * 128.0;
pos[2] += angle.sin() * 128.0;
} else if behaviour == Behaviour::FloodChunks {
// Up to cruising height first, then just under the server's 10 blocks a tick:
// every tick asks for new chunks.
if pos[1] < cruise {
pos[1] = (pos[1] + 0.5).min(cruise);
} else {
pos[0] += heading.cos() * 9.9;
pos[2] += heading.sin() * 9.9;
}
} else if !lock(&bot.held).contains(&here) {
bot.waited.fetch_add(1, Ordering::Relaxed);
} else if pos[1] < cruise {
pos[1] = (pos[1] + 0.5).min(cruise);
} else {
match behaviour {
Behaviour::Idle => {}
Behaviour::Idle | Behaviour::Stand => {}
Behaviour::Walk => {
let (dx, dz) = (target[0] - pos[0], target[2] - pos[2]);
let distance = dx.hypot(dz);
@@ -529,7 +809,37 @@ fn run_bot(
pos[2] += dz / distance * step;
}
}
Behaviour::Explore => {
Behaviour::FloodMove
| Behaviour::FloodChunks
| Behaviour::FloodUse
| Behaviour::FloodClick
| Behaviour::FloodSuggest
| Behaviour::FloodBook
| Behaviour::FloodSign
| Behaviour::FloodRecipe => {}
Behaviour::FloodPackets => {
for i in 0..o.flood {
send(
play::Serverbound::MovePlayerRot(play::MovePlayerRot {
yaw: (i % 360) as f32,
pitch: 0.0,
flags: 0,
})
.encode(),
);
}
}
Behaviour::FloodAcks | Behaviour::Explore => {
if behaviour == Behaviour::FloodAcks {
for _ in 0..o.flood {
send(
play::Serverbound::ChunkBatchReceived(play::ChunkBatchReceived {
chunks_per_tick: 64.0,
})
.encode(),
);
}
}
// Flying in creative, by default about 22 blocks a second, which keeps new
// chunks coming as fast as the server sends them.
let step = o.explore_speed / 20.0;
@@ -538,12 +848,28 @@ fn run_bot(
}
}
}
// The costly floods go out wherever the bot is, chunks or not (--no-read gets none).
for i in 0..o.flood {
sequence += 1;
if let Some(body) = costly(behaviour, i, sequence, pos, &mut random) {
send(body);
}
}
let target = lock(&bot.target).1;
if let Some(id) = target
&& joined.elapsed().as_secs_f64() >= o.attack_after
&& tick % 10 == 0
{
send(play::Serverbound::Attack(play::Attack { id }).encode());
bot.attacks.fetch_add(1, Ordering::Relaxed);
}
tick += 1;
let ok = send(
play::Serverbound::MovePlayerPos(play::MovePlayerPos {
x: pos[0],
y: pos[1],
z: pos[2],
flags: 0,
flags: u8::from(behaviour == Behaviour::Stand || o.ground),
})
.encode(),
) && send(raw(sb::CLIENT_TICK_END, |_| {}));
@@ -551,7 +877,7 @@ fn run_bot(
bot.online.store(false, Ordering::SeqCst);
break;
}
next += Duration::from_millis(50);
next += Duration::from_secs_f64(0.05 / o.timer);
let now = Instant::now();
if next > now {
thread::sleep(next - now);
@@ -567,8 +893,19 @@ fn run_bot(
fn behaviour_of(mode: &str, index: usize) -> Behaviour {
match mode {
"idle" => Behaviour::Idle,
"stand" => Behaviour::Stand,
"walk" => Behaviour::Walk,
"explore" => Behaviour::Explore,
"flood-move" => Behaviour::FloodMove,
"flood-acks" => Behaviour::FloodAcks,
"flood-packets" => Behaviour::FloodPackets,
"flood-chunks" => Behaviour::FloodChunks,
"flood-use" => Behaviour::FloodUse,
"flood-click" => Behaviour::FloodClick,
"flood-suggest" => Behaviour::FloodSuggest,
"flood-book" => Behaviour::FloodBook,
"flood-sign" => Behaviour::FloodSign,
"flood-recipe" => Behaviour::FloodRecipe,
// mixed: of every five bots, three walk, one explores, one stands.
_ => match index % 5 {
0 | 2 | 4 => Behaviour::Walk,
@@ -670,6 +1007,8 @@ fn main() {
"pulled_back": b.pulled_back.load(Ordering::Relaxed),
"waited_ticks": b.waited.load(Ordering::Relaxed),
"error": *lock(&b.error),
"attacks": b.attacks.load(Ordering::Relaxed),
"health": *lock(&b.health),
})).collect::<Vec<_>>(),
});
if o.report.as_deref() == Some("-") {
@@ -494,7 +494,11 @@ pub fn canonical(tag: &Tag) -> Option<Tag> {
_ => string("minecraft:air"),
},
);
f.put("facing", Tag::Int(int(tag.get("facing")).unwrap_or(0)));
// Direction.LEGACY_ID_CODEC: the 3D data value as a byte.
f.put(
"facing",
Tag::Byte(int(tag.get("facing")).unwrap_or(0) as i8),
);
f.float("progress", 0.0);
f.flag("extending", false);
f.flag("source", false);
@@ -873,6 +877,23 @@ mod tests {
);
}
/// `PistonMovingBlockEntity.saveAdditional`: `facing` through
/// `Direction.LEGACY_ID_CODEC` (a byte), `progress` a float.
#[test]
fn moving_piston_fields_match_vanilla() {
let out = canon(
"{id:\"minecraft:piston\",x:1,y:2,z:3,blockState:{Name:\"minecraft:stone\"},\
facing:5,progress:0.5f,extending:1b,source:0b}",
);
assert_eq!(out.get("facing"), Some(&Tag::Byte(5)));
assert_eq!(out.get("progress"), Some(&f32_tag(0.5)));
assert_eq!(out.get("extending"), Some(&Tag::Byte(1)));
assert_eq!(out.get("source"), Some(&Tag::Byte(0)));
let fresh = canon("{id:\"minecraft:piston\",x:1,y:2,z:3}");
assert_eq!(fresh.get("facing"), Some(&Tag::Byte(0)));
assert_eq!(fresh.get("blockState"), Some(&string("minecraft:air")));
}
#[test]
fn loot_table_replaces_items_and_zero_seed_is_omitted() {
let out = canon(
+252 -54
View File
@@ -1,6 +1,6 @@
//! Text components from commands: `ComponentArgument` reads an SNBT (or JSON)
//! value, `ComponentSerialization` turns it into styled text.
use crate::text::{Style, Text, named_color};
use crate::text::{InlineObject, Style, Text, named_color};
use horizon_world::nbt::Tag;
use serde_json::{Map, Value};
@@ -11,13 +11,18 @@ pub fn parse_snbt(input: &str) -> Result<(Value, usize), String> {
let mut p = Parser {
s: input.as_bytes(),
i: 0,
depth: 0,
};
let v = p.value()?;
Ok((v, p.i))
}
/// Nesting deeper than NBT's own limit (`NbtAccounter`'s 512) is refused
/// rather than overflowing the stack.
const SNBT_MAX_DEPTH: usize = 512;
struct Parser<'a> {
s: &'a [u8],
i: usize,
depth: usize,
}
impl Parser<'_> {
fn ws(&mut self) {
@@ -41,57 +46,22 @@ impl Parser<'_> {
self.ws();
match self.peek() {
Some(b'{') => {
self.i += 1;
let mut map = Map::new();
self.ws();
if self.peek() == Some(b'}') {
self.i += 1;
return Ok(Value::Object(map));
}
loop {
self.ws();
let key = self.string()?;
self.expect(b':')?;
map.insert(key, self.value()?);
self.ws();
match self.peek() {
Some(b',') => self.i += 1,
Some(b'}') => {
self.i += 1;
return Ok(Value::Object(map));
}
_ => return Err("expected ',' or '}'".into()),
}
if self.depth >= SNBT_MAX_DEPTH {
return Err(format!("tag nesting too deep at {}", self.i));
}
self.depth += 1;
let v = self.object();
self.depth -= 1;
v
}
Some(b'[') => {
self.i += 1;
let mut list = Vec::new();
self.ws();
// Typed arrays [B;...], [I;...], [L;...].
if matches!(
self.s.get(self.i..self.i + 2),
Some([b'B' | b'I' | b'L', b';'])
) {
self.i += 2;
}
self.ws();
if self.peek() == Some(b']') {
self.i += 1;
return Ok(Value::Array(list));
}
loop {
list.push(self.value()?);
self.ws();
match self.peek() {
Some(b',') => self.i += 1,
Some(b']') => {
self.i += 1;
return Ok(Value::Array(list));
}
_ => return Err("expected ',' or ']'".into()),
}
if self.depth >= SNBT_MAX_DEPTH {
return Err(format!("tag nesting too deep at {}", self.i));
}
self.depth += 1;
let v = self.array();
self.depth -= 1;
v
}
Some(b'"' | b'\'') => Ok(Value::String(self.string()?)),
Some(_) => {
@@ -105,10 +75,20 @@ impl Parser<'_> {
_ => {
let digits = word
.trim_end_matches(['b', 'B', 's', 'S', 'l', 'L', 'f', 'F', 'd', 'D']);
match (digits.parse::<i64>(), digits.parse::<f64>()) {
(Ok(n), _) if digits.len() + 1 >= word.len() => Value::from(n),
(_, Ok(f)) if digits.len() + 1 >= word.len() => Value::from(f),
_ => Value::String(word),
// Only digits (and an exponent) make a number: `nan`
// or `Infinity`, which Rust's parse would take, stays
// a string.
if digits
.bytes()
.any(|c| c.is_ascii_alphabetic() && c != b'e' && c != b'E')
{
Value::String(word)
} else {
match (digits.parse::<i64>(), digits.parse::<f64>()) {
(Ok(n), _) if digits.len() + 1 >= word.len() => Value::from(n),
(_, Ok(f)) if digits.len() + 1 >= word.len() => Value::from(f),
_ => Value::String(word),
}
}
}
})
@@ -116,6 +96,59 @@ impl Parser<'_> {
None => Err("expected a value".into()),
}
}
fn object(&mut self) -> Result<Value, String> {
self.i += 1;
let mut map = Map::new();
self.ws();
if self.peek() == Some(b'}') {
self.i += 1;
return Ok(Value::Object(map));
}
loop {
self.ws();
let key = self.string()?;
self.expect(b':')?;
map.insert(key, self.value()?);
self.ws();
match self.peek() {
Some(b',') => self.i += 1,
Some(b'}') => {
self.i += 1;
return Ok(Value::Object(map));
}
_ => return Err("expected ',' or '}'".into()),
}
}
}
fn array(&mut self) -> Result<Value, String> {
self.i += 1;
let mut list = Vec::new();
self.ws();
// Typed arrays [B;...], [I;...], [L;...].
if matches!(
self.s.get(self.i..self.i + 2),
Some([b'B' | b'I' | b'L', b';'])
) {
self.i += 2;
}
self.ws();
if self.peek() == Some(b']') {
self.i += 1;
return Ok(Value::Array(list));
}
loop {
list.push(self.value()?);
self.ws();
match self.peek() {
Some(b',') => self.i += 1,
Some(b']') => {
self.i += 1;
return Ok(Value::Array(list));
}
_ => return Err("expected ',' or ']'".into()),
}
}
}
fn unquoted(&mut self) -> String {
let start = self.i;
while let Some(c) = self.peek() {
@@ -162,6 +195,7 @@ fn object_style(o: &serde_json::Map<String, Value>) -> Style {
.and_then(|h| u32::from_str_radix(h, 16).ok())
});
}
style.shadow_color = o.get("shadow_color").and_then(shadow_color);
let flag = |k: &str| o.get(k).and_then(Value::as_bool);
style.bold = flag("bold");
style.italic = flag("italic");
@@ -203,7 +237,7 @@ fn object_style(o: &serde_json::Map<String, Value>) -> Style {
_ => None,
};
}
// HoverEvent.ShowText and ShowItem.
// HoverEvent.ShowText, ShowItem and ShowEntity.
if let Some(e) = o.get("hover_event").and_then(Value::as_object) {
match e.get("action").and_then(Value::as_str) {
Some("show_text") => {
@@ -214,6 +248,9 @@ fn object_style(o: &serde_json::Map<String, Value>) -> Style {
Some("show_item") if e.get("id").is_some_and(Value::is_string) => {
style.hover_item = Some(Box::new(Value::Object(e.clone())));
}
Some("show_entity") if e.get("id").is_some_and(Value::is_string) => {
style.hover_entity = Some(Box::new(Value::Object(e.clone())));
}
_ => {}
}
}
@@ -223,6 +260,28 @@ fn object_style(o: &serde_json::Map<String, Value>) -> Style {
.map(str::to_owned);
style
}
/// `Style.shadow_color`: a packed ARGB int or normalized RGBA array.
pub(crate) fn shadow_color(value: &Value) -> Option<u32> {
if let Some(packed) = value.as_i64() {
return i32::try_from(packed).ok().map(|n| n as u32);
}
let rgba = value.as_array()?;
if rgba.len() != 4 {
return None;
}
let channels = rgba
.iter()
.map(|channel| {
// ExtraCodecs.ARGB_COLOR_CODEC reads floats; ARGB.as8BitChannel
// floors each normalized channel multiplied by 255.
let value = channel.as_f64()? as f32;
(value.is_finite() && (0.0..=1.0).contains(&value))
.then_some((value * 255.0_f32).floor() as u32)
})
.collect::<Option<Vec<_>>>()?;
Some(channels[3] << 24 | channels[0] << 16 | channels[1] << 8 | channels[2])
}
/// `ComponentSerialization.CODEC` decoding into text. `selector` components
/// are resolved by `resolve` (ComponentUtils.updateForEntity).
pub fn to_text(v: &Value, resolve: &mut dyn FnMut(&str) -> Text) -> Text {
@@ -263,6 +322,10 @@ pub fn to_text_with(
let style = object_style(o);
let content = if let Some(t) = contents(o) {
t
} else if let Some(object) = inline_object(o, resolve, contents) {
let mut text = Text::literal("\u{fffc}");
text.spans[0].style.inline_object = Some(object);
text
} else if let Some(t) = o.get("text") {
match t {
Value::String(s) => Text::literal(s.clone()),
@@ -299,12 +362,74 @@ pub fn to_text_with(
{
out.push(to_text_with(extra, resolve, contents).with_style(&style));
}
out
if o.contains_key("object") || o.contains_key("player") || o.contains_key("sprite") {
out.with_component(Value::Object(o.clone()))
} else {
out
}
}
Value::Null => Text::empty(),
}
}
fn inline_object(
object: &serde_json::Map<String, Value>,
resolve: &mut dyn FnMut(&str) -> Text,
contents: &mut dyn FnMut(&serde_json::Map<String, Value>) -> Option<Text>,
) -> Option<InlineObject> {
let kind = object.get("object").and_then(Value::as_str);
let fallback = object
.get("fallback")
.map(|value| to_text_with(value, resolve, contents).plain());
match kind {
Some("player") | None if object.contains_key("player") => {
let profile = object.get("player")?.clone();
// PlayerSprite.defaultFallback.
let fallback = fallback.unwrap_or_else(|| {
match profile
.as_str()
.or_else(|| profile.get("name").and_then(Value::as_str))
{
Some(name) => format!("[{name} head]"),
None => "[unknown player head]".to_owned(),
}
});
Some(InlineObject::Player {
profile,
hat: object.get("hat").and_then(Value::as_bool).unwrap_or(true),
fallback,
})
}
Some("atlas") | None if object.contains_key("sprite") => {
let atlas = object
.get("atlas")
.and_then(Value::as_str)
.unwrap_or("minecraft:blocks")
.to_owned();
let sprite = object.get("sprite")?.as_str()?.to_owned();
// AtlasSprite.defaultFallback: short names, the atlas unless blocks.
let fallback = fallback.unwrap_or_else(|| {
let short = |id: &str| match id.split_once(':') {
Some(("minecraft", path)) => path.to_owned(),
Some(_) => id.to_owned(),
None => id.to_owned(),
};
if short(&atlas) == "blocks" {
format!("[{}]", short(&sprite))
} else {
format!("[{}@{}]", short(&sprite), short(&atlas))
}
});
Some(InlineObject::Atlas {
atlas,
sprite,
fallback,
})
}
_ => None,
}
}
/// `CustomName` as text: a string keeps its legacy formatting codes, anything
/// else is a component (`ComponentSerialization`).
pub fn name_text(tag: &Tag) -> Text {
@@ -318,6 +443,61 @@ pub fn name_text(tag: &Tag) -> Text {
mod tests {
use super::*;
#[test]
fn shadow_color_accepts_packed_and_rgba_styles() {
let packed = serde_json::json!({"text": "A", "shadow_color": -16711936});
let text = to_text(&packed, &mut |_| Text::empty());
assert_eq!(text.spans[0].style.shadow_color, Some(0xff00ff00));
assert_eq!(text.component()["shadow_color"], -16711936);
let rgba = serde_json::json!({"text": "A", "shadow_color": [1, 0, 0.5, 1]});
let text = to_text(&rgba, &mut |_| Text::empty());
assert_eq!(text.spans[0].style.shadow_color, Some(0xffff007f));
let translucent = serde_json::json!({"text": "A", "shadow_color": [1, 0, 0.5, 0.5]});
let text = to_text(&translucent, &mut |_| Text::empty());
assert_eq!(text.spans[0].style.shadow_color, Some(0x7fff007f));
let no_shadow = serde_json::json!({"text": "A", "shadow_color": 0});
let text = to_text(&no_shadow, &mut |_| Text::empty());
assert_eq!(text.spans[0].style.shadow_color, Some(0));
}
#[test]
fn player_object_survives_text_parsing_and_keeps_its_siblings() {
let component = serde_json::json!({
"text": "<",
"extra": [
{"object": "player", "player": {"name": "elyapluggin"}, "hat": false,
"fallback": "[head]"},
{"text": " >", "color": "gold"}
]
});
let text = to_text(&component, &mut |_| Text::empty());
assert_eq!(text.plain(), "<[head] >");
assert!(matches!(
&text.spans[1].style.inline_object,
Some(InlineObject::Player { hat: false, .. })
));
assert!(text.spans[2].style.inline_object.is_none());
assert_eq!(text.component(), component);
let sprite = serde_json::json!({"object": "atlas", "sprite": "minecraft:item/porkchop"});
let text = to_text(&sprite, &mut |_| Text::empty());
assert!(matches!(
&text.spans[0].style.inline_object,
Some(InlineObject::Atlas { sprite, .. }) if sprite == "minecraft:item/porkchop"
));
// ObjectContents.visit without a fallback: the sprite's defaultFallback.
assert_eq!(text.plain(), "[item/porkchop]");
let head = serde_json::json!({"object": "player", "player": {"name": "Steve"}});
assert_eq!(
to_text(&head, &mut |_| Text::empty()).plain(),
"[Steve head]"
);
let gui = serde_json::json!({"atlas": "gui", "sprite": "hud/heart/full"});
assert_eq!(
to_text(&gui, &mut |_| Text::empty()).plain(),
"[hud/heart/full@gui]"
);
}
#[test]
fn snbt_and_json_components() {
let input = "{text:'Hi ',color:red,extra:[{text:\"there\",bold:1b}]} rest";
@@ -351,4 +531,22 @@ mod tests {
let t = to_text(&v, &mut |_| Text::empty());
assert_eq!(t.spans[0].style.font, None);
}
#[test]
fn snbt_nesting_is_bounded() {
let nest = |n: usize| format!("{}1{}", "[".repeat(n), "]".repeat(n));
assert!(parse_snbt(&nest(512)).is_ok());
assert!(parse_snbt(&nest(513)).is_err());
assert!(parse_snbt(&nest(100_000)).is_err());
}
#[test]
fn bare_words_that_look_numeric_stay_strings() {
let (v, _) = parse_snbt("{text:nan}").unwrap();
assert_eq!(v["text"], Value::String("nan".into()));
let (v, _) = parse_snbt("{text:Infinity}").unwrap();
assert_eq!(v["text"], Value::String("Infinity".into()));
let (v, _) = parse_snbt("{n:1.5}").unwrap();
assert_eq!(v["n"], Value::from(1.5));
}
}
+87 -18
View File
@@ -1,32 +1,68 @@
//! Key display names (`InputConstants.Key.getDisplayName`), shared by key
//! mappings and `keybind` text components.
use crate::text::Text;
use std::{collections::BTreeMap, sync::RwLock};
/// InputConstants.Key.getDisplayName: one-character keys as the upper-case
/// character, others by translation.
/// The keys that write a symbol, with what they write on a US layout.
const SYMBOLS: [(&str, &str); 11] = [
("key.keyboard.minus", "-"),
("key.keyboard.equal", "="),
("key.keyboard.left.bracket", "["),
("key.keyboard.right.bracket", "]"),
("key.keyboard.backslash", "\\"),
("key.keyboard.semicolon", ";"),
("key.keyboard.apostrophe", "'"),
("key.keyboard.grave.accent", "`"),
("key.keyboard.comma", ","),
("key.keyboard.period", "."),
("key.keyboard.slash", "/"),
];
/// What the player's keyboard layout writes on each key that writes
/// something, by key name, as the window reported it (`learn_layout`).
static LAYOUT: RwLock<BTreeMap<String, String>> = RwLock::new(BTreeMap::new());
/// The keyboard layout writes `text` on the key `key` (`key.keyboard.q`):
/// its name from now on, as `SDL_GetKeyFromScancode` names it with SDL's
/// default keycode options. Only letter and symbol keys take it; a letter
/// key keeps its Latin letter on a non-Latin layout (`latin_letters`), and
/// the number row keeps its digits (`french_numbers`).
pub fn learn_layout(key: &str, text: &str) {
let Some(rest) = key.strip_prefix("key.keyboard.") else {
return;
};
let mut chars = text.chars();
let (Some(c), None) = (chars.next(), chars.next()) else {
return;
};
let letter_key = rest.len() == 1 && rest.as_bytes()[0].is_ascii_lowercase();
let latin = c.is_ascii() || ('\u{C0}'..='\u{24F}').contains(&c);
if c.is_control()
|| c.is_whitespace()
|| !(letter_key || SYMBOLS.iter().any(|(k, _)| *k == key))
|| (letter_key && c.is_alphabetic() && !latin)
{
return;
}
if let Ok(mut layout) = LAYOUT.write() {
layout.insert(key.to_owned(), c.to_uppercase().collect());
}
}
/// InputConstants.Key.getDisplayName: what the layout writes on the key,
/// upper-case, else the US layout's character, else by translation.
pub fn display_name(key: &str) -> Text {
if let Some(label) = LAYOUT.read().ok().and_then(|l| l.get(key).cloned()) {
return Text::literal(label);
}
if let Some(rest) = key.strip_prefix("key.keyboard.")
&& rest.chars().count() == 1
{
let c = rest.to_uppercase();
return Text::literal(c);
}
let symbol = match key {
"key.keyboard.minus" => Some("-"),
"key.keyboard.equal" => Some("="),
"key.keyboard.left.bracket" => Some("["),
"key.keyboard.right.bracket" => Some("]"),
"key.keyboard.backslash" => Some("\\"),
"key.keyboard.semicolon" => Some(";"),
"key.keyboard.apostrophe" => Some("'"),
"key.keyboard.grave.accent" => Some("`"),
"key.keyboard.comma" => Some(","),
"key.keyboard.period" => Some("."),
"key.keyboard.slash" => Some("/"),
_ => None,
};
if let Some(s) = symbol {
return Text::literal(s);
if let Some((_, s)) = SYMBOLS.iter().find(|(k, _)| *k == key) {
return Text::literal(*s);
}
if let Some(n) = key.strip_prefix("key.mouse.")
&& n.parse::<u32>().is_ok()
@@ -36,3 +72,36 @@ pub fn display_name(key: &str) -> Text {
}
Text::translate(key, &[])
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn keys_show_what_the_layout_writes() {
// The layout table is shared: these keys are this test's alone.
assert_eq!(display_name("key.keyboard.q").plain(), "Q");
// AZERTY: A on the Q key, M on the semicolon key, a dead ^ on the
// left bracket, a comma on the M key.
learn_layout("key.keyboard.q", "a");
learn_layout("key.keyboard.semicolon", "m");
learn_layout("key.keyboard.left.bracket", "^");
learn_layout("key.keyboard.m", ",");
assert_eq!(display_name("key.keyboard.q").plain(), "A");
assert_eq!(display_name("key.keyboard.semicolon").plain(), "M");
assert_eq!(display_name("key.keyboard.left.bracket").plain(), "^");
assert_eq!(display_name("key.keyboard.m").plain(), ",");
// The number row keeps its digits; a Cyrillic letter, nothing
// printable and keys that write nothing are left alone.
learn_layout("key.keyboard.1", "&");
learn_layout("key.keyboard.f", "а");
learn_layout("key.keyboard.g", "\u{8}");
learn_layout("key.keyboard.tab", "\t");
learn_layout("key.keyboard.h", "ab");
assert_eq!(display_name("key.keyboard.1").plain(), "1");
assert_eq!(display_name("key.keyboard.f").plain(), "F");
assert_eq!(display_name("key.keyboard.g").plain(), "G");
assert_eq!(display_name("key.keyboard.h").plain(), "H");
assert_eq!(display_name("key.keyboard.slash").plain(), "/");
}
}
+51 -1
View File
@@ -40,6 +40,10 @@ static BIDIRECTIONAL: AtomicBool = AtomicBool::new(false);
/// A source of resource files: the prepared root plus packs, highest priority last.
pub trait ResourceSource {
fn read(&self, path: &str) -> Option<Vec<u8>>;
/// Whether `path` is there, without reading it where the source can tell.
fn exists(&self, path: &str) -> bool {
self.read(path).is_some()
}
/// The namespaces under `assets/` (resource packs may add their own).
fn namespaces(&self) -> Vec<String> {
vec!["minecraft".into()]
@@ -56,6 +60,12 @@ impl ResourceSource for Bundled {
"assets/horizon/lang/ru_ru.json" => {
include_str!("../../horizon-client/assets/horizon/lang/ru_ru.json")
}
"assets/horizon/lang/pl_pl.json" => {
include_str!("../../horizon-client/assets/horizon/lang/pl_pl.json")
}
"assets/horizon/lang/uk_ua.json" => {
include_str!("../../horizon-client/assets/horizon/lang/uk_ua.json")
}
_ => return None,
};
Some(body.as_bytes().to_vec())
@@ -68,6 +78,9 @@ impl ResourceSource for std::path::PathBuf {
fn read(&self, path: &str) -> Option<Vec<u8>> {
std::fs::read(self.join(path)).ok()
}
fn exists(&self, path: &str) -> bool {
self.join(path).is_file()
}
fn namespaces(&self) -> Vec<String> {
std::fs::read_dir(self.join("assets"))
.into_iter()
@@ -190,7 +203,7 @@ pub fn available(sources: &[&dyn ResourceSource]) -> Vec<LanguageInfo> {
for (code, info) in value["language"].as_object().into_iter().flatten() {
// Only languages whose file exists somewhere are selectable.
let file = format!("assets/minecraft/lang/{code}.json");
if code != DEFAULT && !sources.iter().any(|s| s.read(&file).is_some()) {
if code != DEFAULT && !sources.iter().any(|s| s.exists(&file)) {
continue;
}
found.insert(
@@ -259,6 +272,21 @@ mod tests {
assert_eq!(en.get("horizon.options.controller"), Some("Controller"));
let ru = Language::load(&[&empty], "ru_ru");
assert_eq!(ru.get("horizon.options.controller"), Some("Геймпад"));
// Every bundled string carries both languages: a key added to one file
// and forgotten in the other shows as a key name on screen.
for key in en.map().keys() {
assert!(ru.get(key).is_some(), "{key} has no ru_ru value");
}
assert_eq!(en.get("horizon.pages.mods"), Some("Mods"));
assert_eq!(ru.get("horizon.pages.mods"), Some("Моды"));
let pl = Language::load(&[&empty], "pl_pl");
assert_eq!(pl.get("horizon.options.controller"), Some("Kontroler"));
assert_eq!(pl.get("horizon.options.vibration"), Some("Wibracje"));
let uk = Language::load(&[&empty], "uk_ua");
assert_eq!(
uk.get("horizon.options.on_screen_keyboard"),
Some("Екранна клавіатура")
);
}
#[test]
@@ -284,4 +312,26 @@ mod tests {
);
assert_eq!(langs[1].label(), "Français (France)");
}
#[test]
fn languages_are_listed_without_reading_their_files() {
struct NoLangReads;
impl ResourceSource for NoLangReads {
fn read(&self, path: &str) -> Option<Vec<u8>> {
assert!(!path.contains("/lang/"), "read {path}");
(path == "assets/pack.mcmeta").then(|| {
br#"{"language":{"fr_fr":{"name":"French"},"de_de":{"name":"German"}}}"#
.to_vec()
})
}
fn exists(&self, path: &str) -> bool {
path == "assets/minecraft/lang/fr_fr.json"
}
}
let langs = available(&[&NoLangReads]);
assert_eq!(
langs.iter().map(|l| l.code.as_str()).collect::<Vec<_>>(),
["en_us", "fr_fr"]
);
}
}
@@ -55,6 +55,12 @@ particle_types! {
Rain = "rain",
Bubble = "bubble",
SulfurBubbles = "sulfur_bubbles",
NoxiousGas = "noxious_gas",
NoxiousGasCloud = "noxious_gas_cloud",
Geyser = "geyser",
GeyserBase = "geyser_base",
GeyserPoof = "geyser_poof",
GeyserPlume = "geyser_plume",
BubblePop = "bubble_pop",
BubbleColumnUp = "bubble_column_up",
CurrentDown = "current_down",
@@ -149,6 +155,10 @@ impl ParticleType {
matches!(
self,
Self::BlockMarker
| Self::Geyser
| Self::GeyserBase
| Self::GeyserPoof
| Self::GeyserPlume
| Self::DamageIndicator
| Self::ExplosionEmitter
| Self::Explosion
@@ -191,6 +201,8 @@ impl ParticleType {
// SpellParticle.
| Self::Effect | Self::InstantEffect | Self::EntityEffect
| Self::Witch | Self::Infested | Self::TrialOmen | Self::RaidOmen
// NoxiousGasParticle.
| Self::NoxiousGas
// EmissiveRisingParticle.
| Self::Soul | Self::SculkSoul
// SculkChargeParticle, SculkChargePopParticle, ShriekParticle,
+2 -1
View File
@@ -91,6 +91,7 @@ pub fn effect(id: &str) -> Option<&'static EffectDef> {
/// Namespaced effect ids for mob effect storage.
pub fn namespaced(id: &'static str) -> &'static str {
static IDS: OnceLock<HashMap<&'static str, &'static str>> = OnceLock::new();
let key = id.strip_prefix("minecraft:").unwrap_or(id);
IDS.get_or_init(|| {
EFFECTS
.iter()
@@ -102,7 +103,7 @@ pub fn namespaced(id: &'static str) -> &'static str {
})
.collect()
})
.get(id)
.get(key)
.copied()
.unwrap_or(id)
}
@@ -28,6 +28,10 @@ pub struct Registries {
/// Game rule name -> integer-valued.
pub game_rules: Vec<(String, bool)>,
pub players: Vec<String>,
/// `ClientSuggestionProvider.customCompletionSuggestions`: the extra chat
/// completions the server sent (ClientboundCustomChatCompletionsPacket),
/// each once.
pub custom_completions: std::collections::BTreeSet<String>,
/// Scoreboard objective names, team names and tracked score holders.
pub objectives: Vec<String>,
pub teams: Vec<String>,
@@ -54,6 +58,12 @@ pub struct Registries {
pub function_tags: Vec<String>,
/// The block the source is looking at (relevant coordinates).
pub target_block: Option<[i32; 3]>,
/// `ClientSuggestionProvider.getSelectedEntities`: the UUID of the
/// entity under the crosshair (`Entity.getStringUUID`), if any.
pub selected_entities: Vec<String>,
/// `ClientSuggestionProvider.getAbsoluteCoordinates`: the exact point of
/// that block hit, each coordinate as `%.2f` (`Locale.ROOT`).
pub target_point: Option<[String; 3]>,
/// Every registry's element and tag ids (`minecraft:item` -> ...), as
/// the server's registry access and reloadable registries hold them.
pub by_registry: BTreeMap<String, RegistryIds>,
@@ -81,7 +91,55 @@ pub struct RegistryIds {
pub tags: Vec<String>,
}
const EMPTY: &[String] = &[];
/// ClientboundCustomChatCompletionsPacket.Action.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CustomCompletionAction {
Add,
Remove,
Set,
}
impl CustomCompletionAction {
/// `Action.BY_ID`: `ByIdMap.continuous` with `OutOfBoundsStrategy.ZERO`,
/// so an unknown id is `ADD`.
pub fn from_id(id: i32) -> Self {
match id {
1 => Self::Remove,
2 => Self::Set,
_ => Self::Add,
}
}
}
/// `ClientSuggestionProvider.modifyCustomCompletions`: the server adds,
/// removes or replaces the extra chat completions.
pub fn modify_custom_completions(
set: &mut std::collections::BTreeSet<String>,
action: CustomCompletionAction,
entries: &[String],
) {
match action {
CustomCompletionAction::Add => set.extend(entries.iter().cloned()),
CustomCompletionAction::Remove => {
for e in entries {
set.remove(e);
}
}
CustomCompletionAction::Set => {
set.clear();
set.extend(entries.iter().cloned());
}
}
}
impl Registries {
/// `ClientSuggestionProvider.getCustomTabSuggestions`: what chat (not a
/// command) completes, the online players' names with the server's custom
/// completions, each once.
pub fn custom_tab_suggestions(&self) -> std::collections::BTreeSet<&str> {
self.players
.iter()
.chain(&self.custom_completions)
.map(String::as_str)
.collect()
}
/// Fill what the game did not build itself from vanilla's exported
/// registries (`command-registries.json`, written by
/// `VanillaCommandParseOracle registries`): built-in registries such as
+63 -4
View File
@@ -112,6 +112,7 @@ pub fn style_from_json(v: &Value) -> Style {
.and_then(|h| u32::from_str_radix(h, 16).ok())
});
}
s.shadow_color = o.get("shadow_color").and_then(component::shadow_color);
let flag = |k: &str| o.get(k).and_then(Value::as_bool);
s.bold = flag("bold");
s.italic = flag("italic");
@@ -662,10 +663,22 @@ pub fn name_shown(own: Option<&Team>, viewer: Option<&Team>, v: NameView) -> boo
}
!v.hud_hidden && !v.is_camera && visible && !v.is_vehicle
}
/// `ServerPlayer.die`: whether a player's death message reaches the player
/// themselves (the team broadcasts leave out the dying player).
pub fn own_death_message_shown(team: Option<&Team>) -> bool {
team.is_none_or(|t| t.death_message_visibility == Visibility::Always)
/// `ServerPlayer.die`: whether `other` hears the death message of `dying`
/// (`broadcastSystemMessage`, `broadcastSystemToTeam` or
/// `broadcastSystemToAllExceptTeam`, by the team's death message visibility).
pub fn death_message_reaches(board: &Scoreboard, dying: &str, other: &str) -> bool {
let Some(team) = board.players_team(dying) else {
return true;
};
let same_team = board
.players_team(other)
.is_some_and(|t| t.name == team.name);
match team.death_message_visibility {
Visibility::Always => true,
Visibility::Never => false,
Visibility::HideForOtherTeams => same_team && other != dying,
Visibility::HideForOwnTeam => !same_team,
}
}
// ---- objectives and scores ---------------------------------------------
@@ -1551,3 +1564,49 @@ mod tests {
);
}
}
/// `DamageSource.getLocalizedDeathMessage`: the cause's item with a custom
/// name gets the `<id>.item` message, with the item's name as a third
/// argument.
pub fn death_message_args(
key: &str,
victim: &Text,
cause: &Text,
item: Option<&Text>,
) -> (String, Vec<Text>) {
match item {
Some(item) => (
format!("{key}.item"),
vec![victim.clone(), cause.clone(), item.clone()],
),
None => (key.to_owned(), vec![victim.clone(), cause.clone()]),
}
}
#[cfg(test)]
mod death_message_tests {
use super::*;
#[test]
fn the_death_message_names_the_causes_item_when_it_has_a_custom_name() {
let victim = Text::literal("Steve");
let cause = Text::literal("Zombie");
// DamageSource.getLocalizedDeathMessage: the item variant, with the
// item's name as the third argument.
let (key, args) = death_message_args(
"death.attack.arrow",
&victim,
&cause,
Some(&Text::literal("My Sword")),
);
assert_eq!(key, "death.attack.arrow.item");
assert_eq!(args.len(), 3);
assert_eq!(args[0].plain(), "Steve");
assert_eq!(args[1].plain(), "Zombie");
assert_eq!(args[2].plain(), "My Sword");
// Without a custom name the message keeps its two arguments.
let (key, args) = death_message_args("death.attack.arrow", &victim, &cause, None);
assert_eq!(key, "death.attack.arrow");
assert_eq!(args.len(), 2);
}
}
+79 -30
View File
@@ -58,10 +58,34 @@ pub enum ClickEvent {
OpenFile(String),
}
/// A non-character `minecraft:object` embedded in a text component.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum InlineObject {
Player {
profile: Value,
hat: bool,
fallback: String,
},
Atlas {
atlas: String,
sprite: String,
fallback: String,
},
}
impl InlineObject {
pub fn fallback(&self) -> &str {
match self {
Self::Player { fallback, .. } | Self::Atlas { fallback, .. } => fallback,
}
}
}
/// Text style; `None` fields inherit from the enclosing style.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Style {
pub color: Option<u32>,
/// Text component `shadow_color` as packed ARGB. Zero suppresses shadows.
pub shadow_color: Option<u32>,
pub bold: Option<bool>,
pub italic: Option<bool>,
pub underlined: Option<bool>,
@@ -72,11 +96,15 @@ pub struct Style {
pub hover: Option<Box<Text>>,
/// `HoverEvent.ShowItem`: the event's `{id, count?, components?}` object.
pub hover_item: Option<Box<Value>>,
/// `HoverEvent.ShowEntity`: the event's `{id, uuid, name?}` object.
pub hover_entity: Option<Box<Value>>,
/// `Style.insertion`: the text a Shift-click puts in the chat box.
pub insertion: Option<String>,
/// The font (`FontDescription.Resource`), a full id like
/// `minecraft:alt`; the default font when unset.
pub font: Option<String>,
/// Content metadata for a single U+FFFC object span; never inherited.
pub inline_object: Option<InlineObject>,
}
/// A style's `font` field: an identifier (`alt` is `minecraft:alt`).
pub fn font_id(v: &Value) -> Option<String> {
@@ -92,6 +120,7 @@ impl Style {
/// Fill unset fields from `base` (Style.applyTo).
pub fn inherit(&mut self, base: &Style) {
self.color = self.color.or(base.color);
self.shadow_color = self.shadow_color.or(base.shadow_color);
self.bold = self.bold.or(base.bold);
self.italic = self.italic.or(base.italic);
self.underlined = self.underlined.or(base.underlined);
@@ -100,11 +129,12 @@ impl Style {
if self.click.is_none() {
self.click.clone_from(&base.click);
}
if self.hover.is_none() {
// One HoverEvent: its three kinds are inherited together, so a style
// with its own event never picks up another kind from `base`.
if self.hover.is_none() && self.hover_item.is_none() && self.hover_entity.is_none() {
self.hover.clone_from(&base.hover);
}
if self.hover_item.is_none() {
self.hover_item.clone_from(&base.hover_item);
self.hover_entity.clone_from(&base.hover_entity);
}
if self.insertion.is_none() {
self.insertion.clone_from(&base.insertion);
@@ -337,7 +367,13 @@ impl Text {
}
/// Component.getString: the plain characters.
pub fn plain(&self) -> String {
self.spans.iter().map(|s| s.text.as_str()).collect()
self.spans
.iter()
.map(|s| match &s.style.inline_object {
Some(object) if s.text == "\u{fffc}" => object.fallback(),
_ => s.text.as_str(),
})
.collect()
}
pub fn is_empty(&self) -> bool {
self.spans.iter().all(|s| s.text.is_empty())
@@ -403,31 +439,7 @@ impl Text {
continue;
}
let Some(code) = chars.next() else { break };
let code = code.to_ascii_lowercase();
let next = if let Some(c) = COLORS.iter().find(|c| c.0 == code) {
// Style.applyLegacyFormat: a colour clears the decorations.
Style {
color: Some(c.2),
bold: Some(false),
italic: Some(false),
underlined: Some(false),
strikethrough: Some(false),
obfuscated: Some(false),
..style.clone()
}
} else {
let mut s = style.clone();
match code {
'k' => s.obfuscated = Some(true),
'l' => s.bold = Some(true),
'm' => s.strikethrough = Some(true),
'n' => s.underlined = Some(true),
'o' => s.italic = Some(true),
'r' => s = span.style.clone(),
_ => {}
}
s
};
let next = legacy_format(&style, code, &span.style);
if next != style {
if !current.is_empty() {
out.spans.push(Span {
@@ -514,8 +526,9 @@ impl Text {
}
/// The style fields of a component's JSON.
const STYLE_KEYS: [&str; 10] = [
const STYLE_KEYS: [&str; 11] = [
"color",
"shadow_color",
"bold",
"italic",
"underlined",
@@ -536,6 +549,9 @@ fn style_json(style: &Style) -> Vec<(String, Value)> {
.map_or_else(|| format!("#{c:06X}"), |n| n.1.to_owned());
out.push(("color".to_owned(), name.into()));
}
if let Some(shadow) = style.shadow_color {
out.push(("shadow_color".to_owned(), Value::from(shadow as i32)));
}
for (key, flag) in [
("bold", style.bold),
("italic", style.italic),
@@ -578,6 +594,10 @@ fn style_json(style: &Style) -> Vec<(String, Value)> {
let mut event = item.clone();
event.insert("action".into(), "show_item".into());
out.push(("hover_event".to_owned(), Value::Object(event)));
} else if let Some(Value::Object(entity)) = style.hover_entity.as_deref() {
let mut event = entity.clone();
event.insert("action".into(), "show_entity".into());
out.push(("hover_event".to_owned(), Value::Object(event)));
}
if let Some(insertion) = &style.insertion {
out.push(("insertion".to_owned(), insertion.clone().into()));
@@ -588,6 +608,35 @@ fn style_json(style: &Style) -> Vec<(String, Value)> {
out
}
/// `Style.applyLegacyFormat` for the code after a `§`, `§r` going back to
/// `reset` (`StringDecomposer.iterateFormatted`); an unknown code changes nothing.
pub fn legacy_format(style: &Style, code: char, reset: &Style) -> Style {
let code = code.to_ascii_lowercase();
if let Some(c) = COLORS.iter().find(|c| c.0 == code) {
// A colour clears the decorations.
return Style {
color: Some(c.2),
bold: Some(false),
italic: Some(false),
underlined: Some(false),
strikethrough: Some(false),
obfuscated: Some(false),
..style.clone()
};
}
let mut s = style.clone();
match code {
'k' => s.obfuscated = Some(true),
'l' => s.bold = Some(true),
'm' => s.strikethrough = Some(true),
'n' => s.underlined = Some(true),
'o' => s.italic = Some(true),
'r' => s = reset.clone(),
_ => {}
}
s
}
/// TranslatableContents.decomposeTemplate: `%s` takes the next argument,
/// `%n$s` a numbered one and `%%` is a percent sign. Malformed templates are
/// shown verbatim.
@@ -60,6 +60,19 @@ pub fn edit_shortcut(
})
}
/// Windows reports AltGr as Ctrl plus Alt: the system presses a left Control
/// in front of the right Alt.
pub const ALTGR_IS_CTRL_ALT: bool = cfg!(windows);
/// Whether a key's text reaches the screen (`KeyboardHandler.textInput`, then
/// `charTyped` per code point). The edit modifier's shortcuts type nothing,
/// but `textInput` takes no modifiers: with AltGr held (Ctrl plus Alt where
/// `altgr_is_ctrl_alt`) the layout's third-level characters are text, as `/`
/// on ABNT2, `@`, `{` and `[` on German or the Polish letters.
pub fn types_text(edit_modifier: bool, alt: bool, altgr_is_ctrl_alt: bool) -> bool {
!edit_modifier || (altgr_is_ctrl_alt && alt)
}
/// `StringUtil.isAllowedChatCharacter`.
pub fn is_allowed_chat_character(c: char) -> bool {
c != '§' && c >= ' ' && c != '\u{7f}'
@@ -303,6 +316,22 @@ pub fn append_limited(value: &mut String, text: &str, max_chars: usize) -> bool
mod tests {
use super::*;
#[test]
fn altgr_types_text_where_it_is_ctrl_alt() {
// Plain keys and Shift type.
assert!(types_text(false, false, true));
assert!(types_text(false, false, false));
// Ctrl (Cmd) shortcuts do not.
assert!(!types_text(true, false, true));
assert!(!types_text(true, false, false));
// AltGr on Windows is Ctrl+Alt: ABNT2 `/`, German `@` type.
assert!(types_text(true, true, true));
// Where AltGr is its own modifier, Ctrl+Alt is a shortcut.
assert!(!types_text(true, true, false));
// Alt alone (macOS Option, Linux AltGr) types.
assert!(types_text(false, true, false));
}
#[test]
fn shortcuts_need_the_edit_modifier_alone() {
let c = Some('c');
+45
View File
@@ -2,6 +2,9 @@
//! Item-use and sneak multipliers come from the player's current state; this
//! module does not implement sprint eligibility, poses or the input lifecycle.
use std::collections::HashSet;
use winit::keyboard::{KeyCode, ModifiersState};
fn length(v: [f32; 2]) -> f32 {
f64::from(v[0] * v[0] + v[1] * v[1]).sqrt() as f32
}
@@ -65,3 +68,45 @@ fn square_movement(v: [f32; 2]) -> [f32; 2] {
},
)
}
/// `InputQuirks.isQuitShortcutDown`: checks whether the platform's window-closing
/// keyboard shortcut is held.
/// - On macOS: Command (`super_key()`) + `Q`.
/// - On Windows / Linux / other: Alt (`alt_key()`) + `F4`.
pub fn is_quit_shortcut_down(modifiers: ModifiersState, keys: &HashSet<KeyCode>) -> bool {
if cfg!(target_os = "macos") {
modifiers.super_key() && keys.contains(&KeyCode::KeyQ)
} else {
modifiers.alt_key() && keys.contains(&KeyCode::F4)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn quit_shortcut_follows_platform_modifiers_and_keys() {
let mut keys = HashSet::new();
let alt = ModifiersState::ALT;
let super_key = ModifiersState::SUPER;
assert!(!is_quit_shortcut_down(ModifiersState::empty(), &keys));
keys.insert(KeyCode::F4);
if cfg!(target_os = "macos") {
assert!(!is_quit_shortcut_down(alt, &keys));
keys.remove(&KeyCode::F4);
keys.insert(KeyCode::KeyQ);
assert!(is_quit_shortcut_down(super_key, &keys));
assert!(!is_quit_shortcut_down(alt, &keys));
} else {
assert!(is_quit_shortcut_down(alt, &keys));
assert!(!is_quit_shortcut_down(super_key, &keys));
keys.remove(&KeyCode::F4);
keys.insert(KeyCode::KeyQ);
assert!(!is_quit_shortcut_down(alt, &keys));
assert!(!is_quit_shortcut_down(super_key, &keys));
}
}
}
@@ -13,6 +13,12 @@ pub enum Button {
}
pub trait InteractionWorld {
/// Minecraft.pick before handleKeybinds: the tick's `hitResult`, which
/// every click of the tick reads, even after one of them changed the
/// block it names (a creative break, then a use that replaces it).
fn pick_hit_result(&mut self);
/// The tick's clicks are done: later picks follow the crosshair again.
fn release_hit_result(&mut self);
fn using_item(&self) -> bool;
fn release_using(&mut self);
fn start_attack(&mut self) -> bool;
@@ -48,6 +54,7 @@ impl InteractionInput {
pub fn tick(&mut self, world: &mut impl InteractionWorld, attack_down: bool, use_down: bool) {
let [attack, use_item, pick] = std::mem::take(&mut self.clicks);
world.pick_hit_result();
let mut instant = false;
if world.using_item() {
if !use_down {
@@ -70,6 +77,7 @@ impl InteractionInput {
world.start_use();
}
world.continue_attack(!instant && attack_down);
world.release_hit_result();
}
}
@@ -86,6 +94,8 @@ mod tests {
events: Vec<&'static str>,
}
impl InteractionWorld for World {
fn pick_hit_result(&mut self) {}
fn release_hit_result(&mut self) {}
fn using_item(&self) -> bool {
self.using
}
@@ -183,4 +193,67 @@ mod tests {
input.tick(&mut world, false, false);
assert_eq!(world.events, ["release", "continue"]);
}
/// A floating block in front of the crosshair, nothing behind it, and a
/// block item in the hand of a creative player.
#[derive(Default)]
struct Column {
block: bool,
hit: Option<Option<()>>,
placed: bool,
}
impl Column {
/// Minecraft.hitResult: the tick's pick, or a fresh one outside it.
fn target(&self) -> Option<()> {
self.hit.unwrap_or(self.block.then_some(()))
}
}
impl InteractionWorld for Column {
fn pick_hit_result(&mut self) {
self.hit = Some(self.target());
}
fn release_hit_result(&mut self) {
self.hit = None;
}
fn using_item(&self) -> bool {
false
}
fn release_using(&mut self) {}
fn start_attack(&mut self) -> bool {
// MultiPlayerGameMode.startDestroyBlock in creative: the
// predicted break leaves air.
if self.target().is_some() {
self.block = false;
}
!self.block
}
fn start_use(&mut self) {
// BlockItem.place: the clicked block, now air, can be replaced.
if self.target().is_some() && !self.block {
self.block = true;
self.placed = true;
}
}
fn pick(&mut self) {}
fn use_delay(&self) -> u32 {
0
}
fn continue_attack(&mut self, _: bool) {}
}
#[test]
fn attack_and_use_in_one_tick_share_the_hit_result() {
// #3006: both buttons at once replace the block the crosshair is on.
let mut input = InteractionInput::default();
let mut world = Column {
block: true,
..Default::default()
};
input.press(Button::Attack);
input.press(Button::Use);
input.tick(&mut world, false, false);
assert!(world.placed, "the use acts on the block the attack broke");
assert!(world.block);
assert!(world.hit.is_none(), "the next pick follows the crosshair");
}
}
+68 -13
View File
@@ -284,6 +284,26 @@ pub fn mouse_button(name: &str) -> Option<MouseButton> {
}
pub use horizon_client_core::key_display::display_name;
/// A key event: what the layout writes on its key, without modifiers, names
/// that key from now on (`key_display::learn_layout`), so an AZERTY player's
/// Q key shows A.
pub fn learn_layout(event: &winit::event::KeyEvent) {
use winit::keyboard::{Key, PhysicalKey};
use winit::platform::modifier_supplement::KeyEventExtModifierSupplement;
let PhysicalKey::Code(code) = event.physical_key else {
return;
};
let Some(name) = key_name(code) else {
return;
};
let learn = horizon_client_core::key_display::learn_layout;
match event.key_without_modifiers() {
Key::Character(text) => learn(name, &text),
Key::Dead(Some(c)) => learn(name, c.encode_utf8(&mut [0; 4])),
_ => {}
}
}
/// Current bindings by mapping name.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct KeyBinds {
@@ -300,9 +320,8 @@ impl Default for KeyBinds {
}
}
/// Each default key once, in order: two mappings that share a default key
/// (Advancements and the debug profiler both on L) are one press of it, not
/// two (the second would close the screen the first opened). Vanilla clicks
/// both mappings, but the profiler one only acts with F3 held.
/// (Pick Block and the spectator hotbar both on the middle button) are one
/// press of it, not two.
fn distinct<T: PartialEq>(keys: Vec<T>) -> Vec<T> {
let mut out = Vec::with_capacity(keys.len());
for k in keys {
@@ -368,6 +387,25 @@ impl KeyBinds {
.map(|m| m.0)
.filter(move |n| self.get(n) == key)
}
/// The mappings bound to `key` that [`KeyBinds::translate_all`] follows:
/// all but the debug keys. Those act only with F3 held
/// (`KeyboardHandler.handleDebugKeys`, `debug_keys.rs` reads the physical
/// key and the binding itself); their defaults (A, S, D, T, L, 1 to 4...)
/// would otherwise turn the key back into its old gameplay meaning.
fn translated_mappings<'a>(&'a self, key: &'a str) -> impl Iterator<Item = &'static str> + 'a {
MAPPINGS
.iter()
.filter(|m| m.2 != "debug")
.map(|m| m.0)
.filter(move |n| self.get(n) == key)
}
/// Whether a mapping (other than a debug key) whose default is `key` was
/// bound elsewhere.
fn default_moved(&self, key: &str) -> bool {
MAPPINGS
.iter()
.any(|(n, d, c)| *d == key && *c != "debug" && !self.is_default(n))
}
/// Translate a physical key into the default key of the mapping it is
/// bound to, so game code written against vanilla defaults follows the
/// player's bindings. `None` means the key now does nothing; mouse
@@ -384,7 +422,7 @@ impl KeyBinds {
return vec![Ok(key)];
};
let bound: Vec<_> = self
.mappings_for(name)
.translated_mappings(name)
.filter_map(|mapping| {
let default = Self::default_of(mapping);
match key_code(default) {
@@ -397,10 +435,7 @@ impl KeyBinds {
return distinct(bound);
}
// An unbound key whose default meaning was moved elsewhere is inert.
if MAPPINGS
.iter()
.any(|(n, d, _)| *d == name && !self.is_default(n))
{
if self.default_moved(name) {
return Vec::new();
}
vec![Ok(key)]
@@ -413,7 +448,7 @@ impl KeyBinds {
pub fn translate_mouse_all(&self, button: MouseButton) -> Vec<Result<KeyCode, MouseButton>> {
let name = mouse_name(button);
let bound: Vec<_> = self
.mappings_for(&name)
.translated_mappings(&name)
.filter_map(|mapping| {
let default = Self::default_of(mapping);
match mouse_button(default) {
@@ -425,10 +460,7 @@ impl KeyBinds {
if !bound.is_empty() {
return distinct(bound);
}
if MAPPINGS
.iter()
.any(|(n, d, _)| *d == name && !self.is_default(n))
{
if self.default_moved(&name) {
return Vec::new();
}
vec![Err(button)]
@@ -508,6 +540,29 @@ mod tests {
assert_eq!(k.conflicts("key.forward"), ["key.jump"]);
}
#[test]
fn a_moved_key_keeps_no_default_action() {
// #3158: the debug keys (F3+A, F3+1...) share these keys' defaults,
// and must not hand them back their gameplay meaning.
let mut k = KeyBinds::default();
k.set("key.hotbar.1", "key.keyboard.q");
assert_eq!(k.translate_all(KeyCode::Digit1), []);
assert_eq!(
k.translate_all(KeyCode::KeyQ),
[Ok(KeyCode::KeyQ), Ok(KeyCode::Digit1)]
);
k.set("key.inventory", "key.keyboard.a");
k.set("key.left", "key.keyboard.e");
assert_eq!(k.translate_all(KeyCode::KeyA), [Ok(KeyCode::KeyE)]);
assert_eq!(k.translate_all(KeyCode::KeyE), [Ok(KeyCode::KeyA)]);
k.set("key.back", "key.keyboard.down");
assert_eq!(k.translate_all(KeyCode::KeyS), []);
// Keys left alone still act, and a moved debug key changes nothing.
k.set("key.debug.fpsCharts", "key.keyboard.9");
assert_eq!(k.translate_all(KeyCode::Digit2), [Ok(KeyCode::Digit2)]);
assert_eq!(k.translate_all(KeyCode::Digit9), [Ok(KeyCode::Digit9)]);
assert_eq!(k.translate_all(KeyCode::F3), [Ok(KeyCode::F3)]);
}
#[test]
fn display_names() {
assert_eq!(display_name("key.keyboard.w").plain(), "W");
assert_eq!(display_name("key.keyboard.slash").plain(), "/");
+39
View File
@@ -342,6 +342,28 @@ pub fn mouse_position_after_grab_change(
}
}
/// `MacosUtil.setCtrlClickEmulatesRightClick`: with the option on (and only on
/// macOS), a left click made with Control held is a right click, and so is
/// its release, whatever Control does in between.
#[derive(Clone, Copy, Debug, Default)]
pub struct RightClickEmulation {
emulating: bool,
}
impl RightClickEmulation {
/// Whether the left button's `pressed` or released event is a right click.
/// `enabled` is the option on macOS.
pub fn is_right_click(&mut self, pressed: bool, control: bool, enabled: bool) -> bool {
if pressed {
self.emulating = enabled && control;
}
let right = self.emulating;
if !pressed {
self.emulating = false;
}
right
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -526,4 +548,21 @@ mod tests {
None
);
}
#[test]
fn control_click_is_a_right_click_until_released() {
let mut e = RightClickEmulation::default();
// The option off, or no Control: a left click stays one.
assert!(!e.is_right_click(true, true, false));
assert!(!e.is_right_click(false, true, false));
assert!(!e.is_right_click(true, false, true));
assert!(!e.is_right_click(false, true, true));
// Control held at the press: the press and the release are right clicks,
// even when Control was let go before the release.
assert!(e.is_right_click(true, true, true));
assert!(e.is_right_click(false, false, true));
// The next left click is its own.
assert!(!e.is_right_click(true, false, true));
assert!(!e.is_right_click(false, false, true));
}
}
+246 -1
View File
@@ -284,12 +284,18 @@ impl FramerateLimiter {
/// (`Window.updateFullscreenState` -> `Minecraft.fullscreenStateChanged`), and
/// the window is only asked to change when the option itself changes
/// (`Window.updateFullscreenIfChanged`), never re-forced to the option.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct FullscreenState {
/// The state the window system last reported (`Window.fullscreen`).
actual: bool,
/// The state last asked of the window system, or adopted from it.
requested: bool,
/// `Window.exclusiveFullscreen`.
exclusive: bool,
/// `Window.preferredFullscreenVideoMode`.
mode: Option<VideoMode>,
/// `Window.dirty`: the preferred mode changed and the screen has not been left yet.
mode_dirty: bool,
}
impl FullscreenState {
@@ -298,9 +304,70 @@ impl FullscreenState {
Self {
actual: false,
requested,
exclusive: false,
mode: None,
mode_dirty: false,
}
}
/// `Window.updateFullscreenIfChanged` with the exclusive option and preferred mode: asks the
/// window to enter, leave or change its fullscreen only when one of them changed.
/// `in_video_screen` holds a changed mode back until the Video Settings screen closes
/// (`changeFullscreenVideoMode`).
pub fn apply(
&mut self,
window: &winit::window::Window,
wanted: bool,
exclusive: bool,
preferred: Option<VideoMode>,
in_video_screen: bool,
) {
let request = self.request(wanted);
let reapply = self.set_exclusive(exclusive);
self.set_preferred_mode(preferred);
let mode_change = !in_video_screen && self.take_mode_change();
match request {
Some(false) => window.set_fullscreen(None),
Some(true) => window.set_fullscreen(Some(fullscreen_request(
window,
exclusive,
preferred.as_ref(),
))),
None if reapply || mode_change => window.set_fullscreen(Some(fullscreen_request(
window,
exclusive,
preferred.as_ref(),
))),
None => {}
}
}
/// `Window.setExclusiveFullscreen`: true when the option changed while fullscreen is
/// requested, so the window has to enter the other kind of fullscreen at once.
pub fn set_exclusive(&mut self, exclusive: bool) -> bool {
let changed = self.exclusive != exclusive;
self.exclusive = exclusive;
changed && self.requested
}
/// `Window.setPreferredFullscreenVideoMode`: a changed mode waits for `take_mode_change`.
pub fn set_preferred_mode(&mut self, mode: Option<VideoMode>) {
if self.mode != mode {
self.mode = mode;
self.mode_dirty = true;
}
}
/// `Window.changeFullscreenVideoMode` (the Video Settings screen closing): true once when
/// a changed mode has to be applied to a fullscreen window.
pub fn take_mode_change(&mut self) -> bool {
let apply = self.requested && self.mode_dirty;
if apply {
self.mode_dirty = false;
}
apply
}
/// `Window.updateFullscreenIfChanged`: the state to ask of the window
/// when the option `wanted` differs from what was last asked or adopted.
pub fn request(&mut self, wanted: bool) -> Option<bool> {
@@ -321,6 +388,132 @@ impl FullscreenState {
}
}
/// `VideoMode`: one display mode of a monitor, as `fullscreenResolution` in options.txt keeps it.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct VideoMode {
pub width: i32,
pub height: i32,
pub red_bits: i32,
pub green_bits: i32,
pub blue_bits: i32,
pub refresh_rate: f32,
}
impl VideoMode {
/// `VideoMode.read`: the JSON object `write` makes; any other string is None.
pub fn read(text: &str) -> Option<Self> {
let number = |key: &str| -> Option<f64> {
let at = text.find(&format!("\"{key}\""))? + key.len() + 2;
let rest = text[at..].trim_start().strip_prefix(':')?.trim_start();
let end = rest
.find(|c: char| !(c.is_ascii_digit() || matches!(c, '-' | '+' | '.' | 'e' | 'E')))
.unwrap_or(rest.len());
rest[..end].parse().ok()
};
if !text.trim_start().starts_with('{') {
return None;
}
Some(Self {
width: number("width")? as i32,
height: number("height")? as i32,
red_bits: number("red_bits")? as i32,
green_bits: number("green_bits")? as i32,
blue_bits: number("blue_bits")? as i32,
refresh_rate: number("refresh_rate")? as f32,
})
}
/// `VideoMode.write`: the Codec's JSON through Gson (a float prints as `60.0`).
pub fn write(&self) -> String {
let rate = if self.refresh_rate == self.refresh_rate.trunc() {
format!("{:.1}", self.refresh_rate)
} else {
self.refresh_rate.to_string()
};
format!(
"{{\"width\":{},\"height\":{},\"red_bits\":{},\"green_bits\":{},\"blue_bits\":{},\"refresh_rate\":{rate}}}",
self.width, self.height, self.red_bits, self.green_bits, self.blue_bits
)
}
/// `VideoMode.refreshRateLabel`: a whole rate without decimals, any other with two.
pub fn refresh_rate_label(&self) -> String {
if self.refresh_rate == self.refresh_rate.round_ties_even() {
(self.refresh_rate as i32).to_string()
} else {
format!("{:.2}", self.refresh_rate)
}
}
/// The colour depth `options.fullscreen.entry` shows.
pub fn bits(&self) -> i32 {
self.red_bits + self.green_bits + self.blue_bits
}
/// A winit mode: its depth is the total, split evenly over the channels.
pub fn from_winit(mode: &winit::monitor::VideoModeHandle) -> Self {
let depth = i32::from(mode.bit_depth());
let size = mode.size();
Self {
width: size.width as i32,
height: size.height as i32,
red_bits: depth / 3,
green_bits: depth / 3,
blue_bits: depth - 2 * (depth / 3),
refresh_rate: mode.refresh_rate_millihertz() as f32 / 1000.0,
}
}
}
/// `Monitor.videoModes`: the modes a monitor offers.
pub fn monitor_video_modes(monitor: &winit::monitor::MonitorHandle) -> Vec<VideoMode> {
monitor
.video_modes()
.map(|m| VideoMode::from_winit(&m))
.collect()
}
/// `Monitor.currentMode`: the monitor's desktop mode; winit has no such query, so the mode of
/// the monitor's own size with the highest refresh rate and depth.
fn monitor_current_mode(
monitor: &winit::monitor::MonitorHandle,
) -> Option<winit::monitor::VideoModeHandle> {
let size = monitor.size();
monitor
.video_modes()
.filter(|m| m.size() == size)
.max_by_key(|m| (m.refresh_rate_millihertz(), m.bit_depth()))
}
/// `Window.applyFullscreen`: the fullscreen the window asks for. Exclusive fullscreen takes
/// `Monitor.getPreferredVideoMode` (the preferred mode if the monitor has it, else the desktop
/// mode); a monitor with no usable mode falls back to borderless, as vanilla does.
pub fn fullscreen_request(
window: &winit::window::Window,
exclusive: bool,
preferred: Option<&VideoMode>,
) -> winit::window::Fullscreen {
use winit::window::Fullscreen;
if !exclusive {
return Fullscreen::Borderless(None);
}
let Some(monitor) = window
.current_monitor()
.or_else(|| window.primary_monitor())
else {
return Fullscreen::Borderless(None);
};
let wanted = preferred.and_then(|p| {
monitor
.video_modes()
.find(|m| VideoMode::from_winit(m) == *p)
});
match wanted.or_else(|| monitor_current_mode(&monitor)) {
Some(mode) => Fullscreen::Exclusive(mode),
None => Fullscreen::Borderless(None),
}
}
/// Horizon's icon for the window's title bar and the task bar (X11 and Windows; Wayland takes
/// it from the `horizon.desktop` entry, macOS from the app bundle). `None` if it can't be read.
pub fn icon() -> Option<winit::window::Icon> {
@@ -340,11 +533,63 @@ pub fn icon() -> Option<winit::window::Icon> {
mod tests {
use super::*;
#[test]
fn video_mode_round_trips_the_options_json() {
let mode = VideoMode {
width: 1920,
height: 1080,
red_bits: 8,
green_bits: 8,
blue_bits: 8,
refresh_rate: 60.0,
};
let text = mode.write();
assert_eq!(
text,
r#"{"width":1920,"height":1080,"red_bits":8,"green_bits":8,"blue_bits":8,"refresh_rate":60.0}"#
);
assert_eq!(VideoMode::read(&text), Some(mode));
assert_eq!(VideoMode::read("1920x1080"), None);
assert_eq!(VideoMode::read(r#"{"width":1}"#), None);
}
#[test]
fn refresh_rate_label_is_whole_or_two_decimals() {
let mut mode = VideoMode::read(
r#"{"width":1,"height":1,"red_bits":8,"green_bits":8,"blue_bits":8,"refresh_rate":144.0}"#,
)
.unwrap();
assert_eq!(mode.refresh_rate_label(), "144");
mode.refresh_rate = 59.94;
assert_eq!(mode.refresh_rate_label(), "59.94");
assert_eq!(mode.bits(), 24);
}
#[test]
fn the_window_icon_decodes() {
assert!(icon().is_some());
}
#[test]
fn exclusive_fullscreen_reapplies_only_while_fullscreen_is_requested() {
let mut f = FullscreenState::new(false);
assert!(!f.set_exclusive(true));
assert!(!f.set_exclusive(true));
assert_eq!(f.request(true), Some(true));
assert!(f.set_exclusive(false));
assert!(!f.set_exclusive(false));
}
#[test]
fn a_changed_video_mode_applies_once_when_fullscreen() {
let mode = VideoMode::read(
r#"{"width":1,"height":1,"red_bits":8,"green_bits":8,"blue_bits":8,"refresh_rate":60.0}"#,
);
let mut f = FullscreenState::new(false);
f.set_preferred_mode(mode);
assert!(!f.take_mode_change());
f.request(true);
f.set_preferred_mode(mode);
assert!(f.take_mode_change());
assert!(!f.take_mode_change());
}
#[test]
fn fullscreen_follows_the_option_only_when_it_changes() {
let mut f = FullscreenState::new(false);
+69
View File
@@ -0,0 +1,69 @@
[package]
name = "horizon-client-render"
version = "0.1.0"
edition.workspace = true
license.workspace = true
rust-version = "1.90"
description = "The client's world data, rendering and screens: chunks, meshing, light, streaming, the renderer, the HUD, menus and settings; no gameplay"
[dependencies]
horizon-assets = { path = "../horizon-assets" }
horizon-bench = { path = "../horizon-bench" }
horizon-client-core = { path = "../horizon-client-core" }
horizon-client-input = { path = "../horizon-client-input" }
horizon-command = { path = "../horizon-command" }
horizon-core = { path = "../horizon-core" }
horizon-hdr = { path = "../horizon-hdr" }
horizon-items = { path = "../horizon-items" }
horizon-protocol = { path = "../horizon-protocol" }
horizon-shader-api = { path = "../horizon-shader-api" }
horizon-shaders = { path = "../horizon-shaders" }
horizon-telemetry = { path = "../horizon-telemetry", default-features = false, features = ["live"] }
horizon-theme = { path = "../horizon-theme" }
horizon-threads = { path = "../horizon-threads" }
horizon-world = { path = "../horizon-world" }
ab_glyph = "0.2.32"
base64 = "0.22"
bytemuck = { version = "1.24", features = ["derive"] }
# Sound (ALSA on Linux); the `audio` feature.
cpal = { version = "0.16", optional = true }
chrono = { version = "0.4.42", default-features = false, features = ["clock", "std"] }
flate2 = { version = "1.1.10", default-features = false, features = ["zlib-rs", "runtime_detection"] }
glam = "0.30"
log = "0.4"
miniz_oxide = "0.8"
naga = { version = "30.0.1", features = ["wgsl-in", "hlsl-out"] }
png = "0.18"
pollster = "0.4"
serde = { version = "1", features = ["derive"] }
serde_json = { version = "1.0.151", features = ["raw_value"] }
sha1 = "0.10"
sha2 = "0.10.9"
symphonia = { version = "0.5", default-features = false, features = ["ogg", "vorbis"] }
unicode-bidi = "0.3.18"
ureq = { version = "2", default-features = false, features = ["tls"] }
wgpu = { version = "30.0.1", default-features = false, features = ["std", "parking_lot", "vulkan", "metal", "wgsl", "naga-ir"] }
winit = "0.30.13"
zip = { version = "2", default-features = false, features = ["deflate"] }
[target.'cfg(unix)'.dependencies]
rustix = { version = "1", default-features = false, features = ["std", "time"] }
[target.'cfg(windows)'.dependencies]
wgpu = { version = "30.0.1", default-features = false, features = ["dx12"] }
[target.'cfg(not(target_os = "macos"))'.dependencies]
wgpu = { version = "30.0.1", default-features = false, features = ["gles"] }
[features]
# Sound output: horizon-client's `audio` feature.
audio = ["dep:cpal"]
# Helpers horizon-client's tests build their fixtures with.
test-support = []
[lints]
workspace = true
[dev-dependencies]
horizon-items = { path = "../horizon-items", features = ["test-support"] }
naga = { version = "30.0.1", features = ["wgsl-in", "hlsl-out", "msl-out", "spv-out"] }
@@ -0,0 +1,151 @@
//! Two offscreen frames presented in one window, without game resources or player data.
//! `cargo run --profile fast -p horizon-client-render --example compositor -- --smoke`
use horizon_client_render::gfx::compositor::Compositor;
use std::sync::Arc;
use winit::{
application::ApplicationHandler,
event::WindowEvent,
event_loop::{ActiveEventLoop, EventLoop},
window::{Window, WindowId},
};
struct Frames {
window: Arc<Window>,
compositor: Compositor,
textures: [wgpu::Texture; 2],
}
impl Frames {
fn new(event_loop: &ActiveEventLoop) -> horizon_client_render::Result<Self> {
let window = Arc::new(
event_loop.create_window(
Window::default_attributes()
.with_title("Compositor: red and blue views")
.with_inner_size(winit::dpi::PhysicalSize::new(640, 360)),
)?,
);
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_with_display_handle(
Box::new(window.clone()),
));
let surface = instance.create_surface(window.clone())?;
let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
compatible_surface: Some(&surface),
..Default::default()
}))?;
let (device, queue) = pollster::block_on(adapter.request_device(&Default::default()))?;
let textures = [[0, 0, 255, 255], [255, 0, 0, 255]].map(|pixel| {
let size = wgpu::Extent3d {
width: 320,
height: 360,
depth_or_array_layers: 1,
};
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("Offscreen view"),
size,
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Bgra8Unorm,
usage: wgpu::TextureUsages::COPY_SRC | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
queue.write_texture(
texture.as_image_copy(),
&pixel.repeat(320 * 360),
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(320 * 4),
rows_per_image: Some(360),
},
size,
);
texture
});
let compositor = Compositor::new(
surface,
&adapter,
&device,
&queue,
window.inner_size().into(),
)?;
Ok(Self {
window,
compositor,
textures,
})
}
}
#[derive(Default)]
struct Demo {
frames: Option<Frames>,
error: Option<String>,
presented: usize,
smoke: bool,
}
impl ApplicationHandler for Demo {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
if self.frames.is_none() {
match Frames::new(event_loop) {
Ok(frames) => self.frames = Some(frames),
Err(error) => {
self.error = Some(error.to_string());
event_loop.exit();
}
}
}
}
fn window_event(&mut self, event_loop: &ActiveEventLoop, _: WindowId, event: WindowEvent) {
let Some(f) = &mut self.frames else { return };
match event {
WindowEvent::CloseRequested => event_loop.exit(),
WindowEvent::Resized(size) => f.compositor.resize(size.width, size.height),
// Uncovered pixels are black; resize smaller to exercise clipping.
WindowEvent::RedrawRequested
if f.compositor
.present(&[(&f.textures[0], [0, 0]), (&f.textures[1], [320, 0])]) =>
{
self.presented += 1;
if self.smoke && self.presented == 10 {
f.compositor.set_vsync(false);
f.compositor.resize(0, 0);
assert!(!f.compositor.present(&[]));
f.compositor.resize(u32::MAX, u32::MAX);
assert!(!f.compositor.present(&[]));
f.compositor.resize(400, 240);
let _ = f
.window
.request_inner_size(winit::dpi::PhysicalSize::new(400, 240));
}
if self.smoke && self.presented == 20 {
println!("compositor: 20 frames, resize, suspension and clipping passed");
event_loop.exit();
}
}
_ => {}
}
}
fn about_to_wait(&mut self, _: &ActiveEventLoop) {
if let Some(f) = &self.frames {
f.window.request_redraw();
}
}
}
fn main() -> horizon_client_render::Result<()> {
let mut demo = Demo {
smoke: std::env::args().any(|a| a == "--smoke"),
..Default::default()
};
EventLoop::new()?.run_app(&mut demo)?;
if let Some(error) = demo.error {
return Err(error.into());
}
if demo.smoke && demo.presented < 20 {
return Err("smoke test closed early".into());
}
Ok(())
}
@@ -42,11 +42,11 @@ impl HeightmapState for StateFacts {
}
#[derive(Clone)]
pub struct Properties(Arc<Vec<StateFacts>>);
#[cfg(test)]
#[cfg(any(test, feature = "test-support"))]
impl Properties {
/// Heightmap facts for tests, per state: (air, has a fluid, blocks
/// motion, blocks motion but is not leaves). State 0 is minecraft:air.
pub(crate) fn for_tests(states: &[(bool, bool, bool, bool)]) -> Self {
pub fn for_tests(states: &[(bool, bool, bool, bool)]) -> Self {
Self(Arc::new(
states
.iter()
@@ -192,16 +192,30 @@ impl Assets {
pub fn reload(&self, root: &Path) -> Result<RenderAssets> {
RenderAssets::load(root, self)
}
/// [`Assets::reload`] for a server, which draws nothing: the item models
/// and the sprites' layers, without the block models or the textures'
/// pixels.
pub fn reload_for_server(&self, root: &Path) -> Result<RenderAssets> {
RenderAssets::load_with(root, self, false)
}
}
impl RenderAssets {
pub fn load(root: &Path, assets: &Assets) -> Result<Self> {
Self::load_with(root, assets, true)
}
fn load_with(root: &Path, assets: &Assets, draws: bool) -> Result<Self> {
let registry = assets.registry;
let mut sprites = crate::sprites::Sprites::default();
let mut sprites = crate::sprites::Sprites::with_disk_cache(root);
for name in MATERIALS {
sprites.insert(root, &format!("minecraft:block/{name}"))?;
}
let parsed = crate::model_library::ParsedModels::load(root);
let models = crate::models::prepare_with_models(root, registry, &mut sprites, &parsed)?;
// A server draws no block: its models (about 24 MB baked) stay empty.
let models = if draws {
crate::models::prepare_with_models(root, registry, &mut sprites, &parsed)?
} else {
crate::models::PreparedModels::default()
};
let model_library = parsed.into_item_library();
// Fluids are drawn by the fluid renderer, not block models; breaking
// progress uses the destroy-stage overlays.
@@ -217,7 +231,12 @@ impl RenderAssets {
for stage in 0..10 {
sprites.insert(root, &format!("minecraft:block/destroy_stage_{stage}"))?;
}
sprites.finish();
sprites.save_disk_cache();
if draws {
sprites.finish();
} else {
sprites.finish_without_pixels();
}
// `finish` grows the buffer by doubling: keep only the bytes in use.
sprites.pixels.shrink_to_fit();
Ok(Self {
@@ -240,6 +259,7 @@ impl RenderAssets {
#[cfg(test)]
mod tests {
use crate::items::ConsumeEffect;
use horizon_world::loot::PackOverlay;
#[test]
fn always_edible_and_consume_effects_loaded() {
@@ -250,7 +270,8 @@ mod tests {
}
let assets = crate::assets::Assets::load(&root).unwrap();
let facts = crate::blocks::BlockFacts::load(&root, assets.registry).unwrap();
let items = crate::items::Items::load(&root, assets.registry, &facts).unwrap();
let items =
crate::items::Items::load(&root, assets.registry, &facts, &PackOverlay::NONE).unwrap();
let apple = items.get("minecraft:golden_apple").expect("golden apple");
assert!(apple.can_always_eat);
@@ -187,7 +187,14 @@ pub fn list_textures(root: &Path) -> Vec<String> {
};
for e in entries.flatten() {
let p = e.path();
if p.is_dir() {
// `file_type` comes with the listing; `Path::is_dir` is a stat per
// entry, kept for links (which it follows).
let dir = match e.file_type() {
Ok(t) if t.is_symlink() => p.is_dir(),
Ok(t) => t.is_dir(),
Err(_) => p.is_dir(),
};
if dir {
stack.push(p);
} else if p.extension().is_some_and(|x| x == "png")
&& let Ok(rel) = p.strip_prefix(&base)
@@ -144,6 +144,7 @@ type Stream = cpal::Stream;
type Stream = std::convert::Infallible;
pub struct Audio {
_stream: Option<Stream>,
device_name: String,
pub engine: SoundEngine,
backend: MixerBackend,
/// Record `Heard` sounds (the subtitle overlay is listening).
@@ -164,6 +165,8 @@ pub struct Audio {
/// How many times each selection was stopped ("record *", "* minecraft:…", `*` for
/// any), with or without an audio device, as `heard`.
pub stopped: std::collections::BTreeMap<String, u32>,
/// The volume sliders' `SoundPreviewHandler`.
preview: crate::sound_preview::SoundPreview,
}
fn decode(path: &Path) -> Option<(Vec<f32>, u32)> {
use symphonia::core::{
@@ -543,15 +546,48 @@ fn build_stream<T: cpal::SizedSample + cpal::FromSample<f32>>(
)?;
Ok(stream)
}
/// Available audio output device names.
#[cfg(feature = "audio")]
pub fn output_devices() -> Vec<String> {
if output_disabled(|name| std::env::var_os(name)).is_some() {
return Vec::new();
}
let host = cpal::default_host();
let mut names = Vec::new();
if let Ok(devices) = host.output_devices() {
for device in devices {
if let Ok(name) = device.name()
&& !names.contains(&name)
{
names.push(name);
}
}
}
names
}
#[cfg(not(feature = "audio"))]
pub fn output_devices() -> Vec<String> {
Vec::new()
}
impl Audio {
pub fn new(root: &Path) -> Self {
Self::with_output(root, true)
Self::with_device(root, "")
}
pub fn with_device(root: &Path, device_name: &str) -> Self {
Self::with_output(root, true, device_name)
}
/// No output device: the dedicated server.
pub fn silent(root: &Path) -> Self {
Self::with_output(root, false)
Self::with_output(root, false, "")
}
fn with_output(root: &Path, output: bool) -> Self {
/// The output device asked for (`soundDevice`), empty for the system's default; the
/// default plays when that one can't be opened, as in vanilla.
pub fn device(&self) -> &str {
&self.device_name
}
fn with_output(root: &Path, output: bool, target_device: &str) -> Self {
let registry = SoundRegistry::load(root);
if output && registry.is_empty() {
// The game then plays nothing on a working output (#1035, #1076).
@@ -564,7 +600,7 @@ impl Audio {
}
let mixer = Arc::new(Mutex::new(Mixer::default()));
let opened = if output {
Self::open(mixer.clone())
Self::open_named(mixer.clone(), target_device)
} else {
Err("no output on a dedicated server".into())
};
@@ -581,6 +617,7 @@ impl Audio {
engine.loaded = live;
Self {
_stream: stream,
device_name: target_device.to_owned(),
engine,
backend: MixerBackend {
mixer,
@@ -599,13 +636,65 @@ impl Audio {
capture: None,
heard: Default::default(),
stopped: Default::default(),
preview: Default::default(),
}
}
#[cfg(not(feature = "audio"))]
fn open(_: Arc<Mutex<Mixer>>) -> Result<Stream> {
fn open_named(_: Arc<Mutex<Mixer>>, _: &str) -> Result<Stream> {
Err("this build has no sound output (built without the `audio` feature)".into())
}
#[cfg(feature = "audio")]
fn open_named(mixer: Arc<Mutex<Mixer>>, target: &str) -> Result<Stream> {
if let Some(reason) = output_disabled(|name| std::env::var_os(name)) {
return Err(reason.into());
}
let host = cpal::default_host();
if !target.is_empty()
&& let Ok(devices) = host.output_devices()
{
for device in devices {
let name = device.name().unwrap_or_else(|_| "unnamed device".into());
if name == target {
match open_device(&device, mixer.clone()) {
Ok((stream, config)) => {
log::info!(
"Horizon: audio output {name} ({} Hz, {} channels, {})",
config.sample_rate().0,
config.channels(),
config.sample_format()
);
return Ok(stream);
}
Err(e) => {
log::warn!(
"Horizon: could not open audio device '{target}': {e}; falling back to default"
);
}
}
break;
}
}
}
Self::open(mixer)
}
#[cfg(feature = "audio")]
pub fn set_device(&mut self, name: &str) -> Result<()> {
if !self.backend.live && self._stream.is_none() && !self.engine.loaded {
self.device_name = name.to_owned();
return Ok(());
}
self._stream = Some(Self::open_named(self.backend.mixer.clone(), name)?);
self.device_name = name.to_owned();
self.backend.live = true;
self.engine.loaded = true;
Ok(())
}
#[cfg(not(feature = "audio"))]
pub fn set_device(&mut self, name: &str) -> Result<()> {
self.device_name = name.to_owned();
Ok(())
}
#[cfg(feature = "audio")]
fn open(mixer: Arc<Mutex<Mixer>>) -> Result<Stream> {
if let Some(reason) = output_disabled(|name| std::env::var_os(name)) {
return Err(reason.into());
@@ -731,6 +820,13 @@ impl Audio {
false,
);
}
/// `SoundPreviewHandler.preview`: a category volume slider moved to
/// `volume` on the title screen's options.
pub fn preview(&mut self, source: usize, volume: f32) {
let mut preview = std::mem::take(&mut self.preview);
preview.preview(self, source, volume);
self.preview = preview;
}
/// `play` with the seed a sound packet carries.
pub fn play_seeded(
&mut self,
@@ -52,20 +52,20 @@ impl AuthSession {
/// The session file's JSON: {"name", "uuid", "access_token", "user_type": "msa", "xuid"}.
pub fn parse(text: &str) -> Result<Self, String> {
let v: serde_json::Value =
serde_json::from_str(text).map_err(|_| "the session file is not valid JSON")?;
serde_json::from_str(text).map_err(|_| "the session is not valid JSON")?;
let field = |k: &str| v.get(k).and_then(|s| s.as_str());
let name = field("name").ok_or("the session file has no name")?;
let name = field("name").ok_or("the session has no name")?;
if name.is_empty() || name.chars().count() > 16 {
return Err("the session file's name is not a valid player name".into());
return Err("the session's name is not a valid player name".into());
}
let uuid = field("uuid")
.and_then(parse_uuid)
.ok_or("the session file has no valid uuid")?;
.ok_or("the session has no valid uuid")?;
let access_token = field("access_token")
.filter(|t| !t.is_empty())
.ok_or("the session file has no access token")?;
.ok_or("the session has no access token")?;
if field("user_type").is_some_and(|t| t != "msa") {
return Err("the session file is not a Microsoft account session".into());
return Err("the session is not a Microsoft account session".into());
}
Ok(Self {
name: name.to_owned(),
File renamed without changes.
@@ -3,7 +3,12 @@
use crate::Result;
use horizon_core::{noise::SimplexNoise, position::BlockPos, random::LegacyRandom};
use serde_json::Value;
use std::{collections::HashMap, fs, path::Path};
use std::{
collections::{BTreeMap, HashMap},
fs,
path::Path,
sync::Arc,
};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum GrassModifier {
@@ -102,6 +107,9 @@ pub struct BiomeInfo {
/// The biome's own fog colour attribute; otherwise the dimension's applies.
pub fog: Option<u32>,
pub water_fog: u32,
/// `BiomeGenerationSettings.features`: the placed feature ids of each
/// `GenerationStep.Decoration` step, in order.
pub features: Vec<Vec<String>>,
}
impl BiomeInfo {
/// `Biome.getHeightAdjustedTemperature`: the frozen modifier, then cooler
@@ -130,10 +138,27 @@ impl BiomeInfo {
.then(|| self.temperature_at(BlockPos::new(x, y, z), 63) < 0.15)
}
}
/// The game's biomes, by id. The built-in (vanilla) biomes come first, in
/// file order, and keep their ids whatever a world's data packs do; a pack's
/// new biomes follow them (`with_packs`). Those are the client's own ids:
/// the network registry's are `registry_order`'s.
#[derive(Clone)]
pub struct Biomes {
pub list: Vec<BiomeInfo>,
pub index: HashMap<String, u16>,
info_noise: SimplexNoise,
colormaps: Arc<Colormaps>,
/// The built-in biomes, the first of `list` unless packs replaced them.
builtin: Arc<Vec<BiomeInfo>>,
/// The data packs' biome files (new biomes and replaced built-in ones),
/// for the tables that read more of a biome than `BiomeInfo` keeps.
packs: BTreeMap<String, Arc<Value>>,
}
/// The grass, foliage and dry foliage colormaps.
struct Colormaps {
grass: Vec<u32>,
foliage: Vec<u32>,
dry: Vec<u32>,
}
fn color(v: &Value) -> Option<u32> {
match v {
@@ -166,11 +191,90 @@ fn sample(map: &[u32], temperature: f32, downfall: f32, fallback: u32) -> u32 {
let index = (y << 8 | x) as usize;
map.get(index).copied().unwrap_or(fallback)
}
impl BiomeInfo {
/// A biome file's (`worldgen/biome/<name>.json`) values, the colours it
/// leaves out sampled from the colormaps.
fn from_json(name: String, v: &Value, maps: &Colormaps) -> Self {
let effects = &v["effects"];
let attributes = &v["attributes"];
let temperature = v["temperature"].as_f64().unwrap_or(0.5) as f32;
let downfall = v["downfall"].as_f64().unwrap_or(0.5) as f32;
let modifier = match effects["grass_color_modifier"].as_str() {
Some("dark_forest") => GrassModifier::DarkForest,
Some("swamp") => GrassModifier::Swamp,
_ => GrassModifier::None,
};
let mut spawns: HashMap<String, Vec<SpawnEntry>> = HashMap::new();
let natural = &attributes["minecraft:gameplay/natural_mob_spawns"];
// A modifier object ({argument, modifier: overlay}) or the plain value;
// the overlay over the dimension's empty default is the biome's value.
let natural = if natural["argument"].is_object() {
&natural["argument"]
} else {
natural
};
let categories = &natural["spawns_by_category"];
for (category, entries) in categories.as_object().into_iter().flatten() {
let list = parse_entries(entries);
spawns.insert(category.clone(), list);
}
BiomeInfo {
spawns,
spawn_costs: parse_costs(&natural["spawn_costs"]),
creature_probability:
attributes["minecraft:gameplay/creature_world_gen_spawn_probability"]
.as_f64()
.unwrap_or(0.1) as f32,
increased_fire_burnout: attributes["minecraft:gameplay/increased_fire_burnout"]
.as_bool()
.unwrap_or(false),
temperature,
frozen: v["temperature_modifier"].as_str() == Some("frozen"),
downfall,
precipitation: v["has_precipitation"].as_bool().unwrap_or(true),
grass: color(&effects["grass_color"])
.unwrap_or_else(|| sample(&maps.grass, temperature, downfall, 0xFF00FF)),
modifier,
foliage: color(&effects["foliage_color"])
.unwrap_or_else(|| sample(&maps.foliage, temperature, downfall, 0x48B518)),
dry_foliage: color(&effects["dry_foliage_color"])
.unwrap_or_else(|| sample(&maps.dry, temperature, downfall, 0x5D4930)),
water: color(&effects["water_color"]).unwrap_or(0x3F76E4),
sky: color(&attributes["minecraft:visual/sky_color"]),
fog: color(&attributes["minecraft:visual/fog_color"]),
water_fog: color(&attributes["minecraft:visual/water_fog_color"]).unwrap_or(0x050533),
features: v["features"]
.as_array()
.into_iter()
.flatten()
.map(|step| {
step.as_array()
.into_iter()
.flatten()
.filter_map(Value::as_str)
.map(str::to_owned)
.collect()
})
.collect(),
name,
}
}
}
/// `Identifier.compareTo`: the path first, then the namespace.
pub fn identifier_order(a: &str, b: &str) -> std::cmp::Ordering {
fn split(id: &str) -> (&str, &str) {
let (ns, path) = id.split_once(':').unwrap_or(("minecraft", id));
(path, ns)
}
split(a).cmp(&split(b))
}
impl Biomes {
pub fn load(root: &Path) -> Result<Self> {
let grass = colormap(root, "grass")?;
let foliage = colormap(root, "foliage")?;
let dry = colormap(root, "dry_foliage")?;
let maps = Colormaps {
grass: colormap(root, "grass")?,
foliage: colormap(root, "foliage")?,
dry: colormap(root, "dry_foliage")?,
};
let mut list = Vec::new();
let mut names: Vec<_> = fs::read_dir(root.join("data/minecraft/worldgen/biome"))?
.filter_map(|e| e.ok())
@@ -186,57 +290,7 @@ impl Biomes {
.ok_or("bad biome")?
);
let v: Value = serde_json::from_slice(&fs::read(&path)?)?;
let effects = &v["effects"];
let attributes = &v["attributes"];
let temperature = v["temperature"].as_f64().unwrap_or(0.5) as f32;
let downfall = v["downfall"].as_f64().unwrap_or(0.5) as f32;
let modifier = match effects["grass_color_modifier"].as_str() {
Some("dark_forest") => GrassModifier::DarkForest,
Some("swamp") => GrassModifier::Swamp,
_ => GrassModifier::None,
};
let mut spawns: HashMap<String, Vec<SpawnEntry>> = HashMap::new();
let natural = &attributes["minecraft:gameplay/natural_mob_spawns"];
// A modifier object ({argument, modifier: overlay}) or the plain value;
// the overlay over the dimension's empty default is the biome's value.
let natural = if natural["argument"].is_object() {
&natural["argument"]
} else {
natural
};
let categories = &natural["spawns_by_category"];
for (category, entries) in categories.as_object().into_iter().flatten() {
let list = parse_entries(entries);
spawns.insert(category.clone(), list);
}
list.push(BiomeInfo {
spawns,
spawn_costs: parse_costs(&natural["spawn_costs"]),
creature_probability:
attributes["minecraft:gameplay/creature_world_gen_spawn_probability"]
.as_f64()
.unwrap_or(0.1) as f32,
increased_fire_burnout: attributes["minecraft:gameplay/increased_fire_burnout"]
.as_bool()
.unwrap_or(false),
temperature,
frozen: v["temperature_modifier"].as_str() == Some("frozen"),
downfall,
precipitation: v["has_precipitation"].as_bool().unwrap_or(true),
grass: color(&effects["grass_color"])
.unwrap_or_else(|| sample(&grass, temperature, downfall, 0xFF00FF)),
modifier,
foliage: color(&effects["foliage_color"])
.unwrap_or_else(|| sample(&foliage, temperature, downfall, 0x48B518)),
dry_foliage: color(&effects["dry_foliage_color"])
.unwrap_or_else(|| sample(&dry, temperature, downfall, 0x5D4930)),
water: color(&effects["water_color"]).unwrap_or(0x3F76E4),
sky: color(&attributes["minecraft:visual/sky_color"]),
fog: color(&attributes["minecraft:visual/fog_color"]),
water_fog: color(&attributes["minecraft:visual/water_fog_color"])
.unwrap_or(0x050533),
name,
});
list.push(BiomeInfo::from_json(name, &v, &maps));
}
let index = list
.iter()
@@ -244,11 +298,65 @@ impl Biomes {
.map(|(i, b)| (b.name.clone(), i as u16))
.collect();
Ok(Self {
builtin: Arc::new(list.clone()),
list,
index,
colormaps: Arc::new(maps),
packs: BTreeMap::new(),
info_noise: SimplexNoise::with_skipped_offsets(&mut LegacyRandom::new(2345), true),
})
}
/// The built-in biomes with a world's data packs' biome files (id →
/// JSON) over them, as `RegistryDataLoader` builds the registry: a file
/// for a built-in biome replaces it, keeping its id, and new biomes join
/// after the built-in ones, in registry order. Starts from the built-in
/// table whatever packs `self` has.
pub fn with_packs<'a>(&self, files: impl IntoIterator<Item = (&'a str, &'a Value)>) -> Self {
let mut out = self.builtin_only();
let mut files: Vec<(&str, &Value)> = files.into_iter().collect();
files.sort_by(|a, b| identifier_order(a.0, b.0));
for (name, json) in files {
let info = BiomeInfo::from_json(name.to_owned(), json, &out.colormaps);
match out.index.get(name) {
Some(&id) => out.list[usize::from(id)] = info,
None if out.list.len() < usize::from(u16::MAX) => {
out.index.insert(name.to_owned(), out.list.len() as u16);
out.list.push(info);
}
None => continue,
}
out.packs.insert(name.to_owned(), Arc::new(json.clone()));
}
out
}
/// The built-in biomes alone: this table without what packs gave it.
pub fn builtin_only(&self) -> Self {
let mut out = self.clone();
if self.packs.is_empty() {
return out;
}
out.list = self.builtin.to_vec();
let builtin = out.list.len();
out.index.retain(|_, id| usize::from(*id) < builtin);
out.packs.clear();
out
}
/// The data pack file a world's biome `name` came from, if a pack added
/// or replaced it.
pub fn pack_file(&self, name: &str) -> Option<&Value> {
self.packs.get(name).map(|v| &**v)
}
/// Whether data packs added or replaced any biome.
pub fn has_packs(&self) -> bool {
!self.packs.is_empty()
}
/// The biome names in the network registry's order: `RegistryDataLoader`
/// sorts a registry's files by `Identifier` (path, then namespace).
pub fn registry_order(&self) -> Vec<&str> {
let mut names: Vec<&str> = self.list.iter().map(|b| b.name.as_str()).collect();
names.sort_by(|a, b| identifier_order(a, b));
names
}
pub fn id(&self, name: &str) -> u16 {
self.index
.get(name)
@@ -256,6 +364,10 @@ impl Biomes {
.copied()
.unwrap_or(0)
}
/// Whether the game has the biome `name`.
pub fn contains(&self, name: &str) -> bool {
self.index.contains_key(name)
}
pub fn get(&self, id: u16) -> &BiomeInfo {
&self.list[usize::from(id).min(self.list.len() - 1)]
}
@@ -340,6 +452,88 @@ pub fn blend_rgb(weights: &[(u16, f64)], mut value: impl FnMut(u16) -> [f32; 3])
result.unwrap_or([0.0; 3])
}
#[cfg(test)]
mod pack_tests {
use super::*;
use serde_json::json;
/// A built-in table of `names` (no colormaps: sampled colours fall back).
fn builtin(names: &[&str]) -> Biomes {
let maps = Colormaps {
grass: Vec::new(),
foliage: Vec::new(),
dry: Vec::new(),
};
let list: Vec<BiomeInfo> = names
.iter()
.map(|n| BiomeInfo::from_json((*n).to_owned(), &json!({}), &maps))
.collect();
Biomes {
builtin: Arc::new(list.clone()),
index: list
.iter()
.enumerate()
.map(|(i, b)| (b.name.clone(), i as u16))
.collect(),
list,
info_noise: SimplexNoise::with_skipped_offsets(&mut LegacyRandom::new(2345), true),
colormaps: Arc::new(maps),
packs: BTreeMap::new(),
}
}
#[test]
fn registry_order_is_path_then_namespace() {
use std::cmp::Ordering::*;
assert_eq!(identifier_order("minecraft:a", "minecraft:b"), Less);
assert_eq!(identifier_order("z:a", "a:b"), Less);
assert_eq!(identifier_order("b:same", "a:same"), Greater);
assert_eq!(identifier_order("minecraft:a", "minecraft:a_b"), Less);
assert_eq!(identifier_order("minecraft:a-b", "minecraft:a"), Greater);
}
#[test]
fn packs_replace_in_place_and_add_after_the_builtin_biomes() {
let base = builtin(&["minecraft:meadow", "minecraft:plains", "minecraft:river"]);
let red = json!({"temperature": 2.0, "effects": {"water_color": "#ff0000"}});
let hot = json!({"effects": {"grass_color": 0x123456}});
let files = [
("pack:zeta", &hot),
("minecraft:molten_peaks", &hot),
("minecraft:river", &red),
];
let world = base.with_packs(files);
// The built-in ids stay; the river takes the pack's values.
assert_eq!(world.id("minecraft:plains"), 1);
assert_eq!(world.id("minecraft:river"), 2);
assert_eq!(world.get(2).water, 0xFF0000);
assert_eq!(world.get(2).temperature, 2.0);
// New biomes follow, in registry order.
assert_eq!(world.id("minecraft:molten_peaks"), 3);
assert_eq!(world.id("pack:zeta"), 4);
assert_eq!(world.get(3).grass, 0x123456);
assert!(world.pack_file("minecraft:river").is_some());
assert!(world.pack_file("minecraft:plains").is_none());
// The network registry sorts every name by path, then namespace.
assert_eq!(
world.registry_order(),
[
"minecraft:meadow",
"minecraft:molten_peaks",
"minecraft:plains",
"minecraft:river",
"pack:zeta"
]
);
// Another world starts again from the built-in biomes.
let other = world.with_packs([]);
assert_eq!(other.list.len(), 3);
assert!(!other.contains("minecraft:molten_peaks"));
assert_eq!(other.get(2).water, 0x3F76E4);
assert!(!other.has_packs());
}
}
#[cfg(test)]
mod blend_tests {
use super::*;
@@ -23,7 +23,7 @@
//! Which states tick is a table by state id, built by the game from the block
//! registry; whether a slot is made is decided when the slot is promoted at
//! the start of the next tick, from the block state then.
use crate::chunk::block_entity::BlockEntity;
use crate::chunk::block_entity::{BlockEntity, ModContainer};
use crate::fast_hash::{FxHashMap, FxHashSet};
use std::collections::HashMap;
use std::sync::Arc;
@@ -64,8 +64,15 @@ pub struct BlockEntities {
/// Positions whose block entity `take` took and `put` has not brought
/// back.
out: FxHashSet<Pos>,
/// Positions a loaded chunk gave a new block entity over an old one: at
/// the next tick their slot stays only if their block state ticks
/// (`updateBlockEntityTicker`: rebind, or remove without a ticker).
replaced: FxHashSet<Pos>,
/// `Ticking` by block state id; empty until the game builds it.
states: Vec<Ticking>,
/// The containers of mod blocks' block entities, by block state id;
/// built with `states`.
mod_containers: FxHashMap<u32, Arc<ModContainer>>,
}
impl std::ops::Deref for BlockEntities {
@@ -148,6 +155,13 @@ impl BlockEntities {
pub fn ticking(&self, state: u32) -> Ticking {
self.states.get(state as usize).copied().unwrap_or_default()
}
pub fn set_mod_containers(&mut self, containers: FxHashMap<u32, Arc<ModContainer>>) {
self.mod_containers = containers;
}
/// The container of a mod block's block entity in block state `state`.
pub fn mod_container(&self, state: u32) -> Option<&Arc<ModContainer>> {
self.mod_containers.get(&state)
}
fn next(&mut self) -> u32 {
let n = self.next_slot;
@@ -239,15 +253,20 @@ impl BlockEntities {
self.added.clear();
self.pending.clear();
self.out.clear();
self.replaced.clear();
self.freed = false;
}
/// A loaded or generated chunk's block entities, in the order the chunk
/// lists them; they tick in its block entity map's order
/// (`registerAllBlockEntitiesAfterLevelLoad`).
/// (`registerAllBlockEntitiesAfterLevelLoad`). One replacing an entity
/// already at its position (`LevelChunk.setBlockEntity`) drops that one;
/// its slot is decided by the block state at the next tick.
pub fn load_chunk(&mut self, entities: Vec<(Pos, BlockEntity)>) {
let order = chunk_map_order(entities.iter().map(|(p, _)| *p));
for (p, e) in entities {
self.map.insert(p, e);
if self.map.insert(p, e).is_some() {
self.replaced.insert(p);
}
}
for p in order {
self.add(p);
@@ -291,6 +310,11 @@ impl BlockEntities {
self.live.insert(pos, slot);
Arc::make_mut(&mut self.tickers).push((pos, slot));
}
for pos in std::mem::take(&mut self.replaced) {
if self.live.contains_key(&pos) && self.ticking(state(pos)) != Ticking::Yes {
self.cancel(&pos);
}
}
if !std::mem::take(&mut self.freed) && self.out.is_empty() {
return Arc::clone(&self.tickers);
}
@@ -390,4 +414,33 @@ mod tests {
let order = chunk_map_order(many.iter().copied().chain(many.iter().copied()));
assert_eq!(order.len(), 100);
}
/// A chunk loaded over a block entity already there: the new one replaces
/// it, and keeps the old slot only while its block state ticks.
#[test]
fn loaded_chunk_replaces_block_entity_by_state() {
let mut b = level();
let mut s = HashMap::new();
let (p, q) = ((0, 0, 0), (1, 0, 0));
for pos in [p, q] {
s.insert(pos, 1);
}
// Two chunks, so the order is the order they load in.
b.load_chunk(vec![(p, hopper())]);
b.load_chunk(vec![(q, hopper())]);
assert_eq!(order(&mut b, &s), [p, q]);
// Replaced where the state ticks: the slot stays in its place.
b.load_chunk(vec![(p, hopper())]);
assert_eq!(order(&mut b, &s), [p, q]);
// Replaced where the state has no ticker: the slot is freed, and the
// new block entity is the one in the map.
s.insert(p, 2);
b.load_chunk(vec![(p, BlockEntity::Chest(vec![None; 27]))]);
assert_eq!(order(&mut b, &s), [q]);
assert!(matches!(b.get(&p), Some(BlockEntity::Chest(items)) if items.len() == 27));
// Ticking again later appends it, as any new ticker does.
s.insert(p, 1);
b.state_changed(p, 2, 1);
assert_eq!(order(&mut b, &s), [q, p]);
}
}
@@ -654,10 +654,20 @@ fn sign_text_calls(
.collect()
}
/// How far the text of an outlined call sits in front of its outline, in
/// text units (`Font.DisplayMode.POLYGON_OFFSET`, see `draw_text`).
const TEXT_OVER_OUTLINE: f32 = 0.05;
/// Draws a text call with the font atlas: 8x8 glyph cells, the outline as
/// eight one-pixel offsets under the text.
///
/// `TextFeatureRenderer.renderText` draws the outline glyphs NORMAL and the
/// text over them POLYGON_OFFSET, so the coplanar text always wins the depth
/// test. Here both share one mesh, so the text is lifted a hair towards the
/// reader instead (a twentieth of a font pixel, invisible) rather than
/// z-fighting with the outline under it.
fn draw_text(scene: &Scene, call: &TextCall, out: &mut Vec<Vertex>) {
let mut emit = |dx: f32, dy: f32, argb: u32| {
let mut emit = |dx: f32, dy: f32, z: f32, argb: u32| {
let mut x = call.x + dx;
let y0 = call.y + dy;
for ch in call.text.chars() {
@@ -672,7 +682,7 @@ fn draw_text(scene: &Scene, call: &TextCall, out: &mut Vec<Vertex>) {
normal: [0; 4],
pos: call
.matrix
.transform_point3(Vec3::new(cx, cy, 0.0))
.transform_point3(Vec3::new(cx, cy, z))
.to_array(),
uv,
layer: 0,
@@ -697,10 +707,15 @@ fn draw_text(scene: &Scene, call: &TextCall, out: &mut Vec<Vertex>) {
(0.0, 1.0),
(1.0, 1.0),
] {
emit(dx, dy, call.outline);
emit(dx, dy, 0.0, call.outline);
}
}
emit(0.0, 0.0, call.color);
let z = if call.outline != 0 {
TEXT_OVER_OUTLINE
} else {
0.0
};
emit(0.0, 0.0, z, call.color);
}
/// Block-entity fields the level ticks, at one position (vanilla's own field
@@ -850,13 +865,23 @@ const VIEW_DISTANCE: f64 = 64.0;
impl BlockEntityRenderer {
pub fn load(root: &Path, registry: &BlockRegistry) -> crate::Result<Self> {
let visibility = crate::models::load_visibility(root, registry)?;
Self::load_with_visibility(root, registry, visibility)
}
/// [`BlockEntityRenderer::load`] over the face visibility the meshes use
/// (`MeshTables::visibility`): a clone shares its shapes and their faces.
pub fn load_with_visibility(
root: &Path,
registry: &BlockRegistry,
visibility: FaceVisibility,
) -> crate::Result<Self> {
let all: Value = serde_json::from_slice(&std::fs::read(root.join("entity-models.json"))?)?;
let models = MODELS
.iter()
.filter_map(|(key, layer)| all.get(*layer).map(|v| (*key, ModelPart::parse(v))))
.collect();
let mut renderer = Self::new(models, registry);
renderer.visibility = Some(Mutex::new(crate::models::load_visibility(root, registry)?));
renderer.visibility = Some(Mutex::new(visibility));
// Sherd items: provides_pottery_pattern -> decorated_pot_pattern asset id.
if let Ok(bytes) = std::fs::read(root.join("items.json")) {
let items: Value = serde_json::from_slice(&bytes)?;
@@ -1089,7 +1114,11 @@ impl BlockEntityRenderer {
pub fn renders(&self, state: u32) -> bool {
!matches!(self.kind(state), Kind::None)
}
/// ChestRenderer and BannerRenderer submit their animated models for crumbling.
/// `submitCrumblingOverlay` of ChestRenderer.submit, BannerRenderer.submit,
/// SkullBlockRenderer.submitSkull, ShulkerBoxRenderer.submit,
/// BellRenderer.submit, LecternRenderer.submit (the book) and
/// CopperGolemStatueBlockRenderer.submit: the model drawn again under the
/// destroy stage.
pub fn render_breaking(
&self,
scene: &Scene,
@@ -1098,7 +1127,16 @@ impl BlockEntityRenderer {
stage: [f32; 2],
out: &mut Vec<Vertex>,
) {
if !matches!(self.kind(state), Kind::Banner { .. } | Kind::Chest { .. }) {
if !matches!(
self.kind(state),
Kind::Banner { .. }
| Kind::Chest { .. }
| Kind::Skull { .. }
| Kind::Shulker { .. }
| Kind::Bell
| Kind::Lectern { .. }
| Kind::CopperGolem { .. }
) {
return;
}
let origin = (DVec3::new(f64::from(pos[0]), f64::from(pos[1]), f64::from(pos[2]))
@@ -2258,6 +2296,69 @@ mod tests {
assert_eq!(out[2].uv, [0.5, 0.5]);
}
/// TextFeatureRenderer.renderText: an outlined call's text is drawn over
/// its outline (Font.DisplayMode.POLYGON_OFFSET), never level with it,
/// and plain text stays on the sign's plane.
#[test]
fn outlined_sign_text_sits_in_front_of_its_outline() {
let world = World::new(BlockStateId(0), 1);
let sprites = HashMap::new();
let glyph = |_: char| {
let rect = Rect {
u0: 0.0,
v0: 0.0,
u1: 0.0625,
v1: 0.0625,
w: 8,
h: 8,
};
(Some(rect), 6.0)
};
let scene = Scene {
camera: DVec3::ZERO,
orientation: Quat::IDENTITY,
anim: &|_| BlockAnim::default(),
texts: None,
partial: 0.0,
game_time: 0,
max_y: 320,
world: &world,
sprites: &sprites,
nether: false,
lid: &|_| 0.0,
sign: &|_| None,
banner: &|_| Vec::new(),
skull: &|_| None,
glyph: &glyph,
glyphs: 0,
};
let mut call = TextCall {
matrix: Mat4::IDENTITY,
x: 0.0,
y: 0.0,
text: "Hi".into(),
color: 0xFFFF_FF00,
outline: 0xFF33_3300,
light: 240 | 240 << 8,
};
let mut out = Vec::new();
draw_text(&scene, &call, &mut out);
// Eight outline copies, then the text, four corners a glyph.
assert_eq!(out.len(), 9 * 2 * 4);
let (outline, text) = out.split_at(8 * 2 * 4);
let front = outline.iter().map(|v| v.pos[2]).fold(f32::MIN, f32::max);
assert!(
text.iter().all(|v| v.pos[2] > front),
"text z {:?} not in front of the outline at {front}",
text.iter().map(|v| v.pos[2]).collect::<Vec<_>>()
);
call.outline = 0;
out.clear();
draw_text(&scene, &call, &mut out);
assert_eq!(out.len(), 2 * 4);
assert!(out.iter().all(|v| v.pos[2] == 0.0));
}
#[test]
fn cached_sign_text_matches_and_follows_changes() {
let renderer = BlockEntityRenderer {
@@ -2285,6 +2386,7 @@ mod tests {
lines: [line.into(), "two".into(), String::new(), "four".into()],
color: 3,
glowing,
..Default::default()
};
let first_text = crate::signs::SignData {
front: text("one", false),
@@ -7,6 +7,7 @@ use crate::{
items::{ItemStack, Items},
providers::Providers,
};
use horizon_world::loot::PackOverlay;
use serde_json::Value;
use std::{collections::HashSet, fs, path::Path};
@@ -14,14 +15,19 @@ pub use horizon_client_core::potions::{
BASE_POTION_COLOR, Category, EFFECTS, EffectDef, POTIONS, PotionDef, effect, namespaced,
potion, potion_color,
};
/// `MobEffectUtil.formatDuration` at 20 TPS: m:ss or h:mm:ss.
/// `MobEffectUtil.formatDuration` at 20 TPS.
pub fn format_duration(ticks: i32) -> String {
let secs = ticks / 20;
format_tick_duration(ticks, 20.0)
}
/// `StringUtil.formatTickDuration`: mm:ss, or hh:mm:ss from an hour up, every
/// field two digits.
pub fn format_tick_duration(ticks: i32, tick_rate: f32) -> String {
let secs = (ticks as f32 / tick_rate).floor() as i32;
let (h, m, s) = (secs / 3600, secs / 60 % 60, secs % 60);
if h > 0 {
format!("{h}:{m:02}:{s:02}")
format!("{h:02}:{m:02}:{s:02}")
} else {
format!("{m}:{s:02}")
format!("{m:02}:{s:02}")
}
}
/// `item.minecraft.potion.effect.<name>` style translation key for a potion item.
@@ -30,6 +36,22 @@ pub fn potion_name_key(item: &str, potion: Option<&str>) -> String {
let suffix = potion.and_then(self::potion).map_or("empty", |p| p.name);
format!("item.minecraft.{prefix}.effect.{suffix}")
}
/// `PotionContents.getAllEffects`: the potion's effects, then the stack's
/// `custom_effects` (id, duration, amplifier). Unknown effect ids are dropped.
pub fn contents_effects(stack: &ItemStack) -> Vec<(&'static str, i32, i32)> {
let potion = stack.potion().and_then(self::potion);
let custom = stack.custom_potion_effects();
potion
.map_or(&[][..], |d| d.effects)
.iter()
.copied()
.chain(
custom
.iter()
.filter_map(|(id, duration, amp)| Some((effect(id)?.id, *duration, *amp))),
)
.collect()
}
/// Roman amplifier suffix (`potion.potency.N`) and duration lines of a potion.
pub fn tooltip(
potion: Option<&str>,
@@ -45,20 +67,30 @@ pub fn tooltip(
Category::Neutral,
)];
}
def.effects
.iter()
effect_lines(def.effects.iter().copied(), scale, translate)
}
/// `PotionContents.addPotionTooltip`'s line per effect: its name, potency and
/// scaled duration, for (effect id, duration, amplifier) entries.
pub fn effect_lines<'a>(
effects: impl IntoIterator<Item = (&'a str, i32, i32)>,
scale: f32,
translate: &dyn Fn(&str) -> Option<String>,
) -> Vec<(String, Category)> {
effects
.into_iter()
.map(|(e, duration, amp)| {
let e = e.trim_start_matches("minecraft:");
let mut line =
translate(&format!("effect.minecraft.{e}")).unwrap_or_else(|| (*e).to_owned());
if *amp > 0 {
translate(&format!("effect.minecraft.{e}")).unwrap_or_else(|| e.to_owned());
if amp > 0 {
line.push(' ');
line.push_str(
&translate(&format!("potion.potency.{amp}"))
.unwrap_or_else(|| crate::enchant::level_name(*amp as u32 + 1)),
.unwrap_or_else(|| crate::enchant::level_name(amp as u32 + 1)),
);
}
let instant = effect(e).is_some_and(|d| d.instant);
let scaled = (*duration as f32 * scale) as i32;
let scaled = (duration as f32 * scale) as i32;
if !instant && scaled > 20 {
line = format!("{line} ({})", format_duration(scaled));
}
@@ -127,20 +159,41 @@ pub struct Brewing {
pub inputs: HashSet<String>,
}
impl Brewing {
pub fn load(root: &Path, items: &Items) -> Self {
let mut files: Vec<_> = fs::read_dir(root.join("data/minecraft/recipe/brewing"))
.into_iter()
.flatten()
.flatten()
.map(|e| e.path())
.collect();
files.sort();
let mut recipes = Vec::new();
for p in files {
let Ok(bytes) = fs::read(&p) else { continue };
let Ok(v) = serde_json::from_slice::<Value>(&bytes) else {
pub fn load(root: &Path, items: &Items, overlay: &PackOverlay) -> Self {
let mut sources: Vec<(String, Value)> =
fs::read_dir(root.join("data/minecraft/recipe/brewing"))
.into_iter()
.flatten()
.flatten()
.filter_map(|e| {
let bytes = fs::read(e.path()).ok()?;
let v = serde_json::from_slice::<Value>(&bytes).ok()?;
Some((
format!(
"minecraft:brewing/{}",
e.file_name().to_string_lossy().strip_suffix(".json")?
),
v,
))
})
.collect();
// The world's packs' brewing recipes over the game's own: the top
// pack's file for each id replaces it (`SimpleJsonResourceReloadListener`).
for (id, files) in overlay.kind("recipe") {
if !id.starts_with("minecraft:brewing/") {
continue;
}
let Ok(v) = serde_json::from_slice::<Value>(PackOverlay::top(files)) else {
continue;
};
match sources.iter_mut().find(|(n, _)| n == id) {
Some(slot) => slot.1 = v,
None => sources.push((id.to_owned(), v)),
}
}
sources.sort_by(|a, b| a.0.cmp(&b.0));
let mut recipes = Vec::new();
for (id, v) in sources {
if v["type"] != "minecraft:brewing" {
continue;
}
@@ -154,10 +207,7 @@ impl Brewing {
output = output.with_potion(potion);
}
recipes.push(BrewingRecipe {
id: format!(
"minecraft:brewing/{}",
p.file_stem().and_then(|s| s.to_str()).unwrap_or("")
),
id,
input: PotionIngredient::parse(&v["input"], items),
reagent: PotionIngredient::parse(&v["reagent"], items),
output,
@@ -317,10 +367,14 @@ mod tests {
potion_name_key("minecraft:splash_potion", Some("minecraft:long_swiftness")),
"item.minecraft.splash_potion.effect.swiftness"
);
assert_eq!(format_duration(9600), "8:00");
assert_eq!(format_duration(450), "0:22");
assert_eq!(format_duration(9600), "08:00");
assert_eq!(format_duration(450), "00:22");
assert_eq!(format_duration(140), "00:07");
assert_eq!(format_duration(1800), "01:30");
assert_eq!(format_duration(72000), "01:00:00");
assert_eq!(format_tick_duration(9600, 40.0), "04:00");
let t = tooltip(Some("minecraft:strong_poison"), 1.0, &|_| None);
assert_eq!(t[0].0, "poison II (0:21)");
assert_eq!(t[0].0, "poison II (00:21)");
assert_eq!(POTIONS.len(), 46);
}
@@ -377,4 +431,23 @@ mod tests {
stand.tick(&brewing, &items, &providers);
assert_eq!(stand.brew_time, 0);
}
/// #3312: `PotionContents.getPotionDescription` names the effects with
/// translatable components, so a Russian game reads them from ru_ru.json
/// (the prepared one; skipped without it), as the tooltip's other lines.
#[test]
fn effect_lines_read_the_selected_language() {
let root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../.local/horizon-client")
.join(horizon_core::VANILLA_VERSION);
if !root.join("assets/minecraft/lang/ru_ru.json").exists() {
return;
}
let ru = crate::lang::Language::load(&[&root], "ru_ru");
let translate = |key: &str| ru.get(key).map(str::to_owned);
let lines = tooltip(Some("minecraft:turtle_master"), 0.125, &translate);
assert_eq!(lines.len(), 2);
assert!(lines[0].0.starts_with("Замедление IV ("), "{lines:?}");
assert!(lines[1].0.starts_with("Сопротивление III ("), "{lines:?}");
}
}
@@ -627,6 +627,34 @@ pub fn perspective(
m[11] = -1.0;
m
}
/// `Frustum.getNearPlaneBounds`: the box around the four corners of the
/// culling frustum's NDC `z = -1` plane, relative to the camera (the caller
/// moves it to the eye). The culling projection is `Camera.update`'s
/// `setPerspective(fov, aspect, 0.05, depthFar, zZeroToOne)`.
pub fn near_plane_bounds(
fov_degrees: f32,
width: f32,
height: f32,
yaw: f32,
pitch: f32,
far: f32,
) -> ([f64; 3], [f64; 3]) {
let projection =
glam::Mat4::from_cols_array(&perspective(fov_degrees, width, height, 0.05, far, true));
// Frustum.calculateFrustum: projection * modelView.
let clip_to_world = (projection * view_matrix(yaw, pitch)).inverse();
let mut min = [f64::INFINITY; 3];
let mut max = [f64::NEG_INFINITY; 3];
for (x, y) in [(-1.0, -1.0), (1.0, -1.0), (1.0, 1.0), (-1.0, 1.0)] {
let p = clip_to_world * glam::Vec4::new(x, y, -1.0, 1.0);
let p = [p.x / p.w, p.y / p.w, p.z / p.w];
for k in 0..3 {
min[k] = min[k].min(f64::from(p[k]));
max[k] = max[k].max(f64::from(p[k]));
}
}
(min, max)
}
/// `GameRenderer.render3dHud`: the first-person hand and held items are
/// drawn with their own projection at `hudFov` (70 degrees, changed only by
/// death and fluids), never the FOV option or the sprint, speed and bow
@@ -653,18 +681,57 @@ pub fn modify_fov_based_on_death_or_fluid(
fov
}
/// `ClientAvatarState`: the walked distance and view bob of a player.
/// `ClientAvatarState`: the walked distance, view bob and cloak of a player.
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct AvatarBob {
pub walk_dist: f32,
pub walk_dist_o: f32,
pub bob: f32,
pub bob_o: f32,
/// `xCloak`, `yCloak`, `zCloak`: a point trailing the player, which the cape leans from.
pub cloak: [f64; 3],
pub cloak_o: [f64; 3],
}
impl AvatarBob {
/// `ClientAvatarState.tick` (at the start of `AbstractClientPlayer.tick`).
pub fn tick(&mut self) {
/// `ClientAvatarState.tick` (at the start of `AbstractClientPlayer.tick`)
/// with the player's position then: `moveCloak` eases the cloak a quarter
/// of the way there, or puts it there from 10 blocks away.
pub fn tick(&mut self, position: [f64; 3]) {
self.walk_dist_o = self.walk_dist;
self.cloak_o = self.cloak;
for i in 0..3 {
let d = position[i] - self.cloak[i];
if d > 10.0 || d < -10.0 {
self.cloak[i] = position[i];
self.cloak_o[i] = position[i];
} else {
self.cloak[i] += d * 0.25;
}
}
}
/// `AvatarRenderer.extractCapeState`: the cape's `capeFlap`, `capeLean`
/// and `capeLean2` (degrees) for the player at `position` (interpolated)
/// with its body turned `body_yaw` and its `fallFlyingScale`.
pub fn cape(
&self,
partial: f32,
position: [f64; 3],
body_yaw: f32,
fall_flying_scale: f32,
) -> [f32; 3] {
let d: [f64; 3] = std::array::from_fn(|i| {
lerp_d(f64::from(partial), self.cloak_o[i], self.cloak[i]) - position[i]
});
let rad = f64::from(body_yaw * DEG_TO_RAD);
let (sin, cos) = (f64::from(trig::sin(rad)), f64::from(-trig::cos(rad)));
let mut flap = (d[1] as f32 * 10.0).clamp(-6.0, 32.0);
let lean = ((d[0] * sin + d[2] * cos) as f32 * 100.0 * (1.0 - fall_flying_scale))
.clamp(0.0, 150.0);
let lean2 = ((d[0] * cos - d[2] * sin) as f32 * 100.0).clamp(-20.0, 20.0);
let bob = self.interpolated_bob(partial);
let walk = lerp(partial, self.walk_dist_o, self.walk_dist);
flap += trig::sin(f64::from(walk * 6.0)) * 32.0 * bob;
[flap, lean, lean2]
}
/// `LocalPlayer.move`: `addWalkedDistance(Mth.length(dx, dz) * 0.6F)`
/// with the float horizontal displacement.
@@ -795,6 +862,36 @@ pub fn spinning_effect_matrix(intensity: f32, angle_degrees: f32) -> glam::Mat4
#[cfg(test)]
mod tests {
#[test]
fn near_plane_bounds_sit_half_a_near_distance_ahead_of_the_eye() {
// Facing +Z (yaw 0): the zero-to-one culling projection puts NDC
// z = -1 at -far * near / (2 far - near) in front of the camera.
let far = 512.0;
let (min, max) = super::near_plane_bounds(70.0, 1600.0, 900.0, 0.0, 0.0, far);
let depth = f64::from(far * 0.05 / (2.0 * far - 0.05));
assert!((min[2] - depth).abs() < 1e-4 && (max[2] - depth).abs() < 1e-4);
let half_height = depth * 35.0f64.to_radians().tan();
assert!((max[1] - half_height).abs() < 1e-4 && (min[1] + half_height).abs() < 1e-4);
let half_width = half_height * 1600.0 / 900.0;
assert!((max[0] - half_width).abs() < 1e-3 && (min[0] + half_width).abs() < 1e-3);
}
/// `moveCloak` trails a quarter of the way, jumps from 10 blocks away; the
/// cape leans back as the player walks forward (south, body yaw 0).
#[test]
fn the_cloak_trails_the_player() {
let mut a = super::AvatarBob::default();
a.tick([0.0, 64.0, 0.0]);
assert_eq!((a.cloak, a.cloak_o), ([0.0, 64.0, 0.0], [0.0, 64.0, 0.0]));
a.tick([0.0, 64.0, 1.0]);
assert_eq!(a.cloak, [0.0, 64.0, 0.25]);
let [flap, lean, lean2] = a.cape(1.0, [0.0, 64.0, 1.0], 0.0, 0.0);
assert_eq!((flap, lean, lean2), (0.0, 75.0, 0.0));
// Falling lifts the flap, at most 32 degrees.
assert_eq!(a.cape(1.0, [0.0, 60.0, 0.25], 0.0, 0.0)[0], 32.0);
// Gliding takes the lean away.
assert_eq!(a.cape(1.0, [0.0, 64.0, 1.0], 0.0, 1.0)[1], 0.0);
}
#[test]
fn depth_far_covers_the_cloud_range() {
// renderDistance 2 with the default Cloud Distance (128 chunks).
@@ -21,6 +21,12 @@ const COMMAND_HISTORY_FILE: &str = "command_history.txt";
const MAX_PERSISTED_COMMAND_HISTORY: usize = 50;
/// GuiMessageTag.systemSinglePlayer's indicator colour.
pub const SYSTEM_INDICATOR: u32 = 0xD0D0D0;
/// A player message's signature (MessageSignature), which names it to a
/// server's deletion.
pub type Signature = horizon_protocol::secure_chat::Signature;
/// How long a message shows before a deletion replaces it
/// (`ChatComponent.deleteMessageOrDelay`: `addedTime + 60`).
const DELETION_DELAY: u64 = 60;
/// `ChatVisiblity`: the Chat option's three values.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
@@ -165,6 +171,8 @@ struct Message {
content: Text,
indicator: Option<u32>,
source: ChatSource,
/// `GuiMessage.signature`: a signed player message's, for deletion.
signature: Option<Box<Signature>>,
}
/// A wrapped line (`GuiMessage.Line`).
#[derive(Clone, Debug)]
@@ -205,6 +213,9 @@ pub struct Chat {
pub received: u64,
/// The Chat option's `visibleMessageFilter` (`set_abilities`).
abilities: ChatAbilities,
/// `ChatComponent.messageDeletionQueue`: deletions of messages still
/// too new to replace, with the tick from which they may be.
deletions: Vec<(Box<Signature>, u64)>,
}
impl Chat {
pub fn lines_per_page(&self, focused: bool) -> usize {
@@ -227,6 +238,38 @@ impl Chat {
) {
self.add_message(&atlas.font, content, indicator, source, ticks, focused);
}
/// `add` of a player message with its signature, which a server's
/// deletion names it by (`GuiMessage.signature`).
pub fn add_signed(
&mut self,
atlas: &HudAtlas,
content: Text,
signature: Option<Signature>,
ticks: u64,
focused: bool,
) {
self.add_signed_message(&atlas.font, content, signature, ticks, focused);
}
fn add_signed_message(
&mut self,
font: &crate::font::Font,
content: Text,
signature: Option<Signature>,
ticks: u64,
focused: bool,
) {
self.push_message(
font,
Message {
added: ticks,
content,
indicator: None,
source: ChatSource::Player,
signature: signature.map(Box::new),
},
focused,
);
}
fn add_message(
&mut self,
font: &crate::font::Font,
@@ -236,20 +279,28 @@ impl Chat {
ticks: u64,
focused: bool,
) {
if !self.abilities.shows(source) {
self.push_message(
font,
Message {
added: ticks,
content,
indicator,
source,
signature: None,
},
focused,
);
}
fn push_message(&mut self, font: &crate::font::Font, message: Message, focused: bool) {
if !self.abilities.shows(message.source) {
return;
}
// ChatComponent.logChatMessage: every message shown is in the log.
crate::logging::write_only(log::Level::Info, &format!("[CHAT] {}", content.plain()));
let plain = message.content.plain();
crate::logging::write_only(log::Level::Info, &format!("[CHAT] {plain}"));
if self.log {
println!("[CHAT] {}", content.plain());
println!("[CHAT] {plain}");
}
let message = Message {
added: ticks,
content,
indicator,
source,
};
self.add_lines(font, &message, focused);
self.messages.insert(0, message);
self.messages.truncate(100);
@@ -319,8 +370,66 @@ impl Chat {
pub fn clear(&mut self) {
self.messages.clear();
self.lines.clear();
self.deletions.clear();
self.scroll = 0;
}
/// `ChatComponent.deleteMessage`: the server deleted a message. One
/// shown for 60 ticks becomes the grey "deleted" marker now; a newer
/// one waits until then (`messageDeletionQueue`).
pub fn delete_message(&mut self, atlas: &HudAtlas, signature: &Signature, ticks: u64) {
self.delete_message_with(&atlas.font, signature, ticks);
}
fn delete_message_with(&mut self, font: &crate::font::Font, signature: &Signature, ticks: u64) {
if let Some(delayed) = self.delete_or_delay(font, signature, ticks) {
self.deletions.push((Box::new(*signature), delayed));
}
}
/// `ChatComponent.tick` (`processMessageDeletionQueue`): the delayed
/// deletions whose message has shown long enough.
pub fn tick(&mut self, atlas: &HudAtlas, ticks: u64) {
self.process_deletions(&atlas.font, ticks);
}
fn process_deletions(&mut self, font: &crate::font::Font, ticks: u64) {
let mut deletions = std::mem::take(&mut self.deletions);
deletions.retain(|(signature, after)| {
ticks < *after || self.delete_or_delay(font, signature, ticks).is_some()
});
self.deletions = deletions;
}
/// `ChatComponent.deleteMessageOrDelay`: the newest message with this
/// signature, replaced by `createDeletedMarker` once 60 ticks have
/// passed since it was added; otherwise the tick to retry from.
fn delete_or_delay(
&mut self,
font: &crate::font::Font,
signature: &Signature,
ticks: u64,
) -> Option<u64> {
let message = self
.messages
.iter_mut()
.find(|m| m.signature.as_deref() == Some(signature))?;
let deletable = message.added + DELETION_DELAY;
if ticks < deletable {
return Some(deletable);
}
*message = Message {
added: message.added,
content: Self::deleted_marker(),
indicator: Some(SYSTEM_INDICATOR),
source: ChatSource::Server,
signature: None,
};
// refreshTrimmedMessages.
self.rewrap(font);
None
}
/// `ChatComponent.DELETED_CHAT_MESSAGE`: grey, italic.
fn deleted_marker() -> Text {
Text::translate("chat.deleted_marker", &[])
.with_color(text::GRAY)
.italic()
}
/// `new CommandHistory(gameFolder)`: the commands kept from earlier runs
/// start the chat's history (`ChatComponent`'s constructor).
pub fn load_history(&mut self, game_folder: &std::path::Path) {
@@ -653,6 +762,42 @@ pub struct Commands<'a> {
pub registries: &'a Registries,
}
/// `CommandSuggestions`' constructor arguments: how a screen's suggestions
/// parse and where they are drawn.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct SuggestLayout {
/// `commandsOnly`: the input is a command without a leading `/`.
pub commands_only: bool,
/// `onlyShowIfCursorPastError`: completions from the command's start,
/// not only past the `/`.
pub only_past_error: bool,
/// `suggestionLineLimit`.
pub line_limit: usize,
/// `anchorToBottom`: the list and usage over a box at the bottom, else
/// under one at the top (y 72).
pub anchor_to_bottom: bool,
/// `fillColor`: behind the list and the usage lines.
pub fill: i32,
}
impl SuggestLayout {
/// ChatScreen.init's `CommandSuggestions`.
pub const CHAT: Self = Self {
commands_only: false,
only_past_error: false,
line_limit: 10,
anchor_to_bottom: true,
fill: FILL,
};
/// AbstractCommandBlockEditScreen.init's `CommandSuggestions`.
pub const COMMAND_BLOCK: Self = Self {
commands_only: true,
only_past_error: true,
line_limit: 7,
anchor_to_bottom: false,
fill: i32::MIN,
};
}
/// The open chat screen: vanilla's `ChatScreen` with its `EditBox` and
/// `CommandSuggestions` (argument colouring, usage lines, parse errors,
/// the suggestion list, Tab completion, history).
@@ -680,6 +825,7 @@ pub struct ChatScreen {
pub auto_suggestions: bool,
/// `CommandSuggestions.setRestrictions`: what the Chat option allows.
pub abilities: ChatAbilities,
pub layout: SuggestLayout,
}
/// Local completions waiting for the server's reply to request `id`.
struct Awaiting {
@@ -750,6 +896,43 @@ impl ChatScreen {
keep_suggestions: false,
auto_suggestions: true,
abilities,
layout: SuggestLayout::CHAT,
};
s.update_command_info(m, c);
s
}
/// `AbstractCommandBlockEditScreen.init`: the command box at the top,
/// focused, holding `command`, with suggestions from the start.
pub fn command_block(
m: &Measure,
c: &Commands,
command: &str,
width: i32,
height: i32,
) -> Self {
let mut input = TextField::new(width / 2 - 150, 50, 300, 20);
input.max_length = 32500;
input.focused = true;
input.set_value(m, command);
let mut s = Self {
input,
width,
height,
history_buffer: String::new(),
history_pos: 0,
parse: None,
pending: None,
awaiting: None,
request: None,
suggestions: None,
usage: Vec::new(),
usage_position: 0,
usage_width: 0,
allow_suggestions: true,
keep_suggestions: false,
auto_suggestions: true,
abilities: ChatAbilities::new(ChatVisibility::Full),
layout: SuggestLayout::COMMAND_BLOCK,
};
s.update_command_info(m, c);
s
@@ -768,7 +951,11 @@ impl ChatScreen {
let value = self.input.value.clone();
let auto = self.auto_suggestions;
let abilities = self.abilities;
*self = Self::new(m, c, &value, self.history_pos, width, height, abilities);
*self = if self.layout == SuggestLayout::CHAT {
Self::new(m, c, &value, self.history_pos, width, height, abilities)
} else {
Self::command_block(m, c, &value, width, height)
};
self.auto_suggestions = auto;
}
}
@@ -814,11 +1001,18 @@ impl ChatScreen {
self.usage.clear();
let cursor = self.input.cursor;
let cursor_b = byte_at(&command, cursor);
if command.starts_with('/') {
let slash = command.starts_with('/');
if self.layout.commands_only || slash {
let start = usize::from(slash);
if self.parse.is_none() {
self.parse = Some(c.dispatcher.parse_at(&command, 1, c.registries));
self.parse = Some(c.dispatcher.parse_at(&command, start, c.registries));
}
if cursor >= 1 && (self.suggestions.is_none() || !self.keep_suggestions) {
let from = if self.layout.only_past_error {
start
} else {
1
};
if cursor >= from && (self.suggestions.is_none() || !self.keep_suggestions) {
let parse = self.parse.as_ref().expect("parsed above");
let (range, list, ask) =
c.dispatcher
@@ -844,7 +1038,9 @@ impl ChatScreen {
} else if !command.chars().all(char::is_whitespace) {
let partial = &command[..cursor_b];
let mut builder = SuggestionsBuilder::new(partial, last_word_index(partial));
builder.suggest_matching(c.registries.players.iter().map(String::as_str));
// ClientSuggestionProvider.getCustomTabSuggestions: the players
// and the server's custom chat completions.
builder.suggest_matching(c.registries.custom_tab_suggestions());
let (range, list) = merge_suggestions(partial, builder.out);
self.awaiting = None;
self.pending = Some((range.0, list));
@@ -1002,7 +1198,11 @@ impl ChatScreen {
0,
self.input.screen_x(m, 0) + self.input.inner_width() - width,
);
let y = self.height - 12;
let y = if self.layout.anchor_to_bottom {
self.height - 12
} else {
72
};
// sortSuggestions: those starting with the word at the cursor first.
let partial = &value[..byte_at(&value, self.input.cursor)];
let word = partial[last_word_index(partial)..].to_lowercase();
@@ -1011,9 +1211,16 @@ impl ChatScreen {
s.text.starts_with(&word) || s.text.starts_with(&format!("minecraft:{word}"))
});
first.extend(rest);
let n = first.len().min(10) as i32;
// SuggestionsList: beside a bordered box, or over an unbordered one.
let n = first.len().min(self.layout.line_limit) as i32;
let bordered = i32::from(self.input.bordered);
let top = if self.layout.anchor_to_bottom {
y - 3 - n * 12
} else {
y - bordered
};
let mut l = SuggestionList {
rect: [x - 1, y - 3 - n * 12, width + 1, n * 12],
rect: [x - (1 - bordered), top, width + 1, n * 12],
original: value,
list: first,
offset: 0,
@@ -1042,11 +1249,12 @@ impl ChatScreen {
}
fn cycle(&mut self, l: &mut SuggestionList, dir: isize) {
self.select(l, l.current as isize + dir);
let max = l.list.len().saturating_sub(10);
let limit = self.layout.line_limit;
let max = l.list.len().saturating_sub(limit);
if l.current < l.offset {
l.offset = l.current.min(max);
} else if l.current > l.offset + 9 {
l.offset = (l.current as isize + 1 - 10).clamp(0, max as isize) as usize;
} else if l.current >= l.offset + limit {
l.offset = (l.current as isize + 1 - limit as isize).clamp(0, max as isize) as usize;
}
}
/// `SuggestionsList.useSuggestion`.
@@ -1074,7 +1282,57 @@ impl ChatScreen {
ctrl: bool,
) -> ChatAction {
use winit::keyboard::KeyCode as K;
// CommandSuggestions.keyPressed.
if self.suggestions_key(m, c, key, shift) {
return ChatAction::None;
}
match key {
K::Escape => return ChatAction::Close,
K::Enter | K::NumpadEnter => {
// ChatScreen.keyPressed: without `hasAllowedInput` the input
// stays, unsent, and the screen stays open.
if !self.abilities.allows(&self.input.value) {
return ChatAction::None;
}
return ChatAction::Submit(self.normalized());
}
K::ArrowUp => self.move_in_history(m, c, history, -1),
K::ArrowDown => self.move_in_history(m, c, history, 1),
_ => {
self.field_key(m, c, key, shift, ctrl);
}
}
ChatAction::None
}
/// The input box's own keys (`EditBox.keyPressed`), the suggestions
/// following an edit: whether the box took the key.
pub fn field_key(
&mut self,
m: &Measure,
c: &Commands,
key: winit::keyboard::KeyCode,
shift: bool,
ctrl: bool,
) -> bool {
match self.input.key(m, key, shift, ctrl) {
Some(changed) => {
if changed {
self.edited(m, c);
}
true
}
None => false,
}
}
/// `CommandSuggestions.keyPressed`: the list's arrows, Tab and Escape,
/// and Tab to show it. True when the suggestions took the key.
pub fn suggestions_key(
&mut self,
m: &Measure,
c: &Commands,
key: winit::keyboard::KeyCode,
shift: bool,
) -> bool {
use winit::keyboard::KeyCode as K;
if let Some(mut l) = self.suggestions.take() {
let handled = match key {
K::ArrowUp | K::ArrowDown => {
@@ -1091,37 +1349,18 @@ impl ChatScreen {
}
K::Escape => {
self.input.suggestion = None;
return ChatAction::None;
return true;
}
_ => false,
};
self.suggestions = Some(l);
if handled {
return ChatAction::None;
}
handled
} else if key == K::Tab {
self.show_suggestions(m);
return ChatAction::None;
true
} else {
false
}
match key {
K::Escape => return ChatAction::Close,
K::Enter | K::NumpadEnter => {
// ChatScreen.keyPressed: without `hasAllowedInput` the input
// stays, unsent, and the screen stays open.
if !self.abilities.allows(&self.input.value) {
return ChatAction::None;
}
return ChatAction::Submit(self.normalized());
}
K::ArrowUp => self.move_in_history(m, c, history, -1),
K::ArrowDown => self.move_in_history(m, c, history, 1),
_ => {
if self.input.key(m, key, shift, ctrl) == Some(true) {
self.edited(m, c);
}
}
}
ChatAction::None
}
/// `ChatScreen.moveInHistory`.
fn move_in_history(&mut self, m: &Measure, c: &Commands, history: &[String], dir: isize) {
@@ -1175,7 +1414,7 @@ impl ChatScreen {
if !l.contains(mouse.0, mouse.1) {
return false;
}
let max = l.list.len().saturating_sub(10) as i32;
let max = l.list.len().saturating_sub(self.layout.line_limit) as i32;
l.offset = ((l.offset as f64 - amount.clamp(-1.0, 1.0)) as i32).clamp(0, max) as usize;
true
}
@@ -1246,7 +1485,9 @@ impl ChatScreen {
) -> Vec<Draw> {
let (w, h) = (self.width, self.height);
let mut out = Vec::new();
d::fill(&mut out, 2, h - 14, w - 2, h - 2, background);
if self.layout.anchor_to_bottom {
d::fill(&mut out, 2, h - 14, w - 2, h - 2, background);
}
let format = |s: &str, o: usize| self.format(s, o);
self.input.draw(&mut out, m, blink_ms, Some(&format));
if let Some(mut l) = self.suggestions.take() {
@@ -1254,14 +1495,18 @@ impl ChatScreen {
self.suggestions = Some(l);
} else {
for (i, line) in self.usage.iter().enumerate() {
let y = h - 27 - 12 * i as i32;
let y = if self.layout.anchor_to_bottom {
h - 27 - 12 * i as i32
} else {
72 + 12 * i as i32
};
d::fill(
&mut out,
self.usage_position - 1,
y,
self.usage_position + self.usage_width + 1,
y + 12,
FILL,
self.layout.fill,
);
d::text(&mut out, line.clone(), self.usage_position, y + 2, -1, true);
}
@@ -1275,7 +1520,7 @@ impl ChatScreen {
l: &mut SuggestionList,
mouse: (i32, i32),
) {
let limit = l.list.len().min(10);
let limit = l.list.len().min(self.layout.line_limit);
let [rx, ry, rw, rh] = l.rect;
let has_prev = l.offset > 0;
let has_next = l.list.len() > l.offset + limit;
@@ -1284,8 +1529,8 @@ impl ChatScreen {
l.last_mouse = mouse;
}
if has_prev || has_next {
d::fill(out, rx, ry - 1, rx + rw, ry, FILL);
d::fill(out, rx, ry + rh, rx + rw, ry + rh + 1, FILL);
d::fill(out, rx, ry - 1, rx + rw, ry, self.layout.fill);
d::fill(out, rx, ry + rh, rx + rw, ry + rh + 1, self.layout.fill);
if has_prev {
for x in (0..rw).step_by(2) {
d::fill(out, rx + x, ry - 1, rx + x + 1, ry, -1);
@@ -1300,7 +1545,7 @@ impl ChatScreen {
let mut hovered = false;
for i in 0..limit {
let y = ry + 12 * i as i32;
d::fill(out, rx, y, rx + rw, y + 12, FILL);
d::fill(out, rx, y, rx + rw, y + 12, self.layout.fill);
if mouse.0 > rx && mouse.0 < rx + rw && mouse.1 > y && mouse.1 < y + 12 {
if moved {
self.select(l, (i + l.offset) as isize);
@@ -244,6 +244,58 @@ fn mod_commands_are_coloured_and_complete_like_vanilla_ones() {
assert_eq!(screen.value(), "/coords");
}
#[test]
fn chat_completes_the_servers_custom_completions_with_the_players() {
// CommandSuggestions.updateCommandInfo completes chat from
// ClientSuggestionProvider.getCustomTabSuggestions: the online players
// and what ClientboundCustomChatCompletionsPacket added.
use crate::command::{CustomCompletionAction as Action, modify_custom_completions};
let font = font();
let m = Measure(&font);
let mut registries = Registries {
players: vec!["Steve".into()],
..Registries::default()
};
let entries = |e: &[&str]| e.iter().map(|s| s.to_string()).collect::<Vec<_>>();
let set = &mut registries.custom_completions;
modify_custom_completions(set, Action::from_id(0), &entries(&["Alex", "#trade"]));
modify_custom_completions(set, Action::Add, &entries(&["Notch", "Steve"]));
modify_custom_completions(set, Action::Remove, &entries(&["Alex"]));
// An id past the last action is ADD (OutOfBoundsStrategy.ZERO).
modify_custom_completions(set, Action::from_id(7), &entries(&["Zuri"]));
assert_eq!(
registries
.custom_tab_suggestions()
.into_iter()
.collect::<Vec<_>>(),
["#trade", "Notch", "Steve", "Zuri"]
);
let completed = |registries: &Registries, line: &str| {
let dispatcher = crate::command::vanilla::game_dispatcher();
let c = Commands {
dispatcher,
registries,
};
let mut screen = ChatScreen::new(&m, &c, "", 0, 640, 360, full());
screen.type_text(&m, &c, line);
// Chat shows no list by itself: Tab shows it, Tab again uses it.
for _ in 0..2 {
screen.key(&m, &c, &[], winit::keyboard::KeyCode::Tab, false, false);
}
screen.value().to_owned()
};
assert_eq!(completed(&registries, "hi no"), "hi Notch");
assert_eq!(completed(&registries, "sell #tr"), "sell #trade");
assert_eq!(completed(&registries, "hi st"), "hi Steve");
assert_eq!(completed(&registries, "hi al"), "hi al");
// SET replaces them all; the players stay.
let set = &mut registries.custom_completions;
modify_custom_completions(set, Action::Set, &entries(&["Alex"]));
assert_eq!(completed(&registries, "hi no"), "hi no");
assert_eq!(completed(&registries, "hi al"), "hi Alex");
assert_eq!(completed(&registries, "hi st"), "hi Steve");
}
#[test]
fn ask_server_nodes_wait_for_the_servers_completions() {
// ClientSuggestionProvider.customSuggestion: an `ask_server` node at the
@@ -467,3 +519,62 @@ fn a_guests_line_follows_the_player_message_permission() {
);
}
}
/// `ChatComponent.deleteMessageOrDelay`: a deleted message shows for 60
/// ticks from when it was added, then becomes the grey italic
/// `chat.deleted_marker` line with the system bar; the other lines stay.
#[test]
fn a_deleted_message_shows_60_ticks_then_becomes_the_marker() {
let font = font();
let mut chat = Chat::default();
let secret = [7; 256];
chat.add_signed_message(
&font,
Text::literal("<Steve> secret"),
Some(secret),
100,
false,
);
chat.add_signed_message(
&font,
Text::literal("<Alex> hi"),
Some([8; 256]),
110,
false,
);
let lines =
|chat: &Chat| -> Vec<String> { chat.lines.iter().map(|l| l.content.plain()).collect() };
let marker = Chat::deleted_marker().plain();
chat.delete_message_with(&font, &secret, 130);
assert_eq!(
lines(&chat),
["<Alex> hi", "<Steve> secret"],
"too new: delayed"
);
chat.process_deletions(&font, 159);
assert_eq!(lines(&chat), ["<Alex> hi", "<Steve> secret"]);
chat.process_deletions(&font, 160);
assert_eq!(lines(&chat), ["<Alex> hi".to_owned(), marker.clone()]);
assert!(chat.deletions.is_empty());
let line = &chat.lines[1];
assert_eq!(line.indicator, Some(SYSTEM_INDICATOR));
assert_eq!(line.added, 100, "the marker keeps the message's time");
assert!(
line.content
.spans
.iter()
.all(|s| s.style.color == Some(text::GRAY) && s.style.is_italic())
);
assert_eq!(
chat.messages[1].content.translation_key(),
Some("chat.deleted_marker")
);
// A message shown 60 ticks already is replaced at once; an unknown or
// already deleted signature changes nothing.
chat.delete_message_with(&font, &[8; 256], 170);
assert_eq!(lines(&chat), [marker.clone(), marker.clone()]);
chat.delete_message_with(&font, &secret, 200);
chat.delete_message_with(&font, &[9; 256], 200);
assert_eq!(lines(&chat), [marker.clone(), marker]);
assert!(chat.deletions.is_empty());
}
@@ -8,10 +8,208 @@ use std::sync::{Arc, OnceLock};
pub mod block_entity;
/// The vanilla Overworld's column, which every dimension's chunks hold for
/// now (`LevelShape::column`).
pub const MIN_Y: i32 = -64;
pub const HEIGHT: i32 = 384;
pub const SECTIONS: usize = (HEIGHT / 16) as usize;
/// A level's build range and skylight, from its dimension type (`min_y`,
/// `height`, `logical_height`, `has_skylight`), as `LevelHeightAccessor` and
/// `SerializableChunkData.write` read them.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct LevelShape {
pub sky_light: bool,
pub min_y: i32,
pub height: i32,
/// `DimensionType.logicalHeight`: how high portals and chorus fruit reach.
pub logical_height: i32,
}
impl LevelShape {
pub const OVERWORLD: Self = Self {
sky_light: true,
min_y: MIN_Y,
height: HEIGHT,
logical_height: HEIGHT,
};
pub const NETHER: Self = Self {
sky_light: false,
min_y: 0,
height: 256,
logical_height: 128,
};
pub const END: Self = Self {
sky_light: true,
min_y: 0,
height: 256,
logical_height: 256,
};
/// A dimension type's shape, checked as its codec checks it.
pub fn new(
min_y: i32,
height: i32,
logical_height: i32,
sky_light: bool,
) -> Result<Self, String> {
ColumnLayout::new(min_y, height)?;
if !(0..=height).contains(&logical_height) {
return Err(format!(
"logical_height {logical_height} is outside 0..={height}"
));
}
Ok(Self {
sky_light,
min_y,
height,
logical_height,
})
}
/// One above the highest block (`getMaxY() + 1`).
pub fn max_y(self) -> i32 {
self.min_y + self.height
}
pub fn min_section(self) -> i32 {
self.min_y >> 4
}
pub fn section_count(self) -> usize {
(self.height >> 4) as usize
}
/// Whether section `y` (a section coordinate) is inside the level.
pub fn has_section(self, y: i32) -> bool {
(self.min_section()..self.min_section() + self.section_count() as i32).contains(&y)
}
/// The sections a chunk of this level holds: the level's, widened to the
/// vanilla Overworld's `-64..320` so that vanilla's Nether and End keep
/// the 24 sections they have always had.
pub fn column(self) -> ColumnLayout {
ColumnLayout::of_level(self.min_y, self.height)
}
}
/// The shapes of a world's three levels, from its dimension types.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct LevelShapes {
pub overworld: LevelShape,
pub nether: LevelShape,
pub end: LevelShape,
}
impl LevelShapes {
pub const VANILLA: Self = Self {
overworld: LevelShape::OVERWORLD,
nether: LevelShape::NETHER,
end: LevelShape::END,
};
/// The shape of the level `id` (`minecraft:overworld`,
/// `minecraft:the_nether`, `minecraft:the_end`); others get the
/// Overworld's.
pub fn get(&self, id: &str) -> LevelShape {
match id {
"minecraft:the_nether" => self.nether,
"minecraft:the_end" => self.end,
_ => self.overworld,
}
}
pub fn get_mut(&mut self, id: &str) -> &mut LevelShape {
match id {
"minecraft:the_nether" => &mut self.nether,
"minecraft:the_end" => &mut self.end,
_ => &mut self.overworld,
}
}
}
impl Default for LevelShapes {
fn default() -> Self {
Self::VANILLA
}
}
/// The span of block Y a chunk holds: from `min_y`, `sections` sections up
/// (`LevelShape::column`).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ColumnLayout {
min_y: i32,
sections: usize,
}
impl ColumnLayout {
pub const VANILLA: Self = Self {
min_y: MIN_Y,
sections: SECTIONS,
};
/// The column of a level from `min_y`, `height` high
/// (`LevelShape::column`).
pub fn of_level(min_y: i32, height: i32) -> Self {
let low = min_y.min(MIN_Y);
let high = (min_y + height).max(MIN_Y + HEIGHT);
Self {
min_y: low,
sections: ((high - low) / 16) as usize,
}
}
/// `DimensionType.MIN_Y`, `MAX_Y`: 12 bits of `BlockPos` Y, less 32.
pub const LOWEST_Y: i32 = -2032;
pub const HIGHEST_Y: i32 = 2031;
/// A dimension type's `min_y` and `height`, as its codec accepts them:
/// multiples of 16, at least 16 high, within `LOWEST_Y..=HIGHEST_Y`.
pub fn new(min_y: i32, height: i32) -> Result<Self, String> {
if min_y % 16 != 0 || height % 16 != 0 {
return Err(format!(
"min_y {min_y} and height {height} must be multiples of 16"
));
}
if height < 16 {
return Err(format!("height {height} is below 16"));
}
if min_y < Self::LOWEST_Y
|| i64::from(min_y) + i64::from(height) > i64::from(Self::HIGHEST_Y) + 1
{
return Err(format!(
"min_y {min_y} with height {height} leaves {}..={}",
Self::LOWEST_Y,
Self::HIGHEST_Y
));
}
Ok(Self {
min_y,
sections: (height / 16) as usize,
})
}
pub fn min_y(self) -> i32 {
self.min_y
}
/// One above the highest block.
pub fn max_y(self) -> i32 {
self.min_y + self.height()
}
pub fn height(self) -> i32 {
self.sections as i32 * 16
}
pub fn sections(self) -> usize {
self.sections
}
/// The index of the section holding block `y`, if the column holds it.
pub fn section_index(self, y: i32) -> Option<usize> {
usize::try_from((y - self.min_y) >> 4)
.ok()
.filter(|&s| s < self.sections)
}
/// The minimum block Y of section `index`.
pub fn section_y(self, index: usize) -> i32 {
self.min_y + index as i32 * 16
}
}
/// `true` for each section of `layout`: what callers that keep no
/// per-section record (of fluids, say) pass as "every section may have it".
pub fn every_section(layout: ColumnLayout) -> &'static [bool] {
static ALL: [bool; crate::section_mask::SectionMask::CAPACITY] =
[true; crate::section_mask::SectionMask::CAPACITY];
&ALL[..layout.sections()]
}
impl Default for ColumnLayout {
fn default() -> Self {
Self::VANILLA
}
}
/// Biomes are 4x4x4 containers over the biome table (67 entries: a 7-bit
/// global palette).
pub const BIOME_STRATEGY: Strategy = Strategy::biomes(7);
@@ -45,6 +243,47 @@ pub fn free_columns<T: Send + 'static>(columns: T, count: usize) {
pub fn set_block_states(states: usize) {
let bits = horizon_core::paletted::ceil_log2(states).max(16);
BLOCK_BITS.store(bits, std::sync::atomic::Ordering::Relaxed);
forget_shared();
}
/// One allocation per kind of uniform section (a single block, a single
/// biome and one light value), shared by every column that has it, keyed by
/// (block, biome, light). A tall column is mostly such sections: air with
/// full sky light above the terrain. Edits copy them (`Arc::make_mut`).
type SharedSections = std::collections::HashMap<(u32, u32, u8), Arc<Section>>;
static SHARED: std::sync::Mutex<Option<SharedSections>> = std::sync::Mutex::new(None);
/// Replaces the uniform sections of `sections` with the shared ones.
pub fn share_uniform(sections: &mut [Arc<Section>]) {
let key = |s: &Section| -> Option<(u32, u32, u8)> {
let Light::Uniform(light) = s.light else {
return None;
};
if s.blocks.bits() != 0 || s.biomes.bits() != 0 {
return None;
}
Some((s.blocks.uniform()?, s.biomes.uniform()?, light))
};
let Ok(mut shared) = SHARED.lock() else {
return;
};
let shared = shared.get_or_insert_with(Default::default);
for section in sections {
let Some(k) = key(section) else {
continue;
};
let one = shared.entry(k).or_insert_with(|| section.clone());
if !Arc::ptr_eq(one, section) {
*section = one.clone();
}
}
}
/// Forgets the shared sections, when the palette changes.
fn forget_shared() {
if let Ok(mut shared) = SHARED.lock() {
*shared = None;
}
}
/// Packed light per block: sky light in the high nibble, block light in the low one.
#[derive(Clone)]
@@ -138,9 +377,11 @@ impl Section {
pub struct Chunk {
pub x: i32,
pub z: i32,
/// The minimum block Y of `sections[0]` (the layout's when it was made).
min_y: i32,
pub sections: Vec<Arc<Section>>,
/// `Heightmap.Types.WORLD_SURFACE` per column (`z << 4 | x`): one above
/// the highest non-air block, or `MIN_Y`. Built on the first read; a
/// the highest non-air block, or the column's minimum Y. Built on the first read; a
/// `set` that can move a column's surface drops it.
surface: OnceLock<Box<[i16; 256]>>,
/// `Heightmap.Types.MOTION_BLOCKING` per column, kept like `surface`.
@@ -169,13 +410,19 @@ pub struct Chunk {
pub structure_starts: Vec<Arc<horizon_world::worldgen::structure::start::StructureStart>>,
}
impl Chunk {
/// An all-air chunk in the current `layout`.
pub fn empty(x: i32, z: i32) -> Self {
Self::with_layout(x, z, ColumnLayout::VANILLA)
}
pub fn with_layout(x: i32, z: i32, layout: ColumnLayout) -> Self {
let mut one = [Arc::new(Section::default())];
share_uniform(&mut one);
let [empty] = one;
Self {
x,
z,
sections: (0..SECTIONS)
.map(|_| Arc::new(Section::default()))
.collect(),
min_y: layout.min_y(),
sections: (0..layout.sections()).map(|_| empty.clone()).collect(),
surface: OnceLock::new(),
motion_blocking: OnceLock::new(),
lit: false,
@@ -190,13 +437,36 @@ impl Chunk {
entities: Vec::new(),
}
}
/// The minimum block Y of the column.
#[inline]
pub fn min_y(&self) -> i32 {
self.min_y
}
/// The column the chunk holds.
pub fn layout(&self) -> ColumnLayout {
ColumnLayout {
min_y: self.min_y,
sections: self.sections.len(),
}
}
/// One above the column's highest block.
#[inline]
pub fn max_y(&self) -> i32 {
self.min_y + self.sections.len() as i32 * 16
}
/// The index of the section holding block `y`, if the column holds it.
#[inline]
pub fn section_index(&self, y: i32) -> Option<usize> {
usize::try_from((y - self.min_y) >> 4)
.ok()
.filter(|&s| s < self.sections.len())
}
#[inline]
pub fn get(&self, x: i32, y: i32, z: i32) -> u32 {
let s = (y - MIN_Y) >> 4;
if !(0..SECTIONS as i32).contains(&s) {
return 0;
match self.section_index(y) {
Some(s) => self.sections[s].get(index(x, y, z)),
None => 0,
}
self.sections[s as usize].get(index(x, y, z))
}
#[inline]
pub fn block(&self, x: i32, y: i32, z: i32) -> BlockStateId {
@@ -211,16 +481,15 @@ impl Chunk {
.rev()
.filter(|(_, section)| !section.is_uniform(skip))
.find_map(|(s, _)| {
let base = MIN_Y + s as i32 * 16;
let base = self.min_y + s as i32 * 16;
(base..base + 16).rev().find(|&y| matches(y))
})
}
pub fn set(&mut self, x: i32, y: i32, z: i32, v: u32) -> u32 {
let s = (y - MIN_Y) >> 4;
if !(0..SECTIONS as i32).contains(&s) {
let Some(s) = self.section_index(y) else {
return v;
}
let previous = Arc::make_mut(&mut self.sections[s as usize]).set(index(x, y, z), v);
};
let previous = Arc::make_mut(&mut self.sections[s]).set(index(x, y, z), v);
// `Heightmap.update`: a write two or more below a map's height can't
// move it. Any other change rebuilds that map on the next read.
if previous != v {
@@ -236,7 +505,8 @@ impl Chunk {
previous
}
/// `getHeight(Heightmap.Types.WORLD_SURFACE, x, z)` (`x`, `z` within the
/// chunk): one above the highest block that isn't `air`, or `MIN_Y`.
/// chunk): one above the highest block that isn't `air`, or the column's
/// minimum Y.
/// Sections must change only through `set` once this has been read.
pub fn world_surface(&self, x: i32, z: i32, air: impl Fn(u32) -> bool) -> i32 {
let surface = self.surface.get_or_init(|| self.prime(|v| !air(v)));
@@ -244,7 +514,8 @@ impl Chunk {
}
/// `getHeight(Heightmap.Types.MOTION_BLOCKING, x, z)` (`x`, `z` within
/// the chunk): one above the highest state where `blocks` (a motion
/// blocking block or a fluid), or `MIN_Y`. Kept as `world_surface` is.
/// blocking block or a fluid), or the column's minimum Y. Kept as
/// `world_surface` is.
pub fn motion_blocking(&self, x: i32, z: i32, blocks: impl Fn(u32) -> bool) -> i32 {
let heights = self.motion_blocking.get_or_init(|| self.prime(blocks));
i32::from(heights[column(x, z)])
@@ -252,7 +523,7 @@ impl Chunk {
/// `Heightmap.primeHeightmaps`: each section from the top, skipping
/// those of one state that doesn't count, until every column is found.
fn prime(&self, counts: impl Fn(u32) -> bool) -> Box<[i16; 256]> {
let mut surface = Box::new([MIN_Y as i16; 256]);
let mut surface = Box::new([self.min_y as i16; 256]);
let mut found = [false; 256];
let mut left = 256;
let mut values = [0u32; 4096];
@@ -264,7 +535,7 @@ impl Chunk {
continue;
}
section.unpack(&mut values);
let base = MIN_Y + s as i32 * 16;
let base = self.min_y + s as i32 * 16;
for i in 0..256 {
if found[i] {
continue;
@@ -280,11 +551,11 @@ impl Chunk {
}
#[inline]
pub fn light(&self, x: i32, y: i32, z: i32) -> u8 {
let s = (y - MIN_Y) >> 4;
let s = (y - self.min_y) >> 4;
if s < 0 {
return 0;
}
if s >= SECTIONS as i32 {
if s >= self.sections.len() as i32 {
return 0xF0;
}
self.sections[s as usize].light(index(x, y, z))
@@ -297,7 +568,7 @@ impl Chunk {
/// the quart y clamped to the column).
#[inline]
pub fn biome(&self, x: i32, y: i32, z: i32) -> u16 {
let qy = ((y - MIN_Y) >> 2).clamp(0, HEIGHT / 4 - 1);
let qy = ((y - self.min_y) >> 2).clamp(0, self.sections.len() as i32 * 4 - 1);
self.sections[(qy >> 2) as usize]
.biomes
.get(((qy & 3) << 4 | ((z & 15) >> 2) << 2 | (x & 15) >> 2) as usize) as u16
@@ -504,4 +775,69 @@ mod tests {
}
}
}
#[test]
fn shapes_follow_the_dimension_type_codec() {
assert_eq!(ColumnLayout::new(-64, 384), Ok(ColumnLayout::VANILLA));
assert_eq!(ColumnLayout::default(), ColumnLayout::VANILLA);
// To The Max's Overworld: the highest top vanilla allows.
let tall = ColumnLayout::new(-64, 2096).unwrap();
assert_eq!((tall.sections(), tall.max_y()), (131, 2032));
assert!(ColumnLayout::new(-2032, 4064).is_ok());
for (min_y, height) in [(-64, 2112), (-2048, 64), (-60, 384), (0, 250), (0, 0)] {
assert!(
ColumnLayout::new(min_y, height).is_err(),
"{min_y} {height}"
);
}
assert_eq!(tall.section_index(-65), None);
assert_eq!(tall.section_index(-64), Some(0));
assert_eq!(tall.section_index(2031), Some(130));
assert_eq!(tall.section_index(2032), None);
assert_eq!(tall.section_y(130), 2016);
assert_eq!(
LevelShape::new(-64, 384, 384, true),
Ok(LevelShape::OVERWORLD)
);
assert_eq!(LevelShape::new(0, 256, 128, false), Ok(LevelShape::NETHER));
assert!(LevelShape::new(0, 256, 257, true).is_err());
assert!(LevelShape::new(-60, 384, 384, true).is_err());
}
#[test]
fn vanilla_levels_keep_the_overworld_column() {
for shape in [LevelShape::OVERWORLD, LevelShape::NETHER, LevelShape::END] {
assert_eq!(shape.column(), ColumnLayout::VANILLA, "{shape:?}");
}
let tall = LevelShape::new(-64, 2096, 2096, true).unwrap();
assert_eq!(tall.column(), ColumnLayout::new(-64, 2096).unwrap());
let deep = LevelShape::new(-512, 256, 256, false).unwrap();
assert_eq!(deep.column(), ColumnLayout::new(-512, 832).unwrap());
assert!(LevelShape::NETHER.has_section(15) && !LevelShape::NETHER.has_section(16));
assert!(!LevelShape::NETHER.has_section(-1));
}
#[test]
fn a_tall_column_stores_its_whole_height() {
let layout = ColumnLayout::new(-64, 2096).unwrap();
let mut chunk = Chunk::with_layout(0, 0, layout);
assert_eq!(
(chunk.min_y(), chunk.max_y(), chunk.sections.len()),
(-64, 2032, 131)
);
let stone = 1;
assert_eq!(chunk.set(5, 2031, 6, stone), 0);
assert_eq!(chunk.set(5, 2032, 6, stone), stone, "above the column");
assert_eq!(chunk.get(5, 2031, 6), stone);
assert_eq!(chunk.get(5, 2032, 6), 0);
assert_eq!(chunk.world_surface(5, 6, |v| v == 0), 2032);
assert_eq!(chunk.world_surface(0, 0, |v| v == 0), -64);
assert_eq!(chunk.highest(0, |y| y < 1000), None);
chunk.set(5, 999, 6, stone);
assert_eq!(chunk.highest(0, |y| y < 1000), Some(999));
assert_eq!(chunk.light(0, 2100, 0), 0xF0);
assert_eq!(chunk.light(0, -65, 0), 0);
chunk.set_biome_cell(0, 130 * 4 + 3, 0, 9);
assert_eq!(chunk.biome(0, 5000, 0), 9);
}
}
@@ -74,8 +74,99 @@ pub enum BlockEntity {
/// `DaylightDetectorBlockEntity`: no data, only the ticker
/// (`DaylightDetectorBlock.tickEntity`).
DaylightDetector,
/// Suspicious sand and gravel: saved loot and the transient brushed face.
Brushable(Box<Brushable>),
/// A shelf holds three displayed item stacks.
Shelf(Box<Shelf>),
/// `ChiseledBookShelfBlockEntity`: a `Container` of six one-book slots.
ChiseledBookshelf(Box<ChiseledBookshelf>),
/// A block entity of a mod (`native_block_entity`), kept as its saved
/// compound for the mod to read and change.
Mod(Box<ModBlockEntity>),
/// `PotentSulfurBlockEntity` (`game/potent_sulfur.rs`).
PotentSulfur(PotentSulfur),
/// `StructureBlockEntity` (`structure_block.rs`).
StructureBlock(Box<crate::structure_block::StructureBlock>),
/// `JigsawBlockEntity` (`jigsaw_block.rs`).
JigsawBlock(Box<crate::jigsaw_block::JigsawBlock>),
/// `TestBlockEntity` (`test_block.rs`).
TestBlock(Box<crate::test_block::TestBlock>),
/// `TestInstanceBlockEntity` (`test_instance_block.rs`).
TestInstanceBlock(Box<crate::test_instance_block::TestInstanceBlock>),
}
/// A mod's block entity: its type id (`mymod:machine`), its saved
/// compound without `id`, `x`, `y`, `z` and `Items`, its item slots
/// (`Items`) when its block has a container (`ModContainer`), and the data
/// slots its menu shows (`ContainerData`: a furnace menu's flame and arrow),
/// which the mod sets and nothing saves.
#[derive(Clone, Debug, Default, PartialEq)]
pub struct ModBlockEntity {
pub id: String,
pub nbt: Compound,
pub items: Vec<Option<ItemStack>>,
pub data: Vec<i32>,
}
/// The container of a mod block's block entity, as a `WorldlyContainer`
/// says it: its slot count, the slots each face reaches
/// (`getSlotsForFace`, by `Direction`: down, up, north, south, west, east),
/// and the slots hoppers may put into (`canPlaceItemThroughFace`) and take
/// from (`canTakeItemThroughFace`); and the vanilla menu that shows its
/// slots when the block is used, if any (`MOD_MENUS`).
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct ModContainer {
pub slots: usize,
pub faces: [Vec<usize>; 6],
pub inputs: Vec<usize>,
pub outputs: Vec<usize>,
pub menu: Option<String>,
}
/// The vanilla menus a mod container can open, with the slot count each
/// shows: slot grids, and the furnaces' (input, fuel, result), whose four
/// data slots draw the flame (`data[0]` of `data[1]`) and the arrow
/// (`data[2]` of `data[3]`), as `AbstractFurnaceBlockEntity.dataAccess`.
pub const MOD_MENUS: [(&str, usize); 11] = [
("minecraft:furnace", 3),
("minecraft:blast_furnace", 3),
("minecraft:smoker", 3),
("minecraft:hopper", 5),
("minecraft:generic_3x3", 9),
("minecraft:generic_9x1", 9),
("minecraft:generic_9x2", 18),
("minecraft:generic_9x3", 27),
("minecraft:generic_9x4", 36),
("minecraft:generic_9x5", 45),
("minecraft:generic_9x6", 54),
];
/// `PotentSulfurBlockEntity`: the seconds left to the geyser's next change of
/// state (`waitingCountdown`, saved as `countdown`) and the game time of its
/// last eruption block event, which the client's plume counts from.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct PotentSulfur {
pub waiting_countdown: i32,
pub eruption_tick: i64,
}
impl Default for PotentSulfur {
fn default() -> Self {
Self {
waiting_countdown: -1,
eruption_tick: -1,
}
}
}
/// `BrushableBlockEntity`: the loot table is resolved on the first brush.
#[derive(Clone, Debug, Default)]
pub struct Brushable {
pub loot_table: Option<horizon_world::nbt::Tag>,
pub loot_seed: Option<horizon_world::nbt::Tag>,
pub item: Option<ItemStack>,
/// Direction's legacy network id; not saved to disk by vanilla.
pub hit_direction: Option<i32>,
}
/// `ShelfBlockEntity`'s displayed slots and alignment flag.
#[derive(Clone, Debug, Default)]
pub struct Shelf {
pub items: [Option<ItemStack>; 3],
pub align_to_bottom: bool,
}
/// `ChiseledBookShelfBlockEntity`: six slots and `lastInteractedSlot`.
#[derive(Clone, Debug, PartialEq)]
@@ -209,6 +300,8 @@ pub struct SignText {
/// `DyeColor` id.
pub color: u8,
pub glowing: bool,
/// The `click_event` in each line's component style, as saved.
pub click: [Option<Tag>; 4],
}
/// `SignBlockEntity` / `HangingSignBlockEntity` data.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
@@ -217,6 +310,8 @@ pub struct SignData {
pub front: SignText,
pub back: SignText,
pub waxed: bool,
/// `allow_op_features`: the sign's click events may run commands.
pub allow_op_features: bool,
}
/// `VibrationInfo`.
#[derive(Clone, Copy, Debug, PartialEq)]
@@ -230,7 +325,7 @@ pub struct Info {
/// `VibrationSelector`: the closest, then highest-frequency candidate of a tick.
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Selector {
pub(crate) current: Option<(Info, i64)>,
pub current: Option<(Info, i64)>,
}
/// `VibrationSystem.Data`.
#[derive(Clone, Debug, Default, PartialEq)]
@@ -278,7 +373,7 @@ pub struct SpawnData {
/// Which `SpawnData` is next: one of the potentials (the same object in
/// vanilla, so changing it changes the potential) or its own.
#[derive(Clone, Debug, PartialEq)]
pub(crate) enum Next {
pub enum Next {
Potential(usize),
Own(SpawnData),
}
@@ -288,7 +383,7 @@ pub struct Spawner {
pub spawn_delay: i32,
/// `SpawnPotentials`: data and weight.
pub potentials: Vec<(SpawnData, i32)>,
pub(crate) next: Option<Next>,
pub next: Option<Next>,
pub min_spawn_delay: i32,
pub max_spawn_delay: i32,
pub spawn_count: i32,
@@ -301,6 +396,24 @@ pub struct Spawner {
pub spin: f64,
pub o_spin: f64,
}
impl Default for Spawner {
fn default() -> Self {
Self {
spawn_delay: 20,
potentials: Vec::new(),
next: None,
min_spawn_delay: 200,
max_spawn_delay: 800,
spawn_count: 4,
max_nearby_entities: 6,
required_player_range: 16,
spawn_range: 4,
client_delay: 20,
spin: 0.0,
o_spin: 0.0,
}
}
}
/// `TrialSpawnerConfig`.
#[derive(Clone, Debug, PartialEq)]
pub struct TrialConfig {
@@ -314,6 +427,26 @@ pub struct TrialConfig {
pub loot_tables_to_eject: Vec<(String, i32)>,
pub items_to_drop_when_ominous: String,
}
impl Default for TrialConfig {
/// `TrialSpawnerConfig.Builder`'s defaults.
fn default() -> Self {
Self {
spawn_range: 4,
total_mobs: 6.0,
simultaneous_mobs: 2.0,
total_mobs_added_per_player: 2.0,
simultaneous_mobs_added_per_player: 1.0,
ticks_between_spawn: 40,
spawn_potentials: Vec::new(),
loot_tables_to_eject: vec![
("minecraft:spawners/trial_chamber/consumables".into(), 1),
("minecraft:spawners/trial_chamber/key".into(), 1),
],
items_to_drop_when_ominous:
"minecraft:spawners/trial_chamber/items_to_drop_when_ominous".into(),
}
}
}
/// A `Holder<TrialSpawnerConfig>` as saved: a registry id
/// (`minecraft:trial_chamber/breeze/normal`) or an inline config.
#[derive(Clone, Debug, Default, PartialEq)]
@@ -347,6 +480,28 @@ pub struct TrialSpawner {
/// and their weights (the rolled counts).
pub dispensing: Option<Vec<ItemStack>>,
}
impl Default for TrialSpawner {
fn default() -> Self {
Self {
normal: ConfigSlot::default(),
ominous: ConfigSlot::default(),
target_cooldown_length: 36000,
required_player_range: 14,
detected_players: Vec::new(),
current_mobs: Vec::new(),
cooldown_ends_at: 0,
next_mob_spawns_at: 0,
total_mobs_spawned: 0,
next_spawn_data: None,
ejecting_loot_table: None,
is_ominous: false,
has_display: false,
spin: 0.0,
o_spin: 0.0,
dispensing: None,
}
}
}
/// `VaultConfig`.
#[derive(Clone, Debug, PartialEq)]
pub struct VaultConfig {
@@ -82,6 +82,9 @@ impl crate::light::LightWorld for ChunkMap {
fn set_section_light(&mut self, cx: i32, s: usize, cz: i32, light: crate::chunk::Light) {
self.columns.set_section_light(cx, s, cz, light);
}
fn layout(&self) -> crate::chunk::ColumnLayout {
self.columns.layout()
}
}
#[cfg(test)]
@@ -305,7 +305,7 @@ impl<'w> BlockCollisionWorld for SceneCollisions<'w, '_> {
/// From the section's palette, without reading its blocks; a palette
/// of the whole registry reads every cell.
fn may_reach_out(&mut self, chunk: Self::Chunk, section_y: i32) -> io::Result<bool> {
let blocks = usize::try_from(section_y - (crate::chunk::MIN_Y >> 4))
let blocks = usize::try_from(section_y - (chunk.min_y() >> 4))
.ok()
.and_then(|s| chunk.sections.get(s))
.map(|section| &section.blocks);
@@ -2,8 +2,9 @@
//! kinds, ticking as `AbstractFurnaceBlockEntity.serverTick` with fuel times
//! from the data-driven `context_int_provider`/`context_float_provider` files.
pub use crate::chunk::block_entity::{
Banner, Beacon, BlockEntity, Campfire, ChiseledBookshelf, Crafter, CreakingHeart, EndGateway,
Furnace, FurnaceKind, Jukebox, Lectern, Skull, SkullSkin,
Banner, Beacon, BlockEntity, Brushable, Campfire, ChiseledBookshelf, Crafter, CreakingHeart,
EndGateway, Furnace, FurnaceKind, Jukebox, Lectern, MOD_MENUS, ModBlockEntity, ModContainer,
PotentSulfur, Shelf, Skull, SkullSkin,
};
use crate::items::ItemStack;
use std::collections::HashMap;
@@ -42,6 +43,45 @@ pub fn recipe_used_key(id: &str) -> String {
/// A hopper's slots: the only plain container with five.
pub const HOPPER_SLOTS: usize = 5;
/// `getDefaultName` of the `BaseContainerBlockEntity` that `block` holds: the
/// translation key of its menu's title when it has no custom name. Copper
/// chests hold a `ChestBlockEntity`, shulker boxes of every colour a
/// `ShulkerBoxBlockEntity`.
pub fn default_name_key(block: &str) -> Option<&'static str> {
Some(match block {
"minecraft:chest" | "minecraft:trapped_chest" => "container.chest",
n if n.ends_with("copper_chest") => "container.chest",
"minecraft:barrel" => "container.barrel",
n if n.ends_with("shulker_box") => "container.shulkerBox",
"minecraft:dispenser" => "container.dispenser",
"minecraft:dropper" => "container.dropper",
"minecraft:hopper" => "container.hopper",
"minecraft:furnace" => "container.furnace",
"minecraft:smoker" => "container.smoker",
"minecraft:blast_furnace" => "container.blast_furnace",
"minecraft:brewing_stand" => "container.brewing",
"minecraft:crafter" => "container.crafter",
_ => return None,
})
}
/// `BaseContainerBlockEntity.getDisplayName` (`getName`), the title of the
/// `ClientboundOpenScreenPacket` that opens it: the block entity's custom
/// name (`CustomName`, a literal), else its default name.
pub fn display_name(custom: Option<&str>, default_key: &str) -> horizon_world::nbt::Tag {
match custom {
Some(name) => horizon_protocol::text::literal(name),
None => horizon_protocol::text::translatable(default_key, &[]),
}
}
/// `ChestBlock.MENU_PROVIDER_COMBINER`'s `getDisplayName` for a double chest:
/// the first half's custom name (the right half, `DoubleBlockCombiner`'s
/// `FIRST`), else the second's, else `container.chestDouble`.
pub fn double_chest_name(first: Option<&str>, second: Option<&str>) -> horizon_world::nbt::Tag {
display_name(first.or(second), "container.chestDouble")
}
impl Campfire {
/// `placeFood`'s slot: the first empty one, given the item's
/// `campfire_cooking` time; it takes one item of `held`.
@@ -191,6 +231,14 @@ impl BlockEntity {
recipes_used: HashMap::new(),
}));
}
if matches!(
name,
"minecraft:suspicious_sand"
| "minecraft:suspicious_gravel"
| "minecraft:brushable_block"
) {
return Some(Self::Brushable(Box::default()));
}
if name == "minecraft:creaking_heart" {
return Some(Self::CreakingHeart(Box::default()));
}
@@ -253,9 +301,36 @@ impl BlockEntity {
if name == "minecraft:campfire" || name == "minecraft:soul_campfire" {
return Some(Self::Campfire(Box::default()));
}
if name == "minecraft:shelf" || name.ends_with("_shelf") {
return Some(Self::Shelf(Box::default()));
}
if name == "minecraft:chiseled_bookshelf" {
return Some(Self::ChiseledBookshelf(Box::default()));
}
if name == "minecraft:structure_block" {
// The entity's mode is the state's (`StructureBlock.MODE`); the block's
// state is not known here, so a new one starts in its default, SAVE.
return Some(Self::StructureBlock(Box::new(
crate::structure_block::StructureBlock::new(
crate::structure_block::StructureMode::Save,
),
)));
}
if name == "minecraft:test_block" {
// The mode is the state's, which is not known here.
return Some(Self::TestBlock(Box::new(
crate::test_block::TestBlock::new(crate::test_block::TestBlockMode::Fail),
)));
}
if name == "minecraft:test_instance_block" {
return Some(Self::TestInstanceBlock(Box::default()));
}
if name == "minecraft:jigsaw" {
return Some(Self::JigsawBlock(Box::default()));
}
if name == "minecraft:potent_sulfur" {
return Some(Self::PotentSulfur(PotentSulfur::default()));
}
let size = match name {
"minecraft:chest" | "minecraft:trapped_chest" | "minecraft:barrel" => 27,
n if n.ends_with("copper_chest") => 27,
@@ -266,7 +341,7 @@ impl BlockEntity {
};
Some(Self::Chest(vec![None; size]))
}
pub fn id(&self) -> &'static str {
pub fn id(&self) -> &str {
match self {
Self::Chest(items) => match items.len() {
HOPPER_SLOTS => "minecraft:hopper",
@@ -295,8 +370,16 @@ impl BlockEntity {
Self::Skull(_) => "minecraft:skull",
Self::EndGateway(_) => "minecraft:end_gateway",
Self::DaylightDetector => "minecraft:daylight_detector",
Self::Brushable(_) => "minecraft:brushable_block",
Self::Campfire(_) => "minecraft:campfire",
Self::ChiseledBookshelf(_) => "minecraft:chiseled_bookshelf",
Self::Shelf(_) => "minecraft:shelf",
Self::Mod(m) => &m.id,
Self::PotentSulfur(_) => "minecraft:potent_sulfur",
Self::StructureBlock(_) => "minecraft:structure_block",
Self::JigsawBlock(_) => "minecraft:jigsaw",
Self::TestBlock(_) => "minecraft:test_block",
Self::TestInstanceBlock(_) => "minecraft:test_instance_block",
}
}
pub fn contents(&self) -> Vec<ItemStack> {
@@ -315,13 +398,21 @@ impl BlockEntity {
| Self::CommandBlock(_)
| Self::EndGateway(_)
| Self::Skull(_)
| Self::DaylightDetector => Vec::new(),
| Self::PotentSulfur(_)
| Self::StructureBlock(_)
| Self::JigsawBlock(_)
| Self::TestBlock(_)
| Self::TestInstanceBlock(_)
| Self::DaylightDetector
| Self::Brushable(_) => Vec::new(),
Self::Mod(m) => m.items.iter().flatten().cloned().collect(),
Self::Lectern(l) => l.book.iter().cloned().collect(),
Self::Sculk(_) => Vec::new(),
Self::Banner(_) => Vec::new(),
Self::Crafter(c) => c.items.iter().flatten().cloned().collect(),
Self::Campfire(c) => c.items.iter().flatten().cloned().collect(),
Self::ChiseledBookshelf(b) => b.items.iter().flatten().cloned().collect(),
Self::Shelf(b) => b.items.iter().flatten().cloned().collect(),
}
}
pub fn slots(&self) -> &[Option<ItemStack>] {
@@ -337,9 +428,17 @@ impl BlockEntity {
| Self::Banner(_)
| Self::EndGateway(_)
| Self::Skull(_)
| Self::DaylightDetector => &[],
| Self::PotentSulfur(_)
| Self::StructureBlock(_)
| Self::JigsawBlock(_)
| Self::TestBlock(_)
| Self::TestInstanceBlock(_)
| Self::DaylightDetector
| Self::Brushable(_) => &[],
Self::Crafter(c) => &c.items,
Self::ChiseledBookshelf(b) => &b.items,
Self::Shelf(b) => &b.items,
Self::Mod(m) => &m.items,
Self::CreakingHeart(_)
| Self::Spawner(_)
| Self::TrialSpawner(_)
@@ -363,9 +462,17 @@ impl BlockEntity {
| Self::Banner(_)
| Self::EndGateway(_)
| Self::Skull(_)
| Self::DaylightDetector => &mut [],
| Self::PotentSulfur(_)
| Self::StructureBlock(_)
| Self::JigsawBlock(_)
| Self::TestBlock(_)
| Self::TestInstanceBlock(_)
| Self::DaylightDetector
| Self::Brushable(_) => &mut [],
Self::Crafter(c) => &mut c.items,
Self::ChiseledBookshelf(b) => &mut b.items,
Self::Shelf(b) => &mut b.items,
Self::Mod(m) => &mut m.items,
Self::CreakingHeart(_)
| Self::Spawner(_)
| Self::TrialSpawner(_)
@@ -378,6 +485,58 @@ impl BlockEntity {
}
pub use crate::providers::Providers;
/// A `ChestBlock`, which `ChestBlock.combine` can join into a double chest:
/// chests, trapped chests and copper chests in every weathering state, waxed
/// or not (`CopperChestBlock` extends `ChestBlock`). Ender chests are
/// `EnderChestBlock`s and always single.
pub fn is_chest_block(name: &str) -> bool {
matches!(name, "minecraft:chest" | "minecraft:trapped_chest") || name.ends_with("copper_chest")
}
/// A chest block's name with its `type` and `facing` properties.
#[derive(Clone, Copy, Debug)]
pub struct ChestHalf<'a> {
pub block: &'a str,
pub kind: &'a str,
pub facing: &'a str,
}
impl ChestHalf<'_> {
/// `ChestBlock.getConnectedDirection` as an (x, z) step to the other
/// half: clockwise from the facing for a left half, counter-clockwise for
/// a right one. None for a single chest.
pub fn connected_step(&self) -> Option<(i32, i32)> {
let step = |facing: &str| match facing {
"north" => Some((0, -1)),
"south" => Some((0, 1)),
"west" => Some((-1, 0)),
"east" => Some((1, 0)),
_ => None,
};
let (x, z) = step(self.facing)?;
// Turning clockwise seen from above takes (x, z) to (-z, x).
match self.kind {
"left" => Some((-z, x)),
"right" => Some((z, -x)),
_ => None,
}
}
/// `DoubleBlockCombiner.combineWithNeigbour` as `ChestBlock.combine` calls
/// it: `other`, the block in this half's connected direction, is its other
/// half when it is the same block, a half of the other side and faces the
/// same way. Copper chests in different weathering states never join
/// (`CopperChestBlock` keeps both halves in one state instead).
pub fn pairs_with(&self, other: &ChestHalf) -> bool {
is_chest_block(self.block)
&& self.kind != "single"
&& other.block == self.block
&& other.kind != "single"
&& other.kind != self.kind
&& other.facing == self.facing
}
}
/// `AbstractFurnaceBlockEntity.createExperience`: `count * experience`, floored, and one
/// more with the chance of what is left over (a level random draw only when there is some).
pub fn create_experience(
@@ -395,6 +554,24 @@ pub fn create_experience(
xp
}
/// The `MenuType` the block entity of a one-block storage container opens
/// (`createMenu`): `ChestMenu.threeRows` for chests, trapped and copper chests
/// and barrels, `ShulkerBoxMenu` for shulker boxes of any colour (its
/// `ShulkerBoxSlot`s refuse shulker boxes), `DispenserMenu` for dispensers
/// and droppers, `HopperMenu` for hoppers. None for other blocks.
pub fn block_menu_type(block: &str) -> Option<&'static str> {
Some(match block {
"minecraft:chest" | "minecraft:trapped_chest" | "minecraft:barrel" => {
"minecraft:generic_9x3"
}
n if n.ends_with("copper_chest") => "minecraft:generic_9x3",
n if n.ends_with("shulker_box") => "minecraft:shulker_box",
"minecraft:dispenser" | "minecraft:dropper" => "minecraft:generic_3x3",
"minecraft:hopper" => "minecraft:hopper",
_ => return None,
})
}
#[cfg(test)]
mod tests {
use super::*;
@@ -3,6 +3,7 @@
//! Written to `crash-reports/crash-<yyyy-MM-dd_HH.mm.ss>-client.txt` in the game folder when
//! the main thread panics or `main` gets an error back. It leaves the machine only scrubbed, and
//! only if the player allows crash reports (`telemetry`).
use std::cell::{Cell, RefCell};
use std::fmt::Write as _;
use std::path::{Path, PathBuf};
use std::sync::Mutex;
@@ -14,6 +15,51 @@ use chrono::{DateTime, Local};
static DETAILS: Mutex<Vec<(&'static str, String)>> = Mutex::new(Vec::new());
static REPORTED: AtomicBool = AtomicBool::new(false);
thread_local! {
/// How many `contain` calls this thread is inside.
static CONTAINED: Cell<u32> = const { Cell::new(0) };
/// The message of the last panic `contain` caught on this thread, from the hook.
static CAUGHT: RefCell<Option<String>> = const { RefCell::new(None) };
}
/// Runs `f`, and returns a panic in it as an error with the panic's message instead of letting
/// it end the game. For work that is not the game's own and whose failure the caller recovers
/// from, such as handing a shader pack's programs to the GPU driver's compiler: the panic goes
/// to the log with its stack, and no crash report is made for it.
pub fn contain<T>(f: impl FnOnce() -> T) -> Result<T, String> {
CONTAINED.with(|c| c.set(c.get() + 1));
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f));
CONTAINED.with(|c| c.set(c.get() - 1));
result.map_err(|payload| {
CAUGHT
.with(|c| c.borrow_mut().take())
.unwrap_or_else(|| payload_text(&*payload))
})
}
/// For the hook: a panic inside `contain` is logged with its stack and kept for `contain` to
/// return. False for any other panic.
fn contained_panic(info: &std::panic::PanicHookInfo) -> bool {
if CONTAINED.with(Cell::get) == 0 {
return false;
}
let message = panic_message(info);
let stack = stack_lines(
&std::backtrace::Backtrace::force_capture().to_string(),
true,
);
log::error!(
"Recovered from an error: {message}\n{}",
stack
.lines()
.map(|l| format!("\tat {l}"))
.collect::<Vec<_>>()
.join("\n")
);
CAUGHT.with(|c| *c.borrow_mut() = Some(message));
true
}
/// Sets one line of the report's details (replacing the last value under `key`).
pub fn set_detail(key: &'static str, value: impl Into<String>) {
let mut details = DETAILS.lock().unwrap_or_else(|e| e.into_inner());
@@ -250,8 +296,9 @@ fn memory() -> String {
}
}
/// The report's details: Horizon's and the machine's, then what the game set as it ran.
pub fn system_details() -> Vec<(String, String)> {
/// The report's details: Horizon's and the machine's, then what the game set as it ran. A dedicated
/// server renders nothing: it leaves out the render settings it read from options.txt.
pub fn system_details(side: &str) -> Vec<(String, String)> {
let mut out = vec![
("Horizon Version".to_owned(), version()),
(
@@ -262,18 +309,30 @@ pub fn system_details() -> Vec<(String, String)> {
("CPU".to_owned(), cpu()),
("Memory".to_owned(), memory()),
];
out.extend(details().into_iter().map(|(k, v)| (k.to_owned(), v)));
out.extend(
details()
.into_iter()
.filter(|(k, _)| side != "server" || *k != "Render Settings")
.map(|(k, v)| (k.to_owned(), v)),
);
out
}
fn thread_name() -> String {
/// The thread's name as vanilla's reports say it: the main thread is the client's "Render thread"
/// and the dedicated server's "Server thread".
fn thread_name(side: &str) -> String {
let current = std::thread::current();
match current.name() {
Some("main") | None if side == "server" => "Server thread".into(),
Some("main") | None => "Render thread".into(),
Some(name) => name.into(),
}
}
/// What starts the stderr line naming a saved report, the path follows: the Launch API's
/// promise to launchers (docs/api/launch.md), so it never changes within a line of that API.
pub const SAVED_LINE: &str = "Horizon: crash report saved to ";
/// Writes the report and tells where it is; the log gets the error too.
fn deliver(home: &Path, report: Report) -> Option<PathBuf> {
if REPORTED.swap(true, Ordering::SeqCst) {
@@ -297,7 +356,7 @@ fn deliver(home: &Path, report: Report) -> Option<PathBuf> {
crate::telemetry::keep_crash(home, &report);
match report.save(home) {
Ok(path) => {
eprintln!("Horizon: crash report saved to {}", path.display());
eprintln!("{SAVED_LINE}{}", path.display());
Some(path)
}
Err(e) => {
@@ -308,13 +367,35 @@ fn deliver(home: &Path, report: Report) -> Option<PathBuf> {
}
}
fn panic_message(info: &std::panic::PanicHookInfo) -> String {
let payload = info.payload();
let text = payload
/// The report for a hung server tick, written by the watchdog thread
/// (`ServerWatchdog`): the stuck thread's stack is not available to Rust, so
/// the report carries none.
pub fn report_watchdog(home: &Path, side: &'static str, message: String) -> Option<PathBuf> {
deliver(
home,
Report {
time: Local::now(),
description: "Watching Server".into(),
error: format!("{message} (Watching Server)"),
stack: String::new(),
thread: thread_name(side),
side,
details: system_details(side),
log: crate::logging::recent_lines(),
},
)
}
fn payload_text(payload: &(dyn std::any::Any + Send)) -> String {
payload
.downcast_ref::<&str>()
.map(|s| s.to_string())
.or_else(|| payload.downcast_ref::<String>().cloned())
.unwrap_or_else(|| "Box<dyn Any>".into());
.unwrap_or_else(|| "Box<dyn Any>".into())
}
fn panic_message(info: &std::panic::PanicHookInfo) -> String {
let text = payload_text(info.payload());
match info.location() {
Some(at) => format!("panicked at {at}:\n{text}"),
None => format!("panicked:\n{text}"),
@@ -330,7 +411,7 @@ pub fn stack_lines(backtrace: &str, from_panic: bool) -> String {
// separator the build machine used.
let manifest = env!("CARGO_MANIFEST_DIR").replace('\\', "/");
let root = manifest
.strip_suffix("crates/horizon-client")
.strip_suffix("crates/horizon-client-render")
.unwrap_or(&manifest);
let mut frames: Vec<(String, Option<String>)> = Vec::new();
for line in backtrace.lines() {
@@ -408,27 +489,50 @@ pub fn panic_report(info: &std::panic::PanicHookInfo, side: &'static str) -> Rep
&std::backtrace::Backtrace::force_capture().to_string(),
true,
),
thread: thread_name(),
thread: thread_name(side),
side,
details: system_details(),
details: system_details(side),
log: crate::logging::recent_lines(),
}
}
/// Whether `name` is one of the streaming pool's threads, vanilla's `Util` worker threads.
fn is_worker_thread(name: Option<&str>) -> bool {
name.is_some_and(|n| n.starts_with("horizon-worker-"))
}
/// `Util.onThreadException`: a panic on a worker thread is made a report titled "Exception on
/// worker thread", with the thread's name, as `BlockableEventLoop.relayDelayCrash` hands it to
/// the main thread.
pub fn worker_panic_report(info: &std::panic::PanicHookInfo, side: &'static str) -> Report {
Report {
description: "Exception on worker thread".into(),
..panic_report(info, side)
}
}
/// Reports panics: on the main thread a crash report (the panic then ends the game, exit code
/// 101); on other threads the error goes to the log, as vanilla's uncaught exception handler
/// does. The default hook still prints the panic.
/// 101); on a worker thread (`Util.onThreadException`) a crash report too, and the game ends
/// with it; on other threads the error goes to the log, as vanilla's uncaught exception handler
/// does. The default hook still prints the panic. A panic inside `contain` is only logged.
pub fn install(home: PathBuf, side: &'static str) {
let default = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
if contained_panic(info) {
return;
}
default(info);
let main = std::thread::current().name() == Some("main");
if main {
deliver(&home, panic_report(info, side));
} else if is_worker_thread(std::thread::current().name()) {
deliver(&home, worker_panic_report(info, side));
// The crash ends the game, as the relayed report does on vanilla's main thread.
std::process::exit(101);
} else {
log::error!(
"Caught previously unhandled exception in thread {}: {}",
thread_name(),
thread_name(side),
panic_message(info)
);
}
@@ -477,9 +581,9 @@ pub fn report_error(
description: DebugCrash::MESSAGE.into(),
error: DebugCrash::MESSAGE.into(),
stack: debug.stack.clone(),
thread: thread_name(),
thread: thread_name(side),
side,
details: system_details(),
details: system_details(side),
log: crate::logging::recent_lines(),
},
);
@@ -497,9 +601,9 @@ pub fn report_error(
description: "Fatal error".into(),
error: message,
stack: String::new(),
thread: thread_name(),
thread: thread_name(side),
side,
details: system_details(),
details: system_details(side),
log: crate::logging::recent_lines(),
},
)
@@ -597,6 +701,26 @@ mod tests {
));
}
#[test]
fn a_dedicated_servers_report_names_its_server_thread_and_no_render_settings() {
let unnamed = |side: &'static str| {
std::thread::spawn(move || thread_name(side))
.join()
.unwrap()
};
assert_eq!(unnamed("server"), "Server thread");
assert_eq!(unnamed("client"), "Render thread");
set_detail("Render Settings", "render distance 12");
let has = |side| {
system_details(side)
.iter()
.any(|(k, _)| k == "Render Settings")
};
assert!(has("client"));
assert!(!has("server"));
clear_detail("Render Settings");
}
#[test]
fn stacks_start_at_the_panicking_frame() {
let bt = format!(
@@ -605,10 +729,21 @@ mod tests {
);
assert_eq!(
stack_lines(&bt, true),
"horizon_client::crash::debug_crash_tick (crates/horizon-client/src/crash.rs:359:9)\nwinit::dispatch (winit-0.30.13/src/event_loop.rs:642:56)\nmain\n"
"horizon_client::crash::debug_crash_tick (crates/horizon-client-render/src/crash.rs:359:9)\nwinit::dispatch (winit-0.30.13/src/event_loop.rs:642:56)\nmain\n"
);
}
#[test]
fn contained_panics_come_back_as_errors() {
assert_eq!(contain(|| 7), Ok(7));
let error = contain(|| -> u8 { panic!("a pack's program") }).unwrap_err();
assert!(error.contains("a pack's program"), "{error}");
// Nested, and the thread is no longer inside one after.
let outer = contain(|| contain(|| -> u8 { panic!("inner") }));
assert!(outer.unwrap().unwrap_err().contains("inner"));
assert_eq!(CONTAINED.with(Cell::get), 0);
}
#[test]
fn the_debug_crash_is_reported_as_vanillas() {
let error: Box<dyn std::error::Error> = DebugCrash::new().into();
@@ -619,6 +754,13 @@ mod tests {
assert!(first.contains("crash.rs"), "{}", debug.stack);
}
#[test]
fn worker_threads_are_the_streaming_pool() {
assert!(is_worker_thread(Some("horizon-worker-3")));
assert!(!is_worker_thread(Some("main")));
assert!(!is_worker_thread(None));
}
#[test]
fn a_panic_becomes_a_report() {
// A real panic through a hook, as the game's would be.
@@ -2,7 +2,10 @@
//! the parts of them commands use: `.mcfunction` functions (with macros and
//! line continuation), function tags and predicates.
use crate::zip::Zip;
use horizon_world::nbt::Tag;
use horizon_world::{
nbt::Tag,
worldgen::overrides::{self, PackFile, WorldgenOverrides},
};
use std::{
collections::BTreeMap,
path::{Path, PathBuf},
@@ -125,6 +128,114 @@ pub fn discover(world: &Path) -> Vec<DataPack> {
packs
}
// ---- Create World ------------------------------------------------------------
/// Create World's Data Packs screen (`PackSelectionScreen` on vanilla's
/// `mcworld-` folder): the folder's packs (`PackSource.WORLD`) and the
/// built-in one (`resources` holds its `pack.png`), which can be unselected
/// and stays at the bottom. Horizon has no experiments, so their feature
/// packs are not offered.
pub fn screen_packs(dir: &Path, resources: &Path) -> Vec<crate::pack_screen::PackInfo> {
use crate::{
pack_screen::{Compat, PackInfo, Source, load_icon},
packs::Position,
text::{Style, Text},
};
let format = horizon_core::version::DATA_PACK_FORMAT;
let mut infos: Vec<PackInfo> = crate::packs::folder(dir, format)
.into_iter()
.map(|p| PackInfo {
icon: p.icon_png().as_deref().and_then(load_icon),
id: p.id,
title: p.title,
description: p.raw_description,
description_style: p.description_style,
source: Source::World,
compat: p.compatibility,
required: false,
position: Position::Top,
fixed: false,
})
.collect();
infos.push(PackInfo {
id: "vanilla".into(),
title: Text::translate("dataPack.vanilla.name", &[]),
description: Text::translate("dataPack.vanilla.description", &[]),
description_style: Style::default(),
source: Source::BuiltIn,
compat: Compat::Compatible,
icon: std::fs::read(resources.join("pack.png"))
.ok()
.as_deref()
.and_then(load_icon),
required: false,
position: Position::Bottom,
fixed: false,
});
infos
}
/// A new empty folder for Create World's packs
/// (`Files.createTempDirectory("mcworld-")`).
pub fn temp_folder() -> std::io::Result<PathBuf> {
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos();
let dir = std::env::temp_dir().join(format!("mcworld-{}-{nanos}", std::process::id()));
std::fs::create_dir_all(&dir)?;
Ok(dir)
}
/// What's in a folder of packs, to notice packs added or removed while the
/// screen shows it (`PackSelectionScreen.Watcher`).
pub fn folder_stamp(dir: &Path) -> Vec<(String, Option<std::time::SystemTime>)> {
let mut stamp: Vec<_> = std::fs::read_dir(dir)
.into_iter()
.flatten()
.flatten()
.map(|e| {
let modified = e.metadata().and_then(|m| m.modified()).ok();
(e.file_name().to_string_lossy().into_owned(), modified)
})
.collect();
stamp.sort();
stamp
}
/// Create World's packs into the new world before it opens
/// (`copyTempDataPackDirToNewWorld`): every pack of `dir` goes to
/// `<world>/datapacks`, and `datapacks.json` enables those `selected`
/// (lowest priority first) and disables the rest.
pub fn install(dir: &Path, world: &Path, selected: &[String]) -> Result<(), String> {
let err = |p: &Path, e: std::io::Error| format!("{}: {e}", p.display());
let target = world.join("datapacks");
std::fs::create_dir_all(&target).map_err(|e| err(&target, e))?;
let mut disabled = Vec::new();
for pack in crate::packs::folder(dir, horizon_core::version::DATA_PACK_FORMAT) {
let (crate::packs::Source::Dir(from) | crate::packs::Source::Zip(from)) = &pack.source;
copy(from, &target.join(pack.id.trim_start_matches("file/")))?;
if !selected.contains(&pack.id) {
disabled.push(pack.id);
}
}
if !selected.iter().any(|id| id == "vanilla") {
disabled.push("vanilla".into());
}
let json = serde_json::json!({ "enabled": selected, "disabled": disabled });
let file = world.join("datapacks.json");
std::fs::write(&file, json.to_string()).map_err(|e| err(&file, e))
}
/// A file, or a folder and everything in it.
fn copy(from: &Path, to: &Path) -> Result<(), String> {
let err = |e: std::io::Error| format!("{} -> {}: {e}", from.display(), to.display());
if !from.is_dir() {
return std::fs::copy(from, to).map(drop).map_err(err);
}
std::fs::create_dir_all(to).map_err(err)?;
for e in std::fs::read_dir(from).map_err(err)?.flatten() {
copy(&e.path(), &to.join(e.file_name()))?;
}
Ok(())
}
// ---- functions ---------------------------------------------------------------
/// A `.mcfunction`: plain command lines and macro lines.
@@ -312,9 +423,31 @@ pub struct DataStore {
pub predicates: BTreeMap<String, serde_json::Value>,
/// `item_modifier` files, for `/item modify`.
pub item_modifiers: BTreeMap<String, serde_json::Value>,
/// The content tables' raw files from the packs after the built-in one:
/// `recipe/`, `loot_table/`, `advancement/` and `tags/…/` files, by
/// `<kind-dir>/<ns>:<path>`, each id's files in pack order. The tables
/// merge them over what they read from the resource root.
pub files: BTreeMap<String, Vec<Vec<u8>>>,
/// Functions that failed to parse (id, message), for logging.
pub errors: Vec<(String, String)>,
}
/// `<kind-dir>/<ns>:<path>` for a pack file the content tables read:
/// `recipe/`, `loot_table/`, `advancement/` or `tags/<sub>/`, where the id
/// keeps the sub path so a nested tag kind stays addressable.
fn content_id(file: &str) -> Option<String> {
let rest = file.strip_prefix("data/")?;
let (ns, rest) = rest.split_once('/')?;
let (kind, rest) = rest.split_once('/')?;
let kind = match kind {
"recipe" | "loot_table" | "advancement" => kind,
"tags" => {
let (sub, name) = rest.rsplit_once('/')?;
return Some(format!("tags/{sub}/{ns}:{}", name.strip_suffix(".json")?));
}
_ => return None,
};
Some(format!("{kind}/{ns}:{}", rest.strip_suffix(".json")?))
}
/// `ns:path` for a file `data/<ns>/<kind>/<path>.<ext>`.
fn resource_id(file: &str, kind: &str, ext: &str) -> Option<String> {
let rest = file.strip_prefix("data/")?;
@@ -329,7 +462,22 @@ impl DataStore {
pub fn load(packs: &[&DataPack]) -> Self {
let mut s = Self::default();
for pack in packs {
for f in pack.list_kinds(&["function", "item_modifier", "predicate", "tags/function"]) {
// `tags` walks the whole tags tree, so a `tags/function` file is
// listed by both kinds: handle every file once.
let mut seen = std::collections::BTreeSet::new();
let builtin = pack.id == "vanilla";
for f in pack.list_kinds(&[
"advancement",
"function",
"item_modifier",
"loot_table",
"predicate",
"recipe",
"tags",
]) {
if !seen.insert(f.clone()) {
continue;
}
if let Some(id) = resource_id(&f, "function", ".mcfunction")
&& let Some(bytes) = pack.read(&f)
{
@@ -359,6 +507,14 @@ impl DataStore {
&& let Ok(v) = serde_json::from_slice(&bytes)
{
s.item_modifiers.insert(id, v);
} else if !builtin
&& let Some(id) = content_id(&f)
&& let Some(bytes) = pack.read(&f)
{
// The built-in pack's content files are the resource
// root's own, which the tables read from disk; only the
// world's packs come through here.
s.files.entry(id).or_default().push(bytes);
}
}
}
@@ -368,11 +524,21 @@ impl DataStore {
/// included, each function once).
pub fn tag_functions(&self, id: &str) -> Vec<String> {
let mut out = Vec::new();
self.expand(id, &mut out, 0);
let mut seen_fns = std::collections::HashSet::new();
let mut seen_tags = std::collections::HashSet::new();
self.expand(id, &mut out, &mut seen_fns, &mut seen_tags);
out
}
fn expand(&self, id: &str, out: &mut Vec<String>, depth: usize) {
if depth > 16 {
/// Depth-first walk keeping first-seen order. Each tag is expanded at most
/// once, so reference cycles end and nesting depth is not capped.
fn expand<'a>(
&'a self,
id: &'a str,
out: &mut Vec<String>,
seen_fns: &mut std::collections::HashSet<&'a str>,
seen_tags: &mut std::collections::HashSet<&'a str>,
) {
if !seen_tags.insert(id) {
return;
}
let Some(entries) = self.function_tags.get(id) else {
@@ -380,9 +546,9 @@ impl DataStore {
};
for (e, required) in entries {
match e.strip_prefix('#') {
Some(inner) => self.expand(inner, out, depth + 1),
Some(inner) => self.expand(inner, out, seen_fns, seen_tags),
None => {
if (self.functions.contains_key(e) || *required) && !out.contains(e) {
if (self.functions.contains_key(e) || *required) && seen_fns.insert(e) {
out.push(e.clone());
}
}
@@ -421,9 +587,167 @@ fn hash_normalize(s: &str) -> String {
}
}
/// The ids of `packs` (a world's, from `discover`) that are enabled, lowest
/// priority first: those `<world>/datapacks.json` lists as enabled, in its
/// order, then every pack it lists neither way, in name order.
pub fn enabled_ids(world: &Path, packs: &[DataPack]) -> Vec<String> {
let saved: Option<serde_json::Value> = std::fs::read(world.join("datapacks.json"))
.ok()
.and_then(|b| serde_json::from_slice(&b).ok());
let list = |key: &str| -> Vec<String> {
saved
.as_ref()
.and_then(|v| v[key].as_array())
.map(|a| {
a.iter()
.filter_map(|s| s.as_str().map(str::to_owned))
.collect()
})
.unwrap_or_default()
};
let disabled = list("disabled");
let mut enabled: Vec<String> = list("enabled")
.into_iter()
.filter(|id| packs.iter().any(|p| &p.id == id))
.collect();
for p in packs {
if !enabled.contains(&p.id) && !disabled.contains(&p.id) {
enabled.push(p.id.clone());
}
}
enabled
}
/// The level stems of `packs` (`data/<namespace>/dimension/<path>.json`, lowest priority first,
/// as `enabled_ids` orders them): the id `namespace:path` and the parsed JSON, a later pack's
/// file for an id replacing an earlier one's. This is `context.datapackDimensions()` of
/// `WorldOpenFlows.loadWorldStem`.
pub fn dimension_stems(packs: &[&DataPack]) -> BTreeMap<String, serde_json::Value> {
let mut out = BTreeMap::new();
for pack in packs {
for file in pack.list_kinds(&["dimension"]) {
let Some(rest) = file.strip_prefix("data/") else {
continue;
};
let Some((namespace, rest)) = rest.split_once('/') else {
continue;
};
let Some(path) = rest
.strip_prefix("dimension/")
.and_then(|p| p.strip_suffix(".json"))
else {
continue;
};
if let Some(json) = pack
.read(&file)
.and_then(|b| serde_json::from_slice(&b).ok())
{
out.insert(format!("{namespace}:{path}"), json);
}
}
}
out
}
// ---- worldgen ----------------------------------------------------------------
/// The worldgen entries of `packs` (lowest priority first, the built-in pack
/// left out: the prepared bundles are it) for the chunk generator, and what
/// it leaves out, for the log. `biomes` says which biomes the game has: a
/// pack's new biome is left out (the game has no colours, sounds or id for it).
pub fn worldgen_overrides(
packs: &[&DataPack],
biomes: impl Fn(&str) -> bool,
) -> (WorldgenOverrides, Vec<String>) {
let mut out = WorldgenOverrides::default();
let mut not_read: BTreeMap<String, usize> = BTreeMap::new();
let mut warnings = Vec::new();
for pack in packs {
for file in pack.list_kinds(&overrides::PACK_KINDS) {
let read = || String::from_utf8(pack.read(&file)?).ok();
match out.add_file(&pack.id, &file, read) {
PackFile::Read | PackFile::Other => {}
PackFile::NotRead(kind) => {
*not_read.entry(format!("{}: {kind}", pack.id)).or_default() += 1
}
PackFile::Unreadable => warnings.push(format!("{}: {file}: unreadable", pack.id)),
}
}
}
warnings.extend(out.finish(biomes));
for (kind, files) in not_read {
warnings.push(format!("{kind}: {files} files the generator does not read"));
}
(out, warnings)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn enabled_packs_override_worldgen_in_order() {
let dir = std::env::temp_dir().join(format!("hz-datapack-worldgen-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
let write = |pack: &str, p: &str, t: &str| {
let path = dir.join("datapacks").join(pack).join(p);
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(path, t).unwrap();
};
for pack in ["a", "b", "off"] {
write(pack, "pack.mcmeta", "{}");
write(pack, "data/minecraft/worldgen/noise/cave.json", pack);
write(
pack,
"data/minecraft/tags/worldgen/biome/is_hill.json",
"{\"values\":[]}",
);
}
write("a", "data/minecraft/worldgen/biome/new_peaks.json", "{}");
write("a", "data/minecraft/worldgen/biome/plains.json", "{}");
write(
"a",
"data/minecraft/worldgen/world_preset/normal.json",
"{}",
);
write(
"b",
"data/minecraft/dimension_type/overworld.json",
"{\"min_y\":-64,\"height\":2096}",
);
// `b` is enabled first, so `a` (enabled after it) wins.
std::fs::write(
dir.join("datapacks.json"),
"{\"enabled\":[\"file/b\",\"file/a\"],\"disabled\":[\"file/off\"]}",
)
.unwrap();
let packs = discover(&dir);
let enabled = enabled_ids(&dir, &packs);
assert_eq!(enabled, ["file/b", "file/a"]);
let enabled: Vec<&DataPack> = enabled
.iter()
.map(|id| packs.iter().find(|p| &p.id == id).unwrap())
.collect();
let (o, warnings) = worldgen_overrides(&enabled, |b| b == "minecraft:plains");
assert_eq!(
o.entries("noises").collect::<Vec<_>>(),
[("minecraft:cave", "a")]
);
assert_eq!(
o.entries("biomes").collect::<Vec<_>>(),
[("minecraft:plains", "{}")]
);
let tag_packs: Vec<&str> = o.tags("biome_tags").map(|(_, p, _)| p).collect();
assert_eq!(tag_packs, ["file/b", "file/a"]);
assert_eq!(warnings.len(), 2, "{warnings:?}");
assert!(warnings[0].contains("minecraft:new_peaks"));
assert!(warnings[1].contains("worldgen/world_preset"));
assert_eq!(
o.level_height("minecraft:overworld").map(|h| h.height),
Some(2096)
);
let _ = std::fs::remove_dir_all(&dir);
}
use horizon_world::nbt::parse_snbt;
#[test]
@@ -527,6 +851,40 @@ mod tests {
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn tag_resolution_dedupes_fast_ends_cycles_and_nests_deeply() {
let mut store = DataStore::default();
let big: Vec<(String, bool)> = (0..50_000)
.map(|i| (format!("t:f{}", i % 25_000), true))
.collect();
store.function_tags.insert("t:big".into(), big);
let start = std::time::Instant::now();
let got = store.tag_functions("t:big");
assert_eq!(got.len(), 25_000);
assert_eq!(got[0], "t:f0");
assert!(start.elapsed() < std::time::Duration::from_secs(5));
// A cycle terminates and keeps first-seen order.
store.function_tags.insert(
"t:a".into(),
vec![("#t:b".into(), true), ("t:x".into(), true)],
);
store.function_tags.insert(
"t:b".into(),
vec![("#t:a".into(), true), ("t:y".into(), true)],
);
assert_eq!(store.tag_functions("t:a"), ["t:y", "t:x"]);
// Nesting deeper than the old cap of 16 still resolves.
for i in 0..40 {
store
.function_tags
.insert(format!("d:{i}"), vec![(format!("#d:{}", i + 1), true)]);
}
store
.function_tags
.insert("d:40".into(), vec![("t:deep".into(), true)]);
assert_eq!(store.tag_functions("d:0"), ["t:deep"]);
}
#[test]
fn folder_packs_list_the_needed_kinds_in_walk_order() {
let dir = std::env::temp_dir().join(format!("hz-datapack-kinds-{}", std::process::id()));
@@ -566,4 +924,59 @@ mod tests {
assert_eq!(p.list_kinds(&kinds), wanted);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn create_world_installs_its_packs() {
let root = std::env::temp_dir().join(format!("hz-datapack-install-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&root);
let (dir, world) = (root.join("mcworld"), root.join("world"));
let write = |pack: &str, p: &str, t: &str| {
let path = dir.join(pack).join(p);
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(path, t).unwrap();
};
write(
"fns",
"pack.mcmeta",
r#"{"pack":{"min_format":121,"max_format":121}}"#,
);
write("fns", "data/x/function/f.mcfunction", "say hi");
write(
"off",
"pack.mcmeta",
r#"{"pack":{"min_format":121,"max_format":121}}"#,
);
std::fs::create_dir_all(&dir).unwrap();
let mut zip = zip::ZipWriter::new(std::fs::File::create(dir.join("old.zip")).unwrap());
zip.start_file("pack.mcmeta", zip::write::SimpleFileOptions::default())
.unwrap();
std::io::Write::write_all(&mut zip, br#"{"pack":{"pack_format":48}}"#).unwrap();
zip.finish().unwrap();
let infos = screen_packs(&dir, &root);
let ids: Vec<&str> = infos.iter().map(|p| p.id.as_str()).collect();
assert_eq!(ids, ["file/fns", "file/off", "file/old.zip", "vanilla"]);
let compat: Vec<_> = infos.iter().map(|p| p.compat).collect();
use crate::pack_screen::Compat;
assert_eq!(
compat,
[
Compat::Compatible,
Compat::Compatible,
Compat::TooOld,
Compat::Compatible
]
);
let selected = ["vanilla", "file/old.zip", "file/fns"].map(String::from);
install(&dir, &world, &selected).unwrap();
let packs = discover(&world);
let ids: Vec<&str> = packs.iter().map(|p| p.id.as_str()).collect();
assert_eq!(ids, ["file/fns", "file/off", "file/old.zip"]);
let saved: serde_json::Value =
serde_json::from_slice(&std::fs::read(world.join("datapacks.json")).unwrap()).unwrap();
assert_eq!(
saved,
serde_json::json!({"enabled": selected, "disabled": ["file/off"]})
);
let _ = std::fs::remove_dir_all(&root);
}
}
@@ -148,6 +148,15 @@ pub fn instant(id: &str, amp: i32, scale: f64) -> Option<EffectAction> {
_ => None,
}
}
/// `MobEffectInstance.withScaledDuration`: max(floor(d * scale), 1), leaving
/// an infinite (-1) or zero duration alone.
pub fn scaled_duration(duration: i32, scale: f32) -> i32 {
if duration == -1 || duration == 0 {
duration
} else {
((duration as f32 * scale).floor() as i32).max(1)
}
}
/// What an omen's `applyEffectTick` decided.
#[derive(Clone, Debug, PartialEq)]
pub enum OmenTick {
@@ -162,6 +171,9 @@ pub enum OmenTick {
pub struct PlayerEffects {
pub list: Vec<EffectInstance>,
pub tick_count: i32,
/// How many times `add` added or updated an effect: each is one
/// `onEffectAdded` or `onEffectUpdated` (`EffectsChangedTrigger`).
pub changes: u32,
}
impl PlayerEffects {
pub fn get(&self, id: &str) -> Option<&EffectInstance> {
@@ -172,13 +184,17 @@ impl PlayerEffects {
}
/// `LivingEntity.addEffect`; returns whether anything changed.
pub fn add(&mut self, effect: EffectInstance) -> bool {
match self.list.iter_mut().find(|e| e.id == effect.id) {
let changed = match self.list.iter_mut().find(|e| e.id == effect.id) {
Some(existing) => existing.update(&effect),
None => {
self.list.push(effect);
true
}
};
if changed {
self.changes = self.changes.wrapping_add(1);
}
changed
}
pub fn clear(&mut self) {
self.list.clear();
@@ -308,19 +324,29 @@ impl PlayerEffects {
pub fn potion_effects(
potion: Option<&str>,
scale: f32,
) -> (Vec<EffectInstance>, Vec<EffectAction>) {
let effects = potion
.and_then(potions::potion)
.map_or(&[][..], |d| d.effects);
Self::contents_effects(effects, scale)
}
/// `PotionContents.forEachEffect`'s effects, as `potion_effects`.
pub fn contents_effects(
effects: &[(&'static str, i32, i32)],
scale: f32,
) -> (Vec<EffectInstance>, Vec<EffectAction>) {
let mut lasting = Vec::new();
let mut now = Vec::new();
if let Some(def) = potion.and_then(potions::potion) {
for (id, duration, amp) in def.effects {
let id = potions::effect(id).map_or("speed", |e| e.id);
if potions::effect(id).is_some_and(|e| e.instant) {
now.extend(instant(id, *amp, 1.0));
} else {
// withScaledDuration: max(floor(d * scale), 1).
let d = ((*duration as f32) * scale).floor() as i32;
lasting.push(EffectInstance::new(id, d.max(1), *amp));
}
for (id, duration, amp) in effects {
let id = potions::effect(id).map_or("speed", |e| e.id);
if potions::effect(id).is_some_and(|e| e.instant) {
now.extend(instant(id, *amp, 1.0));
} else {
lasting.push(EffectInstance::new(
id,
scaled_duration(*duration, scale),
*amp,
));
}
}
(lasting, now)
@@ -463,6 +489,19 @@ pub fn effect_modifier(effect: &str) -> Option<(&'static str, &'static str, f64,
mod tests {
use super::*;
#[test]
fn adding_counts_a_change_only_when_the_effect_changed() {
let mut fx = PlayerEffects::default();
fx.add(EffectInstance::new("speed", 100, 0));
assert_eq!(fx.changes, 1);
// A longer duration updates the effect (`onEffectUpdated`).
fx.add(EffectInstance::new("speed", 200, 0));
assert_eq!(fx.changes, 2);
// A shorter one with the same amplifier is hidden by nothing: no change.
fx.add(EffectInstance::new("speed", 50, 0));
assert_eq!(fx.changes, 2);
}
#[test]
fn bad_omen_in_a_village_becomes_raid_omen_over_two_ticks() {
let mut fx = PlayerEffects::default();
@@ -22,6 +22,7 @@ struct Globals {
outline_params: vec4<f32>,
// Horizon Extras' dynamic lights: camera-relative position and level (dynamic_light.rs).
dynamic_lights: array<vec4<f32>, 16>,
sunrise_fan: array<vec4<f32>, 32>,
}
@group(0) @binding(0) var<uniform> globals: Globals;
@group(0) @binding(1) var atlas: texture_2d<f32>;
@@ -52,10 +53,13 @@ struct Out {
@location(9) light_color: vec4<f32>,
@location(10) @interpolate(flat) weather_rect: vec4<f32>,
// The `GLINT_TEXTURING` sample for overlay 7 (item foil), or the model UV
// `ARMOR_ENTITY_GLINT_TEXTURING` scrolls for overlay 13 (armour).
// `ARMOR_ENTITY_GLINT_TEXTURING` scrolls for overlay 36 (armour).
@location(11) glint_uv: vec2<f32>,
// 1 drops the quad's back-facing pixels (Vertex::normal w = 1), like a culling render type.
@location(12) @interpolate(flat) cull: u32,
// HDR tier of a light-giving particle (mesher::hdr_tier), from the overlay's
// high bits (particles.rs): fs_hdr_mark marks the frame with it.
@location(13) @interpolate(flat) tier: u32,
}
// Shipped light.glsl; normals are already transformed and RGBA8_SNORM packed.
fn entity_light(normal: vec3<f32>, nether: bool) -> f32 {
@@ -125,8 +129,9 @@ fn entity_vertex(position: vec3<f32>, uv: vec2<f32>,
@vertex fn vs_entity(@location(0) position: vec3<f32>, @location(1) uv: vec2<f32>,
@location(2) overlay: u32, @location(3) color: vec4<f32>, @location(4) light: vec4<u32>,
@location(5) normal: vec4<f32>, @location(6) glint_uv: vec2<f32>) -> Out {
var out = entity_vertex(position, uv, overlay, color, light, normal);
var out = entity_vertex(position, uv, overlay & 0xffffu, color, light, normal);
out.glint_uv = glint_uv;
out.tier = overlay >> 16u;
return out;
}
// Retained bolt-local geometry, translated using the same f64 camera
@@ -249,8 +254,8 @@ fn crumbling_uv(uv: vec2<f32>, packed_origin: vec2<f32>, sprite_size: vec2<f32>,
// Enchantment glint: item.fsh's GLINT path, sampled from
// enchanted_glint_item at the `GLINT_TEXTURING` UV.
color = glint_over(color, textureSample(glint_tex, glint_sampler, in.glint_uv));
} else if in.overlay == 13u {
// Enchanted armour: enchanted_glint_armor at the scrolling model UV.
} else if in.overlay == 36u {
// Enchanted armour (mobs::render::ARMOR_GLINT): enchanted_glint_armor at the scrolling model UV.
color = glint_over(
color,
textureSample(glint_armor_tex, glint_sampler, armor_glint_uv(in.glint_uv)),
@@ -278,6 +283,14 @@ fn crumbling_uv(uv: vec2<f32>, packed_origin: vec2<f32>, sprite_size: vec2<f32>,
return vec4(in.color.rgb, 1.0);
}
// HDR output: a light-giving particle's pixels marked in the frame's alpha the
// way terrain.wgsl marks a light source's, drawn again after the entities with
// only alpha written, so its colour stays as blended.
@fragment fn fs_hdr_mark(in: Out) -> @location(0) vec4<f32> {
if in.tier == 0u || textureSample(atlas, atlas_sampler, in.uv).a < 0.1 { discard; }
return vec4(0.0, 0.0, 0.0, 0.7 * f32(4u - min(in.tier, 4u)) / 4.0);
}
fn celestial_color(in: Out) -> vec4<f32> {
return textureSample(atlas, atlas_sampler, in.uv) * in.color * in.light_color;
}
@@ -389,3 +389,36 @@ pub fn apply_weather(sky: &mut Sky, rain: f32, thunder: f32) {
);
}
}
pub fn sky_darken(environment: &Environment, day_time: i64, rain: f32, thunder: f32) -> i32 {
let mut sky = environment.sample(day_time, None, None);
apply_weather(&mut sky, rain, thunder);
// Level.updateSkyBrightness: darkening from the sky light level.
(15.0 - 15.0 * sky.sky_light_level).round() as i32
}
#[cfg(test)]
mod tests {
use super::*;
fn overworld() -> Option<Environment> {
let root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../.local/horizon-client/26.3");
root.join("data/minecraft/timeline/day.json")
.exists()
.then(|| Environment::load(&root).unwrap())
}
#[test]
fn dark_outside_follows_time_and_weather_levels() {
let Some(env) = overworld() else {
return;
};
let dark = |time, rain, thunder| sky_darken(&env, time, rain, thunder) >= 4;
assert!(!dark(6000, 0.0, 0.0), "clear noon");
assert!(!dark(6000, 1.0, 0.0), "rainy noon");
assert!(dark(6000, 1.0, 1.0), "thunderstorm at noon");
assert!(dark(18000, 0.0, 0.0), "clear midnight");
assert!(!dark(1000, 0.0, 0.0), "clear morning");
}
}
@@ -72,9 +72,8 @@ pub struct Extras {
pub zoom_smooth: bool,
/// Divide mouse turning by the zoom.
pub zoom_sensitivity: bool,
/// The FPS counter and keystrokes extras are official mods now
/// (`moved_extras`): their old settings stay here until the game has
/// installed the mod for a player who had one on.
/// The FPS counter and keystrokes extras are official mods now: their
/// old settings stay here, kept as they were.
pub fps: bool,
pub fps_corner: Corner,
/// Also show the minimum and 1% low.
@@ -83,8 +82,8 @@ pub struct Extras {
pub keystrokes_corner: Corner,
/// Clicks per second under the mouse buttons.
pub keystrokes_cps: bool,
/// The official mods installed for the extras they replace, by mod id
/// (`movedToMods`): each is done once.
/// The official mods once installed for the extras they replace, by mod
/// id (`movedToMods`).
pub moved: Vec<String>,
pub minimap: bool,
pub minimap_corner: Corner,
@@ -97,6 +96,8 @@ pub struct Extras {
pub minimap_rotate: bool,
/// Retry lost multiplayer connections (`reconnect`).
pub auto_reconnect: bool,
/// The player's own name tag over its model in third person.
pub own_name_tag: bool,
/// Mouse Tweaks-style drags in the inventory and storage containers.
pub mouse_tweaks: bool,
/// With them, the wheel over a slot moves its items one at a time.
@@ -137,6 +138,7 @@ impl Default for Extras {
minimap_zoom: 1.0,
minimap_rotate: false,
auto_reconnect: false,
own_name_tag: false,
mouse_tweaks: false,
mouse_tweaks_wheel: true,
sort: false,
@@ -211,6 +213,7 @@ impl Extras {
}
"minimapRotate" => e.minimap_rotate = flag(e.minimap_rotate),
"autoReconnect" => e.auto_reconnect = flag(e.auto_reconnect),
"ownNameTag" => e.own_name_tag = flag(e.own_name_tag),
"mouseTweaks" => e.mouse_tweaks = flag(e.mouse_tweaks),
"mouseTweaksWheel" => e.mouse_tweaks_wheel = flag(e.mouse_tweaks_wheel),
"sort" => e.sort = flag(e.sort),
@@ -225,7 +228,7 @@ impl Extras {
}
pub fn serialize(&self) -> String {
format!(
"fullbright:{}\ndynamicLights:{}\nzoom:{}\nzoomKey:{}\nzoomLevel:{}\nzoomSmooth:{}\nzoomSensitivity:{}\nfps:{}\nfpsCorner:{}\nfpsLows:{}\nkeystrokes:{}\nkeystrokesCorner:{}\nkeystrokesCps:{}\nmovedToMods:{}\nminimap:{}\nminimapCorner:{}\nminimapSize:{}\nminimapShape:{}\nminimapZoom:{}\nminimapRotate:{}\nautoReconnect:{}\nmouseTweaks:{}\nmouseTweaksWheel:{}\nsort:{}\nsortKey:{}\n{}:{}\nagentControl:{}\ndiscordPresence:{}\n",
"fullbright:{}\ndynamicLights:{}\nzoom:{}\nzoomKey:{}\nzoomLevel:{}\nzoomSmooth:{}\nzoomSensitivity:{}\nfps:{}\nfpsCorner:{}\nfpsLows:{}\nkeystrokes:{}\nkeystrokesCorner:{}\nkeystrokesCps:{}\nmovedToMods:{}\nminimap:{}\nminimapCorner:{}\nminimapSize:{}\nminimapShape:{}\nminimapZoom:{}\nminimapRotate:{}\nautoReconnect:{}\nownNameTag:{}\nmouseTweaks:{}\nmouseTweaksWheel:{}\nsort:{}\nsortKey:{}\n{}:{}\nagentControl:{}\ndiscordPresence:{}\n",
bool_text(self.fullbright),
bool_text(self.dynamic_lights),
bool_text(self.zoom),
@@ -251,6 +254,7 @@ impl Extras {
self.minimap_zoom,
bool_text(self.minimap_rotate),
bool_text(self.auto_reconnect),
bool_text(self.own_name_tag),
bool_text(self.mouse_tweaks),
bool_text(self.mouse_tweaks_wheel),
bool_text(self.sort),
@@ -282,7 +286,7 @@ impl Extras {
}
/// The field-of-view modifier target `Camera.tickFov` eases toward: vanilla's
/// (`getFieldOfViewModifier`) divided by a mod's zoom (`render.set-zoom`). A
/// (`getFieldOfViewModifier`) divided by a mod's zoom (`native_zoom`). A
/// zoom of 1 leaves it as is.
pub fn fov_target(vanilla: f32, zoom: f32) -> f32 {
if zoom == 1.0 { vanilla } else { vanilla / zoom }
@@ -378,6 +382,7 @@ impl Cps {
pub enum Item {
Fullbright,
DynamicLights,
OwnNameTag,
AutoReconnect,
MouseTweaks,
MouseTweaksWheel,
@@ -396,6 +401,8 @@ pub fn rows() -> Vec<Result<Item, &'static str>> {
Err("Lighting"),
Ok(Fullbright),
Ok(DynamicLights),
Err("Third person"),
Ok(OwnNameTag),
Err("Inventory"),
Ok(MouseTweaks),
Ok(MouseTweaksWheel),
@@ -410,6 +417,7 @@ impl Item {
match self {
Item::Fullbright => "Fullbright",
Item::DynamicLights => "Dynamic lights",
Item::OwnNameTag => "Own name tag",
Item::AutoReconnect => "Auto-reconnect",
Item::MouseTweaks => "Mouse Tweaks controls",
Item::MouseTweaksWheel => "Scroll wheel moves items",
@@ -426,7 +434,7 @@ impl Item {
Some(match self {
MouseTweaksWheel => MouseTweaks,
SortKey => Sort,
Fullbright | DynamicLights | MouseTweaks | Sort | AutoReconnect | NoAi
Fullbright | DynamicLights | OwnNameTag | MouseTweaks | Sort | AutoReconnect | NoAi
| AgentControl | DiscordPresence => {
return None;
}
@@ -439,6 +447,9 @@ impl Item {
Item::DynamicLights => {
"Torches and other light sources held, dropped or on fire light the blocks around them."
}
Item::OwnNameTag => {
"Shows your name above your head in third person, as other players see it. Hidden when the server hides your team's names or you're invisible."
}
Item::AutoReconnect => {
"Rejoins a server after a lost connection or a restart, never after a kick or ban."
}
@@ -471,7 +482,9 @@ impl Item {
match self {
Item::Fullbright => Some(Cost::None),
Item::DynamicLights => Some(Cost::Low),
Item::AutoReconnect | Item::MouseTweaks | Item::Sort => Some(Cost::None),
Item::OwnNameTag | Item::AutoReconnect | Item::MouseTweaks | Item::Sort => {
Some(Cost::None)
}
_ => None,
}
}
@@ -480,6 +493,7 @@ impl Item {
Some(match self {
Item::Fullbright => e.fullbright,
Item::DynamicLights => e.dynamic_lights,
Item::OwnNameTag => e.own_name_tag,
Item::AutoReconnect => e.auto_reconnect,
Item::MouseTweaks => e.mouse_tweaks,
Item::MouseTweaksWheel => e.mouse_tweaks_wheel,
@@ -506,6 +520,7 @@ impl Item {
match self {
Item::Fullbright => e.fullbright = !e.fullbright,
Item::DynamicLights => e.dynamic_lights = !e.dynamic_lights,
Item::OwnNameTag => e.own_name_tag = !e.own_name_tag,
Item::AutoReconnect => e.auto_reconnect = !e.auto_reconnect,
Item::MouseTweaks => e.mouse_tweaks = !e.mouse_tweaks,
Item::MouseTweaksWheel => e.mouse_tweaks_wheel = !e.mouse_tweaks_wheel,
@@ -534,7 +549,7 @@ mod tests {
let d = Extras::default();
assert!(!d.fullbright && !d.dynamic_lights);
assert!(!d.zoom && !d.fps && !d.keystrokes && !d.minimap && !d.auto_reconnect);
assert!(!d.mouse_tweaks && !d.sort);
assert!(!d.mouse_tweaks && !d.sort && !d.own_name_tag);
assert!(d.discord_presence);
assert_eq!(Extras::parse(""), d);
assert_eq!(Extras::parse(&d.serialize()), d);
@@ -558,6 +573,8 @@ mod tests {
e.minimap_rotate = true;
e.minimap_corner = Corner::BottomLeft;
e.auto_reconnect = true;
Item::OwnNameTag.cycle(&mut e, false);
assert_eq!(Item::OwnNameTag.switch(&e), Some(true));
e.mouse_tweaks = true;
e.mouse_tweaks_wheel = false;
e.sort = true;
@@ -203,6 +203,13 @@ pub struct FogInputs<'a> {
/// `BossHealthOverlay.shouldCreateWorldFog`: a boss bar with
/// `createWorldFog` is shown (the Ender Dragon's).
pub boss_world_fog: bool,
/// The camera entity is a spectator (`LavaFogEnvironment`).
pub spectator: bool,
/// The camera entity has Fire Resistance (`LavaFogEnvironment`).
pub fire_resistance: bool,
/// `GameRenderer.nightVisionScale` while Night Vision is on without
/// Darkness, else 0 (`FogRenderer.computeFogColor`'s brightenFactor).
pub night_vision: f32,
}
/// `FogRenderer.setupFog` + `computeFogColor`.
@@ -272,7 +279,11 @@ pub fn setup_fog(tables: &FogTables, state: &FogState, input: &FogInputs) -> Fog
color[1] += (color[1] * 0.6 - color[1]) * t;
color[2] += (color[2] * 0.6 - color[2]) * t;
}
let brighten = if eye_in_water { water_vision } else { 0.0 };
let brighten = if eye_in_water {
water_vision
} else {
input.night_vision
};
if color.iter().all(|&c| c != 0.0) {
let scale = 1.0 / color[0].max(color[1]).max(color[2]);
color = color.map(|c| c + (c * scale - c) * brighten);
@@ -288,16 +299,26 @@ pub fn setup_fog(tables: &FogTables, state: &FogState, input: &FogInputs) -> Fog
};
match input.fog_type {
FogType::Lava => {
fog.environmental_start = 0.25;
fog.environmental_end = 1.0;
fog.sky_end = 1.0;
fog.cloud_end = 1.0;
// LavaFogEnvironment.setupFog.
(fog.environmental_start, fog.environmental_end) = if input.spectator {
(-8.0, rd * 0.5)
} else if input.fire_resistance {
(0.0, 5.0)
} else {
(0.25, 1.0)
};
fog.sky_end = fog.environmental_end;
fog.cloud_end = fog.environmental_end;
}
FogType::PowderSnow => {
fog.environmental_start = 0.0;
fog.environmental_end = 2.0;
fog.sky_end = 2.0;
fog.cloud_end = 2.0;
// PowderedSnowFogEnvironment.setupFog.
(fog.environmental_start, fog.environmental_end) = if input.spectator {
(-8.0, rd * 0.5)
} else {
(0.0, 2.0)
};
fog.sky_end = fog.environmental_end;
fog.cloud_end = fog.environmental_end;
}
_ if input.blindness.is_some() => {
let (duration, infinite) = input.blindness.unwrap_or((0, true));
@@ -362,6 +383,22 @@ pub fn setup_fog(tables: &FogTables, state: &FogState, input: &FogInputs) -> Fog
}
fog
}
/// Horizon's Fog option turned off (`horizon-graphics.txt` `fog:false`):
/// no distance fog, in any dimension: the dimension's and biome's own fog
/// (`visual/fog_start_distance`, `fog_end_distance`: the Nether's ends at 96
/// blocks) and the render-distance fog. The fog of the fluid or powder snow
/// the camera is in, blindness, darkness and a boss's world fog keep their
/// limits. Vanilla has no such option.
pub fn clear_distance_fog(fog: &mut FogData, input: &FogInputs) {
if input.fog_type == FogType::Atmospheric
&& input.darkness.is_none()
&& !input.boss_world_fog
&& input.blindness.is_none()
{
(fog.render_distance_start, fog.render_distance_end) = (1e9, 2e9);
(fog.environmental_start, fog.environmental_end) = (1e9, 2e9);
}
}
/// `AtmosphericFogEnvironment.updateRainFogState`.
pub fn update_rain_fog(
state: &mut FogState,
@@ -405,6 +442,9 @@ mod tests {
darkness: None,
darken_world: 0.0,
boss_world_fog: false,
spectator: false,
fire_resistance: false,
night_vision: 0.0,
}
}
#[test]
@@ -432,6 +472,56 @@ mod tests {
assert_eq!(water.environmental_start, -8.0);
}
#[test]
fn fog_off_clears_the_nether_fog_too() {
// The Nether's visual/fog_start_distance and fog_end_distance (10, 96)
// end before the render distance; the Fog option still clears them.
let nether = Sky {
fog_start: 10.0,
fog_end: 96.0,
..Sky::default()
};
let tables = FogTables {
water_fog_end: vec![1.0],
};
let mut input = inputs(&nether);
input.skybox = false;
let mut fog = setup_fog(&tables, &FogState::default(), &input);
assert_eq!(fog.environmental_end, 96.0);
clear_distance_fog(&mut fog, &input);
assert!(fog.environmental_start >= 1e9 && fog.render_distance_start >= 1e9);
}
#[test]
fn fog_off_keeps_fluid_and_effect_fog() {
let sky = Sky {
fog_start: 10.0,
fog_end: 96.0,
..Sky::default()
};
let tables = FogTables {
water_fog_end: vec![1.0],
};
let kept = |input: &FogInputs| {
let mut fog = setup_fog(&tables, &FogState::default(), input);
let before = fog;
clear_distance_fog(&mut fog, input);
fog == before
};
for fog_type in [FogType::Lava, FogType::PowderSnow, FogType::Water] {
let mut input = inputs(&sky);
input.fog_type = fog_type;
assert!(kept(&input), "{fog_type:?}");
}
let mut input = inputs(&sky);
input.blindness = Some((100, false));
assert!(kept(&input));
let mut input = inputs(&sky);
input.darkness = Some(0.5);
assert!(kept(&input));
let mut input = inputs(&sky);
input.boss_world_fog = true;
assert!(kept(&input));
}
#[test]
fn boss_darkening_dims_fog_color() {
// FogRenderer.computeFogColor: lerp toward 70/60/60% of each channel.
let sky = Sky {
@@ -8,7 +8,7 @@
//! offsets), styled drawing and StringSplitter-style line wrapping.
use crate::{
hud::Rect,
text::{Style, Text},
text::{Span, Style, Text, legacy_format},
zip::Zip,
};
use ab_glyph::{Font as _, ScaleFont as _};
@@ -896,6 +896,9 @@ impl Font {
std::mem::take(&mut self.cache.borrow_mut().region.pending)
}
pub fn advance(&self, c: char, style: &Style) -> f32 {
if c == '\u{fffc}' && style.inline_object.is_some() {
return 8.0;
}
let g = self.glyph_styled(c, style);
g.advance + if style.is_bold() { g.offset } else { 0.0 }
}
@@ -966,6 +969,83 @@ impl Font {
}
out
}
/// `Font.split`: `StringSplitter.splitLines(text, width, Style.EMPTY)` over
/// the text's parts with their `§` codes in. A code styles what follows
/// (`§r` back to no style while measuring), and the part a line breaks in
/// goes on in the style at the break. Lines keep their codes for drawing.
pub fn split_formatted(&self, t: &Text, max_width: f32) -> Vec<Text> {
let max_width = max_width.max(1.0);
let mut parts: Vec<(Vec<char>, Style)> = t
.spans
.iter()
.filter(|s| !s.text.is_empty())
.map(|s| (s.text.chars().collect(), s.style.clone()))
.collect();
let mut flat: Vec<char> = parts.iter().flat_map(|p| p.0.iter().copied()).collect();
let mut out = Vec::new();
let mut force_new_line = false;
// One LineBreakFinder per line, over the parts left.
while let Some((at, split_style)) = self.line_break(&parts, max_width) {
let tail = flat[at];
let new_line = tail == '\n';
let skip = usize::from(new_line || tail == ' ');
force_new_line = new_line;
out.push(split_at(&mut parts, at, skip, &split_style));
flat.drain(..at + skip);
}
if !parts.is_empty() {
let mut rest = Text::default();
rest.spans = parts
.into_iter()
.map(|(text, style)| Span {
text: text.into_iter().collect(),
style,
})
.collect();
out.push(rest);
} else if force_new_line {
out.push(Text::empty());
}
out
}
/// `StringSplitter.LineBreakFinder` fed by `StringDecomposer.iterateFormatted`:
/// where the parts break (a character index) and the style there.
fn line_break(&self, parts: &[(Vec<char>, Style)], max_width: f32) -> Option<(usize, Style)> {
let (mut width, mut had_visible, mut last_space) = (0.0f32, false, None);
let mut offset = 0;
for (contents, part_style) in parts {
let mut style = part_style.clone();
let mut i = 0;
while i < contents.len() {
let c = contents[i];
if c == '§' {
// A `§` that ends its part ends only that part.
let Some(&code) = contents.get(i + 1) else {
break;
};
style = legacy_format(&style, code, &Style::default());
i += 2;
continue;
}
let at = offset + i;
if c == '\n' {
return Some((at, style));
}
if c == ' ' {
last_space = Some((at, style.clone()));
}
let advance = self.advance(c, &style);
width += advance;
if had_visible && width > max_width {
return Some(last_space.unwrap_or((at, style)));
}
had_visible |= advance != 0.0;
i += 1;
}
offset += contents.len();
}
None
}
/// `ComponentRenderUtils.wrapComponents`: the split lines, wrapped ones
/// indented by an unstyled space; one empty line for empty text.
pub fn wrap_components(&self, t: &Text, max_width: i32) -> Vec<Text> {
@@ -1059,6 +1139,47 @@ impl Font {
}
}
/// `StringSplitter.FlatComponents.splitAt`: the parts before `at` as a line,
/// `skip` characters dropped, and the part it ends in restyled as `style`.
fn split_at(parts: &mut Vec<(Vec<char>, Style)>, at: usize, skip: usize, style: &Style) -> Text {
let mut line = Text::default();
let mut push = |text: &[char], style: &Style| {
line.spans.push(Span {
text: text.iter().collect(),
style: style.clone(),
})
};
// Each part done is removed, so the one at hand is always the first.
let (mut position, mut in_skip) = (at, false);
while let Some((text, part_style)) = parts.first() {
let size = text.len();
if !in_skip {
if position > size {
push(text, part_style);
parts.remove(0);
position -= size;
continue;
}
if position > 0 {
push(&text[..position], part_style);
}
position += skip;
in_skip = true;
}
if position <= size {
if position == size {
parts.remove(0);
} else {
parts[0] = (text[position..].to_vec(), style.clone());
}
break;
}
parts.remove(0);
position -= size;
}
line
}
#[cfg(test)]
mod tests {
use super::*;
@@ -1221,6 +1342,37 @@ mod tests {
assert!(!font.take_patches().is_empty());
}
/// StringDecomposer.iterateFormatted: a `§` that ends a part stops that
/// part only; the next part is still measured and can break.
#[test]
fn a_section_sign_ending_a_part_still_breaks_the_next() {
let sheet = png(16, 8, |x, _| x < 2);
let default = br#"{"providers":[{"type":"space","advances":{" ":4}},{"type":"bitmap","file":"minecraft:font/a.png","ascent":7,"chars":["ab"]}]}"#;
let root = root(
"section-sign",
&[
("assets/minecraft/font/default.json", default),
("assets/minecraft/textures/font/a.png", &sheet),
],
);
let font = load(&root);
let _ = std::fs::remove_dir_all(&root);
let mut text = Text::default();
text.spans = ["a§", "a a"]
.map(|part| Span {
text: part.into(),
style: Style::default(),
})
.into();
// 3 + 3 + 4 + 3 is past 12: the line breaks at the space.
let lines: Vec<String> = font
.split_formatted(&text, 12.0)
.iter()
.map(Text::plain)
.collect();
assert_eq!(lines, ["a§a", "a"]);
}
/// A renderer rebuilt over the same font (the loading screen's handoff)
/// makes its atlas texture from the textures alone, so the glyphs it had
/// rasterized into the previous texture must be placed into the new one.
File diff suppressed because it is too large. Load diff
@@ -103,10 +103,11 @@ impl SpriteAnimator {
let mut counts: Vec<usize> = Vec::new();
let mut sprites = Vec::with_capacity(animations.len());
for animation in animations {
let chains: Vec<_> = (0..animation.frame_count())
.map(|frame| source_chain(animation.pixels(frame), size, settings, animation.layer))
let pixels: Vec<_> = (0..animation.frame_count())
.map(|frame| animation.pixels(frame))
.collect();
let bytes: u64 = chains.iter().map(|(chain, _)| chain.len() as u64).sum();
let (chains, edge) = source_chains(&pixels, size, settings, animation.layer);
let bytes: u64 = chains.iter().map(|chain| chain.len() as u64).sum();
if bytes > limit {
log::warn!(
"Horizon: layer {}'s animation ({bytes} bytes) does not fit in a storage buffer; it stays still",
@@ -124,7 +125,7 @@ impl SpriteAnimator {
let data = &mut frames[buffer];
let offsets = chains
.iter()
.map(|(chain, _)| {
.map(|chain| {
let offset = (data.len() / 4) as u32;
data.extend_from_slice(chain);
offset
@@ -142,11 +143,11 @@ impl SpriteAnimator {
shown: None,
layer: animation.layer % split,
frames: offsets,
edge: chains[0].1,
edge,
group,
});
}
let Some(pipeline) = pipeline else {
let Some((pipeline, layout)) = pipeline else {
let groups = keys
.iter()
.zip(&counts)
@@ -192,7 +193,6 @@ impl SpriteAnimator {
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let layout = pipeline.get_bind_group_layout(0);
let groups = keys
.iter()
.zip(&counts)
@@ -363,11 +363,15 @@ impl SpriteAnimator {
}
}
/// `animate.wgsl`'s pipeline, unless the device fails to make it: an OpenGL
/// driver can list compute shaders among its extensions while wgpu writes
/// the GLSL version of its core version, which has none (Mesa's
/// `MESA_GL_VERSION_OVERRIDE`, older drivers).
fn pipeline(device: &wgpu::Device) -> Option<wgpu::ComputePipeline> {
/// `animate.wgsl`'s pipeline and the bind group layout of its group 0,
/// unless the device fails to make the pipeline: an OpenGL driver can list
/// compute shaders among its extensions while wgpu writes the GLSL version
/// of its core version, which has none (Mesa's `MESA_GL_VERSION_OVERRIDE`,
/// older drivers). A device lost between the scopes' pushes answers pipeline
/// creation with a silent placeholder and reports its invalidity only when
/// the layout is asked for — inside the scopes that error is caught, outside
/// them it is uncaptured and panics (#3866).
fn pipeline(device: &wgpu::Device) -> Option<(wgpu::ComputePipeline, wgpu::BindGroupLayout)> {
let validation = device.push_error_scope(wgpu::ErrorFilter::Validation);
let internal = device.push_error_scope(wgpu::ErrorFilter::Internal);
let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
@@ -382,9 +386,10 @@ fn pipeline(device: &wgpu::Device) -> Option<wgpu::ComputePipeline> {
compilation_options: Default::default(),
cache: None,
});
let layout = pipeline.get_bind_group_layout(0);
let internal = pollster::block_on(internal.pop());
match internal.or(pollster::block_on(validation.pop())) {
None => Some(pipeline),
None => Some((pipeline, layout)),
Some(e) => {
log::warn!("Horizon renderer: animated sprites drawn on the CPU ({e})");
None
@@ -424,15 +429,63 @@ fn mix_level(frames: &[u8], [_, current, next, millis, edge]: [u32; 5], width: u
/// array's size from its own size's chain, its first level repeated into the
/// larger levels: that chain, which the shader repeats. Any other sprite: its
/// chain at the array's size.
#[cfg(test)]
fn source_chain(pixels: &[u8], size: u32, settings: &[Settings], layer: u32) -> (Vec<u8>, u32) {
let native = settings.get(layer as usize).map_or(0, |s| s.native_size);
let edge = if native.is_power_of_two() && native < size {
native
let (mut chains, edge) = source_chains(&[pixels], size, settings, layer);
(chains.swap_remove(0), edge)
}
/// Every frame's chain, laid out as one frame's `source_chain`, but mipmapped as
/// vanilla's `SpriteContents.increaseMipLevel` does: over the whole animation
/// sheet (the frames stacked), so `solidify` and the alpha-to-coverage scale
/// of each level are the sheet's, not each frame's. A frame alone has a
/// single texel square at its 2x2 level, and its coverage scale could round
/// a whole far frame transparent (kelp blinking out at distance).
fn source_chains(
frames: &[&[u8]],
size: u32,
settings: &[Settings],
layer: u32,
) -> (Vec<Vec<u8>>, u32) {
let s = settings.get(layer as usize).copied().unwrap_or_default();
let edge = if s.native_size.is_power_of_two() && s.native_size < size {
s.native_size
} else {
size
};
let frame = crate::sprites::resample(pixels, size, edge);
(super::mip_chain(&frame, edge, settings, layer, 1).0, edge)
let mut sheet = crate::mipmap::Image {
width: edge,
height: edge * frames.len() as u32,
pixels: Vec::with_capacity((edge * edge) as usize * frames.len()),
};
for frame in frames {
let frame = crate::sprites::resample(frame, size, edge);
sheet.pixels.extend_from_slice(frame.as_chunks::<4>().0);
}
let has_transparent = s.has_transparent || sheet.pixels.iter().any(|p| p[3] == 0);
let levels = crate::mipmap::generate(
sheet,
edge.trailing_zeros(),
Settings {
has_transparent,
..s
},
);
let chains = (0..frames.len())
.map(|frame| {
let mut chain = Vec::new();
for level in &levels {
let texels = (level.width * level.width) as usize;
chain.extend(
level.pixels[frame * texels..(frame + 1) * texels]
.iter()
.flatten(),
);
}
chain
})
.collect();
(chains, edge)
}
#[cfg(test)]
@@ -572,6 +625,63 @@ mod tests {
}
}
#[test]
fn kelp_frames_share_the_sheet_mips() {
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../.local/horizon-client")
.join(horizon_core::VANILLA_VERSION);
if !root
.join("assets/minecraft/textures/block/kelp.png")
.exists()
{
return;
}
let mut sprites = crate::sprites::Sprites::default();
for name in ["kelp", "kelp_plant", "seagrass", "tall_seagrass_top"] {
sprites
.insert(&root, &format!("minecraft:block/{name}"))
.unwrap();
}
sprites.finish();
// A level drawn nowhere: no texel passes the cutout's alpha test.
let blank = |chain: &[u8], edge: u32| {
let mut start = 0;
let mut level = edge;
let mut blank = Vec::new();
while level > 0 {
let bytes = (level * level * 4) as usize;
let texels = &chain[start..start + bytes];
blank.push(texels.as_chunks::<4>().0.iter().all(|p| p[3] < 128));
start += bytes;
level >>= 1;
}
blank
};
let mut blinked = 0;
for animation in &sprites.animations {
let size = sprites.size;
let pixels: Vec<_> = (0..animation.frame_count())
.map(|f| animation.pixels(f))
.collect();
let (chains, edge) = source_chains(&pixels, size, &sprites.mips, animation.layer);
for (frame, chain) in chains.iter().enumerate() {
// The 1x1 level has no texel square to measure coverage on
// (vanilla leaves it unscaled); every larger level shows.
let levels = blank(chain, edge);
assert!(
!levels[..levels.len() - 1].contains(&true),
"layer {} frame {frame}: {levels:?}",
animation.layer
);
let (alone, _) = source_chain(pixels[frame], size, &sprites.mips, animation.layer);
let alone = blank(&alone, edge);
blinked += usize::from(alone[..alone.len() - 1].contains(&true));
}
}
// Mipmapped frame by frame, some kelp frames vanished at the 2x2 level.
assert!(blinked > 0);
}
#[test]
fn rounding_matches_f32_mix_but_for_exact_halves() {
// The CPU blend this replaced: f32 mix, then rounded. They differ
@@ -730,8 +840,11 @@ mod tests {
queue.submit([encoder.finish()]);
for animation in &animations {
let blend = animation.blend(tick);
let chain = |f| source_chain(animation.pixels(f), size, &mips, animation.layer);
let ((current, edge), (next, _)) = (chain(blend.frame), chain(blend.next));
let pixels: Vec<_> = (0..animation.frame_count())
.map(|f| animation.pixels(f))
.collect();
let (chains, edge) = source_chains(&pixels, size, &mips, animation.layer);
let (current, next) = (&chains[blend.frame], &chains[blend.next]);
let layers = read_back(
&device,
&queue,
@@ -739,7 +852,7 @@ mod tests {
);
assert_eq!(
layers[(animation.layer % split) as usize],
reference(&current, &next, blend.millis, edge, size),
reference(current, next, blend.millis, edge, size),
"layer {} at tick {tick}",
animation.layer
);
@@ -0,0 +1,214 @@
//! Splitscreen: several players' renderers draw offscreen on one device, and the
//! compositor copies each frame into its place on a window's surface. A window holds views but
//! never owns them, so a view can later move to another window (another monitor, detached).
use super::Renderer;
use crate::Result;
use std::sync::Arc;
use winit::window::Window;
pub struct Compositor {
surface: wgpu::Surface<'static>,
device: wgpu::Device,
queue: wgpu::Queue,
config: wgpu::SurfaceConfiguration,
present_modes: Vec<wgpu::PresentMode>,
suspended: bool,
}
impl Renderer {
/// A compositor for `window` on this renderer's device, presenting on the surface it gave up.
pub fn compositor(&self, window: Arc<Window>) -> Result<Compositor> {
// The surface this renderer set aside for its successor (`release_surface`), else a new
// one: a window can't keep two.
let spare = self
.spare_surface
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.take();
let surface = match spare {
Some(surface) => surface,
None => self.instance.create_surface(window.clone())?,
};
Compositor::new(
surface,
&self.adapter,
&self.device,
&self.queue,
window.inner_size().into(),
)
}
/// The last offscreen frame (`--offscreen`), for the compositor.
pub fn offscreen_frame(&self) -> Option<&wgpu::Texture> {
self.offscreen.as_ref()
}
/// Splitscreen turned off: a compositor's surface, set aside for the renderer that draws on
/// the window again (it takes it, as after `release_surface`).
pub fn take_back(&self, compositor: Compositor) {
*self
.spare_surface
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(compositor.surface);
}
}
impl Compositor {
/// Present offscreen BGRA frames on a compatible surface using their existing device.
/// The surface must support COPY_DST and Bgra8Unorm; unsupported surfaces return an error.
pub fn new(
surface: wgpu::Surface<'static>,
adapter: &wgpu::Adapter,
device: &wgpu::Device,
queue: &wgpu::Queue,
size: [u32; 2],
) -> Result<Self> {
let caps = surface.get_capabilities(adapter);
// ponytail: a plain texture copy needs the offscreen frames' format; a blit pass would
// take any surface format (Rgba8Unorm on some Linux drivers).
if !caps.usages.contains(wgpu::TextureUsages::COPY_DST) {
return Err("surface does not support copying offscreen frames".into());
}
let format = wgpu::TextureFormat::Bgra8Unorm;
if !caps.formats.contains(&format) {
return Err(format!(
"splitscreen needs a {format:?} surface; this one offers {:?}",
caps.formats
)
.into());
}
let max = device.limits().max_texture_dimension_2d;
let config = wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_DST,
format,
width: size[0].clamp(1, max),
height: size[1].clamp(1, max),
present_mode: wgpu::PresentMode::Fifo,
desired_maximum_frame_latency: 2,
alpha_mode: wgpu::CompositeAlphaMode::Opaque,
view_formats: vec![],
color_space: wgpu::SurfaceColorSpace::default(),
};
surface.configure(device, &config);
Ok(Compositor {
surface,
device: device.clone(),
queue: queue.clone(),
config,
present_modes: caps.present_modes,
suspended: !super::drawable_size(size[0], size[1], max),
})
}
/// Vsync on: presents wait for the display. Off: they don't (Mailbox, else Immediate).
pub fn set_vsync(&mut self, vsync: bool) {
let mode = [wgpu::PresentMode::Mailbox, wgpu::PresentMode::Immediate]
.into_iter()
.find(|m| !vsync && self.present_modes.contains(m))
.unwrap_or(wgpu::PresentMode::Fifo);
if mode != self.config.present_mode {
self.config.present_mode = mode;
self.surface.configure(&self.device, &self.config);
}
}
pub fn resize(&mut self, width: u32, height: u32) {
self.suspended =
!super::drawable_size(width, height, self.device.limits().max_texture_dimension_2d);
if !self.suspended {
self.config.width = width;
self.config.height = height;
self.surface.configure(&self.device, &self.config);
}
}
/// Copy each view's frame to its top-left corner on the window and present. Frames that
/// don't fit are clipped; parts of the window no view covers (a view before its first frame,
/// one still at its old size after a resize) are black. Nothing when the window has no frame
/// to give (occluded, resized, timed out). Returns true when a frame was submitted.
pub fn present(&mut self, views: &[(&wgpu::Texture, [u32; 2])]) -> bool {
if self.suspended {
return false;
}
let frame = match self.surface.get_current_texture() {
wgpu::CurrentSurfaceTexture::Success(f)
| wgpu::CurrentSurfaceTexture::Suboptimal(f) => f,
wgpu::CurrentSurfaceTexture::Outdated | wgpu::CurrentSurfaceTexture::Lost => {
self.surface.configure(&self.device, &self.config);
return false;
}
_ => return false,
};
let mut encoder = self
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("Splitscreen compositor"),
});
let target = frame.texture.create_view(&Default::default());
drop(encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("Splitscreen compositor clear"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &target,
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
store: wgpu::StoreOp::Store,
},
})],
..Default::default()
}));
for &(texture, [x, y]) in views {
let [width, height] = clipped_size(
[texture.width(), texture.height()],
[x, y],
[self.config.width, self.config.height],
);
if width == 0 || height == 0 {
continue;
}
encoder.copy_texture_to_texture(
texture.as_image_copy(),
wgpu::TexelCopyTextureInfo {
origin: wgpu::Origin3d { x, y, z: 0 },
..frame.texture.as_image_copy()
},
wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
);
}
self.queue.submit([encoder.finish()]);
self.queue.present(frame);
true
}
}
/// Copies never reach past the surface, including while a resized view still has its old frame.
fn clipped_size(view: [u32; 2], at: [u32; 2], surface: [u32; 2]) -> [u32; 2] {
[
view[0].min(surface[0].saturating_sub(at[0])),
view[1].min(surface[1].saturating_sub(at[1])),
]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn frames_are_clipped_to_the_window_during_resize() {
assert_eq!(clipped_size([640, 480], [0, 0], [1280, 720]), [640, 480]);
assert_eq!(
clipped_size([640, 480], [1000, 600], [1280, 720]),
[280, 120]
);
assert_eq!(clipped_size([640, 480], [1280, 720], [1280, 720]), [0, 0]);
assert_eq!(
clipped_size([640, 480], [u32::MAX, 0], [1280, 720]),
[0, 480]
);
}
}
Loaded 100 of 1269 files, more files were not shown because too many files have changed in this diff. Show more