Compare commits

...
Author SHA1 Message Date
Ludovic Marie d8dd2023dd Merge pull request #1447 from ludovic111/chore/client-mods-feature
🤖 Agent: merged after verify and CI parity passed, built merged with current main. Code mods become horizon-client's `mods` feature: CI's two test commands run with `--features horizon-client/mods,horizon-mods/pack`; tests pass with and without it.
2026-10-24 03:01:51 +02:00
Ludovic Marie d84d7a7e61 Merge pull request #1522 from ludovic111/fix/prepare-netty-natives
🤖 Agent: merged after verify and CI parity passed, built merged with current main (launcher-ship-1). #1521: generated docs keep LF line endings on Windows, so the launcher builds there again (dry run 40035909909 green on all three OSes). #1522: Prepare no longer requires Netty's optional native transport jar, which Prism never downloads (fixes #1487; test plus a real Prism-layout prepare).
2026-10-24 02:53:32 +02:00
Ludovic Marie 1cd0dd8c5d Merge pull request #1529 from ludovic111/fix/ds-parity-flake
🤖 Agent: merged after verify and CI parity passed, built merged with current main. The dedicated_server flake was the harness client never resending its position, so a villager whose chunk went out after its only move was never shown to it (0/21 → 21/21 with the server's tick thread stalled after the join). Failing harnesses now print the reason in CI.
2026-10-24 02:52:06 +02:00
Ludovic Marie 7f60a55ca3 Merge pull request #1468 from ludovic111/fix/one-handed-maps
🤖 Agent: merged after verify and CI parity passed, built merged with current main (reports-gate-2: each claim checked against vanilla 26.3, in-frame and --server-thread; evidence on the issue and in the PR body).
2026-10-24 02:40:52 +02:00
Ludovic Marie 16db2f44ae Merge pull request #1420 from ludovic111/fix/st2-chat
🤖 Agent: merged after verify and CI parity passed, built merged with current main (st2: entity use with either hand and server chat components; lan_e2e checks pass in both modes. The hands check's only flake was a vanilla crit arrow killing the cow, fixed in c89695dd9: 40 of 40 runs pass).
2026-10-24 02:36:33 +02:00
Ludovic Marie 0b56d62273 Merge pull request #1521 from ludovic111/fix/launcher-doc-crlf
🤖 Agent: merged after verify and CI parity passed, built merged with current main (launcher-ship-1). #1521: generated docs keep LF line endings on Windows, so the launcher builds there again (dry run 40035909909 green on all three OSes). #1522: Prepare no longer requires Netty's optional native transport jar, which Prism never downloads (fixes #1487; test plus a real Prism-layout prepare).
2026-10-24 02:16:48 +02:00
Ludovic MarieandClaude Opus 5.5 574e3ca567 dedicated_server parity: the client re-sends its position as vanilla's does
The harness failed about 1 run in 3 on CI with "play-trade villager false" on
Horizon's side: the protocol client never saw the villager it trades with.

A server pairs a player with an entity only while the entity's chunk has been
sent to it, and checks again when either moves (ChunkMap.move / updatePlayers).
VanillaProtocolClient sent its position once, at the spawn teleport, and never
again. Horizon's first chunk batch goes out two ticks after the player is placed
(after the first tick's packets, as vanilla orders them); when a busy machine
held the tick thread back long enough for the client's move to be handled one
tick after placement, the villager's chunk wasn't sent yet, and with no later
move the villager was never paired.

The 26.3 client re-sends its position every 20 ticks standing still
(LocalPlayer.sendPosition, after a ServerboundClientTickEnd each tick). The
parity client now does the same from the time it is loaded until its death, so
an entity whose chunk comes late is paired at the next reminder, on either
server.

On failure the harness now adds the servers' and clients' warnings and errors to
its message and the client log's tail when the client fails, and parity.py's
summary prints the end of the first failing harnesses' logs, so CI shows why.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 01:33:16 +02:00
Ludovic Marie 2a2b4acb5c Merge remote-tracking branch 'origin/main' into fix/st2-chat 2026-10-24 01:07:00 +02:00
Ludovic MarieandClaude Opus 5.5 52bc81ca5b Launcher: the command reference is the same on Windows
launcher.yml's Windows build has failed since the reference test landed: Git for Windows checks
text out with CRLF (core.autocrlf), so docs/launcher-commands.md never equalled what markdown()
generates. .gitattributes now pins LF for the generated files tests compare byte for byte, and
the launcher's check reads the file whatever line endings a clone gave it, with a test of a
CRLF checkout.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 00:48:08 +02:00
Ludovic MarieandClaude Opus 5.5 e1fff77a48 Prepare: Netty's native transports are optional, as in the game
Prism Launcher never downloads io.netty:netty-transport-native-kqueue (or the epoll one on
Linux), so preparing from a Prism install stopped on a jar the game runs without: vanilla's
EventLoopGroupHolder.remote takes KQueue/Epoll only when isAvailable(), else Netty NIO.
Preparing now imports a native transport when the launcher has it (checked against the
version JSON's sha1, as before) and goes on without it otherwise; every other library stays
required. Fixes #1487.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 00:38:13 +02:00
Ludovic Marie 265461f2eb Merge pull request #1467 from ludovic111/fix/bundle-clicks
🤖 Agent: merged after verify and CI parity passed, built merged with current main (reports-gate-2: each claim checked against vanilla 26.3, in-frame and --server-thread; evidence on the issue and in the PR body).
2026-10-24 00:32:56 +02:00
Ludovic Marie d6dc08098a Merge pull request #1419 from ludovic111/fix/st2-entity-hands
🤖 Agent: merged after verify and CI parity passed, built merged with current main (st2: entity use with either hand and server chat components; lan_e2e checks pass in both modes. The hands check's only flake was a vanilla crit arrow killing the cow, fixed in c89695dd9: 40 of 40 runs pass).
2026-10-24 00:08:06 +02:00
Ludovic Marie ba8bdc7179 Merge remote-tracking branch 'origin/main' into fix/st2-chat 2026-10-23 23:07:44 +02:00
Ludovic Marie 455a9996d2 Merge pull request #1462 from ludovic111/fix/amd-regress-1
🤖 Agent: merged after verify and CI parity passed. A stopgap while in-frame is the default: an overloaded world slows down instead of dropping to 2 FPS (2,500 pigs: 1.7 → 16 FPS). It is removed with the in-frame path (FEATURES 4zg) once the server thread is the default again (#1463–#1466 and following).
2026-10-23 23:07:17 +02:00
Ludovic Marie cbc469249e Merge remote-tracking branch 'origin/main' into fix/bundle-clicks 2026-10-23 23:06:24 +02:00
Ludovic Marie e436116e38 Merge pull request #1463 from ludovic111/fix/st-totem
🤖 Agent: merged after verify and CI parity passed, built merged with current main (st-default-1: server-thread gaps checked against vanilla, each with a lan_e2e check that fails on main and passes here, host and guest, both modes; tables in the PR body).
2026-10-23 22:33:17 +02:00
Ludovic Marie 35d2781e9d Merge pull request #1454 from ludovic111/feat/client-probe-binary
🤖 Agent: merged after verify passed, with local proof: all 100 client harnesses ran on main and on the branch and their transcripts were diffed (89 byte-identical, the rest only timings or stamps). 81 client examples become one probe binary.
2026-10-23 22:16:12 +02:00
Ludovic Marie 66d1292d10 Merge pull request #1453 from ludovic111/feat/world-probe-binary
🤖 Agent: merged after verify passed, with local proof: all 217 world harnesses ran on main and on the branch and their transcripts were diffed (203 byte-identical, the rest only timings or known run-to-run counts). 168 world examples become one probe binary, with 1,097 duplicated helper lines and 11 dead examples gone.
2026-10-23 22:15:44 +02:00
Ludovic Marie d3de3cd0a9 Merge remote-tracking branch 'origin/main' into fix/amd-regress-1 2026-10-23 22:10:55 +02:00
Ludovic Marie e3b168688e Merge remote-tracking branch 'origin/feat/world-probe-binary' into feat/client-probe-binary 2026-10-23 22:08:17 +02:00
Ludovic Marie 974e18e52a Merge remote-tracking branch 'origin/main' into feat/world-probe-binary
# Conflicts:
#	crates/horizon-client/examples/lan_flat.rs
2026-10-23 22:05:48 +02:00
Ludovic MarieandClaude Opus 5.5 c89695dd93 lan_e2e hands: shoot the cow with a half drawn bow so it lives
A fully drawn bow fires a crit arrow: AbstractArrow.onHitEntity adds
random.nextInt(damage / 2 + 2) to the 6 damage, so one shot in five does 10
and kills the 10 health cow. The off hand bucket then had nothing to milk
("the host's off hand holds minecraft:bucket"), on either side, in any mode.
Under 20 ticks of draw the arrow is no crit and does at most 6. The check
now also asserts the cow is still there after the shot.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 22:01:47 +02:00
Ludovic Marie 4d9baf60d4 Merge remote-tracking branch 'origin/fix/st2-entity-hands' into mm1-1420
# Conflicts:
#	tools/lan_e2e.py
2026-10-23 21:29:13 +02:00
Ludovic Marie 0a36a6d8c6 Merge remote-tracking branch 'origin/main' into mm1-1419
# Conflicts:
#	tools/lan_e2e.py
2026-10-23 21:20:14 +02:00
Ludovic Marie b9501fc826 Merge remote-tracking branch 'origin/feat/world-probe-binary' into feat/client-probe-binary 2026-10-23 21:09:59 +02:00
Ludovic Marie 9dd9a83699 Merge pull request #1427 from ludovic111/fix/st-particles
🤖 Agent: merged after verify and CI parity passed, built merged with current main (st1: /particle and explosion particles from a server reach its players; lan_e2e evidence in the PR body).
2026-10-23 21:06:12 +02:00
Ludovic Marie 341576a084 Merge remote-tracking branch 'origin/main' into feat/world-probe-binary 2026-10-23 21:06:03 +02:00
Ludovic Marie 0b0e4a8aae Merge remote-tracking branch 'origin/main' into fix/bundle-clicks 2026-10-23 20:53:33 +02:00
Ludovic Marie c19418b55d Merge remote-tracking branch 'origin/main' into fix/one-handed-maps 2026-10-23 20:53:25 +02:00
Ludovic MarieandClaude Opus 5.5 8acb46756b Bundle clicks: the open menu's slotsChanged for the player's inventory
BundleItem.broadcastChangesOnContainerMenu calls slotsChanged(player
inventory) after a bundle click. The menus that don't check which container
changed recompute from their inputs (crafting grids, loom, stonecutter,
cartography, merchant, smithing's hasRecipeError). The menu click harness
found it on a loom with a fixture result: 8000/8000 cases now match (seeds 7
and 11).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 20:52:41 +02:00
Ludovic Marie ee79b8f0cd Merge remote-tracking branch 'origin/main' into mm1-1447 2026-10-23 20:38:10 +02:00
Ludovic Marie 8667cef3a1 Merge remote-tracking branch 'origin/main' into mm1-1420 2026-10-23 20:37:55 +02:00
Ludovic Marie fe1d544d02 Merge remote-tracking branch 'origin/fix/st2-entity-hands' into mm1-1420
# Conflicts:
#	tools/lan_e2e.py
2026-10-23 20:37:55 +02:00
Ludovic Marie 33e35d37c0 Merge pull request #1267 from ludovic111/feat/java-packs-photon-chocapic
🤖 Agent: merged after verify and CI parity passed (shaders stack: Java packs draw their own terrain, water and shadow map, as in Iris). BSL shows real shadows on Vulkan and Metal; frame times and screenshots are on #1326. Without a pack, nothing changes.
2026-10-23 20:32:04 +02:00
Ludovic Marie 0cc3b4a057 Merge remote-tracking branch 'origin/main' into mm1-1447
# Conflicts:
#	.github/workflows/ci.yml
2026-10-23 20:21:46 +02:00
Ludovic Marie 81fb4c7371 Merge remote-tracking branch 'origin/main' into mm1-1419
# Conflicts:
#	crates/horizon-client/src/game.rs
#	crates/horizon-client/src/lan/entity.rs
#	tools/lan_e2e.py
2026-10-23 20:18:12 +02:00
Ludovic Marie e247790c83 Merge pull request #1472 from ludovic111/fix/main-build-1432
🤖 Agent: merged after verify passed; main builds again. Local proof on the Mac mini: cargo check -p horizon-client --bins --tests fails on 8a4ebcc55 with the two E0599 errors and passes here.
2026-10-23 20:15:12 +02:00
Ludovic Marie ceb12718cb Merge pull request #1448 from ludovic111/chore/python-helpers-lab
🤖 Agent: merged after verify and CI parity passed, built merged with current main (review-2-deps-dupes; numbers and harness proof are in the PR body).
2026-10-23 20:10:15 +02:00
Ludovic MarieandClaude Opus 5.5 f74d2fd5f4 Restore Guest::send_all and Behaviours::bone_meal_spreads: main builds again
#1432 deleted both as unused; #1424 (level events) and #1425 (scoreboard)
merged after it and call them, so horizon-client's binary failed with
E0599 at level_particles.rs:54 and scoreboard.rs:338. Both functions come
back unchanged from 4d7f30f14^1.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 20:05:12 +02:00
Ludovic MarieandClaude Opus 5.5 7dac46ec3c Bundles take and give items in inventories and containers
A carried bundle clicked on a stack took its place instead of taking it in
(#1456): Horizon's menus had no BundleItem click overrides. The PICKUP click
now runs them first, as AbstractContainerMenu.tryItemClickBehaviourOverride:

- a carried bundle takes as much of a clicked stack as its weight allows
  (tryTransfer), and a right click on an empty slot puts its first stack
  there (removeOne, the rest going back in);
- a stack clicked onto a bundle in a slot goes in as far as it fits, and a
  right click with an empty hand takes the bundle's first stack out.

BundleContents.Mutable is in horizon_client::bundle (exact Fraction
weights: 1/max stack, 1 for a beehive with bees, a nested bundle's contents
plus 1/16; shulker boxes don't fit), used by the menu engine and the
LAN/server-thread container engine. The insert, insert-fail and remove-one
sounds play at the player.

In single player the inventory screen's empty-hand press went to an older
click path instead of the menu engine; it now goes to the engine, as on a
server, so the right click on a bundle works there too.

The menu click harness now has bundles (empty, loaded, nested; often in
hand) and compares the player's sounds: 3000/3000 bundle-heavy cases match,
2796/3000 without the overrides.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 19:58:48 +02:00
Ludovic MarieandClaude Opus 5.5 8ef418e223 Maps in one hand: the off hand, or the main hand beside an item, draws the map
FirstPersonHandsAndItemsRenderer.submitArmWithItem draws a stack with a
map_id two-handed only from the main hand with an empty off hand; any other
map is renderOneHandedMap in its own hand. Horizon only had the two-handed
case and drew every other held map as its item model (#1459).

MapHold picks the case from the hand renderer's visible items, and
one_handed_map_pose is renderOneHandedMap's transform, checked against
vanilla's PoseStack. renderMap (background, map, decorations) is shared by
both cases. The arms under held maps are still not drawn, as before.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 19:58:47 +02:00
Ludovic Marie 8a4ebcc553 Merge pull request #1425 from ludovic111/fix/st-hud
🤖 Agent: merged after verify and CI parity passed; each fix has a lan_e2e check (host and guest) passing on the server thread and in-frame (bugs-st-1).
2026-10-23 19:38:45 +02:00
Ludovic Marie 9ff9055b4f Merge pull request #1424 from ludovic111/fix/st-level-events
🤖 Agent: merged after verify and CI parity passed; each fix has a lan_e2e check (host and guest) passing on the server thread and in-frame (bugs-st-1).
2026-10-23 19:37:19 +02:00
Ludovic Marie 0495d3cbdd Merge pull request #1423 from ludovic111/fix/st-thrown
🤖 Agent: merged after verify and CI parity passed; each fix has a lan_e2e check (host and guest) passing on the server thread and in-frame (bugs-st-1).
2026-10-23 19:35:52 +02:00
Ludovic Marie 4d7f30f145 Merge pull request #1432 from ludovic111/chore/delete-unused-pub-items
🤖 Agent: merged after verify and CI parity passed (review-0-survey quick win: dead pub items; the generated version.rs is kept as the generator writes it).
2026-10-23 19:17:10 +02:00
Ludovic MarieandClaude Opus 5.5 b8e729b950 Totems save a server's players: the guest's turn uses it up, and entity event 35 reaches every client
A server's players (LAN guests, and single player's window on its server
thread) never had a totem ready, and nothing applied one: lethal damage
killed them with the totem still in hand (#1292). Each guest turn now
readies the totem from the guest's hands and, when its hurt set it off,
uses it up at the end of the turn (LivingEntity.checkTotemDeathProtection):
1 health, the item_used stat, the used_totem trigger, the use vibration,
the totem's effects.

Entity event 35 now goes to every client around, the holder's own too,
and a client plays it as ClientPacketListener.handleEntityEvent does: the
totem particles, the sound heard locally (not sent on as a sound packet,
so no client hears it twice) and the holder's rumble.

lan_e2e totem: host and guest, off hand and main hand, both modes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 18:10:13 +02:00
Ludovic Marie b65e082fd7 Merge pull request #1450 from ludovic111/chore/client-zip-crate
🤖 Agent: merged after verify and CI parity passed (review-2-deps-dupes; numbers and harness proof are in the PR body).
2026-10-23 18:02:49 +02:00
Ludovic MarieandClaude Opus 5.5 55264459ce In-frame ticks stop at a tick's length per frame: an overloaded world slows down instead of drawing at 2 FPS
Single player's world ticks in the frame by default (#1428). When its ticks
take longer than 50 ms, every frame fell behind and ran the client timer's
ten, so the picture stalled at 1-2 FPS (a player's report on an AMD 680M,
in-frame tick at 36.7 ms). Vanilla's world ticks on the integrated server's
thread, which falls behind without costing frames and logs "Can't keep up!".

A frame now stops running world ticks once they took a tick's length and
drops the rest, logging vanilla's overload warning (1 s + 20 ticks behind,
at most every 10 s + 100 ticks). Ticks of a few milliseconds still get the
full ten per frame; sprints and server clients are unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 17:47:22 +02:00
Ludovic Marie 036e6bd8d6 Merge pull request #1444 from ludovic111/fix/parity-shard-world-spawn
🤖 Agent: merged after verify and CI parity passed. world_spawn runs as 4 shards, so the job's tail drops from 1,436 s to about 465 s.
2026-10-23 17:40:37 +02:00
Ludovic Marie 80d6252734 Merge pull request #1443 from ludovic111/fix/parity-prebuild-selection
🤖 Agent: merged after verify passed. The parity probe prebuild repeats each harness's own -p and features selection, so harness builds after the prebuild compile nothing (0 of 10, was 10 of 10). The full harness set ran on it locally in #1453's before/after diff.
2026-10-23 17:39:09 +02:00
Ludovic Marie f7144ccd19 Merge pull request #1435 from ludovic111/fix/ci-verify-no-examples
🤖 Agent: merged after verify passed. CI verify no longer builds the ~275 examples: the job goes from 7.8 to 5.6 min (review-1-ci).
2026-10-23 17:37:42 +02:00
Ludovic Marie a74d6e49e6 Merge pull request #1458 from ludovic111/fix/high-res-pack-gui
🤖 Agent: merged after verify passed, with local parity proof (103/103 on this head). GUI sprites are measured in GUI units whatever the pack's resolution, as in vanilla: inventories, tooltips and toasts look right with Faithful 32x and 64x.
2026-10-23 17:31:27 +02:00
Ludovic Marie ea426bd53d Merge pull request #1457 from ludovic111/fix/resource-pack-crash
🤖 Agent: merged after verify passed, with local parity proof: parity --changed passed 103/103 on #1458's head, which contains this PR. Faithful 64x reproduced a startup panic on main (a 32768 px shared atlas over the 16384 GPU limit); with the fix it reaches the world. A pack that can't fit now falls back to the default resources, as vanilla does.
2026-10-23 17:29:56 +02:00
Ludovic Marie ab2bc22515 Merge origin/main into feat/java-packs-photon-chocapic
# Conflicts:
#	crates/horizon-shaderpack/src/translate.rs
#	docs/shader-packs.md
2026-10-23 17:20:17 +02:00
Ludovic Marie ce3284a516 Merge pull request #1429 from ludovic111/feat/launcher-single-channel
🤖 Agent: merged after verify and CI parity passed; release dry run 39935111815 was green (the smoke test drew a world, no blocker, it would publish as the release and latest). One channel, as Ludovic decided on 2026-10-23.
2026-10-23 17:11:04 +02:00
Ludovic Marie 9137279e10 Merge pull request #1418 from ludovic111/fix/st2-saved-state
🤖 Agent: merged after verify and CI parity passed; lan_e2e (hands, styles, feedback, chat, frames, riding, mounts, beds, contacts, worldspawn) passes on the server thread and in-frame (bugs-st-2).
2026-10-23 17:09:33 +02:00
Ludovic MarieandClaude Opus 5.5 441774c229 held_drop: pass its probe name
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 16:51:55 +02:00
Ludovic MarieandClaude Opus 5.5 693556ce32 Point the particle harnesses' source lists at the probe modules
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 16:51:55 +02:00
Ludovic Marie dfbb106c69 Merge feat/world-probe-binary (harness fixes) 2026-10-23 16:51:54 +02:00
Ludovic MarieandClaude Opus 5.5 38bce65c21 Pass the probe name where the harness codemod missed it
block_survival and state_provider hashed the name as an artifact instead of
passing it; decorated_region runs chunk_dump through chunk_generation's binary.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 16:50:01 +02:00
Ludovic Marie 55034fcce1 Merge pull request #1446 from ludovic111/fix/macos-guest-cdylib-link
🤖 Agent: merged after verify passed. No runtime change for players: #1445 deletes 10 tools/ scripts that nothing references (no harness depends on them); #1446 exports the mod-SDK symbols only on wasm32, so the guest crates link on macOS, and the mod sandbox tests pass.
2026-10-23 16:37:13 +02:00
Ludovic Marie 9a8003df0b Merge pull request #1445 from ludovic111/chore/delete-dead-tools
🤖 Agent: merged after verify passed. No runtime change for players: #1445 deletes 10 tools/ scripts that nothing references (no harness depends on them); #1446 exports the mod-SDK symbols only on wasm32, so the guest crates link on macOS, and the mod sandbox tests pass.
2026-10-23 16:35:43 +02:00
Ludovic Marie d759a89cf0 Merge pull request #1451 from ludovic111/docs/agent-map
🤖 Agent: merged after verify passed; docs only (AGENTS.md, CLAUDE.md symlink, docs/architecture.md). Every path and harness name was checked against the repo.
2026-10-23 16:31:47 +02:00
Ludovic MarieandClaude Opus 5.5 82d6d27567 Point the audio harnesses at audio_common's new place
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 16:30:39 +02:00
Ludovic MarieandClaude Opus 5.5 a6da925837 Split the client probe list from its dispatch the same way
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 16:30:39 +02:00
Ludovic MarieandClaude Opus 5.5 4a922a5772 Document how the runner tracks probe-binary dependencies
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 16:30:38 +02:00
Ludovic MarieandClaude Opus 5.5 858f701ee8 Run the horizon-client parity probes from one binary
81 client examples that harnesses run become modules of examples/probe:
harnesses run `probe <name> ...` (or `cargo run --example probe -- <name>`),
and the shared helpers (audio_common, hud_fixture, chat_screen_fixture)
move with them. The benchmarks and manual tools stay standalone examples.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 16:30:38 +02:00
Ludovic MarieandClaude Opus 5.5 ed847d43dc Keep the probe list out of the harnesses' dependencies
main.rs now only lists the probes; dispatch.rs runs them. The runner leaves
main.rs out of a probe-binary harness's dependencies and keeps dispatch.rs,
so adding a probe no longer reruns (or flake-checks) every harness of its
crate. It also follows crate::<probe> references between probes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 16:30:38 +02:00
Ludovic MarieandClaude Opus 5.5 ab4c246cda Delete the 11 examples nothing references
Eight benches (block_facts, chunk, entity_geometry, json_readers, load,
save, streaming, tick_chunk_membership), lan_flat, save_parity and
vanilla_diff: no harness, tool, doc or workflow names them (review plan,
section 1, dead code). -2,222 lines; Git history keeps them.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 16:30:38 +02:00
Ludovic MarieandClaude Opus 5.5 7092f16a06 horizon-world probes: one copy of the helpers they shared
Now that the probes are modules of one binary, the helpers copied between
them live once in examples/probe/support: the collision probes' shape
printer (emit, values, box_values, bounds, 8 copies each), the canonical
NaN bits of f64 and f32 (50 copies), the registry JSON-to-NBT reader (15)
and the JSON block position (4). -1,097 lines of copies.

f64::NAN.to_bits() is 0x7ff8000000000000, the literal some copies used,
so every probe prints the same bytes.

tools/parity.py follows include!: three probes include another probe's
source, and stay dependent on it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 16:30:38 +02:00
Ludovic MarieandClaude Opus 5.5 fdae4b895c horizon-world probes: one binary, probe <name>
The 168 horizon-world examples the parity harnesses run become modules of
one example, examples/probe, that dispatches on its first argument;
crate::args() gives each the command line its example saw. Linking ~170
binaries, each instantiating horizon-world's generics again, was most of
the parity job's probe build (review plan, section 1).

The harnesses build `--example probe` and run `probe <name> ...`.
tools/parity.py counts only the probes a harness ran, and the support
modules they name, as its dependencies, so editing one probe still reruns
only its own harnesses (and PARITY_RUST_SCOPE keeps working).

The six manual tools (benches, loot_fill, structure_blocks, vanilla_diff,
attribute_memory, behaviour_load_profile) stay separate examples.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 16:30:38 +02:00
Ludovic Marie c1f8ec81d8 Merge #1444 (world_spawn shards), which touches the same harness 2026-10-23 16:30:37 +02:00
Ludovic Marie 2e953c625b Merge #1443 (parity prebuild selection), which the probe binaries rely on 2026-10-23 16:30:37 +02:00
Ludovic MarieandClaude Opus 5.5 1960dc6eb2 AGENTS.md: agents never add release-now
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 16:30:31 +02:00
Ludovic MarieandClaude Opus 5.5 7c42cc63ce parity: leave out of the prebuild probes that no longer exist
A record naming an example since renamed or deleted made the whole Cargo
run fail ("no example target named ..."), so no probe was prebuilt and
every harness built its own: the probe binaries rename ~250 of them.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 16:30:27 +02:00
Ludovic MarieandClaude Opus 5.5 79ee670d79 High-resolution resource packs draw the GUI at its size
With Faithful 32x the inventory showed the top-left quarter of its
background stretched over the screen, tooltips lost their background and
toasts doubled in size (#1442): the GUI code drew every atlas sprite in
texels, where vanilla draws in GUI units whatever the pack's resolution.

- Atlas sprites carry their GUI size: the size of the default resources'
  texture of that name (what vanilla's `blit(..., 256, 256)` calls and
  sprite sizes assume), their texels only for what copies pixels (font
  sheets, fire frames). Textures only a pack has keep their own size.
  The texture size filter reads the GUI size too, so Faithful 64x's
  1024x1024 container backgrounds are no longer dropped.
- Nine-slice sprites follow `GuiGraphicsExtractor.blitNineSlicedSprite`:
  their `gui.scaling` width, height and borders (Faithful's tooltip
  background is 50x50 with a 10-pixel border), `stretch_inner`, the
  single-quad and three-part cases, and borders capped at half the
  drawn size by integer division.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 16:28:50 +02:00
Ludovic MarieandClaude Opus 5.5 22ad65611e High-resolution resource packs no longer crash the game at launch
The shared GUI atlas was always 4096 texels wide, so Faithful 64x needed
4096x32768: past the 16384 texels a side GPUs allow, and wgpu panicked
in create_texture at every launch until the pack was removed (#1442).

- The atlas widens (4096, 8192, 16384) until it fits within 16384 a
  side, so Faithful 64x loads. Its height rounds up to a multiple of
  1024 instead of a power of two: the default resources' atlas drops
  from 4096x8192 to 4096x5120, Faithful 32x's from 4096x16384 to
  4096x12288.
- Textures that still don't fit are an error, not a GPU panic. At
  startup it rolls the packs back as Minecraft.rollbackResourcePacks
  does: the game starts with its own resources, and the selection is
  cleared and saved (clearResourcePacksOnError), so the next launch
  doesn't fail again. The existing "Resource reload failed" screen
  shows the reason.
- The renderer refuses an atlas larger than the device allows with an
  error instead of a wgpu validation panic.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 16:28:50 +02:00
Ludovic Marie 172ae241f1 Merge pull request #1430 from ludovic111/feat/release-single-channel
🤖 Agent: merged after verify and CI parity passed; release dry run 39935111815 was green (the smoke test drew a world, no blocker, it would publish as the release and latest). One channel, as Ludovic decided on 2026-10-23.
2026-10-23 16:24:35 +02:00
Ludovic MarieandClaude Opus 5.5 2eb699228a AGENTS.md and docs/architecture.md: a map of the code for agents
AGENTS.md (CLAUDE.md links to it): the crates and what each owns, how to
build and test fast, where each subsystem of horizon-client lives, how Game
is organised, the two single-player paths and how to test both, the vanilla
reference, and the rules that bite. docs/architecture.md: feature -> files ->
checks for the 40 areas changed most in the last 30 days.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 16:18:44 +02:00
Ludovic Marie 1487a1bff7 Merge pull request #1434 from ludovic111/perf/metal-cull-flat
🤖 Agent: merged after verify and CI parity passed. Metal GPU occlusion culling: on the M4 Pro at RD 32, FPS is +8% to +48% (interleaved A/B against main); the hidden sections match Vulkan's; parity --changed passed 104/104.
2026-10-23 16:03:14 +02:00
Ludovic MarieandClaude Opus 5.5 60bad91a50 Client zip.rs reads archives with the zip crate it already links
The 120-line hand-written central-directory and local-header parser goes:
the zip crate (already the client's dependency for world backups) indexes
the archive, zip64 and odd offsets included. Entries are still read raw and
inflated here, so as with Java's ZipFile (vanilla's FilePackResources)
their CRC isn't checked. New tests: folder entries, a nested pack root, a
Windows-style name, one broken entry among good ones, and files that aren't
zips.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 15:31:51 +02:00
Ludovic MarieandClaude Opus 5.5 3960c09159 tools: one copy of the helpers harnesses share, in lab.py
free_port (7 copies), offline_uuid (4), varint and parse_varint (3 and 2),
dbits (6), fbits (4) and settled (2) move into lab.py; every caller uses
lab's. The other server harnesses keep importing free_port from
test_dedicated_server_parity, which now takes lab's. Imports the moved
code left unused go too.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 15:20:06 +02:00
Ludovic MarieandClaude Opus 5.5 b15e5e893e Code mods are the client's mods feature: tests and probes skip wasmtime
horizon-mods' runtime is no longer a default feature, and its sandbox test
requires runtime and pack instead of a dev-dependency on itself (that
dev-dependency turned the runtime on in every workspace build with examples,
the parity probe build included). The client gets a mods feature that turns
the runtime on; without it game/mods.rs keeps content mods and logs why code
mods don't start, and every hook does nothing.

Release builds (client and server) pass the feature, CI's clippy and tests
pass it (and pack, for the sandbox test), and pr_ready does where horizon-mods
is checked. horizon_mods::find moves to manifest.rs: content mods use it
without the runtime.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 15:18:01 +02:00
Ludovic MarieandClaude Opus 5.5 5ca16b97fc Keep version.rs as tools/gen_version_consts.py writes it
The file is generated from the pinned version lock, and test_gen_version_consts
checks JAVA_VERSION against it; deleting constants by hand breaks the next
regeneration and that test.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 15:07:38 +02:00
Ludovic MarieandClaude Opus 5.5 eeb4cdb680 Mod SDK: export_mod! exports only in WebAssembly, so the guests link on macOS
A host build of a mod (cargo build or test on the workspace) made a cdylib
whose component exports macOS's ld couldn't take: its exported-symbols list
cuts `horizon:mods/[email protected]#init` at the `#`, so coords-hud and
horizon-mods-test-guest failed to link. The exports now exist only for
wasm32; on the host the macro still checks the mod and keeps it used.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 15:06:50 +02:00
Ludovic Marie 2439475716 Merge pull request #1414 from kerneldroid/feat/shader-api2-pipeline
🤖 Agent: merged after the contribution gate. The claims were checked against Iris and OptiFine, and the weak parts were rewritten on the branch: the stage kinds, the Java shadow feed and the fake uniforms were removed; settings now apply on Done; packs are imported by drag and drop; memory is only allocated while a pack reads it; the MakeUp target is fixed. Tested on Vulkan and Metal with 6 packs, profiles, imports and broken packs; local parity passed 96/96. Frame times before and after are in the PR comment. Thanks @kerneldroid.
2026-10-23 15:05:34 +02:00
Ludovic Marie 1959c397a0 Merge pull request #1433 from ludovic111/chore/unify-naga-features
🤖 Agent: merged after verify and CI parity passed (review-0-survey quick win; see horizon-parity reports/review-plan.md).
2026-10-23 14:44:50 +02:00
Ludovic MarieandClaude Opus 5.5 7cc47e2b55 Delete ten tools/ scripts nothing references
command_parse_summary, structure_start_oracle, fetch_client_assets,
compare_structure_starts, profile_behaviour_cache, command_tree_diff,
compare_loot_fill, compare_structure_blocks, test_lan_lifecycle and
PROFILE_SHARED_JSON.md: no workflow, doc, harness, test or other tool
names them (review-plan.md, Dead code). History keeps them.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 14:41:40 +02:00
Ludovic MarieandClaude Opus 5.5 c9c53629d4 parity: run world_spawn in four shards
world_spawn starts a vanilla and a Horizon server for each of 60 seeds,
one at a time: 1,144-1,548 s on the CI runner, the parity job's longest
harness and its floor whenever a PR's changes reach it. With
PARITY_SHARDS = 4 each shard creates every fourth seed's worlds, in its
own build directory.

The shard helper datafix had moves to tools/parity_shard.py, which both
use. It stays out of lab.py, which every harness imports.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 14:36:58 +02:00
Ludovic Marie 6fe88978a7 Merge origin/main into feat/shader-api2-pipeline 2026-10-23 14:17:13 +02:00
Ludovic MarieandClaude Opus 5.5 d9591047fa Shader pack docs: MakeUp, Chocapic13' and Photon on the Java path, and where the world goes
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 14:17:13 +02:00
Ludovic MarieandClaude Opus 5.5 fe3a3af568 Shader Packs page: the pack's settings button sits beside the folder button
As in Iris's ShaderPackScreen: at the end of the list it scrolled out
of sight with a few packs. It's greyed out while the picked pack has no
options.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 14:16:51 +02:00
Ludovic MarieandClaude Opus 5.5 ac54227465 parity: prebuild probes with each harness's own package selection
The probe prebuild ran `cargo build --workspace --examples --bins` (or
named targets with --workspace). The harnesses build with `-p <crate>`, and
Cargo unifies features over the selected packages: under --workspace,
flate2, libc, memchr, syn, thiserror, postcard and digest get other
features for horizon-world, and horizon-mods (pack) and horizon-telemetry
(upload) for horizon-client. So every probe the prebuild made was a
variant no harness used, and each harness rebuilt its crate and its probe
again while the others waited on Cargo's lock.

Record each probe with its command's package and feature flags and
whether that command builds examples (which turns dev-dependencies on),
and prebuild one Cargo run per such selection. --bins and --examples are
recorded too, so the fast-profile horizon-client build that
save_roundtrip and world_reopen run is prebuilt as well. A harness that
never ran here takes its probes from the shared dependency lists instead
of a build of all ~275 examples.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 14:11:22 +02:00
Ludovic MarieandClaude Opus 5.5 4a61134edd CI verify: test without building the examples
cargo test --workspace built and linked all ~275 examples, 59% of the
test build (review-plan section 1), though none of their tests run.
Test the libraries, binaries and integration tests, then the doc tests,
as tools/gate.py and tools/pr_ready.py already do. Clippy --all-targets
still type-checks every example, and the parity job builds and runs them.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 14:06:25 +02:00
Ludovic MarieandClaude Opus 5.5 39ae543796 Shader packs: test importing zips and folders, and refusing what isn't a pack
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 14:02:12 +02:00
Ludovic MarieandClaude Opus 5.5 963d5cedd3 Shader packs: keep the opaque depth and G-buffer only for packs that read them
The world target made the opaque depth, normal and material textures
for every scaled or anti-aliased frame, packs or not: 12 bytes a pixel
nobody read. They now follow what the pack reads, and Post::generation
tells the stages to bind again when its targets change (a recreated
target of the same size, or a shadow map made after the stages bound).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 14:02:12 +02:00
Ludovic MarieandClaude Opus 5.5 fe7bb3a34b Java packs: the world goes where the pack's terrain program draws
MakeUp's gbuffers write colortex1, and its composites read the world
from there: with the world in colortex0, it showed nothing but fog.
Iris's fallbacks for terrain pick the program (gbuffers_terrain, then
textured_lit, textured, basic); its first draw buffer takes the world.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 14:02:12 +02:00
Ludovic MarieandClaude Opus 5.5 eafe97da74 Shader packs: keep what Iris does, drop what doesn't work, apply settings on Done
Review fixes for #1414 (contribution gate):

- Java packs no longer get Horizon's shadow map: it was mirrored in y and
  reversed in depth against the GL conventions the packs sample with, had
  no distortion and only the camera's sections, so BSL drew no shadows
  while the pass cost 0.65 ms (MakeUp, M4 Pro). shadowtex* stays a far
  depth, as on main; the packs' own shadow programs are #1326.
- Invented uniforms removed (centerDepthSmooth fixed at 0.5,
  ambientOcclusionLevel and shadowMapResolution as uniforms, constant
  temp/rainfall/biome/held items); an unknown uniform stays zero as
  before. wetness keeps its Iris smoothing. Unused directives removed.
- texture.<stage>.<sampler> is scoped to Iris's stages (final reads the
  composite ones) instead of overriding the sampler in every pass;
  texture.noise replaces noisetex. One noise generator and one RGBA
  upload for both pack kinds.
- The G-buffer pass, the opaque-depth copy and the shadow pass run only
  for a pack that reads them (naga's per-entry-point binding use, not a
  text search): every pack paid all three before.
- Api 2's prelude additions go in their own file after api 1's, so api 1
  packs keep the names they declare (aces_tonemap, material_at...).
- Stage kinds shadow, gbuffer, deferred and final ran as composite: only
  composite is accepted; api 2 adds formats (validated), profiles,
  option descriptions and a noise texture.
- Shader Pack Settings applies once when the page closes, as Iris's Done
  (each click or slider step rebuilt the pack: 2.7 s per click, 12.7 s
  for one drag with BSL). Labels follow Iris (.lang, else the name).
  A pack whose options can't be read isn't read again every frame.
- Saved option values an option doesn't list are ignored, as OptiFine's
  ShaderOption.setValue (a stale file broke BSL's compile).
- Import: the blocking file dialog (osascript/zenity/kdialog/PowerShell
  on the event loop) is gone; a pack dropped on the Shader Packs page is
  copied and picked, as Iris's onFilesDrop. Drops elsewhere do nothing,
  an existing pack isn't overwritten, links aren't followed.
- Docs and the example say what the code does (SSAO, not GTAO).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 14:02:12 +02:00
Ludovic Marie d95932e62c Merge origin/main into feat/shader-api2-pipeline 2026-10-23 14:02:12 +02:00
Ludovic MarieandClaude Opus 5.5 660407fa58 Metal: cull only frames with 4,096 section draws or more
On Apple's GPUs the passes the test adds cost a fixed share of the frame
(it is stored and reloaded at each), which the hidden sections repay only
at high render distances: at 12 culling cost an M4 Pro 0.05 ms of GPU time
a frame, at 32 it saved 0.25 to 1.6 ms. Other backends keep culling every
frame; HORIZON_GPU_CULL=all culls every frame on Metal too.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 13:22:10 +02:00
Ludovic Marie 6be7daebc8 Merge pull request #1417 from ludovic111/fix/st2-rules
🤖 Agent: merged after verify and CI parity passed; lan_e2e (hands, styles, feedback, chat, frames, riding, mounts, beds, contacts, worldspawn) passes on the server thread and in-frame with all four merged on main (bugs-st-2).
2026-10-23 13:20:42 +02:00
Ludovic Marie 2639e9e1d7 Merge pull request #1422 from ludovic111/fix/st-leads
🤖 Agent: merged after verify and CI parity passed; each fix has a lan_e2e check (host and guest) passing on the server thread and in-frame (bugs-st-1).
2026-10-23 13:20:40 +02:00
Ludovic MarieandClaude Opus 5.5 1d81c67b7b Build naga once for the game, its tests and its examples
horizon-client's dev-dependency on naga added the hlsl-out feature for the
gfx.rs test that translates every shader for DirectX 12. With resolver 2 a
dev-dependency's features apply only to tests and examples, so Cargo kept
two builds of naga, wgpu-hal, wgpu-core, wgpu, horizon-shaderpack and the
horizon-client lib, and rebuilt them on the first example or test after a
game build.

Measured on the Mac mini (cargo build of the game, then one example):
the example took 37.3 s and recompiled that whole stack before, 1.2 s
and only its own crate after; the target dir is 1 GB smaller (4.9 to
3.9 GB). The game binary is the same size: nothing calls the HLSL writer,
so the linker drops it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 12:13:58 +02:00
Ludovic Marie 9c09d2897d Merge pull request #1428 from ludovic111/fix/alpha-regress-1
🤖 Agent: merged after verify and CI parity passed. In-frame single player is the default again until the server thread reaches parity (FEATURES 4zg); release builds were compared with v0.2.0 and v0.2.11 on the Mac mini. The server thread stays available with --server-thread or HORIZON_SERVER_THREAD=1.
2026-10-23 12:11:36 +02:00
Ludovic MarieandClaude Opus 5.5 173a55e608 Delete pub items nothing uses, and the stubs that hid unused code
93 pub items (78 functions and methods, 14 constants, 1 struct) in horizon-client,
horizon-world, horizon-core, horizon-friends and horizon-server-files had
no caller anywhere in the repository: not in the crate, the other crates,
the examples, the tests or the binaries. rustc can't flag them because
horizon-client and the others are libraries, where a pub item counts as
used. The imports and the private helpers only they used go with them
(title.rs date_string, goals.rs flag_index).

Also drops the #[allow(dead_code)]/#[allow(unused)] items that only hid
dead code: chat_debug_fill, merchant_title and Goals::locked_by had no
caller, and four stubs (_keep, _unused, _rule_kind, const _) existed only
to keep an import used. LightTables::from_parts is used by its tests, so
it is #[cfg(test)] instead of allowed.

Protocol id tables and entity data serializer ids stay: they mirror
vanilla's registries in full. The launcher's three unused items are left
for a launcher PR.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 12:08:44 +02:00
Ludovic MarieandClaude Opus 5.5 14be6ca394 Metal: occlusion culling with flat draws too
Flat draws (one index list per frame, the default on Apple GPUs at high
render distances) turned occlusion culling off. Now phase 2's draws are
flattened after the test, by a compute pass that copies only the sections
the test saw, and each page run is drawn indirectly with the count it
reached.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 12:03:39 +02:00
Ludovic MarieandClaude Opus 5.5 9fcf91e4bc Metal: occlusion culling of solid sections, the depth test in the shader
GPU occlusion culling ran on Vulkan only: its test needs early fragment
tests, which naga's Metal backend does not emit. On Metal the boxes are
tested in a pass of their own: phase 1's depth is bound read-only as the
attachment and as a texture, and the fragment shader compares its own
depth with it (reversed Z, greater or equal), as the fixed-function test
would. HORIZON_GPU_CULL=shader takes this path on Vulkan too, for checks.

Frames drawn flat (the default on Apple GPUs at high render distances)
still skip culling here.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 12:03:38 +02:00
Ludovic MarieandClaude Opus 5.5 630f521072 Launcher: one update channel, and the menu entry is never taken over
Every release is now published as the release (one channel, Ludovic's
decision of 2026-10-23), so the launcher drops its Stable/Alpha choice:
the Updates row in Settings, `horizon-launcher update-channel` and its MCP
tool, the channel on the Versions page, in `versions` and in the footer.
It updates to the newest game release that is not a pre-release (GitHub's
latest) and looks every 30 minutes. The pre-releases of the old Alpha
channel stay listed and installable by hand, never as an update. Old
`update_channel` / `beta_builds` keys in settings.json are ignored.

On Linux a release build that starts no longer rewrites the
applications-menu entry to point at itself. It writes the entry only when
there is none, or when the program it starts (TryExec, else Exec) no longer
exists; turning the Settings switch on or `desktop-entry` still points it
at this launcher, and HORIZON_LAUNCHER_DESKTOP_EXEC is still honored. A
test build started with the real HOME took over a working entry and then
left it broken when its target/ was deleted (2026-10-23).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 11:53:00 +02:00
Ludovic MarieandClaude Opus 5.5 0e65e85878 Releases: one channel, every release is the latest once its checks pass
Ludovic's decision (2026-10-23): no more Alpha and Stable. release.yml
publishes every game release as the release and GitHub's latest instead of
a pre-release that promote.yml made stable once a day. The checks that
gated a promotion run in the release itself: `decide` holds the train while
an issue labelled release-blocker is open (forced runs too), and the publish
job runs `release_train.py gate` right before publishing: the Mac mini's
smoke-test.json must be there and show a world was drawn, and no blocker may
be open. Otherwise nothing is published.

Notes cover everything since the last release that is not a pre-release, so
the first single-channel release lists all of it since v0.2.0. promote.yml,
tools/release_channels.py and its tests are deleted. The cadence is
unchanged (5 minutes of settling, 20 at most, release-now at once).

Docs and site: one channel in release-flow, versioning, security, the FAQ,
install, the home page and the Releases page (Game / Launcher filter); no
player-facing Alpha wording left.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 11:53:00 +02:00
Ludovic Marie 62a6b3a620 Merge pull request #1421 from ludovic111/fix/st-entity-data
🤖 Agent: merged after verify and CI parity passed; each fix has a lan_e2e check (host and guest) passing on the server thread and in-frame (bugs-st-1).
2026-10-23 11:12:06 +02:00
Ludovic MarieandClaude Opus 5.5 6049012b52 Single player runs in the frame by default again; the server thread is opt-in
Players on the Alpha channel report single player as broken since v0.2.1
(#1265 made the integrated server thread the default again). On v0.2.11's
server thread, placed signs open no editor (#1380), held books don't open
(#1366), a totem doesn't save its holder (#1292) and death keeps the
inventory with keep_inventory off (#1284); the in-frame world and Stable
v0.2.0 do all four right. About 45 more [server-thread] issues are open.

`--server-thread` or HORIZON_SERVER_THREAD=1 turns it on, as before #1114;
`--no-server-thread` and HORIZON_SERVER_THREAD=0 still work. LAN guests and
dedicated servers are untouched. Temporary until the server thread reaches
parity (FEATURES 4zg).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 10:54:42 +02:00
Ludovic MarieandClaude Opus 5.5 4b8fecfe58 Particles from a server: /particle and explosions reach its players
A server never sent particles: /particle on single player's server thread
drew into the server's own engine, and TNT exploded without its burst for
anyone but an old-mode host. The protocol gains
ClientboundLevelParticlesPacket and ClientboundExplodePacket; /particle
sends the particles to each viewer in range (every player without
viewers, as PlayerList.getPlayers) and an explosion sends its packet to
the players within 64 blocks; their clients draw them as
handleParticleEvent and handleExplosion do.

/particle's spread was measured from the command's source as if it were a
position; it is the spread as given (Vec3Argument.getVec3).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 10:02:19 +02:00
Ludovic MarieandClaude Opus 5.5 b7f4cba355 A server's scoreboard reaches its players: sidebar, teams, scores
The server never sent its scoreboard, so a sidebar shown on single
player's server thread or to a LAN guest stayed empty. Each tick a placed
player is now sent what changed since it was last sent the board, as
ServerScoreboard's callbacks do: every team, and the objectives in a
display slot with their slots and scores (all of it on joining).

A server's client also finds its team's sidebar by its profile name rather
than single player's "Player".

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 10:02:19 +02:00
Ludovic MarieandClaude Opus 5.5 13772b4e5b Level events on a server's clients: spawner bursts, bone meal sparks, waxing
A client of a server (single player on its server thread, a LAN guest, a
dedicated server's player) played only three of the level events the server
sent (a block broken, the dragon egg's and a consumable's teleport): a
spawner's burst, waxing and scraping copper, a composter filling and the rest
showed nothing. Every one now goes through the handler the in-frame game uses
(LevelEventHandler).

Bone meal's level event (1505) played its sound but never its growth
particles, in either mode: it now spawns them as BoneMealItem.
addGrowthParticles does (spread over a grass block's, nylium's, netherrack's
or moss's neighbourhood and water, inside a growing block, below rooted dirt
and mangrove leaves). The server also sends it to the player who used the
bone meal, as vanilla's levelEvent names no player.

Waxing copper with honeycomb and an axe or hoe's transforms (wax off, scrape,
strip, till) make their particles and sound on the clicker's own client, as
vanilla's client runs Item.useOn; the server sends them to the others only.
The transform's preview is shared with the in-frame use.

`horizon-ctl status` counts the particles made by type. lan_e2e's new
`sparks` check: the host and the guest each use bone meal on wheat, wax
copper and take the wax off, and a spawner spawns a zombie; both sides make
the particles.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 10:02:19 +02:00
Ludovic MarieandClaude Opus 5.5 c5fe5d4bcf Thrown potions and eyes of ender fly on a server's clients
A splash or lingering potion, an experience bottle or an eye of ender a
player threw was never an entity a server tracked: on single player's server
thread, for LAN guests and on a dedicated server the item was used up and
nothing flew.

The server now numbers them like its other projectiles and tracks them as
vanilla's entities: splash_potion, lingering_potion and experience_bottle
with ThrowableItemProjectile.DATA_ITEM_STACK (the potion, for its colour),
and eye_of_ender. A client draws them where the server puts them and drops
them when the server removes them.

lan_e2e's new `thrown` check: the host and the guest each throw a splash
potion and an eye of ender, and both sides see them fly.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 10:02:19 +02:00
Ludovic MarieandClaude Opus 5.5 0d8249e124 Leads: a server's clients draw the rope (ClientboundSetEntityLinkPacket)
A server never told its clients who holds a mob's lead, so on single
player's server thread, a LAN guest's or a dedicated server's client the
lead was used up and the mob tied, but no rope showed.

The tracker now sends ClientboundSetEntityLinkPacket as vanilla does: in
ServerEntity.sendPairingData for a leashed entity, and when the lead is tied
or comes off (Leashable.setLeashedTo / dropLeash, the holder 0). The holder
is the player the mob sees (a guest, else the host) or a mob; a fence knot
isn't an entity on Horizon's server yet. The client sets the mob's holder
from it: itself, another player or a mob.

lan_e2e's new `leads` check ties and unties a cow on both sides.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 10:02:19 +02:00
Ludovic MarieandClaude Opus 5.5 c35ea9568f Server's mobs: babies, wool, collars, fire, slime sizes and health reach its clients
A client of a server (single player on its server thread, a LAN guest, a
dedicated server's player) only applied a rabbit's baby flag and a creeper's
swell from SetEntityData. It now reads the synched data vanilla's entities
read in onSyncedDataUpdated: Entity's on-fire flag, LivingEntity's health
(the wither's boss bar follows it), the baby flag of every ageable mob,
Sheep.DATA_WOOL_ID, TamableAnimal's sitting and tame flags, the wolf's and
the cat's collar colour, and the cube mobs' size. A remote wolf or cat starts
with vanilla's red collar.

The server sends the wolf's and cat's collar colour. Pairing still carries
only the values that differ from the defaults; a mob's state now lists the
defaults of the entries that can go back to theirs (health, the baby flag,
the wool, the collar), and the tracker sends that default when one does:
a calf growing up or a sheep's white wool growing back reach the clients.

Sheep saves wrote the colour and shearing they were loaded with (0 unless
the save had them) and loads ignored both: they now save and load the wool
as it is (Sheep.add/readAdditionalSaveData).

lan_e2e's new `looks` check summons the mobs and changes them on both sides.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 10:02:19 +02:00
Ludovic MarieandClaude Opus 5.5 4aa1f8e14a World border on a server's clients: its state, its walls, its damage
The server told joining players of a default border and never of a
change, so on single player's server thread and for LAN guests the wall
neither showed nor stopped anyone. Players now get the real border on
entering a level and each change as BorderChangeListener sends it.

The player's movement collides with the border's walls
(CollisionGetter.borderCollision) instead of being clamped after moving,
which a sprint-jump got past. A guest (and single player's own player on
its server thread) outside the border takes its damage on the server, as
the host's player did.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 10:02:19 +02:00
Ludovic MarieandClaude Opus 5.5 f88fba8d05 A server's attribute updates reach its player; new boss bars start visible
The client of a server (single player on its server thread, LAN guests)
ignored ClientboundUpdateAttributesPacket for its own player, so a new
maximum health never reached it: its hearts stayed at ten and the health it
was sent was clamped to 20. It now applies each synced instance's base and
modifiers (handleUpdateAttributes) and re-applies the received health.

BossBar::new started hidden; ServerBossEvent starts visible.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 10:02:19 +02:00
Ludovic Marie e325fa6587 Merge pull request #1397 from ludovic111/fix/metal-gpu-timer
🤖 Agent: merged after verify and CI parity passed; the change only touches the GPU timer (gfx/timing.rs), proven on the M4 at RD 32.
2026-10-23 09:57:36 +02:00
Ludovic Marie 63baec3ac8 Merge branch 'fix/st2-entity-hands' into fix/st2-chat
# Conflicts:
#	tools/lan_e2e.py
2026-10-23 09:45:17 +02:00
Ludovic Marie 0b7fa14030 Merge remote-tracking branch 'origin/main' into fix/st2-entity-hands
# Conflicts:
#	tools/lan_e2e.py
2026-10-23 09:44:52 +02:00
Ludovic Marie 2423e9da00 Merge remote-tracking branch 'origin/main' into fix/st2-saved-state
# Conflicts:
#	crates/horizon-client/src/game/lan_actions.rs
2026-10-23 09:44:30 +02:00
Ludovic Marie cb81f645da Merge remote-tracking branch 'origin/main' into fix/st2-rules
# Conflicts:
#	crates/horizon-client/src/game.rs
2026-10-23 09:44:12 +02:00
Ludovic MarieandClaude Opus 5.5 7e5e469783 InLove only for vanilla's Animal subclasses (not allays)
save_roundtrip found an allay saved with InLove 0: Horizon's `animal`
flag is broader than vanilla's Animal class.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 09:19:48 +02:00
Ludovic Marie 4680a73f51 Merge pull request #1413 from ludovic111/feat/bench-1080p
🤖 Agent: merged after verify passed; #1190 has interleaved GPU A/B against main on the HD 620 and M4 (within noise) and 103 parity harnesses passing; #1413 was proven with a 1080p run on the desktop.
2026-10-23 09:06:57 +02:00
Ludovic Marie e0cb831dbf Merge pull request #1190 from ludovic111/fix/texture-mcmeta-blur-clamp
🤖 Agent: merged after verify passed; #1190 has interleaved GPU A/B against main on the HD 620 and M4 (within noise) and 103 parity harnesses passing; #1413 was proven with a 1080p run on the desktop.
2026-10-23 09:05:27 +02:00
Ludovic Marie 6b35c35ebf Merge pull request #1416 from ludovic111/fix/bed-respawn
🤖 Agent: merged after verify passed; lan_e2e (absorption breath lava contacts shield deathmining worldspawn drops underwater beds minecarts levels) passes in both single-player modes on the top of the chain, plus clippy and the client tests.
2026-10-23 09:00:44 +02:00
Ludovic Marie 48863fcf84 Merge pull request #1412 from ludovic111/fix/mob-knockback-view
🤖 Agent: merged after verify passed; lan_e2e (absorption breath lava contacts shield deathmining worldspawn drops underwater beds minecarts levels) passes in both single-player modes on the top of the chain, plus clippy and the client tests.
2026-10-23 08:59:16 +02:00
Ludovic Marie 982f7f12be Merge pull request #1411 from ludovic111/fix/items-out-of-blocks
🤖 Agent: merged after verify passed; lan_e2e (absorption breath lava contacts shield deathmining worldspawn drops underwater beds minecarts levels) passes in both single-player modes on the top of the chain, plus clippy and the client tests.
2026-10-23 08:58:50 +02:00
Ludovic Marie 12f2ed9ff7 Merge pull request #1410 from ludovic111/fix/setworldspawn
🤖 Agent: merged after verify passed; lan_e2e (absorption breath lava contacts shield deathmining worldspawn drops underwater beds minecarts levels) passes in both single-player modes on the top of the chain, plus clippy and the client tests.
2026-10-23 08:57:20 +02:00
Ludovic Marie 10db017c7c Merge pull request #1409 from ludovic111/fix/server-player-shield-mining
🤖 Agent: merged after verify passed; lan_e2e (absorption breath lava contacts shield deathmining worldspawn drops underwater beds minecarts levels) passes in both single-player modes on the top of the chain, plus clippy and the client tests.
2026-10-23 08:54:51 +02:00
Ludovic Marie e6acebc23d Merge pull request #1408 from ludovic111/fix/contact-damage
🤖 Agent: merged after verify passed; lan_e2e (absorption breath lava contacts shield deathmining worldspawn drops underwater beds minecarts levels) passes in both single-player modes on the top of the chain, plus clippy and the client tests.
2026-10-23 08:53:24 +02:00
Ludovic Marie 69df5671e7 Merge pull request #1407 from ludovic111/fix/server-air-absorption
🤖 Agent: merged after verify passed; lan_e2e (absorption breath lava contacts shield deathmining worldspawn drops underwater beds minecarts levels) passes in both single-player modes on the top of the chain, plus clippy and the client tests.
2026-10-23 08:48:53 +02:00
Ludovic MarieandClaude Opus 5.5 06b4c7faf5 Rules: crits, mending, phantoms, per-level health rules, /item with air, frozen potions
- Critical hits also need the attacker out of water, not blind and not
  riding (Player.canCriticalAttack) (#1393).
- Mending repairs the off hand too: candidates in EquipmentSlot order,
  main hand, off hand, then the armour (#1349).
- Phantoms spawn only in the Overworld with spawn_mobs, spawn_monsters and
  spawn_phantoms on, near every player (the local one and each guest in
  the level, with their own time since rest), with vanilla's draw order
  (#1388, #1387).
- A level entered by a portal or a dimension change keeps the world's
  natural_health_regeneration (#1339).
- /item replace ... with air empties the slot, and counts it as changed
  as vanilla's SlotAccess.set does (#1332).
- Thrown potions stay where they are on frozen ticks (/tick freeze) and
  land once the game runs again (#1364).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 08:48:41 +02:00
Ludovic Marie 5d2f1804e7 Merge branch 'fix/st2-entity-hands' into fix/st2-chat
# Conflicts:
#	tools/lan_e2e.py
2026-10-23 08:39:57 +02:00
Ludovic MarieandClaude Opus 5.5 c7abc8c718 Chat: styled server components, click and hover events, legacy codes, message order
- A connection's chat keeps the server's component: SystemChat goes
  through the same translation and styles as the action bar, and player
  and disguised chat are decorated from their chat type's translation,
  parameters and style (ChatType.Bound.decorate), so colours, bold,
  hex colours, and the client's language are kept (#1372, #1389).
- Components parse `click_event` (suggest_command, copy_to_clipboard; never
  open_file from a server) and `hover_event` show_text (#1377), and the
  chat screen draws the hovered part's tooltip (#1381).
- `§` codes in chat colour the text with Chat Colors on and are stripped
  with it off (ComponentRenderUtils.wrapComponents) (#1379).
- A command's operator broadcast skips the player whose command it was on
  the integrated server too (#1373), and /give fires InventoryChangeTrigger
  before its message, whose announcement is sent first (#1375).
- The control socket's `messages` gives the chat lines as components;
  lan_e2e `styles` checks a styled tellraw, the advancement order and the
  missing echo for the host and the guest.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 08:39:46 +02:00
Ludovic MarieandClaude Opus 5.5 337d7d7a7a Entity use with either hand: bows and food past a mob, off-hand items on mobs, emptied frames
- The remote client sends Interact for a targeted mob only for a hand
  vanilla's client-side Mob.interact would accept with it (leads, name
  tags, food, buckets, shears, saddled mounts, tamed pets, ...), so a bow
  drawn or food eaten while aiming at a cow goes on to useItem (#1350).
- Both hands: the main hand's item, then the off hand's entity, block and
  item (Minecraft.startUseItem). The server runs Interact with the packet's
  hand (GuestUse::Entity), and mobs, armour stands, leads and mounts use
  the held hand's stack, so an off-hand bucket milks a cow and off-hand
  wheat feeds it (#1362).
- The entity tracker sends an item frame's item and rotation when they go
  back to empty and 0, so a punched frame shows empty on clients (#1355).
- lan_e2e `hands`: the bow, the golden apple, the off-hand bucket and wheat
  and the emptied frame, for the host and the guest.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 08:39:45 +02:00
Ludovic MarieandClaude Opus 5.5 d522d8fe50 Saved state: burning, air and falling survive a reopen; InLove for every animal
- Guests' player data saves Fire, Air and fall_distance and puts them back
  on their body when they come back (single player's players are guests
  of the integrated server), so a burning player still burns after a
  reopen (#1323); the old single player's level.dat keeps fall_distance
  too (#1327).
- Every Animal saves and loads InLove, so a fed animal stays in love
  across a reopen and is not fed again (#1396).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 08:39:29 +02:00
Ludovic Marie 55e7084890 Merge branch 'fix/mob-knockback-view' into fix/bed-respawn 2026-10-23 08:36:16 +02:00
Ludovic Marie cffa98d509 Merge branch 'fix/items-out-of-blocks' into fix/mob-knockback-view 2026-10-23 08:36:16 +02:00
Ludovic Marie 9df27670f2 Merge branch 'fix/setworldspawn' into fix/items-out-of-blocks 2026-10-23 08:36:16 +02:00
Ludovic Marie 83c6d78013 Merge branch 'fix/server-player-shield-mining' into fix/setworldspawn 2026-10-23 08:36:16 +02:00
Ludovic Marie 83db7e3c41 Merge branch 'fix/contact-damage' into fix/server-player-shield-mining 2026-10-23 08:36:16 +02:00
Ludovic MarieandClaude Opus 5.5 39438efd76 lan_e2e contacts: sneak before landing on magma, read health before letting go
The sneaking case read the health after releasing Shift and waiting 10
ticks on the magma, so a hit from standing up counted (and one could land
between the first reading and the key press).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 08:36:16 +02:00
Ludovic MarieandClaude Opus 5.5 0e36cb4d7a Beds: respawning checks the bed and stands up beside it, else the world spawn
A bed sets the respawn point as vanilla's ServerPlayer.startSleepInBed does
(once in reach, day or night): a RespawnConfig at the bed's head with the
player's yaw, not forced; straw beds never set it (straw_bed_rule).
Respawning resolves it through findRespawnAndUseSpawnBlock, which now
handles beds: AbstractBedBlock.findStandUpPosition (beside the bed on the
side away from the yaw it was set from, around it, then on it; bunk beds
too) through DismountHelper, facing the bed. A bed that is gone or boxed
in sends the player to the world spawn with "You have no home bed or
charged respawn anchor, or it was obstructed", and the point is forgotten:
in the window's world, on the server thread and for LAN guests. Leaving
the End goes to the bed too (not using it up).

Saves keep a bed as the player's `respawn` (resolved through its block
when loaded), and a `spawnpoint` in the Overworld as forced. A fallback
that is the world spawn follows `setworldspawn` with a bed or anchor set.

tools/test_use_item_on_parity.py: respawn cases at a bed's head set from
some yaw (intact, bunk, only the foot left or gone, crowded), against
vanilla's real findRespawnAndUseSpawnBlock; 4000/4000 and 12000/12000 on
two seeds. lan_e2e `beds`: intact, gone and boxed in, host and guest.

Fixes #1308

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 08:11:22 +02:00
kerneldroid 4dff61b697 feat(shaders): complete wgpu Iris/Horizon API-2 shader pipeline, shadow & G-buffer passes, GLSL translator fixes, and settings/import UI 2026-10-23 11:07:03 +05:00
Ludovic Marie 13f98233ea Merge remote-tracking branch 'origin/main' into fix/texture-mcmeta-blur-clamp
# Conflicts:
#	crates/horizon-client/src/hud.rs
2026-10-23 07:59:50 +02:00
Ludovic MarieandClaude Opus 5.5 5c9f11b954 Benchmark: a 1080p set next to 720p
perf_bench ran every game in a 1280x720 window only. It now runs the
standard set at 1280x720 and 1920x1080 (--sizes picks others), each size
its own reports: <label>-rd<N>.json at 720p as before, <label>-1080p-rd<N>.json
at 1080p.

Horizon takes vanilla's --width and --height for its window's starting size
(1280x720 by default); perf_bench passes them to every game, and to Horizon
only off 720p, so builds before this one still run the 720p set.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 07:58:53 +02:00
Ludovic Marie c8f00dcf1e Merge branch 'fix/items-out-of-blocks' into fix/mob-knockback-view 2026-10-23 07:52:34 +02:00
Ludovic Marie 3d36883f52 Merge branch 'fix/setworldspawn' into fix/items-out-of-blocks
# Conflicts:
#	tools/lan_e2e.py
2026-10-23 07:52:33 +02:00
Ludovic Marie e84f86a813 Merge branch 'fix/server-player-shield-mining' into fix/setworldspawn
# Conflicts:
#	crates/horizon-client/src/lan/playerdata.rs
#	tools/lan_e2e.py
2026-10-23 07:51:25 +02:00
Ludovic Marie 48c1b04cb0 Merge branch 'fix/contact-damage' into fix/server-player-shield-mining
# Conflicts:
#	tools/lan_e2e.py
2026-10-23 07:50:58 +02:00
Ludovic Marie 10f6b183e3 Merge branch 'fix/server-air-absorption' into fix/contact-damage
# Conflicts:
#	tools/lan_e2e.py
2026-10-23 07:50:48 +02:00
Ludovic Marie f949f08488 Merge remote-tracking branch 'origin/main' into fix/server-air-absorption
# Conflicts:
#	tools/lan_e2e.py
2026-10-23 07:45:28 +02:00
Ludovic MarieandClaude Opus 5.5 144741b7c7 Screen overlays: one texture sample, as on main
The vignette, drawn over every frame, went from one texture sample to up to
four texel loads and per-pixel address arithmetic: +0.85 ms of GPU a frame on
an HD 620 (integer modulo, removed in the commit before), still +0.4 ms after
it.

- A blurred sprite gets a one-texel border in the atlas, copied from its
  opposite edges: the hardware's bilinear sample of the atlas with the
  entity group's linear sampler is then GL_LINEAR with repeat, edges
  included. Clamp-to-edge keeps the sample point half a texel inside the
  sprite, which is what the hardware's clamp does for both filters.
- Each sampling has its own pipeline, the shader specialised by override
  constants: a repeat sprite runs main's arithmetic and one sample, nearest
  or linear; only clamp adds a clamp. The sampling moves from the vertices
  to the quad, so the vertex layout is main's again.
- A black vignette changes no pixel (it darkens by texture x colour), and is
  no longer drawn: in daylight its brightness fades to 0 within half a
  minute, so most frames outdoors skip it, which main never could.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 07:35:26 +02:00
Ludovic Marie 5bb492ca54 Merge pull request #1318 from levzzz5154/fix/tnt-minecart-explosions
🤖 Agent: merged after verify passed and a proof run on the Mac mini: the self-check passed (parity 22/22), and six TNT minecarts now chain (6 → 0; 5 survive on main). Thanks @levzzz5154!
2026-10-23 06:56:31 +02:00
Ludovic Marie 15616aa115 Merge pull request #1317 from levzzz5154/fix/minecart-collisions
🤖 Agent: merged after verify passed and a proof run on the Mac mini: the self-check passed (parity 22/22), and lan_e2e minecarts passes in both single-player modes. Thanks @levzzz5154!
2026-10-23 06:56:07 +02:00
Ludovic Marie 910f59bf0e Merge pull request #1213 from ludovic111/fix/server-respawn-levels
🤖 Agent: merged after verify passed and the wave-3 integration on the desktop by integ-w3: main + 17 PRs, fmt, clippy 0, 1929 workspace tests, parity --changed (only known flakes failed, and those passed on rerun), lan_e2e in both modes, and the perf numbers back to back against main posted on each PR.
2026-10-23 06:27:48 +02:00
Ludovic Marie 247a33d889 Merge pull request #1212 from ludovic111/fix/server-end-gateways
🤖 Agent: merged after verify passed and the wave-3 integration on the desktop by integ-w3: main + 17 PRs, fmt, clippy 0, 1929 workspace tests, parity --changed (only known flakes failed, and those passed on rerun), lan_e2e in both modes, and the perf numbers back to back against main posted on each PR.
2026-10-23 06:26:20 +02:00
Ludovic Marie a1597c041f Merge pull request #1211 from ludovic111/fix/server-command-levels
🤖 Agent: merged after verify passed and the wave-3 integration on the desktop by integ-w3: main + 17 PRs, fmt, clippy 0, 1929 workspace tests, parity --changed (only known flakes failed, and those passed on rerun), lan_e2e in both modes, and the perf numbers back to back against main posted on each PR.
2026-10-23 06:24:51 +02:00
Ludovic Marie 1a4e783a04 Merge pull request #1210 from ludovic111/feat/lan-levels-check
🤖 Agent: merged after verify passed and the wave-3 integration on the desktop by integ-w3: main + 17 PRs, fmt, clippy 0, 1929 workspace tests, parity --changed (only known flakes failed, and those passed on rerun), lan_e2e in both modes, and the perf numbers back to back against main posted on each PR.
2026-10-23 06:23:22 +02:00
Ludovic Marie 91466d160c Merge pull request #1216 from ludovic111/feat/truetype-fonts
🤖 Agent: merged after verify passed and the wave-3 integration on the desktop by integ-w3: main + 17 PRs, fmt, clippy 0, 1929 workspace tests, parity --changed (only known flakes failed, and those passed on rerun), lan_e2e in both modes, and the perf numbers back to back against main posted on each PR.
2026-10-23 06:21:55 +02:00
Ludovic Marie ce04baf1ba Merge pull request #1215 from ludovic111/feat/win-screen
🤖 Agent: merged after verify passed and the wave-3 integration on the desktop by integ-w3: main + 17 PRs, fmt, clippy 0, 1929 workspace tests, parity --changed (only known flakes failed, and those passed on rerun), lan_e2e in both modes, and the perf numbers back to back against main posted on each PR.
2026-10-23 06:20:28 +02:00
Ludovic Marie b75370cf26 Merge remote-tracking branch 'origin/main' into m1215
# Conflicts:
#	crates/horizon-client/src/game/startup_menu.rs
#	crates/horizon-client/src/options_ui.rs
2026-10-23 06:11:37 +02:00
Ludovic MarieandClaude Opus 5.5 feeefb3a33 Mobs: a punch shows smooth and slow, as on vanilla's client
The server sends a knocked-back, pushed or jumping mob's move at once
(Entity.needsSync), and the remote client walks every living entity along
the server's positions with vanilla's SteppedInterpolationHandler: each
position takes the type's update interval to reach, sped up while steps
pile up. Before, the client waited for the next due update and then
lerped there in fewer ticks, so a hit pig jumped away fast.

tools/test_stepped_interpolation_parity.py drives vanilla's unmodified
InterpolationData and the Rust port with the same packets and ticks
(random cases and a punched pig's knockback); 135236 positions and
rotations match bit for bit.

Fixes #1374

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 05:38:46 +02:00
Ludovic MarieandClaude Opus 5.5 65bd2d0d45 Items push out of blocks; a server's client digs at the server's speed
ItemEntity.tick: an item stuck inside a full block (one set on it) has no
physics on the server and moves toward the closest free side
(Entity.moveTowardsClosestSpace: north, south, west, east, up; 0.1 to 0.3 a
tick), instead of staying in the block (#1252).

A server's client (LAN guests, and the window on the server thread) digs at
the same speed as the server: getDestroyProgress with the submerged mining
speed, haste, fatigue and efficiency, not the plain tool speed, so it no
longer finishes early under water and gets the block put back (#1254).

lan_e2e: `drops` (a diamond under a block set on it comes out, in both
views) and `underwater` (dirt by hand at eye height under water: about
halfway at 35 ticks on client and server, broken by 105).

Fixes #1252, #1254.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 05:38:25 +02:00
Ludovic MarieandClaude Opus 5.5 5c4600e403 /setworldspawn moves the world spawn, not the player's respawn point
SetWorldSpawnCommand sets the level's spawn and its angle; a player without
a bed or anchor respawns there (it used to set the player's own spawn
point, so other players and later deaths kept the old one). The angle is
saved in level.dat's spawn (yaw, pitch), and a guest whose saved respawn
point is the world spawn follows it when it moves.

lan_e2e `worldspawn`: /setworldspawn 24 blocks east, then the host and the
guest die and respawn there.

Fixes #1310.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 05:38:25 +02:00
Ludovic MarieandClaude Opus 5.5 2898b40670 Server players: an axe-disabled shield stays down 5 s, death forgets the digging
ItemCooldowns ticks once a tick for each player: a server's players count
their shield cooldown down in tick_guests, and the host's world in tick_world
(it was in tick_using, which ran for the host and again for every guest's
swap, so a guest's shield came back early on the server thread).

A guest that dies stops digging (its mining, the delay and the crack go), so
the block it was breaking doesn't break once it respawns.

lan_e2e: `shield` (a vindicator's axe disables each player's shield; the
server's cooldown then falls 40 in 40 ticks) and `deathmining` (die while
breaking obsidian, respawn, the obsidian stays). `lan status` and the agent
state report the shield cooldown.

Fixes #1315, #1316.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 05:38:24 +02:00
Ludovic MarieandClaude Opus 5.5 ed28e1dd0d Contact damage: magma, cactus, lit campfires and sweet berry bushes hurt in both modes
Entity.applyEffectsFromBlocks: MagmaBlock.stepOn burns a player standing on
it who is not sneaking (hot_floor, 1), CactusBlock and CampfireBlock.entityInside
hurt what touches them (cactus 1, campfire 1, soul campfire 2, lit only).
A server's players (LAN guests, and the host on the server thread) now get
these and the grown sweet berry bush prick along the moves their client sent
that tick: the server rebuilds the tick's movement history and runs the
damage half of the block effects.

lan_e2e `contacts`: magma, sneaking on magma, a lit campfire, a cactus, grown
and young berry bushes, for the host and the guest.

Fixes #1305, #1311, #1312, #1313.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 05:38:24 +02:00
Ludovic MarieandClaude Opus 5.5 7acebbae45 Server players: the HUD's bubbles and golden hearts follow the server, absorption is saved, Nether lava pushes harder
The server sends a player's own air supply and absorption amount
(Entity.DATA_AIR_SUPPLY_ID, Player.DATA_PLAYER_ABSORPTION_ID) when they
change, and the client applies them: on the server thread and as a LAN
guest the breath meter follows the server's Respiration-extended air
(#1304) and the golden hearts show (#1306). The connected client's tick
sets the helmet's Respiration and the Nether's FAST_LAVA as the window's
own tick does (#1309).

AbsorptionAmount is written from the live body and read back on load,
in the window's world and for a server's players (#1307).

lan_e2e: absorption, breath and lava checks.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 05:38:24 +02:00
Ludovic MarieandClaude Opus 5.5 0c133101b6 Screen overlays: wrap texels with a select, not integer modulo
The camera overlays (the vignette every frame) read up to four texels a
pixel, each wrapped with two integer modulos. Intel GPUs emulate integer
division. Reducing the coordinate to one period first (fract, as the shader
did before) leaves every read at most one texel outside the sprite, so a
select wraps it. Same texels and weights; clamp-to-edge is unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 05:21:47 +02:00
Ludovic Marie 9c6b35d4cb Merge remote-tracking branch 'origin/main' into fix/texture-mcmeta-blur-clamp 2026-10-23 05:21:47 +02:00
Ludovic Marie 357dceb999 Merge pull request #1197 from ludovic111/feat/locator-bar
🤖 Agent: merged after verify passed and the wave-3 integration on the desktop by integ-w3: main + 17 PRs, fmt, clippy 0, 1929 workspace tests, parity --changed (only known flakes failed, and those passed on rerun), lan_e2e in both modes, and the perf numbers back to back against main posted on each PR.
2026-10-23 05:21:26 +02:00
Ludovic Marie aec75bbd28 Merge pull request #1194 from ludovic111/feat/armour-trims-worn
🤖 Agent: merged after verify passed and the wave-3 integration on the desktop by integ-w3: main + 17 PRs, fmt, clippy 0, 1929 workspace tests, parity --changed (only known flakes failed, and those passed on rerun), lan_e2e in both modes, and the perf numbers back to back against main posted on each PR.
2026-10-23 05:19:59 +02:00
Ludovic Marie 8b6d49c4b4 Merge pull request #1357 from ludovic111/docs/agent-control
🤖 Agent: merged after verify passed and the agent-control-2 integration of all six PRs on the Mac mini. agent_control_e2e passes in CLI and MCP (one known survival flake), launcher_mcp_e2e passes, No AI is refused, and the options, screen_layout, menu_click and world_options parity harnesses pass. Ludovic asked for this on 2026-10-23.
2026-10-23 05:17:45 +02:00
Ludovic Marie 03c4d2c683 Merge pull request #1353 from ludovic111/feat/agent-connect-game
🤖 Agent: merged after verify passed and the agent-control-2 integration of all six PRs on the Mac mini. agent_control_e2e passes in CLI and MCP (one known survival flake), launcher_mcp_e2e passes, No AI is refused, and the options, screen_layout, menu_click and world_options parity harnesses pass. Ludovic asked for this on 2026-10-23.
2026-10-23 05:16:19 +02:00
Ludovic Marie 891b122783 Merge pull request #1256 from ludovic111/perf/random-tick-counts
🤖 Agent: merged after verify passed and the wave-3 integration on the desktop by integ-w3: main + 17 PRs, fmt, clippy 0, 1929 workspace tests, parity --changed (only known flakes failed, and those passed on rerun), lan_e2e in both modes, and the perf numbers back to back against main posted on each PR.
2026-10-23 05:15:28 +02:00
Ludovic Marie 06b754db4a Merge pull request #1253 from ludovic111/perf/spawn-checks
🤖 Agent: merged after verify passed and the wave-3 integration on the desktop by integ-w3: main + 17 PRs, fmt, clippy 0, 1929 workspace tests, parity --changed (only known flakes failed, and those passed on rerun), lan_e2e in both modes, and the perf numbers back to back against main posted on each PR.
2026-10-23 05:14:01 +02:00
Ludovic Marie 1c18ac6087 Merge pull request #1239 from ludovic111/perf/animated-texture-uploads
🤖 Agent: merged after verify passed and the wave-3 integration on the desktop by integ-w3: main + 17 PRs, fmt, clippy 0, 1929 workspace tests, parity --changed (only known flakes failed, and those passed on rerun), lan_e2e in both modes, and the perf numbers back to back against main posted on each PR.
2026-10-23 05:13:35 +02:00
Ludovic MarieandClaude Opus 5.5 d6f8dda823 Metal: the GPU frame timer measures the frame again (read about 0.01 ms)
On Apple GPUs timestamps go only at pass boundaries, and the frame was
bracketed by two one-thread compute passes. Two things made it read
about 0.01 ms:

- Metal resolves counter samples without waiting for the passes that
  write them. Resolved in the frame's own command buffer, the end
  timestamp was mostly the one an earlier frame left in the shared
  query. Each readback slot now has its own queries, and with markers a
  slot is resolved in a later frame, once on_submitted_work_done says the
  GPU finished the frame it timed.
- A compute pass depends on nothing, so the end marker ran alongside the
  frame's render passes. The end is now a render pass that loads the
  frame's target, shading one pixel with every write masked: it runs
  after every pass that drew into the target. The start stays a compute
  pass (cheaper than a load and store of the target).

M4 Pro, render distance 32, offscreen: GPU p50 0.002 ms before, now
1.95 ms against a 1.95 ms frame (village) and 4.46 ms against 4.51 ms
(mountain), where the frame is GPU-bound. Vulkan and DirectX 12, which
write timestamps between commands, are unchanged but for the per-slot
queries.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 05:13:25 +02:00
Ludovic Marie 0e81f178ec Merge pull request #1258 from ludovic111/perf/frustum-inside-columns
🤖 Agent: merged after verify passed and the wave-3 integration on the desktop by integ-w3: main + 17 PRs, fmt, clippy 0, 1929 workspace tests, parity --changed (only known flakes failed, and those passed on rerun), lan_e2e in both modes, and the perf numbers back to back against main posted on each PR.
2026-10-23 05:12:08 +02:00
Ludovic Marie d4c37adb0b Merge pull request #1199 from ludovic111/feat/far-detail-reduced
🤖 Agent: merged after verify passed and the wave-3 integration on the desktop by integ-w3: main + 17 PRs, fmt, clippy 0, 1929 workspace tests, parity --changed (only known flakes failed, and those passed on rerun), lan_e2e in both modes, and the perf numbers back to back against main posted on each PR.
2026-10-23 05:10:41 +02:00
Ludovic Marie 8120559415 Merge pull request #1198 from ludovic111/perf/frame-encode
🤖 Agent: merged after verify passed and the wave-3 integration on the desktop by integ-w3: main + 17 PRs, fmt, clippy 0, 1929 workspace tests, parity --changed (only known flakes failed, and those passed on rerun), lan_e2e in both modes, and the perf numbers back to back against main posted on each PR.
2026-10-23 05:10:15 +02:00
Ludovic Marie ba85348907 Merge pull request #1191 from ludovic111/perf/mod-content-build
🤖 Agent: merged after verify passed and the wave-3 integration on the desktop by integ-w3: main + 17 PRs, fmt, clippy 0, 1929 workspace tests, parity --changed (only known flakes failed, and those passed on rerun), lan_e2e in both modes, and the perf numbers back to back against main posted on each PR.
2026-10-23 05:08:48 +02:00
Ludovic Marie f73dc03fee Merge pull request #1275 from ludovic111/fix/perf-bench-readiness
🤖 Agent: merged after verify passed and the wave-3 integration on the desktop by integ-w3: main + 17 PRs, fmt, clippy 0, 1929 workspace tests, parity --changed (only known flakes failed, and those passed on rerun), lan_e2e in both modes, and the perf numbers back to back against main posted on each PR.
2026-10-23 05:08:23 +02:00
Ludovic Marie 0623d41954 Merge remote-tracking branch 'origin/main' into m1353
# Conflicts:
#	tools/agent_control_e2e.py
2026-10-23 05:04:53 +02:00
Ludovic Marie f642105e84 Merge remote-tracking branch 'origin/main' into feat/java-packs-photon-chocapic 2026-10-23 04:04:11 +02:00
Ludovic Marie 820dfe1819 Merge pull request #1345 from ludovic111/feat/agent-connect-launcher
🤖 Agent: merged after verify passed and the agent-control-2 integration of all six PRs on the Mac mini. agent_control_e2e passes in CLI and MCP (one known survival flake), launcher_mcp_e2e passes, No AI is refused, and the options, screen_layout, menu_click and world_options parity harnesses pass. Ludovic asked for this on 2026-10-23.
2026-10-23 03:41:19 +02:00
Ludovic Marie 15b2007f54 Merge pull request #1344 from ludovic111/feat/agent-control-launcher
🤖 Agent: merged after verify passed and the agent-control-2 integration of all six PRs on the Mac mini. agent_control_e2e passes in CLI and MCP (one known survival flake), launcher_mcp_e2e passes, No AI is refused, and the options, screen_layout, menu_click and world_options parity harnesses pass. Ludovic asked for this on 2026-10-23.
2026-10-23 03:39:51 +02:00
Ludovic Marie d42c246122 Merge pull request #1343 from ludovic111/feat/agent-control-game
🤖 Agent: merged after verify passed and the agent-control-2 integration of all six PRs on the Mac mini. agent_control_e2e passes in CLI and MCP (one known survival flake), launcher_mcp_e2e passes, No AI is refused, and the options, screen_layout, menu_click and world_options parity harnesses pass. Ludovic asked for this on 2026-10-23.
2026-10-23 03:38:25 +02:00
Ludovic Marie 9e1401cc92 Merge pull request #1342 from ludovic111/feat/no-ai-switch
🤖 Agent: merged after verify passed and the agent-control-2 integration of all six PRs on the Mac mini. agent_control_e2e passes in CLI and MCP (one known survival flake), launcher_mcp_e2e passes, No AI is refused, and the options, screen_layout, menu_click and world_options parity harnesses pass. Ludovic asked for this on 2026-10-23.
2026-10-23 03:37:59 +02:00
Ludovic Marie f6d82abb66 Merge pull request #1358 from ludovic111/feat/site-desktop-hardware
🤖 Agent: merged after verify passed; the desktop benchmark and the breakdowns for the /benchmarks page (site only).
2026-10-23 03:36:33 +02:00
Ludovic Marie 850354ca28 Merge pull request #1337 from ludovic111/feat/telemetry-hardware
🤖 Agent: merged after verify passed. Ludovic asked on 2026-10-23 for every hardware fact players send (opt-in). The end-to-end run against a local server is in the description: breakdowns need ≥3 installs, with no install ID or exact value. The consent version goes up to 2, so players who opted in are asked again, as the privacy page promises.
2026-10-23 03:35:06 +02:00
Ludovic MarieandClaude Opus 5.5 213cffcf3e Benchmark report: the screen read again at the end of the run
Under Wayland the title screen can come before the window is on a screen, so the
report's hardware.display stayed empty (seen end to end in a headless sway).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 03:13:09 +02:00
Ludovic MarieandClaude Opus 5.5 0a450d2963 Benchmarks: the desktop (RTX 4080), players' hardware breakdowns, memory in the estimate
- The desktop measured: Horizon v2026.10.22.13 against vanilla 26.3 at render
  distance 12 and 32, 3 runs each, 1280x720, VSync off, in a headless sway on
  the RTX 4080 (never on its screen). Replaces the "not yet measured" note.
- /benchmarks shows each players' group's hardware split (RAM, VRAM, drive,
  system, GPU kind) and, once the telemetry server has some, a "Players'
  hardware" table from /v1/aggregate's breakdowns. Hidden without data.
- The picker takes the memory too. Players' groups whose installs have neither
  the picked RAM nor the GPU's VRAM class count as further away, and the result
  says what they had and when a game's peak memory is a large share of the RAM.
- Each GPU in the spec table has its memory (0: shared).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 03:11:31 +02:00
Ludovic MarieandClaude Opus 5.5 7db6ce51a6 Docs: control Horizon with your AI agent (players), the full inventory (developers)
website/docs/agents.md, in the nav under "Going further", is the player's page.
It covers what an agent can do, connecting it from the launcher, copy-paste
config for Claude Code, Codex, Cursor and any MCP client, the game's AI Agent
page, a CLI quick reference, safety and No AI.

docs/agent-control.md is rewritten for developers:
- the surfaces, and why one MCP server covers the launcher and the game;
- the inventory, before and after, for the launcher and the game;
- No AI on every surface, and the launcher commands that stay;
- connecting from the launcher and the game;
- the design rules for a new command.

docs/launcher-commands.md lists every launcher command, its arguments and its
MCP tool. It is generated from the clap definition (cli_tools::markdown). The
launcher test the_launcher_reference_is_up_to_date fails when it is stale or a
tool is undocumented, like horizon-ctl's generated reference.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 03:10:58 +02:00
Ludovic MarieandClaude Opus 5.5 768f149190 Horizon Settings > AI Agent: turn agent control on, see who is connected, copy the details
Horizon Settings gains a row: "AI Agent..." and "No AI: Off". No AI hides the
AI Agent page and closes the control socket at once.

The AI Agent page has an Agent Control switch (horizon-extras.txt
agentControl, off by default): on, the game the player starts opens the
default control endpoint, so horizon-ctl and MCP clients reach it without
--control. Turning it off closes the endpoint at once; dropping a control
server now also stops its listener thread. The page shows whether an agent
is connected, when it last sent a command and the socket, and Copy
Connection Details copies the socket path, a horizon-ctl line to try and the
MCP config for this game.

agent_control_e2e.py --scenario setting covers it: Agent Control on opens the
endpoint without --control, the pages list their widgets, Copy works, and
turning the switch off removes the endpoint while the game keeps running.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 03:04:19 +02:00
Ludovic MarieandClaude Opus 5.5 75d818fad6 Telemetry: the screen also when Wayland names it late
Right after a window is made, Wayland doesn't say which screen it is on, so
current_monitor() is None and display was missing (seen end to end in a headless
sway). Fall back to the primary, else the only, monitor, and read it again when a
session ends; agent control's status (the benchmark report's display) uses the same.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 02:55:38 +02:00
Ludovic MarieandClaude Opus 5.5 3bfb345714 horizon-ctl options: the agent page (Horizon Settings > AI Agent)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 02:47:42 +02:00
Ludovic MarieandClaude Opus 5.5 51be242af0 Launcher Settings: Connect an AI agent, and the No AI switch
Settings gets an AI group with the No AI switch (horizon_core::no_ai, the
same switch the game reads) and, while it is off, a Connect an AI agent
section that replaces the old command-line note: a picker for Claude Code,
Codex, Cursor or any MCP client, the ready-made config with a Copy button,
the client's own add command (claude mcp add / codex mcp add) shown and run
only when the player clicks Add, and Test the connection, which starts the
MCP server as a client would and says how many tools answered. Client CLIs
are found on the PATH or in the folders installers use, since a launcher
started from the desktop gets a short PATH on macOS.

horizon-launcher agent-config --test runs the same test; the launcher
harness checks it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 02:35:09 +02:00
Ludovic MarieandClaude Opus 5.5 ef792ec3b9 Launcher MCP: every CLI command is a tool of the same name, plus settings and agent-config
The MCP tools are generated from the CLI's clap definition (cli_tools.rs):
mods_search is horizon-launcher mods search, with the same arguments, and
a call runs that command with --json. So the mod browser, modpacks, hosted
servers, settings, game arguments, data collection, desktop entry and the
benchmark are all reachable, and new CLI commands become tools by
themselves. Destructive ones need confirm: true and carry destructiveHint.
The session keeps play, server (with server_command/log/stop),
account_list and account_sign_in in-process, renamed after the CLI
(launch_client is play; play takes join for an invite code).

New CLI commands: settings [KEY VALUE] (selected version, folders,
install-new-versions, opening animation) and agent-config, the MCP config
for Claude Code, Codex, Cursor and any client. play takes --world and
--creative. Under No AI, mcp and agent-config are hidden and refused.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 02:31:38 +02:00
Ludovic MarieandClaude Opus 5.5 684ec5357e agent_control_e2e manage: close the options with close
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 02:27:59 +02:00
Ludovic MarieandClaude Opus 5.5 b62a866f39 agent_control_e2e manage: the player's position is under player
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 02:27:59 +02:00
Ludovic MarieandClaude Opus 5.5 a6e3fca1dd horizon-ctl: interact-entity beside hold, away from the logs arm
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 02:27:59 +02:00
Ludovic MarieandClaude Opus 5.5 bcbf860aaa horizon-ctl: every player action in the one command table, with a generated reference
The table gains what only the raw socket could do (options pages, connect
and invite codes, disconnect, LAN, Play with Friends, books, signs, mount
inventories, effects, xp, enchant, potion, fill, containers) and new
commands: worlds, world-rename, world-backup, world-delete --confirm (the
Select and Edit World screens, from the title screen only), servers (the
Multiplayer list) and interact-entity (right-click an entity). status,
mouse, pad, release, bench and stats become MCP tools; summon takes a
position. Destructive and network tools carry MCP annotations.

docs/horizon-ctl-commands.md is generated from the table
(horizon-ctl help --markdown) and a test fails when it drifts. The
agent_control_e2e manage scenario exercises the new commands.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 02:27:59 +02:00
Ludovic MarieandClaude Opus 5.5 b117930ae4 No AI: one switch that turns off agent control (socket, horizon-ctl, both MCP servers)
horizon_core::no_ai reads and writes the noAi line of the game's
horizon-extras.txt, so the launcher and the game share one switch. While
it is on the game never opens its control socket, horizon-ctl refuses every
command with exit 4 and kind denied, horizon-ctl mcp and horizon-launcher
mcp refuse to start (and refuse calls if it is turned on mid-session), and
horizon-launcher no-ai [on|off] shows or sets it. The game's extras keep
the key when they rewrite the file. Both e2e harnesses check it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 02:27:58 +02:00
Ludovic MarieandClaude Opus 5.5 a57efd4fa0 Launcher lock file: horizon-bench now depends on horizon-telemetry
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 02:27:54 +02:00
Ludovic MarieandClaude Opus 5.5 7eeb7ebbc2 Merge feat/win-screen (Options test fix) into feat/truetype-fonts
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 02:16:55 +02:00
Ludovic MarieandClaude Opus 5.5 3f58e512e0 Options test: Credits & Attribution follows Horizon's two buttons
main (#1271) pins Horizon Settings and Mods as the grid's last buttons;
this branch adds Credits & Attribution after them.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 02:16:54 +02:00
Ludovic MarieandClaude Opus 5.5 828398b5ec Telemetry: every hardware fact that explains performance, broken down by class
What players send (opt-in, unchanged switches) now describes the whole machine:
CPU cores and base/boost clock, the GPU's maker, integrated or discrete and its
VRAM, the drive under the game folder (NVMe/SSD/HDD), the screen (size, refresh,
scale), on battery or mains, laptop or desktop. Perf, crash and bug items carry
them in `system`; benchmark reports in `hardware`. No serials, host or user
names, or paths. CONSENT_VERSION goes to 2, so players are asked again.

The server keeps the new fields in `systems.extra` (the table is rebuilt once
on old databases, ids kept). /v1/aggregate and /v1/benchmarks/summary add
breakdowns by RAM class, VRAM class, drive, OS and integrated/discrete GPU;
a class is published only with at least 3 different installs, never a raw value.
website/scripts/benchmarks.mjs mirrors the classes on the shared fixtures.

The summary's "not Horizon or plain vanilla" check returned None (accepted)
instead of the rejection; it now matches the site's script.

The launcher's "View what would be sent" and `data show` list every field.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 01:59:39 +02:00
Ludovic MarieandClaude Opus 5.5 e6237aa1da Merge the updated stack base (with origin/main) into fix/server-respawn-levels
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 01:57:17 +02:00
Ludovic MarieandClaude Opus 5.5 f582eae9f4 Merge the updated stack base (with origin/main) into fix/server-end-gateways
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 01:57:17 +02:00
Ludovic MarieandClaude Opus 5.5 e4fb8f0f3f Merge the updated stack base (with origin/main) into fix/server-command-levels
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 01:57:17 +02:00
Ludovic MarieandClaude Opus 5.5 b6a018bf87 Merge origin/main into feat/lan-levels-check
tools/lan_e2e.py: main added the mounts check; levels stays last (it moves
the players between dimensions, and skips itself without --server-thread).
CHECKS and CHECK_FNS: 67 each.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 01:57:10 +02:00
Ludovic MarieandClaude Opus 5.5 bdbe59155d Locator bar: drop the live-backdrop check main removed
#1271 removed the title's live world backdrop (backdrop_live), so the
locator bar hides on the title alone.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 01:56:28 +02:00
Ludovic MarieandClaude Opus 5.5 0ff735b664 Merge the updated stack base (with origin/main) into fix/server-respawn-levels
Brings the base branch's merge of origin/main into this stacked branch.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 01:32:59 +02:00
Ludovic MarieandClaude Opus 5.5 2b088da069 Merge the updated stack base (with origin/main) into fix/server-end-gateways
Brings the base branch's merge of origin/main into this stacked branch.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 01:32:59 +02:00
Ludovic MarieandClaude Opus 5.5 5472d4c668 Merge the updated stack base (with origin/main) into fix/server-command-levels
Brings the base branch's merge of origin/main into this stacked branch.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 01:32:58 +02:00
Ludovic MarieandClaude Opus 5.5 5e55ebd2a7 Merge origin/main into feat/lan-levels-check
tools/lan_e2e.py: main added the minecarts, frames, lightning and riding
checks; levels stays last (it moves the players between dimensions, and
skips itself without --server-thread). CHECKS and CHECK_FNS: 66 each.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 01:32:58 +02:00
Ludovic MarieandClaude Opus 5.5 181c91d1b8 Merge feat/win-screen (now with main) into feat/truetype-fonts
Brings #1215's merge of origin/main into this stacked branch.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 01:32:30 +02:00
Ludovic MarieandClaude Opus 5.5 906d02fff3 Merge origin/main into feat/win-screen
- game.rs: main (#1271) dropped the Horizon-theme checks from menu_blur and
  the title scroll; kept without them, with this branch's WinScreen parts
  (the credits alone blur, the title doesn't scroll under the Win screen).
  Screen keeps main's Friends and this branch's Win.
- control.rs: horizon-ctl options pages keep credits, horizon and
  shader-packs.
- options_ui.rs: Page keeps Credits and main's Horizon. The Options grid is
  main's (Horizon Settings and Mods in vanilla's Telemetry and Credits slots;
  Horizon Extras now sits inside Horizon Settings) with Credits & Attribution
  after them, laid out by this branch's row count.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 01:32:30 +02:00
Ludovic MarieandClaude Opus 5.5 9e11394600 Merge feat/armour-trims-worn (now with main) into feat/locator-bar
No conflicts: brings #1194's merge of origin/main into this stacked branch.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 01:31:20 +02:00
Ludovic MarieandClaude Opus 5.5 901a5b8207 Merge origin/main into feat/armour-trims-worn
hud.rs: HudAtlas::slots keeps this branch's trim size parameter, with
main's doc line (the on-screen keyboard's text replaced the Horizon menu
text, #1271).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 01:31:19 +02:00
Ludovic MarieandClaude Opus 5.5 ed76af1c11 Merge origin/main into feat/far-detail-reduced
graphics.rs: main (#1271) dropped the level key from horizon-graphics.txt;
farDetail stays after lodDistance in main's format (21 keys, 21 values).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 01:31:05 +02:00
levzzz 18078e058d Explosions hurt and push minecarts, so TNT minecarts chain
ServerExplosion.hurtEntities skipped minecarts: a TNT minecart's blast
never reached the carts beside it. Carts now take the explosion's damage
through VehicleEntity.hurtServer (destroyed past 40, where
MinecartTNT.destroy primes a TNT cart with a 0-39 tick fuse, as
damageSourceIgnitesTnt counts explosions) and the knockback from their
eyes. The exploding cart is the explosion's source, so it is removed
before the blast and can't hit itself. Cart ticks now look carts up by id,
since a blast can remove or prime other carts mid-loop.
2026-10-23 03:47:04 +05:00
levzzz 18bf8cd2ef Minecarts push each other in their own ticks, as vanilla does
OldMinecartBehavior.tick ends with pushAndPickupEntities: after a cart
moves, every other cart in its box (0.2 wider) gets push(cart). Horizon
pushed all pairs once before any cart moved, so carts placed inside each
other stayed exactly overlapping (push skips distances under 1e-4) and
never split apart. Pushes now run per cart, after its move, with
pushOtherMinecart's Mth sin table facing and float thresholds.
2026-10-23 03:40:19 +05:00
Ludovic MarieandClaude Opus 5.5 725962b858 Merge feat/win-screen into feat/truetype-fonts
No conflicts: brings #1215's Screen::Win menu-blur arm.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 23:04:30 +02:00
Ludovic MarieandClaude Opus 5.5 9f54087a86 Win screen: cover main's menu-blur match (Screen::Win)
Main's Screen::blurs_background (in-world menu blur) has no arm for the
new Screen::Win. WinScreen.extractBackground fills the screen with the End
portal while the poem rolls and falls back to Screen.extractBackground
(blurred) for the credits alone, so Game::menu_blur blurs only for the
credits without the poem.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 23:04:29 +02:00
Ludovic MarieandClaude Opus 5.5 c8d8a35273 Merge feat/armour-trims-worn (#1194) into feat/locator-bar
Stacks #1197 on #1194 so it merges cleanly after it. game/packs.rs: both
PRs add one line to the resource reload; both are kept (waypoint styles,
then armour-stand trims).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 22:58:43 +02:00
Ludovic MarieandClaude Opus 5.5 65fbde0f15 Merge the updated stack base (with origin/main) into fix/server-respawn-levels
No conflicts: brings the base branch's merge of origin/main into this stacked branch.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 22:58:17 +02:00
Ludovic MarieandClaude Opus 5.5 54e9379bc1 Merge the updated stack base (with origin/main) into fix/server-end-gateways
No conflicts: brings the base branch's merge of origin/main into this stacked branch.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 22:58:16 +02:00
Ludovic MarieandClaude Opus 5.5 5a2d55c863 Merge the updated stack base (with origin/main) into fix/server-command-levels
No conflicts: brings the base branch's merge of origin/main into this stacked branch.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 22:58:15 +02:00
Ludovic MarieandClaude Opus 5.5 b6b6eb9a87 Merge origin/main into feat/lan-levels-check
tools/lan_e2e.py: main added the ladder and boats checks where this branch
added levels. Both are kept, with levels still last: it moves the players
between dimensions (and skips itself without --server-thread).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 22:58:00 +02:00
Ludovic MarieandClaude Opus 5.5 6bd80fe817 Merge feat/win-screen (now with main) into feat/truetype-fonts
No conflicts: brings #1215's merge of origin/main into this stacked branch.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 22:57:07 +02:00
Ludovic MarieandClaude Opus 5.5 3ce3fe0374 Merge origin/main into feat/win-screen
hud.rs: HudBatch keeps both new fields, end_portal (this branch) and
blur_at (main's menu blur), in the struct, its constructor and the test
batch. draw_range already draws EndPortal runs on either side of blur_at.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 22:56:56 +02:00
Ludovic MarieandClaude Opus 5.5 9e1c3ec860 perf_bench: the readiness gate only with a chunk target; a test for the quiet start
Without min_chunks (the old call) wait_loaded behaves as before, which tests/test_perf_bench.py's
old-builds case relies on.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 21:51:16 +02:00
Ludovic MarieandClaude Opus 5.5 7eded537d6 perf_bench: wait for Horizon to actually load before timing a scene
Right after a teleport, and at spawn, Horizon v2026.10.22.04 reports nothing queued and steady chunk counts
for over a second before it starts loading. wait_loaded() took that for "loaded" and stopped the clock with
0 to 3 sections meshed (load 0.1 s), so the flights ran over empty terrain and the frame rates meant nothing.

Now the counts must change once since the wait began (or stay at the full view for 5 s), and the resident
chunks must reach 80% of the view circle (pi x RD^2), before a steady second counts as loaded.

Measured with it on the Mac mini and the ThinkPad for the website's Benchmarks page (#1274). At RD 32 a 4-thread
laptop needs --timeout above the default 300 s.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 21:45:57 +02:00
Ludovic MarieandClaude Opus 5.5 91d89b4abf Java shader packs: modf into a vector component works in an initialiser
The comma form of modf (`v.y = trunc(x), x - trunc(x)`) printed without
brackets in a declaration's initialiser, which naga's GLSL front end then
read as a second declarator. A unary + around it has glsl-lang print the
brackets.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 20:56:08 +02:00
Ludovic MarieandClaude Opus 5.5 60fc787288 Java shader packs: Photon, Chocapic13' and MakeUp Ultra Fast load
The translator now legalises what these packs write and naga refuses:
lookups without a level outside fragment shaders become textureLod at
level 0; struct varyings travel as one varying per field; textureGather
is emulated with texelFetch; a bool given to a numeric constructor
becomes cond ? 1 : 0; `const in` parameters lose the const; array fields
of private structs get their natural stride (naga lays every struct out
as std140); modf into a vector component assigns it instead of passing a
pointer. shaders.properties keeps the empty lines the preprocessor
leaves, so Photon's `) \` before `#else` ends its value.

real_packs_load now also parses every translated program again as WGSL,
which is what wgpu is given.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 20:24:55 +02:00
Ludovic MarieandClaude Opus 5.5 8b16f76b3c Section walk: no per-section frustum test in columns entirely inside
A column whose box lies entirely inside the frustum holds no section the
frustum would reject (plane distances only grow from its nearest corner
to a section's farthest, and rounding keeps the order), so its sections
skip the test. At render distance 32 most columns are past the distance
where the vertical field of view takes in their whole height.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 19:29:54 +02:00
Ludovic MarieandClaude Opus 5.5 abb7a97136 Animated sprites on the GPU: frames uploaded once, one dispatch per mip level
Every animation frame's mip chain goes to video memory once, at the
sprite's own size, as vanilla keeps its animated sprites' frames on the
GPU. A tick that changes sprites writes one 20-byte job per changed
sprite, and a compute dispatch per mip level repeats the chain into the
array's larger layers and mixes interpolated frames
(animate_sprite_interpolate), instead of blending on the CPU at the
array's size and copying each sprite level by level.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 17:56:43 +02:00
Ludovic MarieandClaude Opus 5.5 4ad6bb18ef Natural spawning: resolve the entry's type once, skip the mobsAt recheck where it was picked, measure only nearby players
spawn_category_for_position looked up the picked entry's type twice per
attempt and asked mobsAt again at the position it had just picked the entry
from. The type is now resolved when the entry is picked, and the recheck is
skipped at that position (the list there holds the entry). The nearest player
distance only feeds comparisons with 24 and the despawn distances (128 at
most), so an attempt near its start measures only the players that can be
within 128 blocks of it; farther ones read as infinitely far, which every check
answers the same. The water-ambient biome tag is read only for that category.
Same lists, same random draws.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 17:54:06 +02:00
Ludovic MarieandClaude Opus 5.5 2ced9a451d Server players respawn at their point in the Nether (anchors, spawnpoint)
A server's player (a LAN guest, and since the server thread the single
player too) always respawned in the Overworld. Its respawn anchor was
dropped when its action ended, and its save kept only an Overworld point.

- The guest keeps a respawn point outside the Overworld (respawn_elsewhere,
  saved as the player's `respawn`) and has it in place while it acts, so an
  anchor it uses is its own.
- guest_respawn respawns it in that point's level, waits there for the
  chunks, then resolves the point through its block
  (findRespawnAndUseSpawnBlock): beside the anchor, using a charge, or the
  world spawn and block.minecraft.spawn.not_valid when the block is gone.
- `spawnpoint` in another level than the Overworld sets a forced point there
  (SetSpawnCommand) instead of Overworld coordinates, and a forced point that
  is no anchor holds where a player fits (isPossibleToRespawnInThis), in the
  window's own game too.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 15:32:24 +02:00
Ludovic MarieandClaude Opus 5.5 86fdc72069 Server players go through End gateways
handle_server_portal ignored End gateways, so on a server (dedicated, or
single player's server thread) no player could use one: the in-frame game's
gateway code moved only its own player.

A gateway that is not cooling down now takes a server player at once
(EndGatewayBlock.entityInside, triggerCooldown, setAsInsidePortal), within
the End, keeping its motion and look (Relative.DELTA and ROTATION). An exact
exit is reached straight away; otherwise the player waits at the destination
while it loads, and the exit is found or built there (the in-frame
resolve_gateway, shared through gateway_job).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 15:32:24 +02:00
Ludovic MarieandClaude Opus 5.5 9fe228cd1e Server commands run in their source's level (execute in, execute at)
On a server (dedicated, or single player's server thread) each dimension is
its own level, but a command ran in whichever level was in place. So a guest
in the Nether running `execute in minecraft:overworld run setblock ...` got
"That position is not loaded", and `execute at <a player in another level>`
used the wrong level's blocks.

with_source now puts the source's level in place for the command
(CommandSourceStack.getLevel), as as_target already does for a player in
another level, and puts the old one back after. `execute at` takes the
entity's level (withLevel(entity.level())), and a player's level is the
guest's own (target_level) even while another level is in place.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 15:32:24 +02:00
Ludovic MarieandClaude Opus 5.5 84171cfc94 lan_e2e portals: wait for the guest to be placed in its Nether portal
On a server the guest is held where it entered the Nether until the
chunks around the destination are lit, then placed in its portal. The
check read the guest's position 40 ticks after the dimension changed,
which on a busy machine was still the held spot, and then stepped the
guest out of a portal that wasn't there.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 15:32:24 +02:00
Ludovic MarieandClaude Opus 5.5 bd36508c29 lan_e2e levels: players in their own levels on the server thread; design note
A new `levels` check (--server-thread) runs the host and a guest in different
dimensions: execute in ... tp, mobs ticking in two levels at once with unique
ids, rejoin and save/reload with the guest in the Nether, death there, the
host through a Nether portal alone, the guest through an End portal and the
exit portal alone. All of it already works on the server thread.

docs/server-levels.md says how the server's levels map onto vanilla's, what
the dedicated server and the server thread share, the cost of an extra level
and what's left. docs/server.md no longer says players can't change dimension.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 15:32:24 +02:00
Ludovic MarieandClaude Opus 5.5 a069c2716f Random ticks: ask for the next ticking section, and read a section's blocks faster
tick_chunk_random asks for the lowest randomly ticking section from a height
up, instead of asking about every section: a chunk costs one question per
section that ticks, not 24. PalettedContainer::has tests whole words for the
few matching palette indices (a field-wise zero test) instead of reading
every entry, so a section read again after an edit costs a few hundred word
operations.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 15:24:07 +02:00
Ludovic MarieandClaude Opus 5.5 7e410d93b3 Fonts: TrueType providers and the font picked by text components
Every font definition of the resources (and packs) is now loaded, not
just font/default.json, and a text component's "font" field picks one:
"minecraft:alt" draws the enchanting table's Galactic alphabet,
"minecraft:illageralt" the illager runes, a pack's own font its glyphs,
and a font that doesn't exist draws every character as the missing
glyph (FontManager.getFontSetRaw).

- font.rs: one FontSet per font id with its providers in order (space,
  bitmap, unihex, ttf) and their filters ("uniform", "jp"). Bitmap
  sheets outside the atlas (another namespace or folder) are copied into
  the glyph region when first drawn.
- TrueTypeGlyphProvider: "ttf" providers (file under font/, size,
  oversample, shift, skip; CFF fonts refused) rasterized with ab_glyph
  into the glyph region as rrrr coverage (text_intensity), metrics as
  GlyphBitmap (bearings over the oversampling, whole-pixel advances).
- text::Style gains the font id; it is parsed from JSON and SNBT
  components, pack descriptions and scoreboard number formats, inherited
  by children and written back.
- Obfuscated text picks its random glyph in the style's font.

Fixes #336

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 14:27:47 +02:00
Ludovic MarieandClaude Opus 5.5 4e45a3bddf Credits and end poem (WinScreen)
Leaving the End through the exit portal for the first time now rolls the
end poem, the credits and the post-credits text (WinScreen), as does a
server's WIN_GAME event; Options -> Credits & Attribution -> Credits rolls
the credits alone. The texts come from the resource root
(texts/end.txt, credits.json, postcredits.txt), so packs can replace them.

- win_screen.rs: vanilla's line building (PLAYERNAME, the obfuscated
  names from LegacyRandom(8124371), wrapping at 256), scroll speeds and
  keys (Space, Ctrl, Up, Escape), the last line pinned mid-screen, the
  logo, the vignette and the backgrounds.
- HUD: Blend::Vignette (RenderPipelines.VIGNETTE) and Blend::EndPortal
  (the End portal effect as a GUI fill, the poem's background).
- Obfuscated text draws a random glyph of the same width each frame.
- seenCredits is read and saved; the player stays out of the world while
  the poem rolls and arrives in the Overworld after it.
- horizon-ctl `credits [poem] [SCROLL]` and `options credits`.

Fixes #338

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 14:25:47 +02:00
Ludovic MarieandClaude Opus 5.5 9d58d6f525 Random ticks: a section ticks while it holds a randomly ticking block, as in vanilla
Vanilla random-ticks a section while its count of randomly ticking blocks
and fluids is above zero (LevelChunkSection.isRandomlyTicking), and asks as
it reaches each section. Horizon ticked every section whose palette listed
such a state, even after the last one was gone, and decided before the tick's
scheduled ticks, which drew positions vanilla doesn't and shifted its
position sequence.

RandomTickSections now reads each section's blocks (PalettedContainer::has),
again only for the sections that changed, and tick_chunk_random asks for
each section when it reaches it; the game reads a chunk's sections again only
after a block changed. New harness random_tick_sections compares the
sections and positions with vanilla's real section counts.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 13:31:27 +02:00
Ludovic MarieandClaude Opus 5.5 38276fbc30 Locator bar HUD and waypoint styles
LocatorBar: when the server tracks waypoints for the player, the bar
above the hotbar shows a dot per waypoint within 60 degrees of the
view (farthest first), coloured from the waypoint's colour or its id's
hash, with the sprite its style picks by distance
(assets/*/waypoint_style/*.json, WaypointStyleManager) and the blinking
up or down arrow when the waypoint is above or below the screen. The
background is nine-sliced. Hud.nextContextualInfoState picks between
the jump bar, the experience bar (100 ticks after an experience change)
and the locator bar.

Waypoints come from ClientboundTrackedWaypointPacket on remote servers
and, on a served world, from the host's own connection in
lan/waypoints.rs; entity waypoints in view follow the entity's eyes.

Fixes #339

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 12:42:40 +02:00
Ludovic MarieandClaude Opus 5.5 c77863f163 Armour trims on worn armour (player, LAN guests, armour stands)
EquipmentLayerRenderer's trim layer: the pattern's
textures/trims/entity/<layer>/<asset>.png recoloured from its base
palette (palette metadata section) to the material's palette, or to the
equipment asset's trim_overrides palette (iron armour with an iron trim
uses trim/iron_darker), as PalettedTextureManager does. Each texture is
made once, on first use, into one of 32 atlas trim slots (least recently
drawn reused), and drawn over the armour layer on the player, on the
guests a LAN host sees and on armour stands.

Fixes #337

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 12:13:24 +02:00
Ludovic MarieandClaude Opus 5.5 21baf0dc99 Screen overlays: blur and clamp from the texture's .png.mcmeta
TextureMetadataSection's blur and clamp choose how vanilla samples a
standalone texture (ReloadableTexture.setSampler). The atlas now keeps
each texture's flags and the screen-effect quads (vignette, pumpkin blur,
powder snow outline, underwater, equippable camera overlays) sample
their sprite rectangle linearly and with clamp or repeat as those flags
say, instead of always nearest and repeat.

Fixes #335

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 11:53:03 +02:00
Ludovic MarieandClaude Opus 5.5 8a03758c3a Load time: build a pack's content root and load its sprites and models in parallel
A content pack's first start rewrote 19 per-state tables one after another
and reopened each zip mod's archive for every file it copied (its whole
index each time). Every start then decoded the atlases' mod textures and
parsed the mods' block models one file at a time on the main thread: with
ATM10's 42 data mods that was about 70 % of the sampled startup.

- mod_content: the tables are rewritten on up to three threads, largest
  first; a zip mod's archive is opened once for all its files
  (horizon-mods' Content::reader).
- hud::atlas_sprites and ParsedModels::load list their files first, then
  decode and parse them on every core (par::par_map, results in the input
  order). The sprites come out in the order the one-by-one loop gave, and
  the content root is byte-identical.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 11:47:03 +02:00
Ludovic MarieandClaude Opus 5.5 d0330061bd Far Detail: an opt-in Reduced level for far terrain, suggested on integrated GPUs
Reduced starts each far tile level at half the distance Full does (about a
quarter of the far quads beyond the full-detail edge, whose finest tiles stay).
Full stays the default everywhere and no preset changes it. On integrated
GPUs other than Apple's, the setting's description suggests Reduced.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 10:45:33 +02:00
Ludovic MarieandClaude Opus 5.5 14bd239141 Solid section draws grouped by arena page within 64-block bands
A list sorted only by distance switched vertex buffer every few sections:
about 1200 indirect multi-draws a frame at render distance 32, most of a
few entries, which Mesa's Intel driver unrolls on the CPU. Grouping each
band's sections by page keeps the list front to back within the band, with
one call per page and band.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 10:37:40 +02:00
869 changed files with 30299 additions and 10807 deletions

No files matched your search

+5
View File
@@ -0,0 +1,5 @@
# Generated files that tests compare byte for byte with what they generate: LF on every OS, also
# where Git for Windows' core.autocrlf would check them out with CRLF.
docs/launcher-commands.md text eol=lf
docs/horizon-ctl-commands.md text eol=lf
website/src/tokens.css text eol=lf
-1
View File
@@ -6,7 +6,6 @@
/.github/ @ludovic111
/tools/ci/ @ludovic111
/tools/release_train.py @ludovic111
/tools/release_channels.py @ludovic111
/crates/horizon-release-sign/ @ludovic111
/crates/horizon-server/src/update.rs @ludovic111
/crates/horizon-server/Cargo.toml @ludovic111
+7 -2
View File
@@ -61,12 +61,17 @@ jobs:
if: steps.rust.outputs.required != 'false'
run: |
cargo +stable fmt --all -- --check
cargo +stable clippy --workspace --all-targets -- -D warnings
# With the game's code mods (on in every release build, off by default).
cargo +stable clippy --workspace --all-targets --features horizon-client/mods -- -D warnings
# The server build (released as horizon-engine): the game without sound or controllers.
cargo +stable clippy -p horizon-client --all-targets --no-default-features -- -D warnings
# Libraries, binaries and integration tests, then doc tests: not the ~275 examples, which clippy above already
# type-checks and the parity job builds and runs (they were most of this step's build and link time).
- name: Test Rust core and voxel kernel correctness
if: steps.rust.outputs.required != 'false'
run: cargo +stable test --workspace
run: |
cargo +stable test --workspace --lib --bins --tests --features horizon-client/mods,horizon-mods/pack
cargo +stable test --workspace --doc --features horizon-client/mods,horizon-mods/pack
parity:
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
uses: ./.github/workflows/parity.yml
-55
View File
@@ -1,55 +0,0 @@
name: promote
# Beta game releases become stable once a day (docs/release-flow.md, "Channels"): at 20:00 UTC (21:00 in Paris in
# winter time, 22:00 in summer) tools/release_channels.py promotes the newest beta that had been out 6 hours by then,
# passed its release smoke test and isn't covered by an issue labelled release-blocker. Launchers on the Alpha
# channel install every beta when it is published; Stable gets the day's promotion. The later runs of the evening are
# retries for a run GitHub starts late or skips: they promote the same beta, so stable still moves once.
# By hand, from GitHub's app too (Actions > promote > Run workflow):
# gh workflow run promote -f tag=v0.2.3 # that beta now, without waiting for 20:00
# gh workflow run promote -f tag=v0.2.3 -f force=true # even blocked or without its smoke test
# gh workflow run promote -f dry_run=true # say what would be promoted
# Holding betas back: label an issue release-blocker. While it is open it holds every beta; closed as completed it
# still holds the betas published before it closed. The issue's text is never read (its author could edit it).
# When the oldest waiting beta is 30 hours old (it missed a promotion it was due for), an issue "Promotion to stable
# has stalled" says so; it closes itself after the next promotion.
on:
schedule:
- cron: '7,37 20-21 * * *'
workflow_dispatch:
inputs:
tag:
description: The beta to promote now (empty = the newest beta that is due)
type: string
default: ''
force:
description: Promote that tag even if a release-blocker covers it or its smoke test didn't run
type: boolean
default: false
dry_run:
description: Say what would be promoted, change nothing
type: boolean
default: false
permissions:
contents: write
issues: write
concurrency:
group: promote
cancel-in-progress: false
jobs:
promote:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
with:
# The notes of a promoted release cover every pull request since the last stable one.
fetch-depth: 0
# gh uses GH_TOKEN below; nothing needs git credentials on disk.
persist-credentials: false
- name: Promote
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ inputs.tag }}
FORCE: ${{ inputs.force && 'yes' || '' }}
DRY_RUN: ${{ (inputs.dry_run || github.ref != 'refs/heads/main') && 'yes' || '' }}
run: python3 tools/release_channels.py promote ${TAG:+--tag "$TAG"} ${FORCE:+--force} ${DRY_RUN:+--dry-run}
+30 -13
View File
@@ -3,12 +3,15 @@ name: release
# release waiting for merges to settle goes out without another merge) and by hand, tools/release_train.py decides:
# a release when pull requests merged since the last one changed what players run and the newest merged 5 minutes
# ago (the oldest 20 minutes ago, if merges never pause), or at once for a pull request labelled release-now.
# RELEASES_PAUSED=1 (repository variable) stops them.
# 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 a beta (a pre-release) that promote.yml makes
# stable in its daily promotion. macOS: with the repository variable MACOS_SIGN_IDENTITY set, the programs are signed with
# 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
@@ -32,6 +35,8 @@ on:
default: false
permissions:
contents: read
# The open release-blocker issues (decide).
issues: read
concurrency:
group: game-release
cancel-in-progress: false
@@ -128,7 +133,8 @@ jobs:
if: runner.os == 'Linux'
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends libasound2-dev libudev-dev
- name: Build the client, horizon-ctl, horizon-prepare and horizon-server
run: cargo build --release --locked -p horizon-client --bin horizon-client --bin horizon-ctl -p horizon-prepare --bin horizon-prepare -p horizon-server --bin horizon-server
# `horizon-client/mods`: every released game runs code mods (the feature is off in dev builds).
run: cargo build --release --locked --features horizon-client/mods -p horizon-client --bin horizon-client --bin horizon-ctl -p horizon-prepare --bin horizon-prepare -p horizon-server --bin horizon-server
- name: Every program the release folder takes was built
run: |
for binary in horizon-client horizon-ctl horizon-prepare horizon-server; do
@@ -203,7 +209,7 @@ jobs:
or not report.get("meshed_sections") or not report.get("player"):
sys.exit("the smoke run did not draw a world")
EOF
# Published with the release: a beta becomes stable only with it (tools/release_channels.py).
# Published with the release; the publish job checks it again before it publishes anything.
cp "$home/smoke.json" "$RUNNER_TEMP/smoke-test.json"
- uses: actions/upload-artifact@v4
if: runner.environment == 'self-hosted'
@@ -250,8 +256,9 @@ jobs:
run: |
set -euo pipefail
# After the client is packaged: target/release/horizon-client is rebuilt without the audio and
# controllers features, so it needs neither libasound nor libudev (minimal server images lack them).
cargo build --release --locked -p horizon-client --no-default-features --bin horizon-client
# controllers features, so it needs neither libasound nor libudev (minimal server images lack them),
# and with code mods like the client.
cargo build --release --locked -p horizon-client --no-default-features --features mods --bin horizon-client
cargo run --locked -p xtask -- package-server target/release/horizon-client "dist/${{ matrix.server_dir }}"
folder="dist/${{ matrix.server_dir }}"
python3 tools/check_imports.py --server "$folder/horizon-engine" "$folder/horizon-server" "$folder/horizon-prepare"
@@ -319,6 +326,7 @@ jobs:
timeout-minutes: 15
permissions:
contents: write
issues: read
steps:
- uses: actions/checkout@v4
with:
@@ -341,6 +349,17 @@ jobs:
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:
@@ -348,17 +367,15 @@ jobs:
TAG: ${{ needs.decide.outputs.tag }}
run: |
signature=(); [[ -f dist/SHA256SUMS.sig ]] && signature=(dist/SHA256SUMS.sig)
smoke=(); [[ -f dist/smoke-test.json ]] && smoke=(dist/smoke-test.json)
# A beta (pre-release), the Alpha channel's: promote.yml makes it stable and latest in its daily promotion
# (docs/release-flow.md).
# The release and the latest at once: there is one channel (docs/release-flow.md).
gh release create "$TAG" --target "${{ needs.decide.outputs.head }}" --title "$(cat notes/title.txt)" \
--notes-file notes/body.md --prerelease --latest=false dist/horizon-linux-x86_64.tar.gz \
--notes-file notes/body.md --latest 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 \
dist/SHA256SUMS "${signature[@]}" "${smoke[@]}"
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 }}: $(cat notes/title.txt)"
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:
+175
View File
@@ -0,0 +1,175 @@
# Horizon: a map for agents
Minecraft: Java Edition 26.3 rewritten in Rust (wgpu). Vanilla is the specification wherever it has the
feature. Read [CONTRIBUTING.md](CONTRIBUTING.md) once; this page tells you where things are.
Feature → files → harness for the most-changed areas: [docs/architecture.md](docs/architecture.md).
## Rules that bite
- **Never commit Mojang's files**: game jars, assets, decompiled source, prepared resources (`.local/`).
`tools/check_public_tree.py` rejects them; `git config core.hooksPath .githooks` runs it on commit.
- **Keep your runs away from real player data.** Any game or launcher you start gets fresh `mktemp -d`
folders in `HORIZON_HOME` (the game's data), `HORIZON_LAUNCHER_HOME`, `XDG_DATA_HOME` and
`XDG_CONFIG_HOME`, plus `HORIZON_LAUNCHER_NO_SELF_UPDATE=1` and `HORIZON_NO_AUDIO=1`. Never write to
`~/.local/share/{applications,horizon,horizon-launcher}` or `~/.local/bin`.
- **Mark what an agent posts.** A PR body starts with
`> 🤖 Opened by Ludovic's AI agent (Claude), not by Ludovic himself.` (issues: "Filed by", reviews:
"Review by"); every comment starts with `🤖 Agent:`. Never write as Ludovic.
- **Attribution.** Commits end with `Co-Authored-By: Claude <model> <[email protected]>`; PR bodies end
with `🤖 Generated with [Claude Code](https://claude.com/claude-code)`.
- Everything players run is Rust. Python/Java only for dev tools and harness drivers in `tools/`.
- Horizon-only features follow [docs/design-system.md](docs/design-system.md) and are off by default where
they would change the vanilla look.
- Small PRs (< ~500 lines). Labels: `feature` (players get something new), `skip-release` (they see no
change), none for a fix. Agents never add `release-now`: the maintainer decides it. Every merge to main ships
([docs/release-flow.md](docs/release-flow.md)).
- Contributors first: `gh pr list --state open --search <area>` before you start; never duplicate theirs.
## Crates
Game workspace (root `Cargo.toml`); `launcher/` and `crates/horizon-telemetry-server` are separate workspaces.
| Crate | Owns |
|---|---|
| `horizon-client` | The game: a lib (`src/lib.rs`, ~190 flat modules) and the bin (`src/main.rs` → `game.rs` + `game/`), plus `horizon-ctl` (`src/bin/horizon-ctl/`). Also the dedicated server (`--dedicated`) |
| `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` |
| `horizon-assets` | Resource decoding: block-state and item models, baking, atlases |
| `horizon-prepare` | Builds `.local/horizon-client/26.3` from the player's own Minecraft install (runs the Java exporters) |
| `horizon-server` | `horizon-server`: `server.toml`, CLI, supervisor of the headless game ([docs/hosting.md](docs/hosting.md)) |
| `horizon-server-files` | `server.properties`, eula, ops/whitelist/bans, usercache |
| `horizon-friends`, `horizon-relay` | Play with Friends: invite codes, encrypted tunnel, the relay ([docs/play-with-friends.md](docs/play-with-friends.md)) |
| `horizon-shaderpack` | OptiFine/Iris packs: preprocessor, GLSL → WGSL ([docs/shader-packs.md](docs/shader-packs.md)) |
| `horizon-mods`, `horizon-mod-sdk`, `cargo-horizon-mod` | Sandboxed WebAssembly mods: runtime, SDK + WIT, `cargo horizon-mod` ([docs/mods.md](docs/mods.md)) |
| `horizon-port-pack` | Porting Java modpacks ([docs/modpacks.md](docs/modpacks.md)) |
| `horizon-theme` | Design tokens and fonts for the launcher, the game's own menus and the website |
| `horizon-telemetry`, `horizon-telemetry-server`, `horizon-bench` | Opt-in data collection, its ingest server, the benchmark report format |
| `horizon-dualsense`, `horizon-threads`, `horizon-release-sign`, `horizon-bots`, `voxel-kernels`, `xtask` | DualSense output, thread priorities, signed release checksums, load-test bots, occupancy kernels, repo tasks |
| `launcher/` | GPUI launcher: `main.rs` (UI), `backend.rs` (installs, updates), `cli.rs`, `mcp.rs`, `*_page.rs` |
Other top-level folders: `tools/` (harnesses and dev scripts), `tests/` (Python unit tests: `python3 -m
unittest discover -s tests`), `mods/horizon/src/test/java/dev/horizonlab/parity/` (Java oracles),
`website/` (playhorizon.dev; its paths are relative to `website/`), `docs/`, `.github/workflows/`.
## Build and test fast
```sh
python3 tools/pr_ready.py # the self-check: fmt, clippy -D warnings, touched crates' tests, harnesses; paste its block in the PR
python3 tools/pr_ready.py --quick # same without harnesses
cargo test -p horizon-client --lib --bins --tests # skips the 104 examples (pr_ready does this); `--doc` separately
cargo build --profile fast -p horizon-client # optimized, quick to link: use it to play or run lan_e2e
python3 tools/parity.py --changed origin/main # the harnesses your branch can affect (CI's parity job)
python3 tools/parity.py --changed-files paths.txt --list # which harnesses cover these files, without running them
python3 tools/parity.py mob_ai sculk # named harnesses
python3 tools/lan_e2e.py --check chest # two real games over LAN (host in-frame); --server-thread for the host on its server thread
python3 tools/agent_control_e2e.py # horizon-ctl / MCP end to end
```
- Harnesses and the game need the vanilla data in `.local/` (`minecraft/`, `horizon-client/26.3`, `datagen/`,
`tools/`): see [docs/parity-workflow.md](docs/parity-workflow.md). Without it, pr_ready lists the
harnesses for CI.
- A harness = `tools/test_<name>_parity.py` (324 of them) + a Java oracle (`mods/horizon/src/test/java/dev/horizonlab/parity/Vanilla*Oracle.java`)
+ a Rust probe: `crates/<crate>/examples/<name>_{probe,fixture}.rs` (horizon-world 175, horizon-client 104),
or the game binary with a fixture env var (`game/dispense_fixture.rs`, `game/sculk_fixture.rs`,
`game/command_harness.rs`, `game/net_harness.rs`). Copy a nearby harness for a new one.
- Probes build into `.local/parity-target/`; harnesses find binaries via `lab.cargo_target()`, never `target/`.
- What a change reruns (of 329 harnesses, measured 2026-10-23): a file of the game binary (`game.rs`, `game/`)
~25, a file of the client lib ~103, of `horizon-world` ~103–109, of `horizon-core` ~119. Plan for it.
- Rust integration tests: `crates/horizon-client/tests/` (`lan_bot.rs`, `generation_saves.rs`, `streaming_meshes.rs`).
- **macOS:** `cargo build/test --workspace` may fail to link the two wasm guest cdylibs (undefined
`horizon:mods/events` symbols) until #1446 lands: add `--exclude coords-hud --exclude horizon-mods-test-guest`.
Their real check is `cargo test -p horizon-mods` (builds them for `wasm32-unknown-unknown`).
- Linux needs `libasound2-dev libudev-dev`; `--no-default-features` drops audio and controllers (the server build).
- Run and drive the game (starts the `horizon-client` beside `horizon-ctl`; other arguments go to the game):
`target/fast/horizon-ctl --socket $PWD/ctl.sock launch --world W --creative --offscreen --wait --resources $PWD/.local/horizon-client/26.3`,
then `horizon-ctl --socket … state|keys|turn|command|screenshot|quit`
([docs/horizon-ctl-commands.md](docs/horizon-ctl-commands.md)).
## Vanilla reference
- 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`
([docs/decompilation-status.md](docs/decompilation-status.md)).
- A harness runs the oracle against the real 26.3 jars and the Rust probe on the same inputs, then compares
their JSON. Oracle runs are cached by `tools/parity_shim.py`; `--fresh` reruns them.
- Mirror vanilla's `Math` calls with `horizon_core::jmath`, and vanilla's RNG with `horizon_core::random`:
random draws must happen in vanilla's order.
## horizon-client: where things live
`L:` = lib (`crates/horizon-client/src/`), `G:` = bin (`crates/horizon-client/src/game/`). The lib can't see
`Game`; bin code reaches the lib as `horizon_client::<module>`.
| Subsystem | Files |
|---|---|
| Game loop, tick, frame | `game.rs`: `fn run`, `impl ApplicationHandler for Game` (`window_event`, `about_to_wait`), `fn tick`, `fn tick_world`, `fn render`; L: `clock.rs`, `window.rs`, `spans.rs` |
| Rendering | L: `gfx.rs` (`Renderer::new`, `Renderer::render`), `gfx/` (cull, occlusion, translucency, upload, timing, lod, weather, stars, end_sky), `*.wgsl`, `particles.rs`, `block_entity_render.rs`, `item_render.rs`, `fog.rs`, `clouds.rs`, `camera.rs`, `first_person.rs`; G: `render_tick.rs`, `item_draw.rs` |
| Chunks, streaming, light | L: `streaming.rs` (worker pool: generate, light, mesh), `world.rs`, `chunk.rs`, `chunk_map.rs`, `light.rs`, `mesher.rs` + `mesher/`, `vanillagen.rs` (binds horizon-world's generator), `lod.rs` + `lod/`, `ticket_levels.rs`; G: `world_loading.rs`, `postprocess.rs`; game.rs `fn update_world` |
| World generation | `crates/horizon-world/src/worldgen/` (`generator*.rs`, `feature_graph.rs`, `structure/`, `carver/`) |
| Block behaviour | `crates/horizon-world/src/level/` (`mod.rs`, `tick.rs`, `hooks.rs`, `redstone.rs`, `place.rs`, `interact.rs`); game.rs `fn tick_blocks`, `fn process_level_events` |
| Mobs | L: `mobs.rs` (types), `mobs/` (`ai.rs`, `goals.rs`, `brain.rs`, `*_brain.rs`, `nav.rs`, `path.rs`, `species.rs`, `spawning.rs`, `render.rs`, `anim.rs`); G: `creatures.rs` (ticking, spawning, mob events), one file per mob family (`dragon.rs`, `wither.rs`, `villagers.rs`, `piglins.rs`, `golems.rs`...), `natural_spawning.rs` |
| Other entities | L: `entities.rs`, `projectiles.rs`, `arrows.rs`; G: `boats.rs`, `minecarts.rs`, `mounts.rs`, `armor_stands.rs`, `paintings.rs`, `frames.rs`, `fireworks.rs`, `tnt.rs`, `explosion.rs` |
| Player actions | game.rs (`fn use_hand`, `fn use_item`, `fn attack`, `fn tick_mining`, `fn break_block`); G: `hand.rs`, `combat.rs`, `consume.rs`, `potions.rs`, `shield.rs`, `crossbow.rs`, `trident.rs`, `fishing.rs`; L: `motion.rs` + `motion/` (movement), `collisions.rs`, `items.rs`, `item_data.rs` |
| Inventory and containers | L: `menu.rs` (`AbstractContainerMenu.clicked`), `container_screen.rs`, `containers.rs`, `creative.rs`, `crafting.rs`, `recipe_book*.rs`, `stations.rs`, `merchant.rs`; game.rs `fn menu_ops`, `fn click_inventory`; G: `stations.rs`, `trading.rs`, `recipe_book.rs`, `machines.rs` |
| Commands | parse: L: `command/` (`args.rs`, `selector.rs`, `tree_*.rs`, `vanilla.rs`); run: G: `commands.rs` (dispatch) + `commands/*.rs` (`admin`, `execute`, `items`, `players`, `world_cmds`...); L: `scoreboard_cmd.rs`, `data_cmd.rs`, `nbt_path.rs`; G: `functions.rs`, `command_blocks.rs` |
| UI screens | `Screen` enum in game.rs; L: `title.rs` + `title/`, `options_ui.rs`, `multiplayer.rs`, `pack_screen.rs`, `graphics_ui.rs`, `screen_draw.rs`, `ui.rs` (widgets), `hud.rs`, `hud_layout.rs`, `hud_overlays.rs`, `chat.rs`, `tab_list.rs`; G: `startup_menu.rs`, `screens.rs` (pause, death), `options.rs`, `lan_screen.rs`, `chat.rs`, `sign_edit.rs`, `books.rs` |
| Multiplayer client | L: `net.rs` (network thread); G: `remote.rs` (`fn handle_packet`, `fn tick_remote`), `remote_*.rs` (mob data, objects, projectiles, vehicles, stations) |
| LAN host / server side | L: `lan/` (`conn.rs` handshake + login threads, `hub.rs` + `hub/` play state, `chunks.rs`, `entity.rs`, `tracking.rs`, `guest.rs`, `playerdata.rs`); G: `lan.rs` (`LanHost`), `lan_actions.rs` (`fn as_guest`, `fn tick_guests`), `lan_*.rs` per feature, `lan_entities.rs` (tracked entities) |
| Integrated server | G: `integrated.rs` (the server thread), `levels.rs` (one level per dimension, `fn tick_server`), `server_travel.rs` |
| Dedicated server | G: `dedicated.rs`, `dedicated/status.rs`; crates `horizon-server-files`, `horizon-server` |
| Save and load | L: `save.rs` (level.dat, regions, playerdata), `entity_nbt.rs`, `block_entity_nbt.rs`, `options_file.rs`; G: `saving.rs`, `persist.rs` (entities); `crates/horizon-world/src/{region.rs,nbt.rs,datafix/}` |
| Input | game.rs `fn window_event`, `fn key_pressed`, `fn play_mouse`; L: `keybinds.rs`, `keymapping.rs`, `key_names.rs`, `mouse.rs`, `input.rs`, `interaction_input.rs`, `gamepad.rs`, `osk.rs`, `clipboard.rs`; G: `pad.rs`, `pad_menu.rs` |
| Audio | L: `audio.rs` (mixer), `sound_engine.rs`, `sound_events.rs`, `music.rs`, `ambience.rs`; G: `music.rs`, `subtitle_hud.rs` |
| Resource packs | L: `packs.rs` + `packs/`, `pack_screen.rs`, `atlas_sources.rs`, `sprites.rs`, `mipmap.rs`, `model_library.rs`, `font.rs`, `lang.rs`; crate `horizon-assets`; G: `packs.rs` |
| Shader packs | L: `shader_pack.rs` (Horizon WGSL packs), `java_pack.rs` + crate `horizon-shaderpack` (Iris/OptiFine), `gfx/pack_stages.rs`, `gfx/java_stages.rs`; G: `shader_packs.rs` |
| Agent control | L: `control.rs` (socket), `bin/horizon-ctl/`; G: `control.rs`, `agent.rs`, `events.rs`, `ui_control.rs` ([docs/agent-control.md](docs/agent-control.md)) |
| Mods | crate `horizon-mods`; G: `mods.rs`; L: `mod_content.rs`, `mod_list.rs` |
| Horizon extras, settings | L: `extras.rs`, `graphics.rs`, `settings.rs`, `minimap.rs`; G: `extras.rs`, `tweaks.rs`, `options.rs` |
## How `Game` is organised
- `struct Game` (game.rs, ~290 fields) holds everything. Its methods are spread over ~180 files: game.rs keeps
the core (tick, render, input, inventory, world open/close) and each `game/<feature>.rs` adds an
`impl Game<'_>` block for one feature, with its own state types. Fields are private to the bin.
- Find a feature's code: `grep -rn "fn tick_<thing>\|fn <verb>_<thing>" crates/horizon-client/src/game*`;
the file is usually `game/<thing>s.rs`. Who calls it: grep the name in game.rs's `fn tick` / `fn tick_world`.
- Fields that say which mode runs: `remote` (this window is a client of a server: multiplayer, LAN guest, or
single player on its server thread), `lan` (`LanHost`: this world is open to the network), `integrated`
(the window's own server thread), `serving` (this `Game` *is* that server thread), `options.dedicated`.
`fn tick` returns `tick_remote()` when `remote` is set; nothing else of the local simulation runs.
- A server's players run through the same gameplay code as the local player: `as_guest` swaps the guest's
state (position, inventory, mode, mining, item use) into `Game` for one call (`game/lan_actions.rs`), and
`levels.rs` swaps whole dimensions in the same way. Changes reach clients through the block log, level
events and the entity tracker (`lan/tracking.rs`, `game/lan_entities.rs`).
- Shared read-only data: `Resources` (`res`: blocks, items, recipes, mob types, biomes) and `PackResources`
(`pack`), both in game.rs.
## The two single-player paths
1. **In-frame (default since #1428):** the window's `Game` ticks the world itself, one level that follows
the player (`fn enter_dimension`). This is what Stable players run and what harnesses test.
2. **Server thread (`--server-thread` or `HORIZON_SERVER_THREAD=1`):** `game/integrated.rs` runs a second
`Game` (`serving`) on a "Server thread" with one level per dimension; the window joins it as a client
through memory (`remote`), exactly as a LAN guest or a dedicated server's player does. Not at parity yet:
open `[server-thread]` issues list the gaps.
LAN guests and dedicated servers always use the server code, so a fix there serves both. Test both paths
when you touch player-facing gameplay:
```sh
cargo run --profile fast -p horizon-client -- --resources .local/horizon-client/26.3 --world T # in-frame
cargo run --profile fast -p horizon-client -- --resources .local/horizon-client/26.3 --world T --server-thread # server thread
python3 tools/lan_e2e.py --check <name> # host in-frame, guest is a server client
python3 tools/lan_e2e.py --server-thread --check <name> # host on its server thread (portals, levels, distance, lava need it)
python3 tools/parity.py dedicated_server server_entities server_player_damage
```
## Docs worth knowing
[docs/parity-workflow.md](docs/parity-workflow.md) (runner, gate, shards, scopes) ·
[docs/native-client.md](docs/native-client.md) (options, controls, horizon-ctl) ·
[docs/server-levels.md](docs/server-levels.md) (levels per dimension) ·
[docs/rust-parity.md](docs/rust-parity.md) (what is ported) · [docs/known-issues.md](docs/known-issues.md) ·
[docs/release-flow.md](docs/release-flow.md) · [docs/README.md](docs/README.md) (index).
Symlink
+1
View File
@@ -0,0 +1 @@
AGENTS.md
Generated
+1 -1
View File
@@ -1395,6 +1395,7 @@ dependencies = [
name = "horizon-bench"
version = "0.1.0"
dependencies = [
"horizon-telemetry",
"serde",
"serde_json",
]
@@ -1492,7 +1493,6 @@ dependencies = [
name = "horizon-mods"
version = "0.1.0"
dependencies = [
"horizon-mods",
"log",
"serde",
"serde_json",
+2
View File
@@ -8,3 +8,5 @@ description = "The benchmark report format (docs/benchmark-report.md), shared by
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
# The hardware probes shared with telemetry (cores, clocks, drive, VRAM, power, form).
horizon-telemetry = { path = "../horizon-telemetry", default-features = false }
+51 -19
View File
@@ -16,8 +16,22 @@ pub struct Hardware {
pub cpu: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cpu_threads: Option<usize>,
/// Physical cores.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cpu_cores: Option<u16>,
/// Base and top boost clock, where the OS gives them.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cpu_base_mhz: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cpu_boost_mhz: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ram_mb: Option<u64>,
/// The drive under the game folder: `nvme`, `ssd` or `hdd`.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub storage: Option<String>,
/// `laptop` or `desktop`.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub form: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub gpu: Option<Gpu>,
#[serde(default, skip_serializing_if = "Option::is_none")]
@@ -33,6 +47,9 @@ pub struct Gpu {
pub backend: String,
/// `integrated`, `discrete`, `virtual`, `cpu`, `other`.
pub kind: String,
/// Its own memory in MiB (rounded to 256), for a discrete GPU whose driver says.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub vram_mb: Option<u32>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
@@ -47,17 +64,50 @@ pub struct Display {
impl Hardware {
pub fn detect() -> Hardware {
let probes = probes();
Hardware {
os: std::env::consts::OS.to_owned(),
os_version: os_version(),
arch: std::env::consts::ARCH.to_owned(),
cpu: cpu(),
cpu_threads: std::thread::available_parallelism().ok().map(|n| n.get()),
cpu_cores: probes.cores,
cpu_base_mhz: probes.base,
cpu_boost_mhz: probes.boost,
ram_mb: ram_bytes().map(|b| b >> 20),
storage: None,
form: probes.form,
gpu: None,
display: None,
}
}
/// [`Hardware::detect`] with the kind of drive `game_dir` is on.
pub fn detect_in(game_dir: &std::path::Path) -> Hardware {
Hardware {
storage: horizon_telemetry::system::storage(game_dir).map(str::to_owned),
..Hardware::detect()
}
}
}
/// What the telemetry probes read (`horizon_telemetry::system`).
struct Probes {
cores: Option<u16>,
base: Option<u32>,
boost: Option<u32>,
form: Option<String>,
}
fn probes() -> Probes {
let s = horizon_telemetry::payload::System::detect();
let some = |n: u32| (n > 0).then_some(n);
Probes {
cores: (s.cores > 0).then_some(s.cores),
base: some(s.cpu_base_mhz),
boost: some(s.cpu_boost_mhz),
form: (!s.form.is_empty()).then_some(s.form),
}
}
/// The one-minute load average: above about the number of threads the machine was busy.
@@ -82,25 +132,7 @@ pub fn load_average() -> Option<f64> {
/// `ac` or `battery`; `None` where it can't be read (and on machines without a battery on Linux).
pub fn power_source() -> Option<&'static str> {
if cfg!(target_os = "macos") {
let batt = output("pmset", &["-g", "batt"])?;
return Some(if batt.contains("'Battery Power'") {
"battery"
} else {
"ac"
});
}
let mut on_battery = None;
for entry in std::fs::read_dir("/sys/class/power_supply").ok()?.flatten() {
let read =
|name: &str| std::fs::read_to_string(entry.path().join(name)).unwrap_or_default();
match read("type").trim() {
"Mains" if read("online").trim() == "1" => return Some("ac"),
"Battery" => on_battery = Some("battery"),
_ => {}
}
}
on_battery
horizon_telemetry::system::power()
}
/// Resident memory of a process, in bytes (macOS, Linux).
+8 -6
View File
@@ -18,7 +18,8 @@ horizon-protocol = { path = "../horizon-protocol" }
horizon-server-files = { path = "../horizon-server-files" }
horizon-friends = { path = "../horizon-friends" }
# Horizon mods: sandboxed WebAssembly components, off unless installed and turned on (docs/mods.md).
horizon-mods = { path = "../horizon-mods" }
# Their runtime is the `mods` feature.
horizon-mods = { path = "../horizon-mods", default-features = false }
wgpu = { version = "30.0.1", default-features = false, features = ["std", "parking_lot", "vulkan", "metal", "wgsl"] }
winit = "0.30.13"
pollster = "0.4"
@@ -30,7 +31,9 @@ serde = { version = "1", features = ["derive"] }
serde_json = { version = "1.0.151", features = ["raw_value"] }
# Shader packs: pack.toml, and WGSL checked before it reaches wgpu.
toml = "1.1"
naga = { version = "30.0.1", features = ["wgsl-in"] }
# hlsl-out: the gfx.rs tests translate every shader as wgpu's DirectX 12 backend does. A normal feature,
# not a dev-dependency's, so builds, tests and examples share one build of naga, wgpu and this crate.
naga = { version = "30.0.1", features = ["wgsl-in", "hlsl-out"] }
# Java (OptiFine/Iris) shader packs, translated to WGSL.
horizon-shaderpack = { path = "../horizon-shaderpack" }
# Sound (ALSA on Linux); the `audio` feature.
@@ -76,12 +79,11 @@ default = ["audio", "controllers"]
audio = ["dep:cpal"]
# Controllers: gilrs, and the DualSense's own output. Without it only `horizon-ctl pad`'s virtual pad.
controllers = ["dep:gilrs", "dep:horizon-dualsense"]
# Runs code mods (wasmtime and cranelift: a quarter of a clean build). On in every release build
# (release.yml); the tests and probes don't need it. Content mods load without it.
mods = ["horizon-mods/runtime"]
# Records the call sites of mob random draws (`mob_ai_probe` traces).
rng-trace = ["horizon-core/rng-trace"]
[lints]
workspace = true
[dev-dependencies]
# Every WGSL shader translated as wgpu's DirectX 12 backend does (gfx.rs tests).
naga = { version = "30.0.1", features = ["wgsl-in", "hlsl-out"] }
@@ -1,104 +0,0 @@
//! Full production fact loading, including file reads, interning and temporary destruction.
//! cargo run --profile fast -p horizon-client --example block_facts_bench -- ROOT [SAMPLES]
//! Use SAMPLES=1 with an OS peak-RSS wrapper; `digest` checks all facts
//! and the shape sharing partition (including sharing between outlines and edges).
use horizon_client::blocks::BlockFacts;
use horizon_world::{DATA_VERSION, nbt::Tag, registry::BlockRegistry};
use serde_json::Value;
use std::{
collections::{HashMap, hash_map::DefaultHasher},
fs,
hash::{Hash, Hasher},
hint::black_box,
path::Path,
time::Instant,
};
type Result<T> = horizon_client::Result<T>;
fn tag(value: &Value) -> Result<Tag> {
Ok(match value {
Value::String(s) => Tag::String(s.as_str().into()),
Value::Number(n) => Tag::Int(i32::try_from(n.as_i64().ok_or("registry number")?)?),
Value::Array(a) => Tag::List(a.iter().map(tag).collect::<Result<_>>()?),
Value::Object(o) => Tag::Compound(
o.iter()
.map(|(k, v)| Ok((k.as_str().into(), tag(v)?)))
.collect::<Result<_>>()?,
),
_ => return Err("registry value".into()),
})
}
fn registry(root: &Path) -> Result<BlockRegistry> {
let facts: Value = serde_json::from_slice(&fs::read(root.join("facts.json"))?)?;
Ok(BlockRegistry::from_nbt(&Tag::compound([
("DataVersion", Tag::Int(DATA_VERSION)),
(
"states",
Tag::List(
facts["states"]
.as_array()
.ok_or("registry states")?
.iter()
.map(|s| tag(&s[4]))
.collect::<Result<_>>()?,
),
),
("defaults", tag(&facts["defaults"])?),
]))?)
}
fn main() -> Result<()> {
let args: Vec<_> = std::env::args().skip(1).collect();
let root = Path::new(
args.first()
.ok_or("usage: block_facts_bench ROOT [SAMPLES|digest]")?,
);
let setup = Instant::now();
let registry = registry(root)?;
println!(
"registry_setup_ms={:.3}",
setup.elapsed().as_secs_f64() * 1000.0
);
if args.get(1).is_some_and(|s| s == "digest") {
let facts = BlockFacts::load(root, &registry)?;
let mut hash = DefaultHasher::new();
let mut shapes = HashMap::new();
facts.air.hash(&mut hash);
for state in &facts.states {
format!("{state:?}").hash(&mut hash);
for shape in [&state.outline, &state.outline_edges] {
let next = shapes.len();
shapes
.entry((shape.as_ptr(), shape.len()))
.or_insert(next)
.hash(&mut hash);
}
}
println!(
"states={} shared_shapes={} digest={:016x}",
facts.states.len(),
shapes.len(),
hash.finish()
);
return Ok(());
}
let samples: usize = args.get(1).map(|s| s.parse()).transpose()?.unwrap_or(9);
assert!(samples > 0);
// First load is reported separately; OS file caches are not forcibly evicted.
let load = || -> Result<f64> {
let start = Instant::now();
let facts = black_box(BlockFacts::load(black_box(root), black_box(&registry))?);
let ms = start.elapsed().as_secs_f64() * 1000.0;
drop(facts);
Ok(ms)
};
println!("first_load_ms={:.3}", load()?);
if samples > 1 {
load()?;
load()?;
let mut times = (0..samples).map(|_| load()).collect::<Result<Vec<_>>>()?;
println!("samples_ms={times:.3?}");
times.sort_by(f64::total_cmp);
println!("median_ms={:.3}", times[times.len() / 2]);
}
Ok(())
}
@@ -1,216 +0,0 @@
//! Resident chunk storage benchmark: generates a block of vanilla terrain, then
//! reports the resident block/biome memory, the cost of lookups, meshing and
//! lighting on it.
//!
//! chunk_bench <client resource dir> [--seed S] [--size N]
#![forbid(unsafe_code)]
use horizon_client::{
assets::Assets,
biome::Biomes,
blocks::BlockFacts,
chunk::{Chunk, MIN_Y, SECTIONS},
light::{LightTables, apply_lighting, light_chunk},
mesher::{MeshTables, Mesher},
models,
streaming::{ChunkGenerator, Proto},
vanillagen::VanillaTerrain,
};
use horizon_core::biome::BiomeZoom;
use std::{path::PathBuf, sync::Arc, time::Instant};
type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
fn main() -> Result<()> {
let args: Vec<String> = std::env::args().skip(1).collect();
let root = PathBuf::from(args.first().ok_or("usage: chunk_bench RESOURCES")?);
let flag = |name: &str, default: i64| -> i64 {
args.iter()
.position(|a| a == name)
.and_then(|i| args.get(i + 1))
.and_then(|v| v.parse().ok())
.unwrap_or(default)
};
let (seed, size) = (flag("--seed", 12345), flag("--size", 8) as i32);
let assets = Assets::load(&root)?;
let render = assets.reload(&root)?;
let facts = BlockFacts::load(&root, assets.registry)?;
let biomes = Biomes::load(&root)?;
let tables = MeshTables::new(
&assets,
&render,
&facts,
models::load_visibility(&root, assets.registry)?,
)?;
let light = LightTables::load(&root, assets.registry, &facts)?;
let terrain = VanillaTerrain::new(&root.join("worldgen"), assets.registry, &biomes, seed)?;
let mut worker = terrain.worker();
// Protos for size+4 columns, decorated for the inner size+2, finished as size+2.
let started = Instant::now();
let outer = size + 4;
let mut protos: Vec<Option<Proto>> = (0..outer * outer)
.map(|i| Some(terrain.proto(&mut worker, i % outer, i / outer)))
.collect();
eprintln!("proto: {:.2}s", started.elapsed().as_secs_f64());
let started = Instant::now();
let inner = size + 2;
for z in 1..=inner {
for x in 1..=inner {
let mut region: [Proto; 9] = std::array::from_fn(|k| {
let (dx, dz) = (k as i32 % 3 - 1, k as i32 / 3 - 1);
protos[((z + dz) * outer + x + dx) as usize]
.take()
.expect("proto")
});
terrain.decorate(&mut worker, &mut region);
for (k, p) in region.into_iter().enumerate() {
let (dx, dz) = (k as i32 % 3 - 1, k as i32 / 3 - 1);
protos[((z + dz) * outer + x + dx) as usize] = Some(p);
}
}
}
eprintln!("decorate: {:.2}s", started.elapsed().as_secs_f64());
let started = Instant::now();
let mut chunks: Vec<Chunk> = Vec::new();
for z in 1..=inner {
for x in 1..=inner {
let proto = protos[(z * outer + x) as usize].take().expect("proto");
let mut chunk = terrain.finish(proto);
chunk.x = x;
chunk.z = z;
chunks.push(chunk);
}
}
let finish = started.elapsed().as_secs_f64();
println!(
"finish (proto -> resident chunk): {:.1} ms per chunk",
finish * 1e3 / chunks.len() as f64
);
let chunks: Vec<Arc<Chunk>> = chunks.into_iter().map(Arc::new).collect();
let at = |x: i32, z: i32| chunks[((z - 1) * inner + x - 1) as usize].clone();
// Light all inner-inner columns.
let started = Instant::now();
let mut lit = Vec::new();
for z in 2..inner {
for x in 2..inner {
let region: [[Option<Arc<Chunk>>; 3]; 3] = std::array::from_fn(|dz| {
std::array::from_fn(|dx| Some(at(x + dx as i32 - 1, z + dz as i32 - 1)))
});
let borrowed = horizon_client::mesher::borrow(&region);
lit.push(((x, z), light_chunk(&borrowed, &light, true)));
}
}
println!(
"light: {:.2} ms per column",
started.elapsed().as_secs_f64() * 1e3 / lit.len() as f64
);
let mut chunks: Vec<Chunk> = chunks.iter().map(|c| (**c).clone()).collect();
for ((x, z), sections) in lit {
apply_lighting(&mut chunks[((z - 1) * inner + x - 1) as usize], sections);
}
let chunks: Vec<Arc<Chunk>> = chunks.into_iter().map(Arc::new).collect();
let at = |x: i32, z: i32| chunks[((z - 1) * inner + x - 1) as usize].clone();
// Memory.
let (mut bytes, mut sections, mut uniform) = (0usize, 0usize, 0usize);
for chunk in &chunks {
bytes += chunk.heap_bytes();
for s in &chunk.sections {
sections += 1;
uniform += usize::from(s.is_uniform(s.get(0)));
}
}
println!(
"resident: {} chunks, {} sections ({} uniform), {:.1} KiB per chunk ({:.2} MiB total)",
chunks.len(),
sections,
uniform,
bytes as f64 / 1024.0 / chunks.len() as f64,
bytes as f64 / 1048576.0
);
// Meshing.
let mut mesher = Mesher {
nether_shading: false,
assets: &assets,
render: &render,
tables: &tables,
biomes: &biomes,
visibility: tables.visibility.clone(),
zoom: BiomeZoom::new(7),
blend_radius: 2,
smooth_lighting: true,
cutout_leaves: true,
cull_leaves: false,
};
let mut best = f64::MAX;
let mut quads = 0usize;
for _ in 0..3 {
let started = Instant::now();
let mut jobs = 0;
quads = 0;
for z in 2..inner {
for x in 2..inner {
let region: [[Option<Arc<Chunk>>; 3]; 3] = std::array::from_fn(|dz| {
std::array::from_fn(|dx| Some(at(x + dx as i32 - 1, z + dz as i32 - 1)))
});
let borrowed = horizon_client::mesher::borrow(&region);
for sy in 0..SECTIONS as i32 {
let mesh = mesher.mesh(&borrowed, sy);
quads += (mesh.solid.len() + mesh.translucent.len()) / 4;
jobs += 1;
}
}
}
best = best.min(started.elapsed().as_secs_f64() / f64::from(jobs));
}
println!(
"mesh: {:.1} us per section ({} quads total)",
best * 1e6,
quads
);
// Random lookups, as collision and entity queries do.
let mut state = 0x9E37_79B9_7F4A_7C15u64;
let started = Instant::now();
let mut sum = 0u64;
let lookups = 20_000_000u64;
for _ in 0..lookups {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
let cx = 2 + (state % u64::from((inner - 2) as u32)) as i32;
let cz = 2 + ((state >> 8) % u64::from((inner - 2) as u32)) as i32;
let chunk = &chunks[((cz - 1) * inner + cx - 1) as usize];
let (x, y, z) = (
(state >> 16) as i32 & 15,
MIN_Y + ((state >> 24) % 384) as i32,
(state >> 40) as i32 & 15,
);
sum += u64::from(chunk.get(x, y, z));
}
println!(
"random get: {:.2} ns per lookup (checksum {sum})",
started.elapsed().as_secs_f64() * 1e9 / lookups as f64
);
// Coherent scan, as a column or area query does.
let started = Instant::now();
let mut sum = 0u64;
for _ in 0..4 {
for chunk in &chunks {
for y in MIN_Y..MIN_Y + 384 {
for z in 0..16 {
for x in 0..16 {
sum += u64::from(chunk.get(x, y, z));
}
}
}
}
}
println!(
"scan get: {:.2} ns per lookup (checksum {sum})",
started.elapsed().as_secs_f64() * 1e9 / (4.0 * chunks.len() as f64 * 98304.0)
);
Ok(())
}
@@ -1,140 +0,0 @@
//! Isolated production mob-model builder benchmark; no assets, Game, GPU or full-frame work.
//! CARGO_TARGET_DIR=/home/bob/Documents/horizon/target CARGO_INCREMENTAL=0 \
//! cargo run --profile fast -p horizon-client --example entity_geometry_bench
use horizon_client::{
mesher::Vertex,
mobs::{Part, Polygon, anim::Poses, render},
};
use std::{hint::black_box, time::Instant};
fn cube(name: String, position: f32) -> Part {
let polygons = (0..6)
.map(|face| {
let axis = face / 2;
let side = (face % 2) as f32;
let mut normal = [0.0; 3];
normal[axis] = side * 2.0 - 1.0;
Polygon {
normal,
vertices: [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]].map(|uv| {
let mut vertex = [0.0; 5];
vertex[axis] = side * 8.0;
vertex[(axis + 1) % 3] = uv[0] * 8.0;
vertex[(axis + 2) % 3] = uv[1] * 8.0;
vertex[3] = uv[0];
vertex[4] = uv[1];
vertex
}),
}
})
.collect();
Part {
name,
pose: [position, 0.0, 0.0, 0.1, 0.2, 0.0, 1.0, 1.0, 1.0],
polygons,
children: Vec::new(),
}
}
fn build(model: &Part, poses: &Poses, count: usize, out: &mut Vec<Vertex>) {
for i in 0..count {
render::draw(
black_box(model),
glam::Mat4::from_translation(glam::Vec3::new((i % 32) as f32, 0.0, (i / 32) as f32)),
black_box(poses),
[0.125, 0.25, 0.5, 0.75],
0xffc0_d0e0,
0xf0f0,
0,
out,
);
}
black_box(out.as_slice());
}
fn measure(model: &Part, poses: &Poses, count: usize, iterations: usize, reuse: bool) -> f64 {
let mut retained = Vec::new();
build(model, poses, count, &mut retained); // Untimed warmup.
let start = Instant::now();
for _ in 0..iterations {
let mut out = if reuse {
std::mem::take(&mut retained)
} else {
Vec::new()
};
out.clear();
build(model, poses, count, &mut out);
if reuse {
retained = out;
}
}
start.elapsed().as_secs_f64() * 1e6 / iterations as f64
}
fn main() {
let mut model = cube("body".into(), 0.0);
model.children = (1..6).map(|i| cube(format!("part{i}"), i as f32)).collect();
let poses = Poses::new(&model);
for count in [32, 256, 1024, 4096, 8192] {
let mut expected = Vec::new();
build(&model, &poses, count, &mut expected);
let mut retained = Vec::new();
build(&model, &poses, count, &mut retained);
let (pointer, capacity) = (retained.as_ptr(), retained.capacity());
// Populated -> empty/reset -> smaller -> populated must never leave stale vertices.
for n in [0, count / 2, count, count] {
retained.clear();
build(&model, &poses, n, &mut retained);
let mut fresh = Vec::new();
build(&model, &poses, n, &mut fresh);
assert_eq!(
bytemuck::cast_slice::<Vertex, u8>(&retained),
bytemuck::cast_slice::<Vertex, u8>(&fresh)
);
assert_eq!(
(retained.as_ptr(), retained.capacity()),
(pointer, capacity)
);
}
assert_eq!(
bytemuck::cast_slice::<Vertex, u8>(&retained),
bytemuck::cast_slice::<Vertex, u8>(&expected)
);
let iterations = (100_000 / count).clamp(40, 1000);
let mut fresh = Vec::new();
let mut reused = Vec::new();
let mut paired_speedups = Vec::new();
// Alternate order, reporting medians over eleven warmed batches.
for batch in 0..11 {
let mut pair = [0.0; 2];
for reuse in if batch % 2 == 0 {
[false, true]
} else {
[true, false]
} {
let time = measure(&model, &poses, count, iterations, reuse);
pair[usize::from(reuse)] = time;
if reuse {
reused.push(time);
} else {
fresh.push(time);
}
}
paired_speedups.push(pair[0] / pair[1]);
}
paired_speedups.sort_by(f64::total_cmp);
fresh.sort_by(f64::total_cmp);
reused.sort_by(f64::total_cmp);
println!(
"entities={count} vertices={} capacity={capacity} retained_bytes={} fresh_us={:.3} reused_us={:.3} speedup={:.3}x paired_speedup_median={:.3}x paired_speedup_range={:.3}..{:.3}x iterations={iterations} batches=11 byte_equivalence=pass pointer_capacity_reuse=pass",
retained.len(),
capacity * size_of::<Vertex>(),
fresh[5],
reused[5],
fresh[5] / reused[5],
paired_speedups[5],
paired_speedups[0],
paired_speedups[10]
);
}
}
@@ -1,205 +0,0 @@
//! Compare the exact read/deserialize sequences used by the model loaders.
//! Run with `cargo run -p horizon-client --profile fast --example json_readers_bench`.
//! Optional arguments: resource directory, repetitions (default 9).
use serde_json::{Map, Value, json};
use std::{
collections::BTreeMap,
fmt::Debug,
fs::{self, File},
hint::black_box,
io::BufReader,
path::{Path, PathBuf},
time::Instant,
};
type Result<T> = horizon_client::Result<T>;
#[derive(Clone, Copy)]
enum Reader {
File,
Buffered,
Slice,
}
const READERS: [Reader; 3] = [Reader::File, Reader::Buffered, Reader::Slice];
// These match the map types and reader expressions at all three production sites.
macro_rules! reader {
($name:ident, $map:ty) => {
fn $name(path: &Path, reader: Reader) -> Result<$map> {
Ok(match reader {
Reader::File => serde_json::from_reader(File::open(path)?)?,
Reader::Buffered => serde_json::from_reader(BufReader::new(File::open(path)?))?,
Reader::Slice => serde_json::from_slice(&fs::read(path)?)?,
})
}
};
}
reader!(prepare_facts, Map<String, Value>);
reader!(catalog_facts, BTreeMap<String, Value>);
struct Fixture(PathBuf);
impl Fixture {
fn new() -> Result<Self> {
let root =
std::env::temp_dir().join(format!("horizon-json-readers-{}", std::process::id()));
fs::create_dir(&root)?;
Ok(Self(root))
}
}
impl Drop for Fixture {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.0);
}
}
fn fixtures(root: &Path) -> Result<()> {
let states: Vec<_> = (0..56)
.map(|i| {
json!({"facing": (["north", "east", "south", "west"][i % 4]),
"powered": if i / 4 % 2 == 0 { "false" } else { "true" },
"age": (i / 8).to_string()})
})
.collect();
let mut models = Map::new();
let mut render = Map::new();
for i in 0..1024 {
let name = format!("minecraft:fixture_block_{i:04}");
models.insert(
name.clone(),
json!({
"schema": {"facing": {"named": ["north", "east", "south", "west"]},
"powered": {"named": ["false", "true"]}, "age": {"integer": [0, 6]}},
"states": states
}),
);
render.insert(
name,
json!({"rule": "never", "can_occlude": true, "bars_tag": false}),
);
}
fs::write(root.join("model-states.json"), serde_json::to_vec(&models)?)?;
fs::write(
root.join("render-definitions.json"),
serde_json::to_vec(&render)?,
)?;
Ok(())
}
fn benchmark<T: PartialEq + Debug>(
label: &str,
path: &Path,
repetitions: usize,
read: impl Fn(&Path, Reader) -> Result<T>,
) -> Result<()> {
let expected = read(path, Reader::File)?;
for reader in READERS {
assert_eq!(expected, read(path, reader)?);
for _ in 0..2 {
black_box(read(black_box(path), reader)?);
}
}
drop(expected);
let mut samples: [Vec<f64>; 3] = std::array::from_fn(|_| Vec::new());
for repetition in 0..repetitions {
// Rotate reader order to distribute drift across implementations.
for offset in 0..3 {
let index = (repetition + offset) % 3;
let start = Instant::now();
let value = black_box(read(black_box(path), READERS[index])?);
samples[index].push(start.elapsed().as_secs_f64() * 1000.0);
drop(value); // Match the loaders: retain the parsed tree after loading.
}
}
let median: Vec<_> = samples
.iter_mut()
.map(|s| {
s.sort_by(f64::total_cmp);
s[s.len() / 2]
})
.collect();
println!(
"{label}: {} bytes, {repetitions} samples/reader, median ms: File={:.3}, BufReader={:.3}, read+slice={:.3}; slice speedup={:.2}x",
fs::metadata(path)?.len(),
median[0],
median[1],
median[2],
median[0] / median[2]
);
Ok(())
}
fn main() -> Result<()> {
let args: Vec<_> = std::env::args().skip(1).collect();
let fixture = if args.is_empty() {
Some(Fixture::new()?)
} else {
None
};
let root = fixture
.as_ref()
.map(|f| f.0.clone())
.unwrap_or_else(|| PathBuf::from(&args[0]));
if fixture.is_some() {
fixtures(&root)?;
println!(
"Generated representative fixtures: 1024 blocks, 57344 model states; warm filesystem cache."
);
}
let repetitions = args.get(1).map(|s| s.parse()).transpose()?.unwrap_or(9);
assert!(repetitions > 0, "repetitions must be positive");
benchmark(
"prepare model states",
&root.join("model-states.json"),
repetitions,
prepare_facts,
)?;
benchmark(
"catalog model states",
&root.join("model-states.json"),
repetitions,
catalog_facts,
)?;
benchmark(
"render definitions",
&root.join("render-definitions.json"),
repetitions,
catalog_facts,
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn reader_equivalence_and_errors() -> Result<()> {
let fixture = Fixture::new()?;
let path = fixture.0.join("test.json");
fs::write(&path, br#"{"minecraft:stone":{"schema":{},"states":[{}]},"unicode":"\u2603","duplicate":1,"duplicate":2}"#)?;
let expected = prepare_facts(&path, Reader::File)?;
for reader in READERS {
assert_eq!(expected, prepare_facts(&path, reader)?);
assert_eq!(
expected.iter().collect::<BTreeMap<_, _>>(),
catalog_facts(&path, reader)?.iter().collect()
);
}
for invalid in [b"{\"a\": ".as_slice(), b"{} trailing", b"{\"a\":\"\xff\"}"] {
fs::write(&path, invalid)?;
for reader in READERS {
let error = prepare_facts(&path, reader).unwrap_err();
assert!(error.downcast_ref::<serde_json::Error>().is_some());
assert!(catalog_facts(&path, reader).is_err());
}
}
fs::remove_file(&path)?;
for reader in READERS {
let error = prepare_facts(&path, reader).unwrap_err();
assert_eq!(
error.downcast_ref::<std::io::Error>().unwrap().kind(),
std::io::ErrorKind::NotFound
);
}
Ok(())
}
}
-143
View File
@@ -1,143 +0,0 @@
//! Serve a small flat world on the LAN server without the game, for joining
//! with a real client while developing the protocol:
//! `cargo run --release --example lan_flat -- RESOURCES PORT`.
use horizon_client::{
assets::Assets,
blocks::BlockFacts,
chunk::{Chunk, MIN_Y},
lan::{
Lan,
chunks::ChunkEncoder,
data::SyncData,
hub::{HostEvent, HostState, Hub, HubConfig, TickCtx},
},
world::World,
};
use horizon_protocol::packets::play::Serverbound as S;
use std::{path::PathBuf, sync::Arc, time::Duration};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut args = std::env::args().skip(1);
let root = PathBuf::from(args.next().ok_or("RESOURCES PORT")?);
let port: u16 = args.next().ok_or("port")?.parse()?;
let assets = Assets::load(&root)?;
let facts = BlockFacts::load(&root, assets.registry)?;
let state = |name: &str| assets.registry.default_state(name).map_or(0, |s| s.0);
let sync = Arc::new(SyncData::load(&root)?);
let biomes: Vec<String> = sync
.dynamic
.registries
.get("minecraft:worldgen/biome")
.map(|v| v.iter().map(|(n, _)| n.clone()).collect())
.unwrap_or_default();
let encoder = ChunkEncoder::new(
facts
.states
.iter()
.map(|s| (s.name.clone(), s.fluid_level > 0 || s.waterlogged)),
&sync.builtin,
&biomes,
&biomes,
);
let (stone, dirt, grass) = (
state("minecraft:stone"),
state("minecraft:dirt"),
state("minecraft:grass_block"),
);
let mut world = World::new(facts.air, 0);
for cz in -8..=8 {
for cx in -8..=8 {
let mut chunk = Chunk::empty(cx, cz);
for z in 0..16 {
for x in 0..16 {
for y in MIN_Y..60 {
let v = if y == 59 {
grass
} else if y > 55 {
dirt
} else {
stone
};
chunk.set(x, y, z, v);
}
}
}
chunk.lit = true;
world.chunks.insert((cx, cz), Arc::new(chunk));
}
}
let lan = Lan::start(sync.clone(), port, "Flat - Example".into(), 8)?;
println!("listening on {}", lan.port);
let mut hub = Hub::new(
lan,
sync,
encoder,
HubConfig {
default_game_mode: 1,
guest_commands: true,
force_game_mode: false,
hardcore: false,
view_distance: 8,
simulation_distance: 8,
dimension: "minecraft:overworld".into(),
seed: 0,
max_players: 8,
dedicated: false,
integrated: false,
},
);
let mut next_entity = 100u32;
let host = HostState {
uuid: 42,
name: "Host".into(),
entity_id: 1,
position: [0.5, 60.0, 0.5],
yaw: 0.0,
pitch: 0.0,
head_yaw: 0.0,
on_ground: true,
game_mode: 1,
sneaking: false,
sprinting: false,
equipment: Default::default(),
health: 20.0,
attributes: Vec::new(),
bed: None,
living_flags: 0,
};
loop {
world.game_time += 1;
world.day_time += 1;
let mut ctx = TickCtx {
world: &mut world,
host: Some(&host),
entities: &[],
next_entity: &mut next_entity,
spawn: [8.5, 60.0, 8.5],
difficulty: 2,
difficulty_locked: false,
clock_rate: 1.0,
tick_rate: 20.0,
tick_frozen: false,
tick_steps: (0, 0),
collision: None,
};
for event in hub.tick(&mut ctx) {
match event {
HostEvent::Joined(conn) => hub.place_new_player(conn, &mut ctx),
HostEvent::Packet(_, packet) => {
if std::env::var_os("HORIZON_NET_DEBUG").is_some()
&& !matches!(*packet, S::ClientTickEnd(_))
{
eprintln!("[lan] {packet:?}");
}
}
HostEvent::Chat { line, .. } | HostEvent::Warn(line) | HostEvent::Error(line) => {
println!("{line}")
}
HostEvent::Left(_) => {}
}
}
std::thread::sleep(Duration::from_millis(50));
}
}
@@ -1,153 +0,0 @@
//! Read-only saved chunk benchmark. All timings exclude resource/corpus setup.
//!
//! load_bench RESOURCES DIMENSION_DIR [--rounds N] [--limit N] [--checksum]
//! Add --profile-worker when a sampling collector cannot sample the main thread.
//! Reports warm filesystem SaveDir::load (read, inflate, NBT, decode and light),
//! plus isolated inflate/NBT and native decode. No files in the save are changed.
use horizon_client::{
assets::Assets,
biome::Biomes,
blocks::BlockFacts,
save::{SaveDir, SaveTables, decode_with},
};
use horizon_world::{nbt::Limits, region::RegionFile};
use sha2::{Digest, Sha256};
use std::{hint::black_box, path::PathBuf, sync::Arc, time::Instant};
type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
fn main() -> Result<()> {
if std::env::args().any(|a| a == "--profile-worker") {
std::thread::spawn(|| run().map_err(|e| e.to_string()))
.join()
.expect("benchmark worker")
.map_err(Into::into)
} else {
run()
}
}
fn run() -> Result<()> {
let args: Vec<String> = std::env::args().skip(1).collect();
let usage = "load_bench RESOURCES DIMENSION_DIR [--rounds N] [--limit N] [--checksum]";
let root = PathBuf::from(args.first().ok_or(usage)?);
let dimension = PathBuf::from(args.get(1).ok_or(usage)?);
let flag = |name: &str, default: usize| -> Result<usize> {
Ok(match args.iter().position(|a| a == name) {
Some(i) => args.get(i + 1).ok_or(usage)?.parse()?,
None => default,
})
};
let rounds = flag("--rounds", 5)?;
let limit = flag("--limit", 256)?;
if rounds == 0 || limit == 0 {
return Err("rounds and limit must be positive".into());
}
let assets = Assets::load(&root)?;
let facts = BlockFacts::load(&root, assets.registry)?;
let biomes = Biomes::load(&root)?;
let tables = Arc::new(SaveTables::new(&assets).with_block_entities(&facts));
let source = SaveDir::new(dimension.clone());
let _ = source.tables.set(tables.clone());
let mut paths: Vec<_> = std::fs::read_dir(dimension.join("region"))?
.map(|e| e.map(|e| e.path()))
.collect::<std::io::Result<_>>()?;
paths.retain(|p| p.extension().is_some_and(|e| e == "mca"));
paths.sort();
let mut corpus = Vec::new();
'regions: for path in paths {
let mut region = RegionFile::open(path)?;
let mut positions: Vec<_> = region.chunks().map(|(pos, _)| pos).collect();
positions.sort_by_key(|p| (p.x, p.z));
for pos in positions {
let record = region.read(pos)?.ok_or("missing record")?;
let tag = record.nbt(Limits::default())?;
if decode_with(&tag, assets.registry, &biomes, Some(&tables)).is_none() {
continue;
}
corpus.push((pos, record, tag));
if corpus.len() == limit {
break 'regions;
}
}
}
if corpus.is_empty() {
return Err("no full chunks in corpus".into());
}
let mut records_hash = Sha256::new();
for (pos, record, _) in &corpus {
records_hash.update(pos.x.to_le_bytes());
records_hash.update(pos.z.to_le_bytes());
records_hash.update((record.compressed.len() as u64).to_le_bytes());
records_hash.update(&record.compressed);
}
println!(
"{}",
serde_json::json!({"corpus": dimension, "chunks": corpus.len(), "rounds": rounds,
"compressed_records_sha256": format!("{:x}", records_hash.finalize()),
"compressed_bytes": corpus.iter().map(|(_, r, _)| r.compressed.len()).sum::<usize>()})
);
// Prime SaveDir's region handles and the filesystem cache before measurement.
for (pos, _, _) in &corpus {
black_box(
source
.load(pos.x, pos.z, assets.registry, &biomes)
.ok_or("load failed")?,
);
}
for round in 0..rounds {
let started = Instant::now();
for (pos, _, _) in &corpus {
black_box(
source
.load(pos.x, pos.z, assets.registry, &biomes)
.ok_or("load failed")?,
);
}
let load_s = started.elapsed().as_secs_f64();
let started = Instant::now();
for (_, record, _) in &corpus {
black_box(record.nbt(Limits::default())?);
}
let nbt_s = started.elapsed().as_secs_f64();
let started = Instant::now();
for (_, _, tag) in &corpus {
black_box(
decode_with(tag, assets.registry, &biomes, Some(&tables)).ok_or("decode failed")?,
);
}
let decode_s = started.elapsed().as_secs_f64();
println!(
"{}",
serde_json::json!({"round": round, "load_s": load_s,
"inflate_nbt_s": nbt_s, "decode_s": decode_s})
);
}
if args.iter().any(|a| a == "--checksum") {
let mut hash = Sha256::new();
let mut light_loaded = 0;
for (pos, _, _) in &corpus {
let chunk = source
.load(pos.x, pos.z, assets.registry, &biomes)
.ok_or("load failed")?;
hash.update(pos.x.to_le_bytes());
hash.update(pos.z.to_le_bytes());
light_loaded += usize::from(chunk.light_loaded);
for section in &chunk.sections {
for i in 0..4096 {
hash.update(section.get(i).to_le_bytes());
hash.update([section.light(i)]);
}
for i in 0..64 {
hash.update(section.biomes.get(i).to_le_bytes());
}
}
}
println!(
"{}",
serde_json::json!({"blocks_biomes_light_sha256": format!("{:x}", hash.finalize()),
"light_loaded": light_loaded})
);
}
Ok(())
}
@@ -1,8 +1,7 @@
//! Replays `tools/test_ambient_sound_parity.py` scripts through Horizon's
//! ambient sound handlers over its `SoundEngine`:
//! `ambient_fixture ROOT INPUT OUTPUT`.
#[path = "audio_common/mod.rs"]
mod common;
use crate::audio_common as common;
use common::{Fake, d, f, flush};
use horizon_client::ambience::{Ambience, AmbientContext, AmbientSounds, Mood, bubble_column_in};
use horizon_client::sound_engine::{Bound, SoundEngine, TickContext, Ticking};
@@ -47,8 +46,8 @@ fn sounds(a: &[&str]) -> AmbientSounds {
}
}
fn main() {
let args: Vec<String> = std::env::args().collect();
pub fn main() {
let args: Vec<String> = crate::args().collect();
let mut engine = SoundEngine::new(SoundRegistry::load(std::path::Path::new(&args[1])));
engine.listening = true;
let mut backend = Fake::default();
@@ -50,8 +50,8 @@ fn particle(k: ArrowParticle) -> &'static str {
ArrowParticle::Splash => "splash",
}
}
fn main() -> Result<()> {
let args: Vec<_> = std::env::args().skip(1).collect();
pub fn main() -> Result<()> {
let args: Vec<_> = crate::args().skip(1).collect();
if args.len() != 3 {
return Err("usage: arrow_probe RESOURCES FIXTURE_DIRECTORY --describe|REFERENCE".into());
}
@@ -18,8 +18,8 @@ fn music(m: Option<&Music>) -> String {
})
}
fn main() {
let args: Vec<String> = std::env::args().collect();
pub fn main() {
let args: Vec<String> = crate::args().collect();
let root = std::path::Path::new(&args[1]);
let tables = MusicTables::load(root);
let ambient = AmbientTables::load(root);
@@ -24,8 +24,8 @@ fn db(v: &Value) -> f64 {
f64::from_bits(u64::from_str_radix(v.as_str().unwrap(), 16).unwrap())
}
fn main() -> Result<()> {
let args: Vec<_> = std::env::args().skip(1).collect();
pub fn main() -> Result<()> {
let args: Vec<_> = crate::args().skip(1).collect();
let [root, input, output, scene_json] = &args[..] else {
return Err("usage: auto_jump_probe RESOURCES INPUT OUTPUT SCENE_JSON".into());
};
@@ -118,8 +118,8 @@ fn status(s: ShulkerStatus) -> &'static str {
}
}
fn main() -> Result<()> {
let args: Vec<PathBuf> = std::env::args_os().skip(1).map(PathBuf::from).collect();
pub fn main() -> Result<()> {
let args: Vec<PathBuf> = crate::args_os().skip(1).map(PathBuf::from).collect();
let [root, inputs, output] = args.as_slice() else {
return Err("usage: block_entity_anim_fixture RESOURCES INPUTS OUTPUT".into());
};
@@ -110,8 +110,8 @@ fn intern(s: &str) -> &'static str {
}
}
fn main() -> Result<()> {
let args: Vec<PathBuf> = std::env::args_os().skip(1).map(PathBuf::from).collect();
pub fn main() -> Result<()> {
let args: Vec<PathBuf> = crate::args_os().skip(1).map(PathBuf::from).collect();
let [root, inputs, textures, output] = args.as_slice() else {
return Err("usage: block_entity_render_fixture RESOURCES INPUTS TEXTURES OUTPUT".into());
};
@@ -26,8 +26,8 @@ fn entity(name: &str) -> BlockEntity {
.unwrap_or_else(|| panic!("no block entity for {name}"))
}
fn main() {
let args: Vec<String> = std::env::args().collect();
pub fn main() {
let args: Vec<String> = crate::args().collect();
let input = std::fs::read_to_string(&args[1]).expect("input");
let mut out = String::new();
let mut entities = BlockEntities::default();
@@ -8,8 +8,8 @@ use std::{
io::{BufRead, BufReader, BufWriter, Write},
path::Path,
};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<_> = std::env::args().collect();
pub fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<_> = crate::args().collect();
let root = Path::new(&args[1]);
let assets = Assets::load(root)?;
let render = assets.reload(root)?;
@@ -51,8 +51,8 @@ impl Around for Column {
}
}
fn main() -> horizon_client::Result<()> {
let args: Vec<String> = std::env::args().collect();
pub fn main() -> horizon_client::Result<()> {
let args: Vec<String> = crate::args().collect();
let root = Path::new(
args.get(1)
.ok_or("usage: block_transform_fixture RESOURCES INPUT OUTPUT")?,
@@ -16,8 +16,8 @@ use std::{
};
/// The stage sprite origin the cracks carry (the oracle compares the geometry and UVs only).
const STAGE: [f32; 2] = [0.25, 0.5];
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<_> = std::env::args().collect();
pub fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<_> = crate::args().collect();
let root = Path::new(&args[1]);
let assets = Assets::load(root)?;
let render = assets.reload(root)?;
@@ -323,8 +323,8 @@ fn avatar(c: &Value, out: &mut impl Write) -> Result<()> {
Ok(())
}
fn main() -> Result<()> {
let args: Vec<String> = std::env::args().collect();
pub fn main() -> Result<()> {
let args: Vec<String> = crate::args().collect();
let input = BufReader::new(File::open(&args[1])?);
let mut out = BufWriter::new(File::create(&args[2])?);
for (n, line) in input.lines().enumerate() {
@@ -70,8 +70,8 @@ fn describe(state: &BlockState) -> String {
format!("{}|{}", state.name, props.join(","))
}
fn main() -> horizon_client::Result<()> {
let args: Vec<String> = std::env::args().collect();
pub fn main() -> horizon_client::Result<()> {
let args: Vec<String> = crate::args().collect();
let root = Path::new(
args.get(1)
.ok_or("usage: cauldron_fixture RESOURCES INPUT OUTPUT")?,
@@ -5,6 +5,7 @@
//! the oracle's JSON form.
//!
//! usage: chat_screen_fixture RESOURCES REGISTRIES CASES OUTPUT
use crate::pack_screen_fixture;
use horizon_client::{
chat::{ChatAction, ChatScreen, Commands},
command::vanilla,
@@ -15,14 +16,11 @@ use horizon_client::{
use serde_json::{Value, json};
use std::{fmt::Write as _, fs, path::PathBuf};
use winit::keyboard::KeyCode as K;
#[path = "pack_screen_fixture.rs"]
#[allow(dead_code)]
mod pack_screen_fixture;
#[path = "chat_screen_fixture/registries.rs"]
mod registries;
fn main() -> horizon_client::Result<()> {
let args: Vec<PathBuf> = std::env::args_os().skip(1).map(PathBuf::from).collect();
pub fn main() -> horizon_client::Result<()> {
let args: Vec<PathBuf> = crate::args_os().skip(1).map(PathBuf::from).collect();
let [root, registries, inputs, output] = args.as_slice() else {
return Err("usage: chat_screen_fixture RESOURCES REGISTRIES CASES OUTPUT".into());
};
@@ -1,8 +1,8 @@
//! CloudRenderer face selection and mesh traversal, without a GPU.
use horizon_client::clouds;
use std::{fmt::Write, fs};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<_> = std::env::args().collect();
pub fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<_> = crate::args().collect();
let mut output = String::new();
for (case, line) in fs::read_to_string(&args[1])?.lines().enumerate() {
let values: Vec<i32> = line
@@ -132,8 +132,8 @@ fn tree(d: &Dispatcher, node: usize) -> Value {
v
}
fn main() -> horizon_client::Result<()> {
let args: Vec<String> = std::env::args().collect();
pub fn main() -> horizon_client::Result<()> {
let args: Vec<String> = crate::args().collect();
if args.get(1).map(String::as_str) == Some("tree") {
let d = &vanilla::registered(false);
let mut v = tree(d, 0);
@@ -24,8 +24,8 @@ fn input_name(input: Input) -> &'static str {
}
}
fn main() -> Result<()> {
let args: Vec<String> = std::env::args().collect();
pub fn main() -> Result<()> {
let args: Vec<String> = crate::args().collect();
let input = BufReader::new(File::open(&args[1])?);
let mut out = BufWriter::new(File::create(&args[2])?);
for (n, line) in input.lines().enumerate() {
@@ -11,8 +11,8 @@ use horizon_client::{
use serde_json::{Value, json};
use std::{fs, path::Path};
fn main() -> horizon_client::Result<()> {
let args: Vec<String> = std::env::args().collect();
pub fn main() -> horizon_client::Result<()> {
let args: Vec<String> = crate::args().collect();
let usage = "usage: creative_search_fixture RESOURCES CREATIVE_JSON VANILLA_JSON OUTPUT";
let names = Names::load(Path::new(args.get(1).ok_or(usage)?));
let creative = Creative::from_json(&serde_json::from_slice(&fs::read(
@@ -0,0 +1,49 @@
//! How the probe binary runs a probe. main.rs lists the probes and nothing else (tools/parity.py leaves it out of
//! the harnesses' dependencies); code every probe relies on goes here.
use std::process::ExitCode;
/// `probes! { a, b, }` declares the binary's `main`, which runs the probe module named by its first argument.
macro_rules! probes {
($($name:ident,)*) => {
fn run(name: &str) -> Option<std::process::ExitCode> {
use std::process::Termination;
Some(match name {
$(stringify!($name) => $name::main().report(),)*
_ => return None,
})
}
fn main() -> std::process::ExitCode {
dispatch::main(&[$(stringify!($name)),*], run)
}
};
}
/// The probe's command line as its own example saw it: the program, then the arguments after the probe's name.
pub fn args() -> std::vec::IntoIter<String> {
let mut args: Vec<String> = std::env::args().collect();
if args.len() > 1 {
args.remove(1);
}
args.into_iter()
}
/// args(), as OsStrings.
pub fn args_os() -> std::vec::IntoIter<std::ffi::OsString> {
let mut args: Vec<_> = std::env::args_os().collect();
if args.len() > 1 {
args.remove(1);
}
args.into_iter()
}
pub fn main(names: &[&str], run: fn(&str) -> Option<ExitCode>) -> ExitCode {
let name = std::env::args().nth(1).unwrap_or_default();
run(&name).unwrap_or_else(|| {
eprintln!(
"usage: probe <name> [arguments...]\nprobes: {}",
names.join(" ")
);
ExitCode::from(2)
})
}
@@ -14,8 +14,8 @@ fn d(v: f64) -> String {
format!("{:016x}", v.to_bits())
}
fn main() -> Result<()> {
let args: Vec<String> = std::env::args().collect();
pub fn main() -> Result<()> {
let args: Vec<String> = crate::args().collect();
let mut out = BufWriter::new(File::create(&args[2])?);
for (n, line) in BufReader::new(File::open(&args[1])?).lines().enumerate() {
let c: Value = serde_json::from_str(&line?)?;
@@ -10,8 +10,8 @@ use horizon_client::{
};
use std::{fmt::Write as _, fs, path::Path};
fn main() -> horizon_client::Result<()> {
let args: Vec<String> = std::env::args().collect();
pub fn main() -> horizon_client::Result<()> {
let args: Vec<String> = crate::args().collect();
let root = Path::new(
args.get(1)
.ok_or("usage: enchanting_fixture RESOURCES INPUT OUTPUT")?,
@@ -3,7 +3,7 @@
//! posed model with `mobs::render::draw` under the case's living-entity root
//! transform and writes each drawn part's packed vertex normals.
//!
//! cargo run --profile fast -p horizon-client --example entity_anim_fixture -- \
//! cargo run --profile fast -p horizon-client --example probe -- entity_anim_fixture \
//! RESOURCES VANILLA.jsonl OUT.jsonl
use horizon_client::mesher::Vertex;
use horizon_client::mobs::{
@@ -288,8 +288,8 @@ fn entity_state(mob: &mut Mob, species: Species, case: &Value) {
}
}
fn main() -> horizon_client::Result<()> {
let args: Vec<String> = std::env::args().skip(1).collect();
pub fn main() -> horizon_client::Result<()> {
let args: Vec<String> = crate::args().skip(1).collect();
let root = PathBuf::from(&args[0]);
let types = MobTypes::load(&root)?;
let animations = MobData::load_animations(&root);
@@ -61,8 +61,8 @@ fn species(id: &str) -> Species {
}
}
fn main() -> Result<()> {
let args: Vec<String> = std::env::args().collect();
pub fn main() -> Result<()> {
let args: Vec<String> = crate::args().collect();
let input = BufReader::new(File::open(&args[1])?);
let mut out = BufWriter::new(File::create(&args[2])?);
for (n, line) in input.lines().enumerate() {
@@ -53,8 +53,8 @@ fn line(s: &LootStack) -> String {
out
}
fn main() -> horizon_client::Result<()> {
let args: Vec<String> = std::env::args().collect();
pub fn main() -> horizon_client::Result<()> {
let args: Vec<String> = crate::args().collect();
let root = Path::new(
args.get(1)
.ok_or("usage: entity_death_loot_fixture RESOURCES INPUT OUTPUT")?,
@@ -13,8 +13,8 @@ fn d(v: f64) -> String {
format!("{:016x}", v.to_bits())
}
fn main() -> Result<()> {
let args: Vec<String> = std::env::args().collect();
pub fn main() -> Result<()> {
let args: Vec<String> = crate::args().collect();
let mut out = BufWriter::new(File::create(&args[2])?);
for (n, line) in BufReader::new(File::open(&args[1])?).lines().enumerate() {
let c: Value = serde_json::from_str(&line?)?;
@@ -9,8 +9,8 @@ use horizon_client::{
};
use std::{collections::HashMap, fmt::Write as _, fs, path::Path};
fn main() -> horizon_client::Result<()> {
let args: Vec<String> = std::env::args().collect();
pub fn main() -> horizon_client::Result<()> {
let args: Vec<String> = crate::args().collect();
let root = Path::new(
args.get(1)
.ok_or("usage: environment_fixture RESOURCES INPUT OUTPUT")?,
@@ -388,8 +388,8 @@ fn describe(m: &Mob, all: &[Mob], regs: &SpawnRegistries) -> String {
t.join(" ")
}
fn main() -> horizon_client::Result<()> {
let args: Vec<String> = std::env::args().collect();
pub fn main() -> horizon_client::Result<()> {
let args: Vec<String> = crate::args().collect();
let root = Path::new(
args.get(1)
.ok_or("usage: finalize_spawn_fixture RESOURCES INPUT OUTPUT HALLOWEEN")?,
@@ -11,8 +11,8 @@ use std::{
io::{BufRead, BufReader, BufWriter, Write},
};
fn main() -> Result<()> {
let args: Vec<String> = std::env::args().collect();
pub fn main() -> Result<()> {
let args: Vec<String> = crate::args().collect();
let input = BufReader::new(File::open(&args[1])?);
let mut out = BufWriter::new(File::create(&args[2])?);
for line in input.lines() {
@@ -4,8 +4,8 @@
use horizon_client::{enchant::Enchantments, item_data::parse_stack, item_tooltip::firework_lines};
use std::{error::Error, fmt::Write, path::PathBuf};
fn main() -> Result<(), Box<dyn Error>> {
let args: Vec<String> = std::env::args().collect();
pub fn main() -> Result<(), Box<dyn Error>> {
let args: Vec<String> = crate::args().collect();
let input = std::fs::read_to_string(&args[1])?;
let root = input
.lines()
@@ -458,8 +458,8 @@ impl HandWorld for ResetWorld<'_, '_> {
}
}
fn main() -> Result<()> {
let args: Vec<String> = std::env::args().collect();
pub fn main() -> Result<()> {
let args: Vec<String> = crate::args().collect();
let input = BufReader::new(File::open(&args[1])?);
let mut out = BufWriter::new(File::create(&args[2])?);
for (n, line) in input.lines().enumerate() {
@@ -50,8 +50,8 @@ impl LimiterClock for Script {
}
}
fn main() -> Result<()> {
let args: Vec<String> = std::env::args().collect();
pub fn main() -> Result<()> {
let args: Vec<String> = crate::args().collect();
let input = BufReader::new(File::open(&args[1])?);
let mut out = BufWriter::new(File::create(&args[2])?);
for (n, line) in input.lines().enumerate() {
@@ -291,8 +291,8 @@ fn key(c: &Value, out: &mut impl Write) -> Result<()> {
Ok(())
}
fn main() -> Result<()> {
let args: Vec<_> = std::env::args().skip(1).collect();
pub fn main() -> Result<()> {
let args: Vec<_> = crate::args().skip(1).collect();
let mut out = BufWriter::new(File::create(&args[1])?);
for line in BufReader::new(File::open(&args[0])?).lines() {
let c: Value = serde_json::from_str(&line?)?;
@@ -13,8 +13,8 @@ use horizon_client::{
use serde_json::Value;
use std::{fmt::Write as _, fs, path::Path};
fn main() -> horizon_client::Result<()> {
let args: Vec<String> = std::env::args().collect();
pub fn main() -> horizon_client::Result<()> {
let args: Vec<String> = crate::args().collect();
let usage = "usage: ghost_recipe_fixture RESOURCES CASES OUTPUT";
let root = Path::new(args.get(1).ok_or(usage)?);
let cases = fs::read_to_string(args.get(2).ok_or(usage)?)?;
@@ -2,7 +2,7 @@
use horizon_client::command::args::GameMode;
use horizon_client::items::{ItemStack, take_held_drop};
fn main() {
pub fn main() {
for mode in [
GameMode::Survival,
GameMode::Creative,
@@ -10,8 +10,8 @@ use serde_json::Value;
use std::path::Path;
use std::{fmt::Write as _, fs};
fn main() -> horizon_client::Result<()> {
let args: Vec<String> = std::env::args().collect();
pub fn main() -> horizon_client::Result<()> {
let args: Vec<String> = crate::args().collect();
let usage = "usage: hotbar_fixture RESOURCES CASES OUTPUT";
let items = Items::catalog(Path::new(args.first().and(args.get(1)).ok_or(usage)?))?;
let known = |id: &str| items.get(id).is_some();
@@ -20,8 +20,8 @@ type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
#[path = "hud_fixture/kinds.rs"]
mod kinds;
fn main() -> Result<()> {
let args: Vec<PathBuf> = std::env::args_os().skip(1).map(PathBuf::from).collect();
pub fn main() -> Result<()> {
let args: Vec<PathBuf> = crate::args_os().skip(1).map(PathBuf::from).collect();
let [root, inputs, output] = args.as_slice() else {
return Err("usage: hud_fixture RESOURCES INPUTS OUTPUT".into());
};
@@ -306,6 +306,7 @@ fn chat(c: &Value, h: i32, atlas: &HudAtlas) -> Vec<GuiDraw> {
height_focused: option("chatHeightFocused"),
height_unfocused: option("chatHeightUnfocused"),
line_spacing: option("chatLineSpacing"),
colors: c["chatColors"].as_bool().unwrap_or(true),
};
for m in c["messages"].as_array().into_iter().flatten() {
// GuiMessageTag indicator colours: systemSinglePlayer and chatError.
@@ -11,8 +11,8 @@ fn number(v: &Value) -> f32 {
fn bits(v: f32) -> u32 {
if v.is_nan() { 0x7fc00000 } else { v.to_bits() }
}
fn main() -> Result<()> {
let args: Vec<_> = std::env::args().skip(1).collect();
pub fn main() -> Result<()> {
let args: Vec<_> = crate::args().skip(1).collect();
let mut out = BufWriter::new(File::create(&args[1])?);
for line in BufReader::new(File::open(&args[0])?).lines() {
let f: Value = serde_json::from_str(&line?)?;
@@ -82,8 +82,8 @@ impl TargetUseWorld for World {
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<_> = std::env::args().collect();
pub fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<_> = crate::args().collect();
let input = std::io::BufReader::new(std::fs::File::open(&args[1])?);
let mut out = std::io::BufWriter::new(std::fs::File::create(&args[2])?);
for (i, line) in input.lines().enumerate() {
@@ -14,8 +14,8 @@ fn unhex(s: &str) -> Vec<u8> {
.collect()
}
fn main() {
let args: Vec<String> = std::env::args().collect();
pub fn main() {
let args: Vec<String> = crate::args().collect();
let cases = std::fs::read_to_string(&args[1]).unwrap();
let mut out = String::new();
let mut games: Vec<LanGame> = Vec::new();
@@ -2,7 +2,7 @@
//! server with Horizon's light engine and compare every SkyLight/BlockLight
//! value with the arrays vanilla stored.
//!
//! cargo run --profile fast -p horizon-client --example light_parity -- \
//! cargo run --profile fast -p horizon-client --example probe -- light_parity \
//! RESOURCES DIMENSION_DIR [--nether] [--chunks X0 Z0 X1 Z1]
use horizon_client::{
assets::Assets,
@@ -21,8 +21,8 @@ use std::{collections::HashMap, path::PathBuf};
type Arrays = HashMap<i32, (Option<Vec<u8>>, Option<Vec<u8>>)>;
fn main() -> horizon_client::Result<()> {
let args: Vec<String> = std::env::args().skip(1).collect();
pub fn main() -> horizon_client::Result<()> {
let args: Vec<String> = crate::args().skip(1).collect();
let root = PathBuf::from(
args.first()
.ok_or("usage: light_parity RESOURCES DIMENSION_DIR")?,
@@ -0,0 +1,178 @@
//! Every horizon-client parity probe in one binary: `probe <name> [arguments]` runs `<name>.rs` as its own
//! example did (tools/test_*_parity.py, tools/oracle_harness.py, tools/audio_harness.py). Linking ~80 examples,
//! each with the whole client in it, was most of the parity job's probe build; one binary builds it once.
//!
//! This file only lists the probes, so that adding one reruns no harness: tools/parity.py leaves it out of
//! their dependencies. How a probe runs is in dispatch.rs.
#[macro_use]
mod dispatch;
use dispatch::{args, args_os};
mod ambient_fixture;
mod arrow_probe;
mod audio_attributes_fixture;
/// Sound-engine fakes the audio probes share.
mod audio_common;
mod auto_jump_probe;
mod block_entity_anim_fixture;
mod block_entity_render_fixture;
mod block_entity_tickers_fixture;
mod block_particles_fixture;
mod block_transform_fixture;
mod breaking_fixture;
mod camera_probe;
mod cauldron_fixture;
mod chat_screen_fixture;
mod cloud_fixture;
mod command_parse_fixture;
mod container_screen_probe;
mod creative_search_fixture;
mod emitter_particles_probe;
mod enchanting_fixture;
mod entity_anim_fixture;
mod entity_components_probe;
mod entity_death_loot_fixture;
mod entity_particles_probe;
mod environment_fixture;
mod finalize_spawn_fixture;
mod fire_starter_sound_probe;
mod firework_tooltip_fixture;
mod first_person_probe;
mod frame_limiter_probe;
mod game_loop_probe;
mod ghost_recipe_fixture;
mod held_drop_fixture;
mod hotbar_fixture;
mod hud_fixture;
mod input_probe;
mod interaction_input_fixture;
mod lan_probe;
mod light_parity;
mod marker_particles_probe;
mod menu_fixture;
mod mipmap_probe;
mod mob_ai_probe;
mod mob_attributes_fixture;
mod music_fixture;
mod natural_spawn_fixture;
mod options_probe;
mod pack_resources_fixture;
mod pack_screen_fixture;
mod pack_selection_fixture;
mod particle_providers_probe;
mod pick_block_data_fixture;
mod place_recipe_fixture;
mod player_sync_fixture;
mod raid_fixture;
mod random_tick_sections_probe;
mod recipe_book_fixture;
mod recipe_book_render_fixture;
mod recipe_book_view_fixture;
mod recipe_fixture;
mod scene_collision_probe;
mod scene_motion_probe;
mod scoreboard_fixture;
mod screen_layout_fixture;
mod section_mesh_fixture;
mod server_files_probe;
mod situational_music_fixture;
mod skin_image_fixture;
mod sound_engine_fixture;
mod sound_event_fixture;
mod spawn_rules_fixture;
mod sprint_probe;
mod subtitle_fixture;
mod tab_list_fixture;
mod team_rules_fixture;
mod toggle_keys_probe;
mod tooltip_display_fixture;
mod trading_fixture;
mod translucency_fixture;
mod weather_fixture;
mod world_border_render_fixture;
mod world_options_fixture;
probes! {
ambient_fixture,
arrow_probe,
audio_attributes_fixture,
auto_jump_probe,
block_entity_anim_fixture,
block_entity_render_fixture,
block_entity_tickers_fixture,
block_particles_fixture,
block_transform_fixture,
breaking_fixture,
camera_probe,
cauldron_fixture,
chat_screen_fixture,
cloud_fixture,
command_parse_fixture,
container_screen_probe,
creative_search_fixture,
emitter_particles_probe,
enchanting_fixture,
entity_anim_fixture,
entity_components_probe,
entity_death_loot_fixture,
entity_particles_probe,
environment_fixture,
finalize_spawn_fixture,
fire_starter_sound_probe,
firework_tooltip_fixture,
first_person_probe,
frame_limiter_probe,
game_loop_probe,
ghost_recipe_fixture,
held_drop_fixture,
hotbar_fixture,
hud_fixture,
input_probe,
interaction_input_fixture,
lan_probe,
light_parity,
marker_particles_probe,
menu_fixture,
mipmap_probe,
mob_ai_probe,
mob_attributes_fixture,
music_fixture,
natural_spawn_fixture,
options_probe,
pack_resources_fixture,
pack_screen_fixture,
pack_selection_fixture,
particle_providers_probe,
pick_block_data_fixture,
place_recipe_fixture,
player_sync_fixture,
raid_fixture,
random_tick_sections_probe,
recipe_book_fixture,
recipe_book_render_fixture,
recipe_book_view_fixture,
recipe_fixture,
scene_collision_probe,
scene_motion_probe,
scoreboard_fixture,
screen_layout_fixture,
section_mesh_fixture,
server_files_probe,
situational_music_fixture,
skin_image_fixture,
sound_engine_fixture,
sound_event_fixture,
spawn_rules_fixture,
sprint_probe,
subtitle_fixture,
tab_list_fixture,
team_rules_fixture,
toggle_keys_probe,
tooltip_display_fixture,
trading_fixture,
translucency_fixture,
weather_fixture,
world_border_render_fixture,
world_options_fixture,
}
@@ -22,8 +22,8 @@ use std::{
sync::Arc,
};
fn main() -> Result<()> {
let args: Vec<String> = std::env::args().collect();
pub fn main() -> Result<()> {
let args: Vec<String> = crate::args().collect();
let root = Path::new(".local/horizon-client/26.3");
let assets = Assets::load(root)?;
let facts = BlockFacts::load(root, assets.registry)?;
@@ -58,11 +58,12 @@ fn state(menu: &Menu) -> String {
.map(|(slot, stack)| format!("{slot}:{}", canon_stack(stack.as_ref())))
.collect();
let line = format!(
"{} | {} |{data} | {} | {}",
"{} | {} |{data} | {} | {} | {}",
slots.join(" "),
canon_stack(menu.carried.as_ref()),
menu.drops.join(";"),
equipped.join(";")
equipped.join(";"),
menu.player_sounds.join(";")
);
if menu.kind() != Kind::Lectern {
return line;
@@ -74,8 +75,8 @@ fn state(menu: &Menu) -> String {
format!("{line} | inv{items}")
}
fn main() -> horizon_client::Result<()> {
let args: Vec<String> = std::env::args().collect();
pub fn main() -> horizon_client::Result<()> {
let args: Vec<String> = crate::args().collect();
let root = Path::new(
args.get(1)
.ok_or("usage: menu_fixture RESOURCES INPUT OUTPUT")?,
@@ -171,6 +172,7 @@ fn main() -> horizon_client::Result<()> {
});
menu.drops.clear();
menu.equipped.clear();
menu.player_sounds.clear();
out += &format!("init {}\n", state(&menu));
for op in case["ops"].as_array().ok_or("ops")? {
let slot = op[0].as_i64().unwrap_or(0) as i32;
@@ -4,8 +4,8 @@
//! rows like VanillaMipmapOracle.
use std::{fmt::Write as _, fs, path::Path};
fn main() -> horizon_client::Result<()> {
let args: Vec<String> = std::env::args().skip(1).collect();
pub fn main() -> horizon_client::Result<()> {
let args: Vec<String> = crate::args().skip(1).collect();
let input = fs::read_to_string(&args[0])?;
let mut lines = input.lines();
let root = Path::new(lines.next().ok_or("no resources root")?);
@@ -759,8 +759,8 @@ fn villager(m: &Mob) -> String {
s
}
fn main() -> horizon_client::Result<()> {
let args: Vec<String> = std::env::args().skip(1).collect();
pub fn main() -> horizon_client::Result<()> {
let args: Vec<String> = crate::args().skip(1).collect();
if args.len() != 3 {
return Err("usage: mob_ai_probe RESOURCES SCENARIOS OUTPUT".into());
}
@@ -6,8 +6,8 @@ use horizon_client::mobs::{Mob, MobTypes};
use horizon_core::geometry::Vec3;
use std::{fmt::Write as _, fs, path::Path, sync::Arc};
fn main() -> horizon_client::Result<()> {
let args: Vec<String> = std::env::args().collect();
pub fn main() -> horizon_client::Result<()> {
let args: Vec<String> = crate::args().collect();
let root = Path::new(
args.get(1)
.ok_or("usage: mob_attributes_fixture RESOURCES OUTPUT")?,
@@ -1,7 +1,6 @@
//! Replays `tools/test_music_parity.py` scripts through Horizon's
//! `MusicManager` over its `SoundEngine`: `music_fixture ROOT INPUT OUTPUT`.
#[path = "audio_common/mod.rs"]
mod common;
use crate::audio_common as common;
use common::{Fake, describe, f, flush};
use horizon_client::music::{Music, MusicFrequency, MusicManager};
use horizon_client::sound_engine::{Bound, MUSIC, SoundEngine, TickContext};
@@ -21,8 +20,8 @@ fn music(spec: &str) -> Option<Music> {
))
}
fn main() {
let args: Vec<String> = std::env::args().collect();
pub fn main() {
let args: Vec<String> = crate::args().collect();
let mut engine = SoundEngine::new(SoundRegistry::load(std::path::Path::new(&args[1])));
engine.listening = true;
let mut backend = Fake::default();
@@ -111,6 +111,8 @@ struct World<'a> {
slime: f32,
starts: Vec<Start>,
players: Vec<([f64; 3], String)>,
/// Where the non-spectators among them are.
nearest: Vec<[f64; 3]>,
respawn: Option<[i32; 3]>,
unloaded: HashSet<(i32, i32)>,
spawn_mobs: bool,
@@ -302,19 +304,8 @@ impl NaturalLevel for World<'_> {
fn structure_spawns(&self) -> &StructureSpawns {
self.s.structures
}
fn nearest_player_distance_sqr(&self, x: f64, y: f64, z: f64) -> Option<f64> {
let mut best: Option<f64> = None;
for (p, mode) in &self.players {
if mode == "spectator" {
continue;
}
let d = [p[0] - x, p[1] - y, p[2] - z];
let d = d[0] * d[0] + d[1] * d[1] + d[2] * d[2];
if best.is_none_or(|b| d < b) {
best = Some(d);
}
}
best
fn nearest_player_positions(&self) -> &[[f64; 3]] {
&self.nearest
}
fn respawn(&self) -> Option<[i32; 3]> {
self.respawn
@@ -517,8 +508,8 @@ fn yaw_text(entity: &str, v: f32) -> String {
}
}
fn main() -> horizon_client::Result<()> {
let args: Vec<String> = std::env::args().collect();
pub fn main() -> horizon_client::Result<()> {
let args: Vec<String> = crate::args().collect();
let root = Path::new(
args.get(1)
.ok_or("usage: natural_spawn_fixture RESOURCES INPUT OUTPUT")?,
@@ -730,6 +721,11 @@ fn main() -> horizon_client::Result<()> {
dimension_id: dimension_id.clone(),
slime: get("slime", "0.0").parse()?,
starts,
nearest: players
.iter()
.filter(|(_, mode)| mode != "spectator")
.map(|(p, _)| *p)
.collect(),
players,
respawn,
unloaded,
@@ -7,8 +7,8 @@ use horizon_client::{
};
use std::{fs, io::Write};
fn main() -> Result<()> {
let args: Vec<_> = std::env::args().skip(1).collect();
pub fn main() -> Result<()> {
let args: Vec<_> = crate::args().skip(1).collect();
let mut names: Vec<_> = fs::read_dir(&args[0])?
.map(|e| e.map(|e| e.path()))
.collect::<std::io::Result<_>>()?;
@@ -30,8 +30,8 @@ fn types(root: &std::path::Path, file: PathBuf, key: &str) -> String {
})
.collect()
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<_> = std::env::args().collect();
pub fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<_> = crate::args().collect();
let input = fs::read_to_string(&args[1])?;
let mut lines = input.lines();
let root: Vec<&str> = lines.next().ok_or("empty input")?.split(' ').collect();
@@ -66,8 +66,8 @@ pub fn encode(draws: &[Draw]) -> Value {
)
}
fn main() -> horizon_client::Result<()> {
let args: Vec<PathBuf> = std::env::args_os().skip(1).map(PathBuf::from).collect();
pub fn main() -> horizon_client::Result<()> {
let args: Vec<PathBuf> = crate::args_os().skip(1).map(PathBuf::from).collect();
let [root, inputs, output] = args.as_slice() else {
return Err("usage: pack_screen_fixture RESOURCES CASES OUTPUT".into());
};
@@ -35,8 +35,8 @@ fn lists(m: &SelectionModel) -> Value {
json!({"unselected": side(false), "selected": side(true)})
}
fn main() -> Result<()> {
let args: Vec<PathBuf> = std::env::args_os().skip(1).map(PathBuf::from).collect();
pub fn main() -> Result<()> {
let args: Vec<PathBuf> = crate::args_os().skip(1).map(PathBuf::from).collect();
let [inputs, output] = args.as_slice() else {
return Err("usage: pack_selection_fixture INPUTS OUTPUT".into());
};
@@ -140,8 +140,8 @@ fn world(assets: &Assets, facts: &BlockFacts, floor: bool, water: bool) -> Resul
Ok(world)
}
fn main() -> Result<()> {
let args: Vec<String> = std::env::args().collect();
pub fn main() -> Result<()> {
let args: Vec<String> = crate::args().collect();
let root = &Path::new(".local/horizon-client").join(horizon_core::VANILLA_VERSION);
let assets = Assets::load(root)?;
let facts = BlockFacts::load(root, assets.registry)?;
@@ -78,8 +78,8 @@ fn case(line: &str) -> Option<String> {
Some(out)
}
fn main() -> horizon_client::Result<()> {
let args: Vec<String> = std::env::args().collect();
pub fn main() -> horizon_client::Result<()> {
let args: Vec<String> = crate::args().collect();
let input = fs::read_to_string(
args.get(1)
.ok_or("usage: pick_block_data_fixture INPUT OUTPUT")?,
@@ -28,8 +28,8 @@ fn state(menu: &Menu) -> String {
)
}
fn main() -> horizon_client::Result<()> {
let args: Vec<String> = std::env::args().collect();
pub fn main() -> horizon_client::Result<()> {
let args: Vec<String> = crate::args().collect();
let usage = "usage: place_recipe_fixture RESOURCES CASES ORDERS OUTPUT";
let root = Path::new(args.get(1).ok_or(usage)?);
let cases = fs::read_to_string(args.get(2).ok_or(usage)?)?;
@@ -8,8 +8,8 @@ fn number(value: &Value) -> f64 {
f64::from_bits(u64::from_str_radix(value.as_str().unwrap(), 16).unwrap())
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<_> = std::env::args().collect();
pub fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<_> = crate::args().collect();
let input = std::io::BufReader::new(std::fs::File::open(&args[1])?);
let mut out = std::io::BufWriter::new(std::fs::File::create(&args[2])?);
for line in input.lines() {
@@ -459,8 +459,8 @@ fn run_codec(c: &BTreeMap<&str, &str>) -> Result<String, String> {
Ok(log.join(" ; "))
}
fn main() -> horizon_client::Result<()> {
let args: Vec<String> = std::env::args().collect();
pub fn main() -> horizon_client::Result<()> {
let args: Vec<String> = crate::args().collect();
let root = Path::new(
args.get(1)
.ok_or("usage: raid_fixture RESOURCES INPUT OUTPUT")?,
@@ -0,0 +1,132 @@
//! Random-tick sections companion for tools/test_random_tick_sections_parity.py:
//! the game's `RandomTickSections` choosing the sections of a chunk that
//! `Behaviours::tick_chunk_random` ticks, after the case's edits (see
//! VanillaRandomTickSectionsOracle for the format; the `#resources` line names
//! the prepared resources). The level the ticks run against reads as air
//! everywhere, so nothing ticks for real: its reads are the drawn positions.
use horizon_client::{
blocks::BlockFacts,
chunk::{Chunk, MIN_Y, Section},
random_tick_sections::RandomTickSections,
};
use horizon_core::{
position::{BlockPos, ChunkPos},
random::LegacyRandom,
};
use horizon_world::{
fluid::FluidKind,
level::{GameRules, LevelAccess, LevelEvent, Ticks, data},
registry::BlockStateId,
};
use std::{cell::RefCell, error::Error, fmt::Write, path::PathBuf, sync::Arc};
struct Air {
reads: RefCell<Vec<BlockPos>>,
random: LegacyRandom,
speed: i32,
}
impl LevelAccess for Air {
fn block_state(&self, pos: BlockPos) -> BlockStateId {
self.reads.borrow_mut().push(pos);
BlockStateId(0)
}
fn set_raw(&mut self, _: BlockPos, _: BlockStateId) -> Option<BlockStateId> {
None
}
fn is_loaded(&self, _: BlockPos) -> bool {
true
}
fn random(&mut self) -> &mut LegacyRandom {
&mut self.random
}
fn game_time(&self) -> i64 {
0
}
fn game_rules(&self) -> GameRules {
GameRules {
random_tick_speed: self.speed,
..Default::default()
}
}
fn emit(&mut self, _: LevelEvent) {}
}
pub fn main() -> Result<(), Box<dyn Error>> {
let args: Vec<String> = crate::args().collect();
let input = std::fs::read_to_string(&args[1])?;
let root = input
.lines()
.find_map(|l| l.strip_prefix("#resources "))
.map(PathBuf::from)
.ok_or("no #resources line")?;
let registry = data::load_registry(&root)?;
let (facts, behaviours) = BlockFacts::load_pair(&root, &registry)?;
let mut output = String::new();
for line in input.lines().filter(|l| !l.starts_with('#')) {
let mut words = line.split(' ');
let mut ticks = Ticks::new(0);
ticks.random_tick_value = words.next().ok_or("no randValue")?.parse()?;
let mut level = Air {
reads: RefCell::default(),
random: LegacyRandom::new(0),
speed: words.next().ok_or("no speed")?.parse()?,
};
let mut chunk = Arc::new(Chunk::empty(0, 0));
let mut cache = RandomTickSections::default();
for word in words {
let op: Vec<&str> = word.split(':').collect();
let int = |i: usize| -> Result<i32, Box<dyn Error>> { Ok(op[i].parse()?) };
match op[0] {
"s" => {
let (section, index) = (int(1)?, int(2)?);
Arc::make_mut(&mut chunk).set(
index & 15,
MIN_Y + section * 16 + (index >> 8),
index >> 4 & 15,
int(3)? as u32,
);
}
"f" => {
let (a, b, m) = (int(2)? as u32, int(3)? as u32, int(4)? as usize);
let values = std::array::from_fn(|i| if i % m == m - 1 { b } else { a });
Arc::make_mut(&mut Arc::make_mut(&mut chunk).sections[int(1)? as usize])
.blocks = Section::from_blocks(&values);
}
"t" => {
output.push('|');
level.reads.borrow_mut().clear();
behaviours.tick_chunk_random(
&mut level,
&mut ticks,
ChunkPos::new(0, 0),
|_, from| cache.get((0, 0), &chunk, &facts).from(from),
);
for pos in level.reads.borrow().iter() {
let state = chunk.block(pos.x, pos.y, pos.z);
let fluid = matches!(
behaviours.fluid(state).kind(),
FluidKind::Lava | FluidKind::FlowingLava
);
write!(
output,
" {},{},{},{}{}",
pos.x,
pos.y,
pos.z,
if behaviours.is_randomly_ticking(state) {
'B'
} else {
'-'
},
if fluid { 'F' } else { '-' },
)?;
}
}
other => return Err(format!("unknown op {other}").into()),
}
}
writeln!(output, " | {}", ticks.random_tick_value)?;
}
std::fs::write(&args[2], output)?;
Ok(())
}
@@ -57,8 +57,8 @@ fn compound(tag: &Tag) -> String {
out
}
fn main() -> horizon_client::Result<()> {
let args: Vec<String> = std::env::args().collect();
pub fn main() -> horizon_client::Result<()> {
let args: Vec<String> = crate::args().collect();
let usage = "usage: recipe_book_fixture RESOURCES CASES OUTPUT";
let root = Path::new(args.get(1).ok_or(usage)?);
let cases = fs::read_to_string(args.get(2).ok_or(usage)?)?;
@@ -30,8 +30,8 @@ fn round(v: f32) -> f32 {
(v * 1e4).round() / 1e4
}
fn main() -> horizon_client::Result<()> {
let args: Vec<String> = std::env::args().collect();
pub fn main() -> horizon_client::Result<()> {
let args: Vec<String> = crate::args().collect();
let usage = "usage: recipe_book_render_fixture RESOURCES CASES OUTPUT";
let root = Path::new(args.get(1).ok_or(usage)?);
let cases = fs::read_to_string(args.get(2).ok_or(usage)?)?;
@@ -15,8 +15,8 @@ use horizon_client::{
use serde_json::Value;
use std::{fmt::Write as _, fs, path::Path};
fn main() -> horizon_client::Result<()> {
let args: Vec<String> = std::env::args().collect();
pub fn main() -> horizon_client::Result<()> {
let args: Vec<String> = crate::args().collect();
let usage = "usage: recipe_book_view_fixture RESOURCES CASES OUTPUT";
let root = Path::new(args.get(1).ok_or(usage)?);
let cases = fs::read_to_string(args.get(2).ok_or(usage)?)?;
@@ -12,8 +12,8 @@ use horizon_client::{
};
use std::{fs, path::Path};
fn main() -> horizon_client::Result<()> {
let args: Vec<String> = std::env::args().collect();
pub fn main() -> horizon_client::Result<()> {
let args: Vec<String> = crate::args().collect();
let root = Path::new(
args.get(1)
.ok_or("usage: recipe_fixture RESOURCES INPUT OUTPUT")?,
@@ -28,8 +28,8 @@ fn vector(value: &Value) -> Result<Vec3> {
};
Ok(Vec3::new(number(0)?, number(1)?, number(2)?))
}
fn main() -> Result<()> {
let args: Vec<_> = std::env::args().skip(1).collect();
pub fn main() -> Result<()> {
let args: Vec<_> = crate::args().skip(1).collect();
if args.len() != 3 {
return Err("usage: scene_collision_probe RESOURCES FIXTURE_DIRECTORY RADIUS".into());
}
@@ -78,6 +78,7 @@ fn main() -> Result<()> {
let mut selected = collisions::EntityCollisions {
blocks: scene,
entities: &entities,
border: None,
};
let movement = world_movement::resolve_world_movement(
&mut selected,
@@ -233,8 +233,8 @@ fn snapshot(out: &mut impl Write, player: &PlayerMotion) -> Result<()> {
writeln!(out, "events {}", events.join(" | "))?;
Ok(())
}
fn main() -> Result<()> {
let args: Vec<_> = std::env::args().skip(1).collect();
pub fn main() -> Result<()> {
let args: Vec<_> = crate::args().skip(1).collect();
if args.len() != 3 {
return Err(
"usage: scene_motion_probe RESOURCES FIXTURE_DIRECTORY --describe|REFERENCE".into(),
@@ -370,8 +370,8 @@ fn score_json(sc: &sb::Score) -> Value {
})
}
fn main() {
let args: Vec<String> = std::env::args().collect();
pub fn main() {
let args: Vec<String> = crate::args().collect();
let registries: Value =
serde_json::from_str(&fs::read_to_string(&args[1]).expect("registries")).expect("json");
let list = |k: &str| -> Vec<String> {
@@ -60,8 +60,8 @@ fn draw_json(d: &ScreenDraw) -> Value {
}
}
fn main() -> horizon_client::Result<()> {
let args: Vec<String> = std::env::args().collect();
pub fn main() -> horizon_client::Result<()> {
let args: Vec<String> = crate::args().collect();
let usage = "usage: screen_layout_fixture RESOURCES INPUT OUTPUT";
let banners = Banners::load(Path::new(args.get(1).ok_or(usage)?));
let input = fs::read_to_string(args.get(2).ok_or(usage)?)?;
@@ -170,8 +170,8 @@ fn scene(
}
Ok(json!({"name": row["name"], "quads": quads}))
}
fn main() -> Result<()> {
let args: Vec<_> = std::env::args().skip(1).collect();
pub fn main() -> Result<()> {
let args: Vec<_> = crate::args().skip(1).collect();
if args.len() != 3 {
return Err(
"usage: section_mesh_fixture RESOURCES (INPUT.jsonl | facts) OUTPUT.jsonl".into(),
@@ -11,8 +11,8 @@ use horizon_client::server::{
use horizon_protocol::crypto::uuid_string;
use std::{fmt::Write as _, path::Path};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut args = std::env::args().skip(1);
pub fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut args = crate::args().skip(1);
let root = std::path::PathBuf::from(args.next().ok_or("CASES OUTPUT")?);
let output = args.next().ok_or("OUTPUT")?;
let mut cases: Vec<_> = std::fs::read_dir(&root)?
@@ -5,8 +5,8 @@ use horizon_client::music::{BackgroundMusic, Music, MusicTables, Situation};
use horizon_client::sound_events::normalize;
use std::io::Write;
fn main() {
let args: Vec<String> = std::env::args().collect();
pub fn main() {
let args: Vec<String> = crate::args().collect();
let tables = MusicTables::load(std::path::Path::new(&args[1]));
let input = std::fs::read_to_string(&args[2]).expect("input");
let mut out = std::io::BufWriter::new(std::fs::File::create(&args[3]).expect("output"));
@@ -9,8 +9,8 @@ use std::{
};
type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
fn main() -> Result<()> {
let args: Vec<PathBuf> = std::env::args_os().skip(1).map(PathBuf::from).collect();
pub fn main() -> Result<()> {
let args: Vec<PathBuf> = crate::args_os().skip(1).map(PathBuf::from).collect();
let [inputs, output] = args.as_slice() else {
return Err("usage: skin_image_fixture INPUTS OUTPUT".into());
};
@@ -1,8 +1,7 @@
//! Replays `tools/test_sound_engine_parity.py` scripts through Horizon's
//! `SoundEngine` with a backend recording what an OpenAL source would hold:
//! `sound_engine_fixture ROOT INPUT OUTPUT`.
#[path = "audio_common/mod.rs"]
mod common;
use crate::audio_common as common;
use common::{Fake, d, describe, f, flush, result_name};
use horizon_client::sound_engine::{
Attenuation, Backend, CartSound, InstanceId, SoundEngine, SoundInstance, TickContext,
@@ -12,8 +11,8 @@ use horizon_core::random::LegacyRandom;
use std::collections::BTreeMap;
use std::io::Write;
fn main() {
let args: Vec<String> = std::env::args().collect();
pub fn main() {
let args: Vec<String> = crate::args().collect();
let mut engine = SoundEngine::new(SoundRegistry::load(std::path::Path::new(&args[1])));
engine.listening = true;
let mut backend = Fake::default();
@@ -9,8 +9,8 @@ fn bits(v: f32) -> String {
format!("{:08x}", v.to_bits())
}
fn main() {
let args: Vec<String> = std::env::args().collect();
pub fn main() {
let args: Vec<String> = crate::args().collect();
let registry = SoundRegistry::load(std::path::Path::new(&args[1]));
let mut engine = SoundEngine::new(registry);
let input = std::fs::read_to_string(&args[2]).expect("input");
@@ -136,8 +136,8 @@ fn reason(name: &str) -> Reason {
}
}
fn main() -> horizon_client::Result<()> {
let args: Vec<String> = std::env::args().collect();
pub fn main() -> horizon_client::Result<()> {
let args: Vec<String> = crate::args().collect();
let root = Path::new(
args.get(1)
.ok_or("usage: spawn_rules_fixture RESOURCES INPUT OUTPUT")?,
@@ -20,8 +20,8 @@ fn keys(mask: u64) -> Keys {
analog: None,
}
}
fn main() -> Result<()> {
let args: Vec<_> = std::env::args().skip(1).collect();
pub fn main() -> Result<()> {
let args: Vec<_> = crate::args().skip(1).collect();
let mut out = BufWriter::new(File::create(&args[1])?);
for line in BufReader::new(File::open(&args[0])?).lines() {
let f: Value = serde_json::from_str(&line?)?;
@@ -21,8 +21,8 @@ fn pose(m: [f32; 6]) -> Value {
json!(m)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<PathBuf> = std::env::args_os().skip(1).map(PathBuf::from).collect();
pub fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<PathBuf> = crate::args_os().skip(1).map(PathBuf::from).collect();
let [root, inputs, output] = args.as_slice() else {
return Err("usage: subtitle_fixture RESOURCES INPUTS OUTPUT".into());
};
@@ -53,8 +53,8 @@ fn runs(t: &Text) -> Value {
out.into_iter().map(|(r, k)| json!([r, k])).collect()
}
fn main() -> Result<()> {
let args: Vec<PathBuf> = std::env::args_os().skip(1).map(PathBuf::from).collect();
pub fn main() -> Result<()> {
let args: Vec<PathBuf> = crate::args_os().skip(1).map(PathBuf::from).collect();
let [root, inputs, output] = args.as_slice() else {
return Err("usage: tab_list_fixture RESOURCES INPUTS OUTPUT".into());
};
@@ -8,8 +8,8 @@ use horizon_client::scoreboard::{self as sb, CollisionRule, NameView, Scoreboard
use serde_json::{Value, json};
use std::{fs, io::Write};
fn main() {
let args: Vec<String> = std::env::args().collect();
pub fn main() {
let args: Vec<String> = crate::args().collect();
let mut out = std::io::BufWriter::new(fs::File::create(&args[2]).expect("output"));
for line in fs::read_to_string(&args[1]).expect("inputs").lines() {
let c: Value = serde_json::from_str(line).expect("case");
@@ -11,8 +11,8 @@ use std::{
io::{BufRead, BufReader, BufWriter, Write},
};
fn main() -> Result<()> {
let args: Vec<String> = std::env::args().collect();
pub fn main() -> Result<()> {
let args: Vec<String> = crate::args().collect();
let input = BufReader::new(File::open(&args[1])?);
let mut out = BufWriter::new(File::create(&args[2])?);
for (n, line) in input.lines().enumerate() {
@@ -3,8 +3,8 @@ use horizon_client::{item_tooltip::TooltipDisplay, items::ItemStack};
use horizon_world::nbt::Tag;
use std::{error::Error, fmt::Write};
fn main() -> Result<(), Box<dyn Error>> {
let args: Vec<String> = std::env::args().collect();
pub fn main() -> Result<(), Box<dyn Error>> {
let args: Vec<String> = crate::args().collect();
let mut output = String::new();
for line in std::fs::read_to_string(&args[1])?.lines() {
let fields: Vec<&str> = line.split('\t').collect();
@@ -231,8 +231,8 @@ fn prepare(m: &mut Merchant, gossips: &mut Gossips, kv: &HashMap<String, String>
}
}
fn main() -> horizon_client::Result<()> {
let args: Vec<String> = std::env::args().collect();
pub fn main() -> horizon_client::Result<()> {
let args: Vec<String> = crate::args().collect();
let usage = "usage: trading_fixture RESOURCES INPUT VANILLA_OUTPUT OUTPUT";
let root = Path::new(args.get(1).ok_or(usage)?);
let input = fs::read_to_string(args.get(2).ok_or(usage)?)?;
@@ -14,8 +14,8 @@ struct Case {
cameras: Vec<[f64; 3]>,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<_> = std::env::args().skip(1).collect();
pub fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<_> = crate::args().skip(1).collect();
let [input, output] = args.as_slice() else {
return Err("usage: translucency_fixture INPUT OUTPUT".into());
};
Loaded 100 of 869 files, more files were not shown because too many files have changed in this diff. Show more