Compare commits

...
Author SHA1 Message Date
Ludovic Marie 285ecb6e89 Merge branch 'heads/pt3/2586' into union/pt3-2 2026-10-29 10:30:23 +01:00
Ludovic Marie 5509fe8a5a Merge branch 'heads/pt3/2544' into union/pt3-2 2026-10-29 10:30:23 +01:00
Ludovic Marie 547ea7cdef Merge branch 'heads/pt3/3433' into union/pt3-2 2026-10-29 10:30:23 +01:00
Ludovic Marie 372b701542 Merge branch 'heads/pt3/3417' into union/pt3-2 2026-10-29 10:30:23 +01:00
Ludovic Marie abd2d1742a Merge branch 'heads/pt3/3000' into union/pt3-2 2026-10-29 10:30:23 +01:00
Ludovic Marie b423fa09d2 Merge branch 'heads/pt3/2981' into union/pt3-2 2026-10-29 10:30:23 +01:00
Ludovic Marie f352418364 Merge branch 'heads/pt3/2951' into union/pt3-2 2026-10-29 10:30:23 +01:00
Ludovic Marie 1e57b381ef Merge branch 'heads/pt3/2417' into union/pt3-2 2026-10-29 10:30:23 +01:00
Ludovic MarieandClaude Opus 5.5 0eba0b37b8 Merge main into this branch
game.rs: main's spawn-wait areas are taken back out of streaming.extra right after
the update, then this branch's fresh chunks get their animals.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 10:30:15 +01:00
Ludovic MarieandClaude Opus 5.5 390794b454 Merge main into this branch
entity_nbt.rs: main's farm animal variant test and this branch's structure mob
tests are both kept.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 10:30:09 +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 a185086f24 Merge main into this branch
remote.rs: leaving a server clears both the world icon state (this branch) and
main's bed state (InBedChatScreen goes with the level).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 10:29:19 +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 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 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
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 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
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
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
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
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
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 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
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
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 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
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
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 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
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
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
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
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
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 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
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 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
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
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
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
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
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
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
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
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 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
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 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
Hector EcclesandOpenCode a1542cb263 Give a single-player world its icon from the first frame
GameRenderer.tryTakeScreenshotIfNeeded and takeAutoScreenshot: once a
second, a world with no icon.png takes the centre square of its first
fully meshed frame, resampled to 64x64. Only the window's own
single-player world qualifies; joining a server clears it.

The snapshot runs after the hand and screen effects and before the
entity outline and post chain, as vanilla does, so the icon is the
scene rather than the GUI.

Not bit-identical to vanilla: the resize is a block average rather than
STB's filtered one, the PNG is written by the png crate, and the alpha
channel is forced opaque. The crop, size, timing and "only if missing"
rule match, so the icons look the same.

Co-Authored-By: OpenCode (DeepSeek V4.1 Flash) <[email protected]>
2026-10-28 18:23:21 -04: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 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
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 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
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 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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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 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
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
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
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 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
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
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
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 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
Kacpherek 632179bda6 Format
cargo fmt.
2026-10-27 22:36:44 +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
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 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 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
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
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 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
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 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 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 305ffc4e39 Send End portal frame fill effects to nearby players 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 1db835fa0c Apply arrow weapon knockback enchantments 2026-10-27 22:02:19 +01:00
Kacpherek 60c5d9fefd Apply falling stalactite damage and landing sounds 2026-10-27 22:02:18 +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 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
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
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 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
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
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
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 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
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
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
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
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
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
iLostName b042426180 lang: opening Options lists the languages without reading every file 2026-10-27 12:00:36 +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
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
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 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
506 changed files with 30051 additions and 1978 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
```
+12 -5
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,7 +229,8 @@ 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)" \
@@ -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
+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"
+7 -2
View File
@@ -35,11 +35,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` |
@@ -99,6 +100,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 +114,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 |
Generated
+43 -2
View File
@@ -1378,7 +1378,6 @@ dependencies = [
"ab_glyph",
"bytemuck",
"chrono",
"cpal",
"flate2",
"gilrs",
"glam",
@@ -1386,6 +1385,7 @@ dependencies = [
"horizon-bench",
"horizon-client-core",
"horizon-client-input",
"horizon-client-render",
"horizon-command",
"horizon-core",
"horizon-dualsense",
@@ -1412,7 +1412,6 @@ dependencies = [
"serde_json",
"sha1",
"sha2",
"symphonia",
"unicode-bidi",
"ureq",
"wgpu",
@@ -1441,6 +1440,48 @@ dependencies = [
"winit",
]
[[package]]
name = "horizon-client-render"
version = "0.1.0"
dependencies = [
"ab_glyph",
"bytemuck",
"chrono",
"cpal",
"flate2",
"glam",
"horizon-assets",
"horizon-bench",
"horizon-client-core",
"horizon-client-input",
"horizon-command",
"horizon-core",
"horizon-hdr",
"horizon-items",
"horizon-protocol",
"horizon-shader-api",
"horizon-shaders",
"horizon-telemetry",
"horizon-theme",
"horizon-threads",
"horizon-world",
"log",
"miniz_oxide 0.8.9",
"naga",
"png",
"pollster",
"rustix 1.1.5",
"serde",
"serde_json",
"sha2",
"symphonia",
"unicode-bidi",
"ureq",
"wgpu",
"winit",
"zip",
]
[[package]]
name = "horizon-command"
version = "0.1.0"
+1
View File
@@ -9,6 +9,7 @@ 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",
+9 -4
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">
@@ -23,13 +23,13 @@
# 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).
@@ -48,7 +48,7 @@ The [documentation](https://playhorizon.dev/docs/) covers each of these.
- **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.
- **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:** Horizon has its own mods, installed from the launcher. The official ones (Zoom, FPS Counter, Keystrokes and Minimap) are native Rust that the launcher builds into your game, with no Rust to install; older WebAssembly mods still run while they move. Java mod jars do not run in Horizon. See [Mods and data packs](https://playhorizon.dev/docs/mods).
Screenshots, captured in the Rust client itself with vanilla assets and no mods or shaders:
@@ -139,6 +139,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 |
+111 -30
View File
@@ -16,7 +16,7 @@ Horizon is Minecraft: Java Edition 26.3 rewritten from scratch in Rust, so it ru
- **Single player as a server: the last differences**: Single player now runs a small server in the background, as vanilla does. We are fixing what still behaves differently there; every fix also works for LAN and server players.
- **Horizon's mod registry**: One place for mods, modpacks and resource packs made for Horizon: browse them on the website and install them from the launcher in one click.
**Next**: Microsoft sign-in (coming soon); CurseForge in the mod browser (coming soon); Fresh benchmarks against vanilla; Pistons push players and mobs; Horizon on Intel Macs; Faster world creation and entry on small CPUs; Servers that hold 20 TPS with 50 to 100 players; Low-end computers: Steam Deck, old Macs, 8 GB; Native upscaling; A batch of targeted optimisations; A Convert to Rust button for what isn't rewritten yet; Mods with code: machines, magic, mobs; Data packs in the mod browser; Prepare a version without Java; Server console, bans and a full admin API; Hosting polish: Windows service, safe updates; Guided hosting with Tailscale and an invite button; An official Docker image for servers; One single-player path: remove the in-frame world; Villages and combat, exactly as in vanilla; Choose the audio output device; Mods that read and change the world; Machines and blocks with inventories (API 0.4); Lua and other languages for mods; Well-known shader looks, rewritten for Horizon; A visible beta label; Wayland and Hyprland polish; A versioned agent-control protocol; Port mods with your own AI agent; Horizon on X (coming soon); Pipeline protection and a GitHub App for agents; Signed updates everywhere, installers included; A server you install with one command.
**Next**: A new launcher design; CurseForge in the mod browser (coming soon); Fresh benchmarks against vanilla; Pistons push players and mobs; Horizon on Intel Macs; Faster world creation and entry on small CPUs; Servers that hold 20 TPS with 50 to 100 players; Low-end computers: Steam Deck, old Macs, 8 GB; Native upscaling; A batch of targeted optimisations; A Convert to Rust button for what isn't rewritten yet; Mods with code: machines, magic, mobs; Data packs in the mod browser; Prepare a version without Java; Server console, bans and a full admin API; Hosting polish: Windows service, safe updates; Guided hosting with Tailscale and an invite button; An official Docker image for servers; One single-player path: remove the in-frame world; Villages and combat, exactly as in vanilla; Choose the audio output device; Mods that read and change the world; Machines and blocks with inventories (API 0.4); Well-known shader looks, rewritten for Horizon; A visible beta label; Wayland and Hyprland polish; A versioned agent-control protocol; Port mods with your own AI agent; Horizon on X (coming soon); Pipeline protection and a GitHub App for agents; Signed updates everywhere, installers included; A server you install with one command; Ready-made setups for hosting panels.
**How to help**: Play it and tell us what differs from vanilla (Discord or GitHub), run the benchmark on your computer, vote for what you want next, or pick one of the Help wanted items.
@@ -104,37 +104,49 @@ More: [docs/mods.md](docs/mods.md)
</details>
#### Native mods, built by the launcher
Horizon's own mods become native code that the launcher builds into your game, with no Rust to install: Zoom, FPS Counter, Keystrokes and Minimap already are, and Recipes is next.
<details><summary>Technical</summary>
Stable hook points in the game (game/native_hooks.rs, [#3345](https://github.com/horizon-rust/horizon/issues/3345), [#3346](https://github.com/horizon-rust/horizon/issues/3346); v2 with the item panel and recipe pages, [#3434](https://github.com/horizon-rust/horizon/issues/3434), [#3435](https://github.com/horizon-rust/horizon/issues/3435)), horizon-mixins applied by the launcher with its own portable Rust ([#3388](https://github.com/horizon-rust/horizon/issues/3388)), on/off in Options > Mods. The official mods live in horizon-mods/native/ and are in the registry as native mods. Next: Recipes, native mods on automatically when a player installs one (with a notice about the first build), then the WebAssembly runtime is removed ([#3350](https://github.com/horizon-rust/horizon/issues/3350), [#3351](https://github.com/horizon-rust/horizon/issues/3351), [#3353](https://github.com/horizon-rust/horizon/issues/3353), [#3371](https://github.com/horizon-rust/horizon/issues/3371)).
More: [docs/native-mods.md](docs/native-mods.md)
</details>
#### Mods that run on the server
Mods that run on the server side of a world, so a dedicated server or a LAN world can have mods that react to players joining, chat and blocks being broken or placed, and that add commands.
<details><summary>Technical</summary>
API 0.3, experimental first ([#1833](https://github.com/horizon-rust/horizon/issues/1833), in review): the same sandboxed component runs on the server thread and the dedicated server, with world events (block broken or placed, entity spawned or killed, player joined or left), game rules and server commands with typed arguments. The world, item and entity interfaces come after (see 'Mods that read and change the world'). [docs/api/mods.md](docs/api/mods.md), [docs/mods.md](docs/mods.md).
The same hook points on the server thread and the dedicated server: world events (block broken or placed, entity spawned or killed, player joined or left), game rules and server commands with typed arguments. The earlier WebAssembly API 0.3 ([#1833](https://github.com/horizon-rust/horizon/issues/1833)) is the reference for what to cover.
More: [docs/native-mods.md](docs/native-mods.md)
</details>
#### Screens, input and drawing for mods
Mods will be able to open their own screens and settings pages and react to keys and item use, so bigger mods, such as the Orbital Laser, can run on the normal game.
Mods will be able to open their own screens and settings pages, react to keys and item use, and draw, so bigger mods, such as the Orbital Laser, run on the normal game.
<details><summary>Technical</summary>
Capability `ui` is in review ([#1864](https://github.com/horizon-rust/horizon/issues/1864)): mod-owned screens and a settings page under Options > Mods, built from vanilla's widgets. Still to design: `input` (an item's use, key binds, the crosshair target), `render` (a budgeted draw list on the native shader API) and `world` (changing blocks). Orbital Laser is the acceptance test: it needs all four today through its author's fork (see 'Orbital Laser on the normal game').
More: [docs/mods.md](docs/mods.md)
On native hook points ([docs/native-mods.md](docs/native-mods.md)): settings pages under Options > Mods and the item panel are there ([#3346](https://github.com/horizon-rust/horizon/issues/3346), [#3435](https://github.com/horizon-rust/horizon/issues/3435)). Next: mod-owned screens built from vanilla's widgets, an item's use, key binds and the crosshair target, and a budgeted draw list on the native shader API. Orbital Laser is the acceptance test.
</details>
#### Tools for people who make mods
Better tools for mod makers: a test runner that needs no game window, templates for each kind of mod, and clear promises about what keeps working from one version to the next.
Better tools for mod makers: templates for each kind of mod, a test run with no game window, and clear promises about which hook points keep working from one version to the next.
<details><summary>Technical</summary>
`cargo horizon-mod test` runs a mod's test scripts against a fake game snapshot ([#1816](https://github.com/horizon-rust/horizon/issues/1816)) and, for server mods, scripted server events ([#1837](https://github.com/horizon-rust/horizon/issues/1837)); the content template and the guides are merged ([#1859](https://github.com/horizon-rust/horizon/issues/1859), [#1810](https://github.com/horizon-rust/horizon/issues/1810), [docs/api/stability.md](docs/api/stability.md)). Left: templates for hud, server and machine mods, typed wrappers in the SDK, a frozen 0.1 compatibility mod that every runtime must load, a CI check that fails when a stable reference entry is removed without a new major version, and the SDK on crates.io once the API is stable.
Native mod template and the content mod template (horizon-mods), `native.py check` that applies mods on a chosen Horizon and builds them together, a weekly check of every official mod on the newest Nightly. Left: a versioned list of stable hook points (NATIVE_HOOKS_API), a test runner without a window, and docs on the site for native mods.
More: [https://playhorizon.dev/docs/test-a-mod](https://playhorizon.dev/docs/test-a-mod)
More: [#3388](https://github.com/horizon-rust/horizon/issues/3388) · [docs/native-mods.md](docs/native-mods.md)
</details>
@@ -244,6 +256,20 @@ Horizon's own way to make shader packs, with tools to write and test them. Every
</details>
### Launcher
#### Minecraft Java instances in the launcher
Play the real Java game from the Horizon launcher too: vanilla and modded instances, any version, mods from Modrinth filtered to what fits, and your instances imported from other launchers.
<details><summary>Technical</summary>
For an account that owns the game: any version downloaded with the Java it needs ([#3406](https://github.com/horizon-rust/horizon/issues/3406)), Java instances, the version list and the CLI ([#3395](https://github.com/horizon-rust/horizon/issues/3395)), the start command read from the version JSON ([#3419](https://github.com/horizon-rust/horizon/issues/3419)). Next: Play for a Java instance, Fabric and Quilt, then NeoForge and Forge; Modrinth with compatibility filtering and dependencies; imports from Prism, MultiMC, the CurseForge app, the Modrinth App, ATLauncher and the official launcher, each from its documented on-disk format.
More: [#3506](https://github.com/horizon-rust/horizon/issues/3506)
</details>
### Horizon extras
#### Discord Rich Presence
@@ -268,6 +294,16 @@ A review of the whole tree: dead code and stubs removed ([#1432](https://github.
</details>
#### Every vanilla method, tracked
A public list of every method of the Java game and where Horizon does it, so anyone can see what is ported, what is missing and pick something to fix.
<details><summary>Technical</summary>
[tools/port_ledger.py](tools/port_ledger.py) and tools/port_map/ ([#3139](https://github.com/horizon-rust/horizon/issues/3139)): every 26.3 method outside the skipped packages has a verdict (port, inline, missing, skip, trivial), linked to the harnesses that cover it ([#3526](https://github.com/horizon-rust/horizon/issues/3526)), with one tracking issue per area. Bug fixes start from it (.claude/skills/vanilla-parity).
</details>
## Next
### Performance
@@ -444,6 +480,16 @@ A player's idea (#horizon-ideas). `horizon-server` already runs headless; left:
</details>
#### Ready-made setups for hosting panels
Install a Horizon server from the panels hosts use, such as Pterodactyl and AMP, the usual way for each panel.
<details><summary>Technical</summary>
Asked in the suggestion queue for Pterodactyl eggs and AMP templates. Builds on the one-command install ('A server you install with one command') and horizon-server's server.toml ([docs/hosting.md](docs/hosting.md)).
</details>
### Mods and API
#### Mods that read and change the world
@@ -452,9 +498,9 @@ Mods that can see and change blocks, read inventories and items, and find and af
<details><summary>Technical</summary>
After server mods: read blocks, states and biomes in loaded chunks and change them through the server's block log, so clients and the parity harnesses see vanilla's updates; the player's inventory, held item, open container and item stacks with components; entity queries near a position, a safe subset of entity data and data-driven mod entity types. Snapshots and batched calls per tick, never per-block host calls in hot paths. [docs/api/mods.md](docs/api/mods.md) ('Not there yet').
Hook points for reading blocks, states and biomes and changing them through the server's block log, so clients and the parity harnesses see vanilla's updates; the player's inventory and item stacks with components; entity queries near a position. Batched per tick, never per-block calls in hot paths.
More: [docs/modpacks.md](docs/modpacks.md)
More: [docs/native-mods.md](docs/native-mods.md) · [docs/modpacks.md](docs/modpacks.md)
</details>
@@ -468,16 +514,6 @@ Block entities and menus: ticking machines with slots, server menu logic and the
</details>
#### Lua and other languages for mods
Write mods as Lua scripts, in the style of KubeJS, with no compiler, and later in other languages that pass the tests. Every language runs in the same sandbox.
<details><summary>Technical</summary>
Lua first: Horizon ships a Lua interpreter that is itself a WebAssembly mod, and a script mod's manifest names it and the capabilities it uses, so a script can do exactly what a compiled mod with the same manifest can, with hot reload of `main.lua`. Then C, C++, Zig, Go (TinyGo), AssemblyScript and JavaScript or TypeScript are measured against Rust on the same tick-heavy example, and each is supported only with a template, an example and a CI test. [docs/api/mods.md](docs/api/mods.md) ('Ways in').
</details>
### Gameplay parity
#### Pistons push players and mobs
@@ -536,15 +572,13 @@ More: [docs/shaders.md](docs/shaders.md)
### Launcher
#### Microsoft sign-in (coming soon)
#### A new launcher design
Coming soon: sign in with your Microsoft account to play on online-mode servers. The code is ready; it waits for Microsoft's approval.
A redesigned launcher: every button where you expect it, the fewest clicks to play, add an instance or add mods, smooth animations, and instant startup.
<details><summary>Technical</summary>
Device-code sign-in in the launcher and online-mode login in the client ([docs/microsoft-account.md](docs/microsoft-account.md)). It is turned on by setting the app's client id once Minecraft API access is approved; until then the launcher shows "Coming soon".
More: [#1270](https://github.com/horizon-rust/horizon/issues/1270)
Based on the site concept by the Team (Bricolage Grotesque, a night sky and an animated pixel sunset). Screens and flows are designed first, then small PRs; tokens in [crates/horizon-theme](crates/horizon-theme) and rules in [docs/design-system.md](docs/design-system.md), the game's vanilla look unchanged. Measured before and after: startup, frame time and idle CPU/GPU, reduced motion honoured. Mods screens separate Horizon native mods from Java mods; a running instance can be stopped from the launcher.
</details>
@@ -708,6 +742,18 @@ Issue [#1270](https://github.com/horizon-rust/horizon/issues/1270) asks for Ely.
</details>
#### Join servers on older versions
Join servers that run older Minecraft versions, such as 1.21 or 1.8 PvP servers, from Horizon.
<details><summary>Technical</summary>
Asked often in the suggestion queue. Horizon speaks 26.3's protocol; the path is a native protocol adapter in Rust that translates older versions' packets, not a downloaded Java proxy. Java instances in the launcher already play any version with the real game.
More: [#3023](https://github.com/horizon-rust/horizon/issues/3023)
</details>
#### Online-mode checks for Play with Friends
Friends who join through an invite code could prove who they are with their Microsoft account, as on online-mode servers.
@@ -720,6 +766,18 @@ Play with Friends joins over an encrypted tunnel with an invite code ([docs/play
### Mods and API
#### Scripts for mods
Small mods written as scripts, in the style of KubeJS, with no compiler.
<details><summary>Technical</summary>
Waits for native mods to settle: the earlier plan ran Lua inside the WebAssembly sandbox, which is being removed. A script layer over the native hook points is to be designed.
More: [docs/native-mods.md](docs/native-mods.md)
</details>
#### Orbital Laser on the normal game
The community mod Orbital Laser, made for a fork of Horizon, running on the normal game and listed in the registry as the showcase of the mod API.
@@ -732,13 +790,13 @@ A pull request on [https://github.com/Stink-O/orbital-laser](https://github.com/
#### Native versions of popular client mods
Built-in versions of the client mods players ask for: block info on hover, a recipe viewer like JEI, a world map and schematic overlays.
Built-in versions of the client mods players ask for: block info on hover, a world map, inventory tweaks and schematic overlays.
<details><summary>Technical</summary>
Done as extras: a mod list and menu, a minimap and a zoom ([docs/extras.md](docs/extras.md)). Left: block info tooltips, a recipe viewer (a contributor offered to port JEI if it becomes a native feature), a world map and a schematic overlay. They need the read-only interfaces `inventory-read`, `recipes`, `world-read` and `render` in the mod API, or they go into the game directly.
Done: a mod list and menu, minimap, zoom, keystrokes, an FPS counter and a recipe viewer, as native mods (horizon-mods/native/). Asked for: an angled world map that shows hills, overhangs and caves, inventory tweaks (drag and scroll to move items), block info tooltips and schematic overlays.
More: [docs/mods.md](docs/mods.md)
More: [docs/extras.md](docs/extras.md) · [docs/native-mods.md](docs/native-mods.md)
</details>
@@ -784,6 +842,16 @@ The game side is merged ([docs/controllers.md](docs/controllers.md)): pads read
</details>
#### Splitscreen
Play with a friend on the same screen, each with a controller.
<details><summary>Technical</summary>
Issue [#2054](https://github.com/horizon-rust/horizon/issues/2054); a contributor is working on it. Two local players on the same world, each with their own camera and HUD, built on the server thread's multiplayer.
</details>
#### The full debug screen
Vanilla's complete F3 screen: debug options, a profiler chart, ten-second captures and an atlas dump.
@@ -834,7 +902,7 @@ More graphics options: multisample anti-aliasing, anisotropic filtering and an e
<details><summary>Technical</summary>
Not implemented ([docs/graphics.md](docs/graphics.md)). Anisotropic filtering needs linear magnification in wgpu, which blurs close blocks, so close-up sharpness has to be kept first; MSAA would sit beside FXAA; exclusive fullscreen where the backend supports it.
Not implemented ([docs/graphics.md](docs/graphics.md)). Anisotropic filtering needs linear magnification in wgpu, which blurs close blocks, so close-up sharpness has to be kept first; MSAA would sit beside FXAA, and SMAA or TAA are asked for too; exclusive fullscreen where the backend supports it.
</details>
@@ -862,6 +930,18 @@ Issue [#1709](https://github.com/horizon-rust/horizon/issues/1709): aarch64 buil
</details>
#### Horizon on Windows ARM64
A native Horizon for Windows on ARM computers, such as Snapdragon laptops.
<details><summary>Technical</summary>
Releases are x64 for Windows and Linux and arm64 for macOS. Needs an aarch64-pc-windows-msvc build in the release workflow and a machine to test on.
More: [docs/release-flow.md](docs/release-flow.md)
</details>
#### Android, iOS and the browser
Versions of Horizon for Android, iOS and web browsers. Nothing official yet; contributors have started the first pieces.
@@ -968,6 +1048,7 @@ Armour and tools in End city chests should come enchanted, as in vanilla.
## Recently shipped
- **Microsoft sign-in** (launcher@0.2.17): Sign in with your Microsoft account in the launcher to play on online-mode servers. [docs/microsoft-account.md](docs/microsoft-account.md) · [#2879](https://github.com/horizon-rust/horizon/issues/2879)
- **OpenGL for old computers** (v0.2.63): Horizon now starts on old PCs whose graphics card has no Vulkan or DirectX 12 driver: it switches to OpenGL by itself, and Options > Graphics can pick OpenGL by hand. [#1917](https://github.com/horizon-rust/horizon/issues/1917) · [#1920](https://github.com/horizon-rust/horizon/issues/1920) · [#1936](https://github.com/horizon-rust/horizon/issues/1936)
- **Server resource packs** (v0.2.48): Servers can send you their resource pack: Horizon asks, downloads it and applies it, like vanilla. [#1562](https://github.com/horizon-rust/horizon/issues/1562) · [#1569](https://github.com/horizon-rust/horizon/issues/1569) · [#1570](https://github.com/horizon-rust/horizon/issues/1570)
- **Single player on its own server thread** (v0.2.39): Single player runs on a server thread, as vanilla does, so the world keeps ticking smoothly while the window draws. [#1514](https://github.com/horizon-rust/horizon/issues/1514) · [docs/server-levels.md](docs/server-levels.md)
+30 -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()]
@@ -68,6 +72,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 +197,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(
@@ -284,4 +291,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"]
);
}
}
+67
View File
@@ -0,0 +1,67 @@
[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"
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"] }
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"] }
@@ -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()
@@ -196,7 +196,7 @@ impl Assets {
impl RenderAssets {
pub fn load(root: &Path, assets: &Assets) -> 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}"))?;
}
@@ -217,6 +217,7 @@ impl RenderAssets {
for stage in 0..10 {
sprites.insert(root, &format!("minecraft:block/destroy_stage_{stage}"))?;
}
sprites.save_disk_cache();
sprites.finish();
// `finish` grows the buffer by doubling: keep only the bytes in use.
sprites.pixels.shrink_to_fit();
@@ -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)
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -230,7 +230,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 +278,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 +288,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 +301,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 +332,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 +385,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 {
@@ -330,7 +330,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() {
@@ -605,7 +605,7 @@ 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"
);
}
File renamed without changes.
File renamed without changes.
@@ -574,6 +574,9 @@ pub struct Renderer {
pub screenshot: Option<PathBuf>,
/// Outcome of the last screenshot: frame size, or the error.
pub screenshot_result: Option<std::result::Result<[u32; 2], String>>,
/// Write the next frame's centre square as the world's 64x64 `icon.png`
/// (GameRenderer.takeAutoScreenshot).
pub world_icon: Option<PathBuf>,
/// Brightness option (lightmap gamma), 0..1.
pub gamma: f32,
/// Optional full-light lightmap (Horizon Extras).
@@ -2669,6 +2672,7 @@ impl Renderer {
started: Instant::now(),
screenshot: None,
screenshot_result: None,
world_icon: None,
gamma: 0.5,
fullbright: false,
animated_textures: true,
@@ -5113,6 +5117,18 @@ impl Renderer {
pass.draw_indexed(0..6, (i * 4) as i32, 0..1);
}
}
// GameRenderer.tryTakeScreenshotIfNeeded runs after the level (hand and
// screen effects included) and before the entity outline and the post
// chain: the icon is the plain scene, with no GUI either.
let eight_bit = self.config.format.block_copy_size(None) == Some(4);
let icon = self.world_icon.take().and_then(|path| {
if eight_bit {
Some((path, self.copy_frame(&mut encoder, &target_texture)))
} else {
log::warn!("Couldn't save auto screenshot: no 32-bit surface format");
None
}
});
// GameRenderer.render3dHud: the debug crosshair after the hand and
// the screen effects.
if crosshair > 0 {
@@ -5192,7 +5208,7 @@ impl Renderer {
let readback = self.screenshot.take().and_then(|path| {
// The copy and PNG writer both assume 8-bit pixels; a float surface would make
// the row stride too small for the copy and abort in validation.
if self.config.format.block_copy_size(None) == Some(4) {
if eight_bit {
Some((path, self.copy_frame(&mut encoder, &target_texture)))
} else {
self.screenshot_result = Some(Err(format!(
@@ -5233,6 +5249,11 @@ impl Renderer {
.map_err(|e| e.to_string()),
);
}
if let Some((path, buffer)) = icon
&& let Err(e) = self.write_world_icon(&path, &buffer)
{
log::warn!("Couldn't save auto screenshot: {e}");
}
let present = Instant::now();
if let Some(surface_frame) = surface_frame {
// On Wayland this ties RedrawRequested to the compositor's frame
@@ -5313,6 +5334,16 @@ impl Renderer {
buffer
}
fn write_screenshot(&self, path: &std::path::Path, buffer: &wgpu::Buffer) -> Result<[u32; 2]> {
let (width, height, pixels) = self.read_frame(buffer)?;
write_png(path, width, height, &pixels)?;
Ok([width, height])
}
/// GameRenderer.takeAutoScreenshot: the centre square, scaled to 64x64.
fn write_world_icon(&self, path: &std::path::Path, buffer: &wgpu::Buffer) -> Result<()> {
let (width, height, pixels) = self.read_frame(buffer)?;
write_png(path, 64, 64, &world_icon_pixels(&pixels, width, height))
}
fn read_frame(&self, buffer: &wgpu::Buffer) -> Result<(u32, u32, Vec<u8>)> {
if !self.config.usage.contains(wgpu::TextureUsages::COPY_SRC) {
return Err("this surface does not support frame readback".into());
}
@@ -5350,17 +5381,51 @@ impl Renderer {
}
}
buffer.unmap();
let mut encoder = png::Encoder::new(
std::io::BufWriter::new(std::fs::File::create(path)?),
width,
height,
);
encoder.set_color(png::ColorType::Rgba);
encoder.set_depth(png::BitDepth::Eight);
encoder.write_header()?.write_image_data(&pixels)?;
Ok([width, height])
Ok((width, height, pixels))
}
}
fn write_png(path: &std::path::Path, width: u32, height: u32, pixels: &[u8]) -> Result<()> {
let mut encoder = png::Encoder::new(
std::io::BufWriter::new(std::fs::File::create(path)?),
width,
height,
);
encoder.set_color(png::ColorType::Rgba);
encoder.set_depth(png::BitDepth::Eight);
encoder.write_header()?.write_image_data(pixels)?;
Ok(())
}
/// `NativeImage.resizeSubRectTo` from the screen's centre to 64x64. Vanilla
/// resamples with STB; this averages each destination texel's source block,
/// which differs by a texel where a block does not divide evenly.
fn world_icon_pixels(pixels: &[u8], width: u32, height: u32) -> Vec<u8> {
let side = width.min(height) as usize;
if side == 0 {
return vec![0; 64 * 64 * 4];
}
let (left, top) = ((width as usize - side) / 2, (height as usize - side) / 2);
let mut out = Vec::with_capacity(64 * 64 * 4);
for y in 0..64 {
let y0 = y * side / 64;
let y1 = ((y + 1) * side).div_ceil(64).max(y0 + 1).min(side);
for x in 0..64 {
let x0 = x * side / 64;
let x1 = ((x + 1) * side).div_ceil(64).max(x0 + 1).min(side);
let mut sum = [0u32; 4];
for sy in y0..y1 {
for sx in x0..x1 {
let i = ((top + sy) * width as usize + left + sx) * 4;
for c in 0..4 {
sum[c] += u32::from(pixels[i + c]);
}
}
}
let n = ((y1 - y0) * (x1 - x0)) as u32;
out.extend(sum.map(|c| (c / n) as u8));
}
}
out
}
/// Camera-relative clip planes (`Frustum`).
#[derive(Clone, Copy, Debug)]
pub struct Frustum {
@@ -5452,6 +5517,32 @@ fn frustum(m: Mat4) -> [Vec4; 6] {
#[cfg(test)]
mod tests {
#[test]
fn world_icon_crops_the_centre_and_scales_to_64() {
use super::world_icon_pixels;
// 128x64, red left half and blue right half: the centre square is
// 64x64 at x=32, so it keeps half of each colour.
let mut pixels = Vec::with_capacity(128 * 64 * 4);
for _ in 0..128 * 64 {
pixels.extend_from_slice(&[255, 0, 0, 255]);
}
for y in 0..64 {
for x in 64..128 {
let i = (y * 128 + x) * 4;
pixels[i..i + 4].copy_from_slice(&[0, 0, 255, 255]);
}
}
let icon = world_icon_pixels(&pixels, 128, 64);
assert_eq!(icon.len(), 64 * 64 * 4);
assert_eq!(&icon[..4], [255, 0, 0, 255]);
let last = (63 * 64 + 63) * 4;
assert_eq!(&icon[last..last + 4], [0, 0, 255, 255]);
// A source smaller than the icon repeats its texels rather than
// indexing past them.
let tiny = world_icon_pixels(&[1, 2, 3, 255], 1, 1);
assert!(tiny.as_chunks::<4>().0.iter().all(|p| p == &[1, 2, 3, 255]));
}
#[test]
fn surface_is_8_bit_and_not_srgb() {
use super::surface_format;
@@ -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,7 +143,7 @@ impl SpriteAnimator {
shown: None,
layer: animation.layer % split,
frames: offsets,
edge: chains[0].1,
edge,
group,
});
}
@@ -424,15 +425,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 +621,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 +836,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 +848,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
);
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -136,6 +136,35 @@ pub fn draw_progress(batch: &mut HudBatch, atlas: &HudAtlas, line: &str, progres
batch.fill(atlas, left, top, done, 2.0, opaque(0x00FF00));
}
/// `ConnectScreen`'s Cancel button corner on a GUI `w` x `h`:
/// (width / 2 - 100, height / 4 + 120 + 12), 200 x 20.
pub fn connect_cancel(w: f32, h: f32) -> (f32, f32) {
((w / 2.0).trunc() - 100.0, (h / 4.0).trunc() + 132.0)
}
/// `ConnectScreen.extractRenderState` over the menu background: the status
/// centred at (width / 2, height / 2 - 50) and, while the connection can
/// still be cancelled, Cancel.
pub fn draw_connecting(
batch: &mut HudBatch,
atlas: &HudAtlas,
status: &Text,
cursor: (f32, f32),
cancel: bool,
) {
let (w, h) = (batch.width, batch.height);
crate::ui::menu_background(batch, atlas, 0.0, 0.0, w, h, false);
let tw = atlas.font.width_text(status);
let (x, y) = (((w - tw) / 2.0).trunc(), (h / 2.0).trunc() - 50.0);
batch.text_styled(atlas, status, x, y, u32::MAX, true);
if cancel {
let (x, y) = connect_cancel(w, h);
let hover = crate::ui::hovered(cursor, x, y, 200.0, 20.0);
let label = Text::translate("gui.cancel", &[]);
crate::ui::button(batch, atlas, x, y, 200.0, 20.0, &label, true, hover);
}
}
#[cfg(test)]
mod tests {
use super::*;
Loaded 100 of 506 files, more files were not shown because too many files have changed in this diff. Show more