Compare commits

...
Author SHA1 Message Date
Ludovic Marie c002989c82 union: #2126 fix 2026-10-27 09:08:11 +01:00
Ludovic MarieandClaude Opus 5.5 523280e781 Draw the new level-event sound pitches from the client level's random
LevelEventHandler plays 1501, 1502, 1046 and 1047 with
ClientLevel.getRandom(), not the server level's random: drawing them from
the world's random moved the level random in use_item_on's respawn
anchor cases (12 of 4000 differed).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 09:07:42 +01:00
Ludovic Marie 65b0cb2521 union: #2261
# Conflicts:
#	crates/horizon-client/src/game/potions.rs
2026-10-27 07:15:26 +01:00
Ludovic Marie add777206d union: #2259
# Conflicts:
#	crates/horizon-client/src/game/advancements.rs
2026-10-27 07:13:37 +01:00
Ludovic Marie 91c84ca76d union: #2257
# Conflicts:
#	crates/horizon-client/src/game.rs
2026-10-27 07:12:10 +01:00
Ludovic Marie de23ad4338 union: #2256
# Conflicts:
#	crates/horizon-client/src/game.rs
2026-10-27 07:11:33 +01:00
Ludovic Marie 56108d1b37 union: #2254
# Conflicts:
#	crates/horizon-client/src/game/advancements.rs
2026-10-27 07:10:55 +01:00
Ludovic Marie e6bd4cf76c union: #2252
# Conflicts:
#	crates/horizon-client/src/game/combat.rs
#	crates/horizon-client/src/game/wither.rs
2026-10-27 07:10:54 +01:00
Ludovic Marie cf7e635cb0 union: #2251
# Conflicts:
#	crates/horizon-client/src/game/advancements.rs
2026-10-27 07:09:35 +01:00
Ludovic Marie d6f9367ff1 union: #2245
# Conflicts:
#	crates/horizon-client/src/advancements.rs
#	crates/horizon-client/src/game/advancements.rs
#	crates/horizon-client/src/game/creatures.rs
2026-10-27 07:09:28 +01:00
Ludovic Marie fbbf0ac64c union: #2243
# Conflicts:
#	crates/horizon-client/src/game/creatures.rs
2026-10-27 07:08:22 +01:00
Ludovic Marie c07049a77f union: #2235
# Conflicts:
#	crates/horizon-client/src/game/potions.rs
2026-10-27 07:08:01 +01:00
Ludovic Marie 83fcd89847 union: #2233
# Conflicts:
#	crates/horizon-client/src/game/advancements.rs
2026-10-27 07:05:38 +01:00
Ludovic Marie 6f67726a5b union: #2229
# Conflicts:
#	crates/horizon-client/src/game/advancements.rs
2026-10-27 07:05:11 +01:00
Ludovic Marie 1318853025 union: #2280 2026-10-27 07:04:26 +01:00
Ludovic Marie 3fef8426d4 union: #2275 2026-10-27 07:04:26 +01:00
Ludovic Marie bd374a9f30 union: #2271 2026-10-27 07:04:26 +01:00
Ludovic Marie f1cb0920ce union: #2268 2026-10-27 07:04:26 +01:00
Ludovic Marie b2604a3963 union: #2267 2026-10-27 07:04:26 +01:00
Ludovic Marie 18d98c064a union: #2266 2026-10-27 07:04:26 +01:00
Ludovic Marie 686697840d union: #2265 2026-10-27 07:04:26 +01:00
Ludovic Marie dcfab95680 union: #2264 2026-10-27 07:04:26 +01:00
Ludovic Marie d560f1534f union: #2263 2026-10-27 07:04:26 +01:00
Ludovic Marie 8194ef9740 union: #2262 2026-10-27 07:04:26 +01:00
Ludovic Marie 2cb20cda62 union: #2260 2026-10-27 07:04:25 +01:00
Ludovic Marie fa09f3486f union: #2258 2026-10-27 07:04:25 +01:00
Ludovic Marie b3e41ac6b5 union: #2255 2026-10-27 07:04:25 +01:00
Ludovic Marie 6cead02e34 union: #2253 2026-10-27 07:04:25 +01:00
Ludovic Marie 02f0970cd4 union: #2250 2026-10-27 07:04:25 +01:00
Ludovic Marie cfbbc077cc union: #2249 2026-10-27 07:04:25 +01:00
Ludovic Marie db87ac7956 union: #2248 2026-10-27 07:04:25 +01:00
Ludovic Marie 5fc4723bab union: #2247 2026-10-27 07:04:25 +01:00
Ludovic Marie c3198c9c17 union: #2246 2026-10-27 07:04:25 +01:00
Ludovic Marie 3c83854938 union: #2244 2026-10-27 07:04:25 +01:00
Ludovic Marie 3e10fb465b union: #2242 2026-10-27 07:04:25 +01:00
Ludovic Marie 06f666db57 union: #2241 2026-10-27 07:04:25 +01:00
Ludovic Marie 789326bb92 union: #2240 2026-10-27 07:04:25 +01:00
Ludovic Marie c8d55feca3 union: #2239 2026-10-27 07:04:25 +01:00
Ludovic Marie 3f8f346851 union: #2238 2026-10-27 07:04:25 +01:00
Ludovic Marie 755a6acc76 union: #2237 2026-10-27 07:04:25 +01:00
Ludovic Marie 21117506d2 union: #2236 2026-10-27 07:04:24 +01:00
Ludovic Marie ed6125756c union: #2234 2026-10-27 07:04:24 +01:00
Ludovic Marie 8ae648d67d union: #2232 2026-10-27 07:04:24 +01:00
Ludovic Marie 7c92e49971 union: #2231 2026-10-27 07:04:24 +01:00
Ludovic Marie 6f29f197a0 union: #2230 2026-10-27 07:04:24 +01:00
Ludovic Marie e6605b2364 union: #2228 2026-10-27 07:04:24 +01:00
Ludovic Marie 11d72958f4 union: #2227 2026-10-27 07:04:24 +01:00
Ludovic Marie e304294056 union: #2226 2026-10-27 07:04:24 +01:00
Ludovic Marie 0904969572 union: #2225 2026-10-27 07:04:24 +01:00
Ludovic Marie a2950bce2f union: #2224 2026-10-27 07:04:24 +01:00
Ludovic Marie 4f6b549e5d union: #2223 2026-10-27 07:04:24 +01:00
Ludovic Marie 1397756847 union: #2222 2026-10-27 07:04:24 +01:00
Ludovic Marie 71827f3b4d union: #2221 2026-10-27 07:04:24 +01:00
Ludovic Marie 2cb278ab3c union: #2220 2026-10-27 07:04:24 +01:00
Ludovic Marie 56f2552241 union: #2219 2026-10-27 07:04:24 +01:00
Ludovic Marie aae895a096 union: #2218 2026-10-27 07:04:23 +01:00
Ludovic Marie e414d77f68 union: #2136 2026-10-27 07:04:23 +01:00
Ludovic Marie c33e47a33b union: #2135 2026-10-27 07:04:23 +01:00
Ludovic Marie 260b564fe1 union: #2134 2026-10-27 07:04:23 +01:00
Ludovic Marie 9d008e809f union: #2133 2026-10-27 07:04:23 +01:00
Ludovic Marie 98d98f4dde union: #2132 2026-10-27 07:04:23 +01:00
Ludovic Marie 6def29e16f union: #2129 2026-10-27 07:04:23 +01:00
Ludovic Marie fab334a930 union: #2128 2026-10-27 07:04:23 +01:00
Ludovic Marie fe0d9733ad union: #2126 2026-10-27 07:04:23 +01:00
Ludovic Marie b9381720d4 union: #2125 2026-10-27 07:04:23 +01:00
Ludovic Marie e70f4650ab union: #2124 2026-10-27 07:04:23 +01:00
Ludovic Marie 1c71afafab union: #2123 2026-10-27 07:04:23 +01:00
Ludovic Marie 8ff13ea185 union: #2121 2026-10-27 07:04:23 +01:00
Ludovic Marie 9f89c65d34 union: #2119 2026-10-27 07:04:23 +01:00
Ludovic Marie f2460134fe union: #2116 2026-10-27 07:04:23 +01:00
Ludovic Marie f0e1d992ef union: #2115 2026-10-27 07:04:23 +01:00
Ludovic Marie e60ce10870 union: #2111 2026-10-27 07:04:22 +01:00
Ludovic Marie 292cecbda3 union: #2099 2026-10-27 07:04:22 +01:00
Ludovic Marie 5211bdd224 union: #1969 2026-10-27 07:04:22 +01:00
Ludovic MarieandClaude Opus 5.5 e42a82e9b9 Name the difficulty id field as #2239 does
#2239 adds the same LocalDifficulty field as `id`; with the same lines
here the two branches merge without a conflict.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 07:03:08 +01:00
Ludovic MarieandClaude Opus 5.5 d77569c672 The dragon's ambient pitch draws from its own random
LivingEntity.getVoicePitch uses this.random, the same random the
ambient roll draws from.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 07:02:35 +01:00
Ludovic MarieandClaude Opus 5.5 937dcba5b8 A TNT minecart's fuse blast uses its horizontal speed only
MinecartTNT.tick calls explode(getDeltaMovement().horizontalDistanceSqr())
when the fuse runs out; the vertical-speed branch has no vanilla source.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 07:02:05 +01:00
Ludovic MarieandClaude Opus 5.5 d8b49e8697 Merge main into fix/2213-frog-speed
Keeps both registrations: the frog's move control and main's turtle.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 07:01:02 +01:00
Ludovic Marie c1ebf5d1b0 Merge pull request #2626 from ludovic111/feat/launcher-instance-commands
🤖 Agent: launcher-features-1: base main, merge-tree clean against main, CI verify + launcher green; queued.
2026-10-27 07:00:52 +01:00
Ludovic MarieandClaude Opus 5.5 71b8f39eb5 Merge main into fix/allay-delivery-and-death-drops
Keeps both death drops: the allay's load and main's piglin inventory.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 07:00:33 +01:00
Ludovic MarieandClaude Opus 5.5 36f6ab9839 Infested, Weaving and Wind Charged draw from the bearer's random
InfestedMobEffect.onMobHurt, WeavingMobEffect.spawnCobwebsRandomlyAround
and WindChargedMobEffect.onMobRemoved all use mob.getRandom(): a mob's
own random, the world's for the player as before.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 06:59:59 +01:00
Ludovic MarieandClaude Opus 5.5 d3b28edfb6 Merge main into fix/death-and-hurt-effects
The Infested event stays immediate (onMobHurt runs in hurtServer); the
other events a hit raises are left in the outbox for the end of the
tick, as on main.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 06:58:20 +01:00
Ludovic MarieandClaude Opus 5.5 9c49424b4d Merge main into fix/wither-rose-kills
A blast's attacker is the projectile's owner, else main's exploding mob
(getIndirectSourceEntity); both CombatTracker fields kept.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 06:57:29 +01:00
Ludovic MarieandClaude Opus 5.5 6a65ee1124 A guest saved by a totem is not a player kill
The guest's health goes through the same totem path as the host's
(applied on the next tick_totem), so the kill check skips a fired totem
for both.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 06:56:46 +01:00
Ludovic MarieandClaude Opus 5.5 ad70f755d7 Merge main into fix/stats-combat
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 06:56:43 +01:00
Ludovic MarieandClaude Opus 5.5 d2ca443d6f Merge main into fix/mob-effect-merge
Keeps this branch's add_effect (infinite durations, isShorterDurationThan)
over main's similar fix, and main's hidden_effects cleanup with infinite
effects kept.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 06:56:05 +01:00
Ludovic MarieandClaude Opus 5.5 716cc7f102 Merge main into fix/crossbow-advancement-triggers
Keeps main's piercing count with this branch's fired_from_weapon.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 06:55:21 +01:00
Ludovic MarieandClaude Opus 5.5 64279fa1ce Merge main into fix/detector-rail-container-signal
Keeps both GameLevel fields: cart_signals and main's arrow_boxes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 06:54:47 +01:00
Ludovic MarieandClaude Opus 5.5 c35436a7da Merge main into fix/instant-effects-rounding-splash-distance
Splashes carry both the potion's age (for the distance) and main's
thrower (for the indirectMagic source).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 06:54:15 +01:00
Ludovic MarieandClaude Opus 5.5 393b8ef7e6 Merge main into fix/player-hurt-entity-trigger
The trident's PLAYER_HURT_ENTITY now keys off main's hurt_at (kept for
Channeling).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 06:53:29 +01:00
Ludovic MarieandClaude Opus 5.5 15be176380 Merge main into fix/fishing-rod-hooked-trigger
Keeps the hooked trigger with main's item-entity damage (3) and main's
fish_caught per #fishes catch.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 06:52:43 +01:00
Ludovic MarieandClaude Opus 5.5 8905aaf0dd Merge main into fix/armadillo-scutes
Keeps this branch's ShedScute event and main's GiftLoot (with its mob
and the turtle's mob_drops check).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 06:51:37 +01:00
Ludovic Marie 9ad735b9d7 Merge pull request #2625 from ludovic111/feat/launcher-shortcuts
🤖 Agent: launcher-features-1: base main, merge-tree clean against main, CI verify + launcher green; queued.
2026-10-27 06:51:21 +01:00
Ludovic MarieandClaude Opus 5.5 b463f1c80a Waxed hanging signs play their own failed-interaction sound
HangingSignBlockEntity.getSignInteractionFailedSoundEvent is
block.hanging_sign.waxed_interact_fail, not the standing sign's.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 06:50:22 +01:00
Ludovic MarieandClaude Opus 5.5 a8ccf43d2b Merge main into fix/sign-reopen-and-applicators
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 06:49:57 +01:00
Ludovic MarieandClaude Opus 5.5 6964ee8ece Merge main into fix/stats-movement
Keeps both fall arms: the fall_one_cm statistic and main's landing dust.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 06:48:18 +01:00
Ludovic Marie 6f19f06634 Merge pull request #2350 from kacpherek/fix/mining-hit-sound-level-events
🤖 Agent: verified against vanilla 26.3 (mining hit sounds now come from the level events, once per hit as MultiPlayerGameMode/LevelRenderer do). I merged main into the branch (one conflict in level_particles.rs, both functions kept). Tested on a tree of main + this PR + #2279: the 33 harnesses its files reach passed, and two real games over LAN (host in-frame and on its server thread) heard 5-7 stone hits per block, with digging, cracks, instant digging, death-while-mining, respawn and hurt checks passing. Thanks @kacpherek!
2026-10-27 06:48:05 +01:00
Ludovic MarieandClaude Opus 5.5 bbc1ffb408 Merge main into fix/invisibility-mob-detection
Keeps both PlayerView methods: visibility_percent and main's has_effect.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 06:47:27 +01:00
Ludovic MarieandClaude Opus 5.5 4c78d32e36 Merge main into fix/explosion-beehive-bees
Keeps main's ExplosionSource event and charged_exploders, and this
branch's Species on Explode and hive_emptying_blast.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 06:46:09 +01:00
Ludovic MarieandClaude Opus 5.5 4516f54e2b Merge main into fix/furnace-wet-sponge-bucket
Keeps main's recipe_used_key with this branch's wet sponge bucket fill.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 06:44:25 +01:00
Ludovic MarieandClaude Opus 5.5 9809bf38b6 Merge main into fix/remote-hurt-tilt
indicateDamage now uses main's knockback direction, which is zero when
the hit has no source position (as in dealDefaultKnockback).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 06:43:52 +01:00
Ludovic MarieandClaude Opus 5.5 38cd3095e5 Merge main into fix/missing-level-events
Keeps both sets of level-event sounds: this branch's chorus flower and
dripstone-into-cauldron, and main's zombie door and potion splashes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 06:43:02 +01:00
Ludovic MarieandClaude Opus 5.5 c752c21246 Merge main into fix/remote-entity-events
Keeps both entity-event handlers in remote.rs: this branch's events 20,
6/7/12/13/14/18 and 3, and main's honey slide (53).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 06:42:22 +01:00
Ludovic MarieandClaude Opus 5.5 82055fb57d Merge main into fix/1893-crystal-explosion
lan_explode now takes main's `small` flag; the explosions a guest's
turn puts off keep it too.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 06:41:27 +01:00
Ludovic MarieandClaude Opus 5.5 739d4fe187 Merge main into fix/2095-signal-save
Keeps both: the stop-signal counter from this branch and main's
release_free_memory.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 06:40:30 +01:00
Ludovic Marie 89225ec081 Merge pull request #2279 from kacpherek/fix/2276-dead-hurt-tilt
🤖 Agent: verified against vanilla 26.3: LivingEntity.baseTick counts hurtTime down for a dead or riding player too, so the hurt tilt no longer restarts every frame and shakes the camera on the server-thread path. Tested on a tree of main + #2350 + this PR: the 33 harnesses its files reach passed, and the LAN hurt, death-while-mining and respawn checks passed with the host in-frame and on its server thread. Thanks @kacpherek!
2026-10-27 06:39:59 +01:00
Ludovic MarieandClaude Opus 5.5 24744aae44 Merge main into fix/1361-lingering-cloud
Resolves potions.rs against main's thrower tracking: splashes keep
their indirectMagic source, and a lingering cloud keeps the thrower too
(AreaEffectCloud.getOwner), so its instant damage names them, else the
cloud.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 06:39:43 +01:00
Ludovic Marie 6044c6ff88 Merge pull request #2623 from ludovic111/feat/launcher-hide-on-play
🤖 Agent: launcher-features-1: base main, merge-tree clean against main, CI verify + launcher green; queued.
2026-10-27 06:30:43 +01:00
Ludovic Marie d74d39cb05 Merge pull request #2622 from ludovic111/feat/launcher-console
🤖 Agent: launcher-features-1: base main, merge-tree clean against main, CI verify + launcher green; queued.
2026-10-27 06:21:26 +01:00
Ludovic MarieandClaude Opus 5.5 73aef0ce8e Merge main into fix/mining-hit-sound-level-events
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 06:17:53 +01:00
Ludovic Marie 91e332a5eb Merge pull request #2621 from ludovic111/feat/launcher-import-ui
🤖 Agent: launcher-features-1: base main, merge-tree clean against main, CI verify + launcher green; queued.
2026-10-27 06:10:22 +01:00
Ludovic Marie a8fe843daa Merge pull request #2620 from ludovic111/feat/launcher-import
🤖 Agent: launcher-features-1: base main, merge-tree clean against main, CI verify + launcher green; queued.
2026-10-27 06:01:09 +01:00
Ludovic Marie 37d31da543 Merge pull request #2473 from kacpherek/fix/piglin-dance
🤖 Agent: Merged by Ludovic's AI agent (orchestrator). Reviewed and approved; verify passed. The two parity failures (server_player_sculk, remote_fireworks) are the load flakes fixed by #2629, which merged after this run started (03:30Z); both pass on the Mac mini at this head. Thanks @kacpherek!
2026-10-27 05:49:18 +01:00
Ludovic Marie c5165a98cf Merge pull request #2618 from ludovic111/feat/launcher-export-ui
🤖 Agent: launcher-features-1: base main, merge-tree clean against main, CI verify + launcher green; queued.
2026-10-27 05:48:52 +01:00
Ludovic Marie e1f7ad93ee Merge pull request #2617 from ludovic111/feat/launcher-export
🤖 Agent: launcher-features-1: base main, merge-tree clean against main, CI verify + launcher green; queued.
2026-10-27 05:30:25 +01:00
Ludovic Marie 7731b4274a Merge pull request #2616 from ludovic111/feat/launcher-datapack-browser
🤖 Agent: launcher-features-1: base main, merge-tree clean against main, CI verify + launcher green; queued.
2026-10-27 05:21:08 +01:00
Ludovic Marie 7df621fdb5 Merge pull request #2614 from ludovic111/feat/launcher-mods-per-instance
🤖 Agent: launcher-features-1: base main after #2612 merged, merge-tree clean against main, CI verify + launcher green; queued.
2026-10-27 05:19:29 +01:00
Ludovic Marie 3eb3d1fc5f Merge pull request #2612 from ludovic111/feat/launcher-datapacks
🤖 Agent: launcher-features-1: base main after #2609 merged, merge-tree clean against main, CI verify + launcher green; queued.
2026-10-27 05:10:09 +01:00
Ludovic Marie 3052cc41db Merge pull request #2629 from ludovic111/fix/window-harnesses-under-load
🤖 Agent: playhorizon: remote.sh runs on CPUs 0-3 (CI keeps 4-7); world_reopen and server_player_sculk count ticks; 6 of 6 under load.
2026-10-27 05:10:05 +01:00
Ludovic Marie 43f5388038 Merge pull request #2464 from kacpherek/fix/damage-command-player-source
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `/damage` builds `DamageSource(type, cause, by)` with the location or the direct entity's position for the knockback, and the causing mob gets the kill credit. It touched the same code as #2452, so the merged tree uses your `hurt_player_from` for both. Main merged in at f1f587e7c after #2452 landed (world_cmds.rs and creatures.rs: one `hurt_player_from` path that also records the direct entity, as in the union): clippy and the horizon-client lib and bin tests pass on it, and CI parity runs on this head. Gate: this wave's 50 PRs merged on main in one tree (union 9033a9c3b): fmt and clippy `-D warnings` clean, the horizon-client (1136 lib plus the integration tests), horizon-client-core (37), horizon-world (165) and horizon-protocol (100) tests pass, the fast build links, and the 140 harnesses `parity.py` picks for the tree's changed files pass on playhorizon (remote_fireworks, a known load flake there, passed on a rerun on a quiet Mac mini). `lan_e2e.py` passes 14 checks with the host on its server thread and in-frame (chest, xp, damage, kills, projectiles, mobs, drops, body, stopsound, digging, instant_digging, books, knockback, barter); `hurt` fails on main too when run after `damage` (check-order dependency, not this wave). Thanks @kacpherek!
2026-10-27 05:02:10 +01:00
Ludovic Marie 08492a3007 Merge pull request #2609 from ludovic111/feat/launcher-update-all
🤖 Agent: launcher-features-1: base main, merge-tree clean against main, CI verify + launcher green; queued.
2026-10-27 04:50:37 +01:00
Ludovic Marie 3d0ef4da75 Merge pull request #2441 from kacpherek/fix/effect-swirls
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `LivingEntity.tickEffects` draws the swirls from `DATA_EFFECT_PARTICLES` and `DATA_EFFECT_AMBIENCE_ID`, only for visible effects, and your follow-up keeps `hideParticles` out of both, which matches. Main merged in at 0ab24d080 after #2432 landed (sprint dust and effect swirls both kept in client_ambient.rs and lan.rs, as in the union): clippy and the horizon-client lib and bin tests pass on it, and CI parity runs on this head. Gate: this wave's 50 PRs merged on main in one tree (union 9033a9c3b): fmt and clippy `-D warnings` clean, the horizon-client (1136 lib plus the integration tests), horizon-client-core (37), horizon-world (165) and horizon-protocol (100) tests pass, the fast build links, and the 140 harnesses `parity.py` picks for the tree's changed files pass on playhorizon (remote_fireworks, a known load flake there, passed on a rerun on a quiet Mac mini). `lan_e2e.py` passes 14 checks with the host on its server thread and in-frame (chest, xp, damage, kills, projectiles, mobs, drops, body, stopsound, digging, instant_digging, books, knockback, barter); `hurt` fails on main too when run after `damage` (check-order dependency, not this wave). Thanks @kacpherek!
2026-10-27 04:48:54 +01:00
Ludovic MarieandClaude Opus 5.5 a2f62b1545 parity_hosts: playhorizon's workers run on CPUs 0-3, 3 harnesses at a time
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 04:42:19 +01:00
Ludovic MarieandClaude Opus 5.5 cb20bb508b remote.sh: runs get CPUs 0-3 by default; benchmarks hold them with --exclusive
playhorizon's CI runner has CPUs 4-7, and remote.sh pinned agents' runs there
too: CPUs 4-7 sat at 100% while 0-3 idled, and window harnesses in pull
requests' parity jobs lost ticks. Runs share a lock per CPU set; an
--exclusive run (a benchmark) holds it alone. world_reopen also waits for
the world to load before its first count, as after each reopen.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 04:31:40 +01:00
Ludovic Marie 4168590221 Merge remote-tracking branch 'origin/main' into fix/window-harnesses-under-load 2026-10-27 04:30:35 +01:00
Ludovic MarieandClaude Opus 5.5 2a7b79dfb6 Merge main into this branch (resolves the conflict with main)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 04:26:15 +01:00
Ludovic MarieandClaude Opus 5.5 f1f587e7c2 Merge main into this branch (resolves the conflict with main)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 04:24:30 +01:00
Ludovic Marie 51447edda4 Merge pull request #2604 from ludovic111/feat/launcher-test-seams
🤖 Agent: launcher-features-1 checked this: base main after #2602 merged, clean merge with main, launcher CI green; fmt, clippy -D warnings and the launcher tests pass locally. Queued for merge.
2026-10-27 04:23:13 +01:00
Ludovic Marie 4018fa9559 Merge pull request #2452 from kacpherek/fix/magic-death-message
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3's `DamageSource.getLocalizedDeathMessage` (the causing entity, else the direct one: the potion or cloud) and `indirectMagic`. I pushed one fix: with no entity at all, vanilla sends the one-argument message and `TranslatableContents.decompose` shows a template it can't fill as written, so I removed the fallback to `magic` / `generic`. Main merged in at e338690bf after #2442 landed (potions.rs: `splash` keeps both the owner and the block): clippy and the horizon-client lib and bin tests pass on it, and CI parity runs on this head. Gate: this wave's 50 PRs merged on main in one tree (union 9033a9c3b): fmt and clippy `-D warnings` clean, the horizon-client (1136 lib plus the integration tests), horizon-client-core (37), horizon-world (165) and horizon-protocol (100) tests pass, the fast build links, and the 140 harnesses `parity.py` picks for the tree's changed files pass on playhorizon (remote_fireworks, a known load flake there, passed on a rerun on a quiet Mac mini). `lan_e2e.py` passes 14 checks with the host on its server thread and in-frame (chest, xp, damage, kills, projectiles, mobs, drops, body, stopsound, digging, instant_digging, books, knockback, barter); `hurt` fails on main too when run after `damage` (check-order dependency, not this wave). Thanks @kacpherek!
2026-10-27 04:23:09 +01:00
Ludovic MarieandClaude Opus 5.5 7115c708e1 launcher: pre-launch command, wrapper and environment per instance
Home's Instance row gets Launch...: a shell command run in the instance's
folder before the game (which starts only if it succeeds), a wrapper the
game runs through, and environment variables, kept in instances.json.
The game stays one process: /bin/sh runs the command and execs the
wrapper and game through "$@" on Unix, cmd /C on Windows. The window's
Play and `play` both use them. CLI and MCP: instance launch (confirm
needed over MCP). The Instance row's actions move under its name, and
editing an instance that shares its name with an import works again.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 04:22:34 +01:00
Ludovic Marie 95e7cd69dd Merge pull request #2602 from ludovic111/feat/launcher-packs-ui
🤖 Agent: launcher-features-1 checked this: base main after #2601 merged, clean merge with main, launcher CI green; fmt, clippy -D warnings and the launcher tests pass locally. Queued for merge.
2026-10-27 04:16:39 +01:00
Ludovic Marie 5a6ae76ec3 Merge pull request #2486 from ludovic111/fix/parity-reap-orphans
🤖 Agent: parity.py reaps the harness processes a failed or timed-out run leaves behind; parity passed on playhorizon.
2026-10-27 04:16:36 +01:00
Ludovic Marie 4a523b5ce8 Merge pull request #2484 from kacpherek/fix/2463-break-block-lagback
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: breaking the block under you no longer sends a position correction the client didn't need. Fixes #2463. Gate: this wave's 50 PRs merged on main in one tree (union 9033a9c3b): fmt and clippy `-D warnings` clean, the horizon-client (1136 lib plus the integration tests), horizon-client-core (37), horizon-world (165) and horizon-protocol (100) tests pass, the fast build links, and the 140 harnesses `parity.py` picks for the tree's changed files pass on playhorizon (remote_fireworks, a known load flake there, passed on a rerun on a quiet Mac mini). `lan_e2e.py` passes 14 checks with the host on its server thread and in-frame (chest, xp, damage, kills, projectiles, mobs, drops, body, stopsound, digging, instant_digging, books, knockback, barter); `hurt` fails on main too when run after `damage` (check-order dependency, not this wave). Thanks @kacpherek!
2026-10-27 04:16:33 +01:00
Ludovic Marie eae5b13fc6 Merge pull request #2483 from kacpherek/fix/held-item-outline
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: the block outline uses the shape for the held item (light, scaffolding) and the shulker box's lid, as here. Gate: this wave's 50 PRs merged on main in one tree (union 9033a9c3b): fmt and clippy `-D warnings` clean, the horizon-client (1136 lib plus the integration tests), horizon-client-core (37), horizon-world (165) and horizon-protocol (100) tests pass, the fast build links, and the 140 harnesses `parity.py` picks for the tree's changed files pass on playhorizon (remote_fireworks, a known load flake there, passed on a rerun on a quiet Mac mini). `lan_e2e.py` passes 14 checks with the host on its server thread and in-frame (chest, xp, damage, kills, projectiles, mobs, drops, body, stopsound, digging, instant_digging, books, knockback, barter); `hurt` fails on main too when run after `damage` (check-order dependency, not this wave). Thanks @kacpherek!
2026-10-27 04:14:39 +01:00
Ludovic Marie 1ad1e957a3 Merge pull request #2477 from kacpherek/fix/polar-bear-attack
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3's `PolarBear` (its goals, `PolarBearMeleeAttackGoal`'s rearing and warning sound, `PolarBearHurtByTargetGoal`, `PolarBearAttackPlayersGoal`, the stand animation and `DATA_STANDING_ID`). Left for later: the height doesn't grow while standing, the anger isn't saved, and anger at LAN guests isn't tracked. Gate: this wave's 50 PRs merged on main in one tree (union 9033a9c3b): fmt and clippy `-D warnings` clean, the horizon-client (1136 lib plus the integration tests), horizon-client-core (37), horizon-world (165) and horizon-protocol (100) tests pass, the fast build links, and the 140 harnesses `parity.py` picks for the tree's changed files pass on playhorizon (remote_fireworks, a known load flake there, passed on a rerun on a quiet Mac mini). `lan_e2e.py` passes 14 checks with the host on its server thread and in-frame (chest, xp, damage, kills, projectiles, mobs, drops, body, stopsound, digging, instant_digging, books, knockback, barter); `hurt` fails on main too when run after `damage` (check-order dependency, not this wave). Thanks @kacpherek!
2026-10-27 04:14:35 +01:00
Ludovic Marie b5f1e65162 Merge pull request #2467 from kacpherek/fix/execute-on-origin-leasher
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `execute on origin` (the thrower or shooter) and `on leasher`; your follow-up records the thrower of a guest's drops and the player behind a lead. Left for later: a fence knot leasher isn't an entity in Horizon, so it finds nothing. Gate: this wave's 50 PRs merged on main in one tree (union 9033a9c3b): fmt and clippy `-D warnings` clean, the horizon-client (1136 lib plus the integration tests), horizon-client-core (37), horizon-world (165) and horizon-protocol (100) tests pass, the fast build links, and the 140 harnesses `parity.py` picks for the tree's changed files pass on playhorizon (remote_fireworks, a known load flake there, passed on a rerun on a quiet Mac mini). `lan_e2e.py` passes 14 checks with the host on its server thread and in-frame (chest, xp, damage, kills, projectiles, mobs, drops, body, stopsound, digging, instant_digging, books, knockback, barter); `hurt` fails on main too when run after `damage` (check-order dependency, not this wave). Thanks @kacpherek!
2026-10-27 04:14:31 +01:00
Ludovic Marie 39910efabf Merge pull request #2466 from kacpherek/fix/autosave-indicator
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `Gui.renderSavingIndicator` (the fade in and out around the autosave), as here. Gate: this wave's 50 PRs merged on main in one tree (union 9033a9c3b): fmt and clippy `-D warnings` clean, the horizon-client (1136 lib plus the integration tests), horizon-client-core (37), horizon-world (165) and horizon-protocol (100) tests pass, the fast build links, and the 140 harnesses `parity.py` picks for the tree's changed files pass on playhorizon (remote_fireworks, a known load flake there, passed on a rerun on a quiet Mac mini). `lan_e2e.py` passes 14 checks with the host on its server thread and in-frame (chest, xp, damage, kills, projectiles, mobs, drops, body, stopsound, digging, instant_digging, books, knockback, barter); `hurt` fails on main too when run after `damage` (check-order dependency, not this wave). Thanks @kacpherek!
2026-10-27 04:14:28 +01:00
Ludovic Marie b0103bd62f Merge pull request #2465 from kacpherek/fix/baby-walk-position
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: the baby leg animation uses the interpolated `walkAnimation` position, not the tick value, as here. Gate: this wave's 50 PRs merged on main in one tree (union 9033a9c3b): fmt and clippy `-D warnings` clean, the horizon-client (1136 lib plus the integration tests), horizon-client-core (37), horizon-world (165) and horizon-protocol (100) tests pass, the fast build links, and the 140 harnesses `parity.py` picks for the tree's changed files pass on playhorizon (remote_fireworks, a known load flake there, passed on a rerun on a quiet Mac mini). `lan_e2e.py` passes 14 checks with the host on its server thread and in-frame (chest, xp, damage, kills, projectiles, mobs, drops, body, stopsound, digging, instant_digging, books, knockback, barter); `hurt` fails on main too when run after `damage` (check-order dependency, not this wave). Thanks @kacpherek!
2026-10-27 04:14:24 +01:00
Ludovic Marie 44eb7008f1 Merge pull request #2461 from kacpherek/fix/guest-text-commands
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `/tellraw`, `/title` and `/playsound` reach a server's players as their packets, and your follow-up plays a remote sound in the packet's category (`handleSoundEvent`). Gate: this wave's 50 PRs merged on main in one tree (union 9033a9c3b): fmt and clippy `-D warnings` clean, the horizon-client (1136 lib plus the integration tests), horizon-client-core (37), horizon-world (165) and horizon-protocol (100) tests pass, the fast build links, and the 140 harnesses `parity.py` picks for the tree's changed files pass on playhorizon (remote_fireworks, a known load flake there, passed on a rerun on a quiet Mac mini). `lan_e2e.py` passes 14 checks with the host on its server thread and in-frame (chest, xp, damage, kills, projectiles, mobs, drops, body, stopsound, digging, instant_digging, books, knockback, barter); `hurt` fails on main too when run after `damage` (check-order dependency, not this wave). Thanks @kacpherek!
2026-10-27 04:12:11 +01:00
Ludovic Marie 179e410923 Merge pull request #2460 from kacpherek/fix/bonus-chest
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3's `BonusChestFeature.place` (shuffled columns, the chest with the `spawn_bonus_chest` loot table, a torch on each side that survives). I pushed one fix: `setInitialSpawn` passes the level random and the feature sets the blocks with flags 2 (`UPDATE_CLIENTS`). Left for later: it is skipped when the spawn chunk is not lit yet. Gate: this wave's 50 PRs merged on main in one tree (union 9033a9c3b): fmt and clippy `-D warnings` clean, the horizon-client (1136 lib plus the integration tests), horizon-client-core (37), horizon-world (165) and horizon-protocol (100) tests pass, the fast build links, and the 140 harnesses `parity.py` picks for the tree's changed files pass on playhorizon (remote_fireworks, a known load flake there, passed on a rerun on a quiet Mac mini). `lan_e2e.py` passes 14 checks with the host on its server thread and in-frame (chest, xp, damage, kills, projectiles, mobs, drops, body, stopsound, digging, instant_digging, books, knockback, barter); `hurt` fails on main too when run after `damage` (check-order dependency, not this wave). Thanks @kacpherek!
2026-10-27 04:12:07 +01:00
Ludovic Marie b0a6e63ce7 Merge pull request #2459 from kacpherek/fix/jigsaw-distance-padding-defaults
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `JigsawStructure`'s codec defaults (`max_distance_from_center` 80, the default `dimension_padding`), as here. Gate: this wave's 50 PRs merged on main in one tree (union 9033a9c3b): fmt and clippy `-D warnings` clean, the horizon-client (1136 lib plus the integration tests), horizon-client-core (37), horizon-world (165) and horizon-protocol (100) tests pass, the fast build links, and the 140 harnesses `parity.py` picks for the tree's changed files pass on playhorizon (remote_fireworks, a known load flake there, passed on a rerun on a quiet Mac mini). `lan_e2e.py` passes 14 checks with the host on its server thread and in-frame (chest, xp, damage, kills, projectiles, mobs, drops, body, stopsound, digging, instant_digging, books, knockback, barter); `hurt` fails on main too when run after `damage` (check-order dependency, not this wave). Thanks @kacpherek!
2026-10-27 04:12:04 +01:00
Ludovic Marie 4517d75a65 Merge pull request #2458 from kacpherek/fix/drowned-thrown-trident
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3's `ThrownTrident` (8 damage, `dealtDamage`, the 0.99 water inertia, the hit and ground sounds, `deflect(REVERSE, ..., (0.02, 0.2, 0.02))`) and the drowned's `TridentAttackGoal`. I pushed one fix: the damage goes through `EnchantmentHelper.modifyDamage` of the trident, so Impaling adds only for `#sensitive_to_impaling` (its `minecraft:damage` requirement), not for every mob that swims. Fixes #2447. Gate: this wave's 50 PRs merged on main in one tree (union 9033a9c3b): fmt and clippy `-D warnings` clean, the horizon-client (1136 lib plus the integration tests), horizon-client-core (37), horizon-world (165) and horizon-protocol (100) tests pass, the fast build links, and the 140 harnesses `parity.py` picks for the tree's changed files pass on playhorizon (remote_fireworks, a known load flake there, passed on a rerun on a quiet Mac mini). `lan_e2e.py` passes 14 checks with the host on its server thread and in-frame (chest, xp, damage, kills, projectiles, mobs, drops, body, stopsound, digging, instant_digging, books, knockback, barter); `hurt` fails on main too when run after `damage` (check-order dependency, not this wave). Thanks @kacpherek!
2026-10-27 04:12:00 +01:00
Ludovic Marie 4b042eae28 Merge pull request #2457 from kacpherek/fix/template-block-entity-nbt
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `StructureTemplate.placeInWorld` loads each block's template NBT into its block entity, as here. Gate: this wave's 50 PRs merged on main in one tree (union 9033a9c3b): fmt and clippy `-D warnings` clean, the horizon-client (1136 lib plus the integration tests), horizon-client-core (37), horizon-world (165) and horizon-protocol (100) tests pass, the fast build links, and the 140 harnesses `parity.py` picks for the tree's changed files pass on playhorizon (remote_fireworks, a known load flake there, passed on a rerun on a quiet Mac mini). `lan_e2e.py` passes 14 checks with the host on its server thread and in-frame (chest, xp, damage, kills, projectiles, mobs, drops, body, stopsound, digging, instant_digging, books, knockback, barter); `hurt` fails on main too when run after `damage` (check-order dependency, not this wave). Thanks @kacpherek!
2026-10-27 04:11:57 +01:00
Ludovic Marie 30c6a4ac7b Merge pull request #2456 from kacpherek/fix/particle-count-zero
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `ClientPacketListener.handleParticleEvent` with count 0 makes one particle moving at max speed times the delta. I pushed one fix: both are floats in the packet, multiplied as floats before the widening. Gate: this wave's 50 PRs merged on main in one tree (union 9033a9c3b): fmt and clippy `-D warnings` clean, the horizon-client (1136 lib plus the integration tests), horizon-client-core (37), horizon-world (165) and horizon-protocol (100) tests pass, the fast build links, and the 140 harnesses `parity.py` picks for the tree's changed files pass on playhorizon (remote_fireworks, a known load flake there, passed on a rerun on a quiet Mac mini). `lan_e2e.py` passes 14 checks with the host on its server thread and in-frame (chest, xp, damage, kills, projectiles, mobs, drops, body, stopsound, digging, instant_digging, books, knockback, barter); `hurt` fails on main too when run after `damage` (check-order dependency, not this wave). Thanks @kacpherek!
2026-10-27 04:09:46 +01:00
Ludovic Marie ce73764585 Merge pull request #2455 from kacpherek/fix/horse-tail-swing
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `AbstractHorse.aiStep`'s 1-in-200 `moveTail` and its 8-tick `tailCounter`, with the model's tail swing, match. Gate: this wave's 50 PRs merged on main in one tree (union 9033a9c3b): fmt and clippy `-D warnings` clean, the horizon-client (1136 lib plus the integration tests), horizon-client-core (37), horizon-world (165) and horizon-protocol (100) tests pass, the fast build links, and the 140 harnesses `parity.py` picks for the tree's changed files pass on playhorizon (remote_fireworks, a known load flake there, passed on a rerun on a quiet Mac mini). `lan_e2e.py` passes 14 checks with the host on its server thread and in-frame (chest, xp, damage, kills, projectiles, mobs, drops, body, stopsound, digging, instant_digging, books, knockback, barter); `hurt` fails on main too when run after `damage` (check-order dependency, not this wave). Thanks @kacpherek!
2026-10-27 04:09:41 +01:00
Ludovic Marie 59d0b45349 Merge pull request #2454 from kacpherek/fix/score-text-components
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3's `ScoreContents` resolution in `/tellraw` and `/title`; your follow-up gives armor stands, frames, paintings, boats, carts and others their UUID name, as `getScoreboardName` does. Left for later: the `*` holder resolves to the acting player, not to each recipient. Gate: this wave's 50 PRs merged on main in one tree (union 9033a9c3b): fmt and clippy `-D warnings` clean, the horizon-client (1136 lib plus the integration tests), horizon-client-core (37), horizon-world (165) and horizon-protocol (100) tests pass, the fast build links, and the 140 harnesses `parity.py` picks for the tree's changed files pass on playhorizon (remote_fireworks, a known load flake there, passed on a rerun on a quiet Mac mini). `lan_e2e.py` passes 14 checks with the host on its server thread and in-frame (chest, xp, damage, kills, projectiles, mobs, drops, body, stopsound, digging, instant_digging, books, knockback, barter); `hurt` fails on main too when run after `damage` (check-order dependency, not this wave). Thanks @kacpherek!
2026-10-27 04:09:37 +01:00
Ludovic Marie 7373ee8da5 Merge pull request #2453 from kacpherek/fix/entity-shadows-option
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: the Entity Shadows option gates `EntityRenderDispatcher`'s shadow pass, as here. Gate: this wave's 50 PRs merged on main in one tree (union 9033a9c3b): fmt and clippy `-D warnings` clean, the horizon-client (1136 lib plus the integration tests), horizon-client-core (37), horizon-world (165) and horizon-protocol (100) tests pass, the fast build links, and the 140 harnesses `parity.py` picks for the tree's changed files pass on playhorizon (remote_fireworks, a known load flake there, passed on a rerun on a quiet Mac mini). `lan_e2e.py` passes 14 checks with the host on its server thread and in-frame (chest, xp, damage, kills, projectiles, mobs, drops, body, stopsound, digging, instant_digging, books, knockback, barter); `hurt` fails on main too when run after `damage` (check-order dependency, not this wave). Thanks @kacpherek!
2026-10-27 04:09:33 +01:00
Ludovic Marie a6af0d647f Merge pull request #2445 from kacpherek/fix/2384-sort-cursor-item
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: the client's `AbstractContainerMenu.clicked` predicts the sort and tweak clicks like any other, so the last sorted item stays under the cursor until the server answers. Gate: this wave's 50 PRs merged on main in one tree (union 9033a9c3b): fmt and clippy `-D warnings` clean, the horizon-client (1136 lib plus the integration tests), horizon-client-core (37), horizon-world (165) and horizon-protocol (100) tests pass, the fast build links, and the 140 harnesses `parity.py` picks for the tree's changed files pass on playhorizon (remote_fireworks, a known load flake there, passed on a rerun on a quiet Mac mini). `lan_e2e.py` passes 14 checks with the host on its server thread and in-frame (chest, xp, damage, kills, projectiles, mobs, drops, body, stopsound, digging, instant_digging, books, knockback, barter); `hurt` fails on main too when run after `damage` (check-order dependency, not this wave). Thanks @kacpherek!
2026-10-27 04:09:29 +01:00
Ludovic MarieandClaude Opus 5.5 8b87d128e6 parity: world_reopen and server_player_sculk count ticks, not request round trips
On playhorizon (frames drawn on the CPU, control requests answered slowly)
world_reopen read the minecarts before the mineshaft's last one arrived (3
then 4 on every reopen), and the sculk walk polled the sensor with separate
requests that missed its 30-tick active window. The minecart list is read
once it stops changing; both clients sample the sensor every tick from one
ctl script, alongside the walk.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 04:08:32 +01:00
Ludovic Marie 8a2c7eee4a Merge pull request #2444 from kacpherek/fix/enchanted-hit
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `Player.attack` sends the enchanted hit (animate action 2, `MAGIC_CRITICAL_HIT`) when the weapon's enchantments add damage, as here. Gate: this wave's 50 PRs merged on main in one tree (union 9033a9c3b): fmt and clippy `-D warnings` clean, the horizon-client (1136 lib plus the integration tests), horizon-client-core (37), horizon-world (165) and horizon-protocol (100) tests pass, the fast build links, and the 140 harnesses `parity.py` picks for the tree's changed files pass on playhorizon (remote_fireworks, a known load flake there, passed on a rerun on a quiet Mac mini). `lan_e2e.py` passes 14 checks with the host on its server thread and in-frame (chest, xp, damage, kills, projectiles, mobs, drops, body, stopsound, digging, instant_digging, books, knockback, barter); `hurt` fails on main too when run after `damage` (check-order dependency, not this wave). Thanks @kacpherek!
2026-10-27 04:07:17 +01:00
Ludovic Marie 8a572c9239 Merge pull request #2443 from kacpherek/fix/mob-death-poof
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `LivingEntity.tickDeath` sends entity event 60 at 20 ticks and the client's `makePoofParticles` matches. Gate: this wave's 50 PRs merged on main in one tree (union 9033a9c3b): fmt and clippy `-D warnings` clean, the horizon-client (1136 lib plus the integration tests), horizon-client-core (37), horizon-world (165) and horizon-protocol (100) tests pass, the fast build links, and the 140 harnesses `parity.py` picks for the tree's changed files pass on playhorizon (remote_fireworks, a known load flake there, passed on a rerun on a quiet Mac mini). `lan_e2e.py` passes 14 checks with the host on its server thread and in-frame (chest, xp, damage, kills, projectiles, mobs, drops, body, stopsound, digging, instant_digging, books, knockback, barter); `hurt` fails on main too when run after `damage` (check-order dependency, not this wave). Thanks @kacpherek!
2026-10-27 04:07:12 +01:00
Ludovic Marie 6ea9f9580c Merge pull request #2440 from kacpherek/fix/bundle-selected-item
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `BundleItem` / `ClientBundleTooltip` mouse-wheel selection (`ServerboundSelectBundleItemPacket`, cleared when the cursor leaves the slot, now on a server's client too). Left for later: the selected item's name line in the tooltip. Gate: this wave's 50 PRs merged on main in one tree (union 9033a9c3b): fmt and clippy `-D warnings` clean, the horizon-client (1136 lib plus the integration tests), horizon-client-core (37), horizon-world (165) and horizon-protocol (100) tests pass, the fast build links, and the 140 harnesses `parity.py` picks for the tree's changed files pass on playhorizon (remote_fireworks, a known load flake there, passed on a rerun on a quiet Mac mini). `lan_e2e.py` passes 14 checks with the host on its server thread and in-frame (chest, xp, damage, kills, projectiles, mobs, drops, body, stopsound, digging, instant_digging, books, knockback, barter); `hurt` fails on main too when run after `damage` (check-order dependency, not this wave). Thanks @kacpherek!
2026-10-27 04:07:09 +01:00
Ludovic Marie e9bcd10869 Merge pull request #2435 from kacpherek/fix/landing-dust
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `LivingEntity.checkFallDamage` sends the landing `block` particles (`150 * scale` of them, scaled by the fall distance, at speed 0.15) where the fall ends, as here. Gate: this wave's 50 PRs merged on main in one tree (union 9033a9c3b): fmt and clippy `-D warnings` clean, the horizon-client (1136 lib plus the integration tests), horizon-client-core (37), horizon-world (165) and horizon-protocol (100) tests pass, the fast build links, and the 140 harnesses `parity.py` picks for the tree's changed files pass on playhorizon (remote_fireworks, a known load flake there, passed on a rerun on a quiet Mac mini). `lan_e2e.py` passes 14 checks with the host on its server thread and in-frame (chest, xp, damage, kills, projectiles, mobs, drops, body, stopsound, digging, instant_digging, books, knockback, barter); `hurt` fails on main too when run after `damage` (check-order dependency, not this wave). Thanks @kacpherek!
2026-10-27 04:07:05 +01:00
Ludovic Marie 3bd504c6c4 Merge pull request #2433 from kacpherek/fix/llama-spit-burst
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `LlamaSpit.recreateFromPacket` adds seven spit particles at 0.4 + 0.1 i of its movement, as here. Gate: this wave's 50 PRs merged on main in one tree (union 9033a9c3b): fmt and clippy `-D warnings` clean, the horizon-client (1136 lib plus the integration tests), horizon-client-core (37), horizon-world (165) and horizon-protocol (100) tests pass, the fast build links, and the 140 harnesses `parity.py` picks for the tree's changed files pass on playhorizon (remote_fireworks, a known load flake there, passed on a rerun on a quiet Mac mini). `lan_e2e.py` passes 14 checks with the host on its server thread and in-frame (chest, xp, damage, kills, projectiles, mobs, drops, body, stopsound, digging, instant_digging, books, knockback, barter); `hurt` fails on main too when run after `damage` (check-order dependency, not this wave). Thanks @kacpherek!
2026-10-27 04:07:01 +01:00
Ludovic Marie a0343bceab Merge pull request #2431 from kacpherek/fix/explosion-emitter-size
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `ServerExplosion.isSmall` (radius under 2, or the blocks kept) picks the small particle, as here. Gate: this wave's 50 PRs merged on main in one tree (union 9033a9c3b): fmt and clippy `-D warnings` clean, the horizon-client (1136 lib plus the integration tests), horizon-client-core (37), horizon-world (165) and horizon-protocol (100) tests pass, the fast build links, and the 140 harnesses `parity.py` picks for the tree's changed files pass on playhorizon (remote_fireworks, a known load flake there, passed on a rerun on a quiet Mac mini). `lan_e2e.py` passes 14 checks with the host on its server thread and in-frame (chest, xp, damage, kills, projectiles, mobs, drops, body, stopsound, digging, instant_digging, books, knockback, barter); `hurt` fails on main too when run after `damage` (check-order dependency, not this wave). Thanks @kacpherek!
2026-10-27 04:04:41 +01:00
Ludovic Marie 25ad1db6d3 Merge pull request #2430 from kacpherek/fix/item-tooltip-lines
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3's `ItemStack.addDetailsToTooltip`: the order here (instrument, bees, container, banner patterns, written book, charged projectiles with their indented details, fireworks, potion with `When Applied:`, jukebox, trim, stored and plain enchantments, dyed, lore, attributes, unbreakable, block state, spawner, then the advanced durability, id and component count) is vanilla's, and the crossbow lines match `ChargedProjectiles.addProjectileTooltip`. Gate: this wave's 50 PRs merged on main in one tree (union 9033a9c3b): fmt and clippy `-D warnings` clean, the horizon-client (1136 lib plus the integration tests), horizon-client-core (37), horizon-world (165) and horizon-protocol (100) tests pass, the fast build links, and the 140 harnesses `parity.py` picks for the tree's changed files pass on playhorizon (remote_fireworks, a known load flake there, passed on a rerun on a quiet Mac mini). `lan_e2e.py` passes 14 checks with the host on its server thread and in-frame (chest, xp, damage, kills, projectiles, mobs, drops, body, stopsound, digging, instant_digging, books, knockback, barter); `hurt` fails on main too when run after `damage` (check-order dependency, not this wave). Thanks @kacpherek!
2026-10-27 04:04:38 +01:00
Ludovic Marie 65cc4c6f6b Merge pull request #2429 from kacpherek/fix/lan-player-pose-flags
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3's `Entity.DATA_SHARED_FLAGS_ID` bits (on fire, crouching, swimming, invisible, glowing, fall flying) and the pose for LAN players; your follow-up also sends the crouching pose under a low ceiling, which matches `Player.updatePlayerPose`. Gate: this wave's 50 PRs merged on main in one tree (union 9033a9c3b): fmt and clippy `-D warnings` clean, the horizon-client (1136 lib plus the integration tests), horizon-client-core (37), horizon-world (165) and horizon-protocol (100) tests pass, the fast build links, and the 140 harnesses `parity.py` picks for the tree's changed files pass on playhorizon (remote_fireworks, a known load flake there, passed on a rerun on a quiet Mac mini). `lan_e2e.py` passes 14 checks with the host on its server thread and in-frame (chest, xp, damage, kills, projectiles, mobs, drops, body, stopsound, digging, instant_digging, books, knockback, barter); `hurt` fails on main too when run after `damage` (check-order dependency, not this wave). Thanks @kacpherek!
2026-10-27 04:04:34 +01:00
Ludovic Marie 8892f0fabf Merge pull request #2427 from kacpherek/fix/creaking-heart-explosion
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `CreakingHeartBlock` / `CreakingHeartBlockEntity.removeProtector` on an explosion kills the bound creaking. Left for later: no XP for a heart broken by a player's TNT (Horizon doesn't track the TNT's owner yet). Gate: this wave's 50 PRs merged on main in one tree (union 9033a9c3b): fmt and clippy `-D warnings` clean, the horizon-client (1136 lib plus the integration tests), horizon-client-core (37), horizon-world (165) and horizon-protocol (100) tests pass, the fast build links, and the 140 harnesses `parity.py` picks for the tree's changed files pass on playhorizon (remote_fireworks, a known load flake there, passed on a rerun on a quiet Mac mini). `lan_e2e.py` passes 14 checks with the host on its server thread and in-frame (chest, xp, damage, kills, projectiles, mobs, drops, body, stopsound, digging, instant_digging, books, knockback, barter); `hurt` fails on main too when run after `damage` (check-order dependency, not this wave). Thanks @kacpherek!
2026-10-27 04:04:31 +01:00
Ludovic Marie d6fc0f6e43 Merge pull request #2424 from kacpherek/fix/note-block-particle
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `NoteBlock.triggerEvent` adds the note particle 1.2 above the block with note / 24 as its colour, as here. Gate: this wave's 50 PRs merged on main in one tree (union 9033a9c3b): fmt and clippy `-D warnings` clean, the horizon-client (1136 lib plus the integration tests), horizon-client-core (37), horizon-world (165) and horizon-protocol (100) tests pass, the fast build links, and the 140 harnesses `parity.py` picks for the tree's changed files pass on playhorizon (remote_fireworks, a known load flake there, passed on a rerun on a quiet Mac mini). `lan_e2e.py` passes 14 checks with the host on its server thread and in-frame (chest, xp, damage, kills, projectiles, mobs, drops, body, stopsound, digging, instant_digging, books, knockback, barter); `hurt` fails on main too when run after `damage` (check-order dependency, not this wave). Thanks @kacpherek!
2026-10-27 04:04:27 +01:00
Ludovic MarieandClaude Opus 5.5 82c8398c6b launcher: desktop shortcut that plays an instance
Home's Instance row gets Shortcut / Remove shortcut, and the CLI and MCP
instance shortcut [instance] [--remove]. The shortcut runs
horizon-launcher play --instance <id>: a .desktop file on Linux (and in
the applications menu), a script app with Horizon's icon on macOS, and a
Shell Link written from Microsoft's published MS-SHLLINK layout on
Windows. HORIZON_LAUNCHER_DESKTOP points it at a scratch folder.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 04:02:20 +01:00
Ludovic Marie 517cbe76bd Merge pull request #2423 from kacpherek/fix/entity-broadcast-range
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `ChunkMap.TrackedEntity.getEffectiveRange` ends in `scaledRange`, which applies `entity-broadcast-range-percentage`, as done here. Gate: this wave's 50 PRs merged on main in one tree (union 9033a9c3b): fmt and clippy `-D warnings` clean, the horizon-client (1136 lib plus the integration tests), horizon-client-core (37), horizon-world (165) and horizon-protocol (100) tests pass, the fast build links, and the 140 harnesses `parity.py` picks for the tree's changed files pass on playhorizon (remote_fireworks, a known load flake there, passed on a rerun on a quiet Mac mini). `lan_e2e.py` passes 14 checks with the host on its server thread and in-frame (chest, xp, damage, kills, projectiles, mobs, drops, body, stopsound, digging, instant_digging, books, knockback, barter); `hurt` fails on main too when run after `damage` (check-order dependency, not this wave). Thanks @kacpherek!
2026-10-27 04:02:02 +01:00
Ludovic Marie 236371d06d Merge pull request #2422 from kacpherek/fix/honey-block-slide
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3's `HoneyBlock` (`isSlidingDown`, `doSlideMovement`, `maybeDoSlideEffects` with its 1-in-5 sound and particle rolls, entity event 53). Your follow-up runs it for a server's player too, with the sound to the others and event 53 to everyone, which is how vanilla splits it. Left for later: mobs don't slide yet, and there is no `honey_block_slide` criterion. Gate: this wave's 50 PRs merged on main in one tree (union 9033a9c3b): fmt and clippy `-D warnings` clean, the horizon-client (1136 lib plus the integration tests), horizon-client-core (37), horizon-world (165) and horizon-protocol (100) tests pass, the fast build links, and the 140 harnesses `parity.py` picks for the tree's changed files pass on playhorizon (remote_fireworks, a known load flake there, passed on a rerun on a quiet Mac mini). `lan_e2e.py` passes 14 checks with the host on its server thread and in-frame (chest, xp, damage, kills, projectiles, mobs, drops, body, stopsound, digging, instant_digging, books, knockback, barter); `hurt` fails on main too when run after `damage` (check-order dependency, not this wave). Thanks @kacpherek!
2026-10-27 04:01:58 +01:00
Ludovic Marie 06c2371360 Merge pull request #2421 from kacpherek/fix/bee-wither-rose-eyeblossom-contact
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `WitherRoseBlock`'s effect (wither for 40 ticks on the mobs inside it) and the eyeblossom's poison on bees. Since then you added `MobEffectInstance.update`'s rule (a stronger effect replaces, an equal one keeps the longer duration, a weaker one changes nothing), which matches. Left for later: a weaker, longer effect is not kept hidden to resume afterwards (`hiddenEffect`). Gate: this wave's 50 PRs merged on main in one tree (union 9033a9c3b): fmt and clippy `-D warnings` clean, the horizon-client (1136 lib plus the integration tests), horizon-client-core (37), horizon-world (165) and horizon-protocol (100) tests pass, the fast build links, and the 140 harnesses `parity.py` picks for the tree's changed files pass on playhorizon (remote_fireworks, a known load flake there, passed on a rerun on a quiet Mac mini). `lan_e2e.py` passes 14 checks with the host on its server thread and in-frame (chest, xp, damage, kills, projectiles, mobs, drops, body, stopsound, digging, instant_digging, books, knockback, barter); `hurt` fails on main too when run after `damage` (check-order dependency, not this wave). Thanks @kacpherek!
2026-10-27 04:01:55 +01:00
Ludovic Marie b3a1f66c35 Merge pull request #2419 from kacpherek/fix/animate-tick-extras
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3's particle providers and `animateTick`s: honey drips (`DripParticle`), the poplar and azalea leaf particles, falling dust under gravity blocks and the campfire food smoke match vanilla's rates, sprites and velocities. Gate: this wave's 50 PRs merged on main in one tree (union 9033a9c3b): fmt and clippy `-D warnings` clean, the horizon-client (1136 lib plus the integration tests), horizon-client-core (37), horizon-world (165) and horizon-protocol (100) tests pass, the fast build links, and the 140 harnesses `parity.py` picks for the tree's changed files pass on playhorizon (remote_fireworks, a known load flake there, passed on a rerun on a quiet Mac mini). `lan_e2e.py` passes 14 checks with the host on its server thread and in-frame (chest, xp, damage, kills, projectiles, mobs, drops, body, stopsound, digging, instant_digging, books, knockback, barter); `hurt` fails on main too when run after `damage` (check-order dependency, not this wave). Thanks @kacpherek!
2026-10-27 04:01:51 +01:00
Ludovic Marie cca3b735cc Merge pull request #2462 from kacpherek/fix/mob-block-layers
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `SnowGolem.DATA_PUMPKIN_ID` and `MushroomCow.DATA_TYPE` with their layers (`SnowGolemHeadLayer`, `MushroomCowMushroomLayer`), as here. Gate: this wave's 50 PRs merged on main in one tree (union 9033a9c3b): fmt and clippy `-D warnings` clean, the horizon-client (1136 lib plus the integration tests), horizon-client-core (37), horizon-world (165) and horizon-protocol (100) tests pass, the fast build links, and the 140 harnesses `parity.py` picks for the tree's changed files pass on playhorizon (remote_fireworks, a known load flake there, passed on a rerun on a quiet Mac mini). `lan_e2e.py` passes 14 checks with the host on its server thread and in-frame (chest, xp, damage, kills, projectiles, mobs, drops, body, stopsound, digging, instant_digging, books, knockback, barter); `hurt` fails on main too when run after `damage` (check-order dependency, not this wave). Thanks @kacpherek!
2026-10-27 04:01:48 +01:00
Ludovic MarieandClaude Opus 5.5 e338690bf9 Merge main into this branch (resolves the conflict with main)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 03:58:56 +01:00
Ludovic MarieandClaude Opus 5.5 0ab24d0805 Merge main into this branch (resolves the conflict with main)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 03:58:55 +01:00
Ludovic Marie dfa907d28e Merge pull request #2601 from ludovic111/feat/launcher-packs-core
🤖 Agent: launcher-features-1 checked this: base main, clean merge with main, launcher CI green; fmt, clippy -D warnings and the launcher tests pass locally. Queued for merge.
2026-10-27 03:57:27 +01:00
Ludovic Marie d9dc1c866b Merge pull request #2442 from kacpherek/fix/splash-potion-burst
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `LevelEvent` 2002 / 2007 (`LevelEventHandler`: the item crumbs, then the coloured spell or instant effect particles) and the 1053 / 1054 break sounds. It conflicted with main in `game.rs` (the zombie door sounds 1019 / 1021 landed there), so I merged main in, keeping both lists. Gate: this wave's 50 PRs merged on main in one tree (union 9033a9c3b): fmt and clippy `-D warnings` clean, the horizon-client (1136 lib plus the integration tests), horizon-client-core (37), horizon-world (165) and horizon-protocol (100) tests pass, the fast build links, and the 140 harnesses `parity.py` picks for the tree's changed files pass on playhorizon (remote_fireworks, a known load flake there, passed on a rerun on a quiet Mac mini). `lan_e2e.py` passes 14 checks with the host on its server thread and in-frame (chest, xp, damage, kills, projectiles, mobs, drops, body, stopsound, digging, instant_digging, books, knockback, barter); `hurt` fails on main too when run after `damage` (check-order dependency, not this wave). Thanks @kacpherek!
2026-10-27 03:56:51 +01:00
Ludovic Marie ec8d5f1e06 Merge pull request #2432 from kacpherek/fix/sprint-dust
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `Entity.canSpawnSprintParticle` and `spawnSprintParticle` (the block under `getOnPosLegacy`, clamped into it, fences and walls taken from the supporting block) match. Gate: this wave's 50 PRs merged on main in one tree (union 9033a9c3b): fmt and clippy `-D warnings` clean, the horizon-client (1136 lib plus the integration tests), horizon-client-core (37), horizon-world (165) and horizon-protocol (100) tests pass, the fast build links, and the 140 harnesses `parity.py` picks for the tree's changed files pass on playhorizon (remote_fireworks, a known load flake there, passed on a rerun on a quiet Mac mini). `lan_e2e.py` passes 14 checks with the host on its server thread and in-frame (chest, xp, damage, kills, projectiles, mobs, drops, body, stopsound, digging, instant_digging, books, knockback, barter); `hurt` fails on main too when run after `damage` (check-order dependency, not this wave). Thanks @kacpherek!
2026-10-27 03:56:48 +01:00
Ludovic Marie a9dcea59e7 Merge pull request #1901 from ludovic111/feat/server-mods-commands
🤖 Agent: the parity stop at c5534f0d7 was friends_integration, which main broke through #2501 and #2598 fixes, plus remote_fireworks and server_player_sculk (load on the old runner; both pass on main on the Mac mini). None of the three is from this PR. Merged main (fde2dfffe, a clean merge); pr_ready --quick passed: 1357 tests, clippy, launcher.
2026-10-27 03:56:44 +01:00
Ludovic Marie 4ba8105ecc Merge pull request #2104 from rdbtCVS/fix/1482-evoker-fangs
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: the server sends entity event 4 once its warmup ends (`EvokerFangs.tick`), `handleEntityEvent(4)` starts the client's attack and plays the sound locally, and the client only counts `lifeTicks` down for the animation, while the server deals the damage and discards. Moving the host's sound to `play_unshared` matches that, since each client plays its own. It conflicted with #2094 (both hook entity events) once that merged, so I merged main into it, keeping both calls. `lan_e2e.py --check fangs` passes in both modes. It conflicted again with main in `tools/lan_e2e.py` (new checks on both sides), so I merged main in once more, keeping every check; clippy is clean. Fixes #1482. Thanks @rdbtCVS! Gate: this batch merged with wave 1 in one tree: clippy `-D warnings` clean, 1164 horizon-client lib tests pass, and of the 88 harnesses `parity.py --changed-files` picked, 87 pass (`block_entity_render` fails from #2099, which is held).
2026-10-27 03:56:41 +01:00
Ludovic Marie 226b42a046 Merge pull request #2449 from RoyalPixelsDE/fix/end-portal-without-players
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `EnderDragonFight.tick` scans the state and places the exit portal only when `dragonEvent.getPlayers()` is not empty, so a force-loaded End with nobody in it gets no portal, as here; the new `forced_end` harness compares both servers. Gate: this wave's 50 PRs merged on main in one tree (union 9033a9c3b): fmt and clippy `-D warnings` clean, the horizon-client (1136 lib plus the integration tests), horizon-client-core (37), horizon-world (165) and horizon-protocol (100) tests pass, the fast build links, and the 140 harnesses `parity.py` picks for the tree's changed files pass on playhorizon (remote_fireworks, a known load flake there, passed on a rerun on a quiet Mac mini). `lan_e2e.py` passes 14 checks with the host on its server thread and in-frame (chest, xp, damage, kills, projectiles, mobs, drops, body, stopsound, digging, instant_digging, books, knockback, barter); `hurt` fails on main too when run after `damage` (check-order dependency, not this wave). Thanks @FusselTV!
2026-10-27 03:54:13 +01:00
Ludovic Marie 12381ec668 Merge pull request #2439 from RoyalPixelsDE/fix/generated-block-entities
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `WorldGenRegion.setBlock` gives a block with a block entity a `DUMMY` entry that becomes `newBlockEntity`'s default, so the deep dark's sensors, catalysts and shriekers and potent sulfur get theirs, while placed data is kept. Gate: this wave's 50 PRs merged on main in one tree (union 9033a9c3b): fmt and clippy `-D warnings` clean, the horizon-client (1136 lib plus the integration tests), horizon-client-core (37), horizon-world (165) and horizon-protocol (100) tests pass, the fast build links, and the 140 harnesses `parity.py` picks for the tree's changed files pass on playhorizon (remote_fireworks, a known load flake there, passed on a rerun on a quiet Mac mini). `lan_e2e.py` passes 14 checks with the host on its server thread and in-frame (chest, xp, damage, kills, projectiles, mobs, drops, body, stopsound, digging, instant_digging, books, knockback, barter); `hurt` fails on main too when run after `damage` (check-order dependency, not this wave). Thanks @FusselTV!
2026-10-27 03:54:10 +01:00
Ludovic Marie 8376200697 Merge pull request #2438 from RoyalPixelsDE/fix/end-crystals-at-generation
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `SpikeFeature` places the end crystals during decoration, so they are saved with the chunk that generates them, as here. Gate: this wave's 50 PRs merged on main in one tree (union 9033a9c3b): fmt and clippy `-D warnings` clean, the horizon-client (1136 lib plus the integration tests), horizon-client-core (37), horizon-world (165) and horizon-protocol (100) tests pass, the fast build links, and the 140 harnesses `parity.py` picks for the tree's changed files pass on playhorizon (remote_fireworks, a known load flake there, passed on a rerun on a quiet Mac mini). `lan_e2e.py` passes 14 checks with the host on its server thread and in-frame (chest, xp, damage, kills, projectiles, mobs, drops, body, stopsound, digging, instant_digging, books, knockback, barter); `hurt` fails on main too when run after `damage` (check-order dependency, not this wave). Thanks @FusselTV!
2026-10-27 03:54:07 +01:00
Ludovic Marie 2dd5792ae3 Merge pull request #2436 from RoyalPixelsDE/fix/brushable-fall
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `FallingBlockEntity` with a `BrushableBlock` breaks it where it lands (`onBrokenAfterFall`'s sound and level event). Left for later: the game event uses the block position, not the bounding-box centre. Gate: this wave's 50 PRs merged on main in one tree (union 9033a9c3b): fmt and clippy `-D warnings` clean, the horizon-client (1136 lib plus the integration tests), horizon-client-core (37), horizon-world (165) and horizon-protocol (100) tests pass, the fast build links, and the 140 harnesses `parity.py` picks for the tree's changed files pass on playhorizon (remote_fireworks, a known load flake there, passed on a rerun on a quiet Mac mini). `lan_e2e.py` passes 14 checks with the host on its server thread and in-frame (chest, xp, damage, kills, projectiles, mobs, drops, body, stopsound, digging, instant_digging, books, knockback, barter); `hurt` fails on main too when run after `damage` (check-order dependency, not this wave). Thanks @FusselTV!
2026-10-27 03:54:03 +01:00
Ludovic Marie 94d063684c Merge pull request #2434 from RoyalPixelsDE/fix/forced-decoration-order
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `ForceLoadCommand` adds its FORCED tickets x-outer, z-inner, all at once, so the decoration order follows from that; the new `forced_order` reproduces it with the same ticket simulation as `vanilla_order`. Gate: this wave's 50 PRs merged on main in one tree (union 9033a9c3b): fmt and clippy `-D warnings` clean, the horizon-client (1136 lib plus the integration tests), horizon-client-core (37), horizon-world (165) and horizon-protocol (100) tests pass, the fast build links, and the 140 harnesses `parity.py` picks for the tree's changed files pass on playhorizon (remote_fireworks, a known load flake there, passed on a rerun on a quiet Mac mini). `lan_e2e.py` passes 14 checks with the host on its server thread and in-frame (chest, xp, damage, kills, projectiles, mobs, drops, body, stopsound, digging, instant_digging, books, knockback, barter); `hurt` fails on main too when run after `damage` (check-order dependency, not this wave). Thanks @FusselTV!
2026-10-27 03:54:00 +01:00
Ludovic MarieandClaude Opus 5.5 cde33a8e42 launcher: minimise or hide the window while the game runs
Settings > While playing: Stay (the default), Minimise, or Hide (macOS).
The window goes away when the game starts and comes back to the front
when it closes, with the run's play time, errors and log. CLI and MCP:
settings while-playing stay|minimize|hide.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 03:51:54 +01:00
Ludovic Marie 9d8ecc533d Merge pull request #2450 from RoyalPixelsDE/fix/structure-starts-split
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: a chunk's `STRUCTURE_STARTS` are its `getAllStarts`, whether or not a piece reaches it; `starts_at` now gives those, as vanilla saves them. Gate: this wave's 50 PRs merged on main in one tree (union 9033a9c3b): fmt and clippy `-D warnings` clean, the horizon-client (1136 lib plus the integration tests), horizon-client-core (37), horizon-world (165) and horizon-protocol (100) tests pass, the fast build links, and the 140 harnesses `parity.py` picks for the tree's changed files pass on playhorizon (remote_fireworks, a known load flake there, passed on a rerun on a quiet Mac mini). `lan_e2e.py` passes 14 checks with the host on its server thread and in-frame (chest, xp, damage, kills, projectiles, mobs, drops, body, stopsound, digging, instant_digging, books, knockback, barter); `hurt` fails on main too when run after `damage` (check-order dependency, not this wave). Thanks @FusselTV!
2026-10-27 03:51:42 +01:00
Ludovic Marie e2428e8735 Merge pull request #2437 from RoyalPixelsDE/fix/structure-mob-finalize
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `StructureTemplate.placeEntities` and the pieces' markers call `finalizeSpawn(level, difficulty, STRUCTURE, null)` inside the `WorldGenRegion` with its random, and the save keys now follow `addAdditionalSaveData`. Your rebased head is the reviewed change plus the comment cleanup, on current main. Gate: this wave's 50 PRs merged on main in one tree (union 9033a9c3b): fmt and clippy `-D warnings` clean, the horizon-client (1136 lib plus the integration tests), horizon-client-core (37), horizon-world (165) and horizon-protocol (100) tests pass, the fast build links, and the 140 harnesses `parity.py` picks for the tree's changed files pass on playhorizon (remote_fireworks, a known load flake there, passed on a rerun on a quiet Mac mini). `lan_e2e.py` passes 14 checks with the host on its server thread and in-frame (chest, xp, damage, kills, projectiles, mobs, drops, body, stopsound, digging, instant_digging, books, knockback, barter); `hurt` fails on main too when run after `damage` (check-order dependency, not this wave). Thanks @FusselTV!
2026-10-27 03:49:24 +01:00
Ludovic Marie bad8c9eba4 Merge pull request #2474 from kacpherek/fix/unavailable-options
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: these three options have no system in Horizon yet, so they are drawn inactive with the same notice the other unavailable options use, and clicks do nothing. Gate: this wave's 50 PRs merged on main in one tree (union 9033a9c3b): fmt and clippy `-D warnings` clean, the horizon-client (1136 lib plus the integration tests), horizon-client-core (37), horizon-world (165) and horizon-protocol (100) tests pass, the fast build links, and the 140 harnesses `parity.py` picks for the tree's changed files pass on playhorizon (remote_fireworks, a known load flake there, passed on a rerun on a quiet Mac mini). `lan_e2e.py` passes 14 checks with the host on its server thread and in-frame (chest, xp, damage, kills, projectiles, mobs, drops, body, stopsound, digging, instant_digging, books, knockback, barter); `hurt` fails on main too when run after `damage` (check-order dependency, not this wave). Thanks @kacpherek!
2026-10-27 03:48:41 +01:00
Ludovic Marie 70cba407b5 Merge pull request #2472 from kacpherek/fix/music-frequency
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3's `MusicManager.MusicFrequency` and the option's listener (`setMinutesBetweenSongs`). I pushed one fix: in `SoundOptionsScreen` Music Frequency starts its own row (beside Music Toast), not beside Show Subtitles. Gate: this wave's 50 PRs merged on main in one tree (union 9033a9c3b): fmt and clippy `-D warnings` clean, the horizon-client (1136 lib plus the integration tests), horizon-client-core (37), horizon-world (165) and horizon-protocol (100) tests pass, the fast build links, and the 140 harnesses `parity.py` picks for the tree's changed files pass on playhorizon (remote_fireworks, a known load flake there, passed on a rerun on a quiet Mac mini). `lan_e2e.py` passes 14 checks with the host on its server thread and in-frame (chest, xp, damage, kills, projectiles, mobs, drops, body, stopsound, digging, instant_digging, books, knockback, barter); `hurt` fails on main too when run after `damage` (check-order dependency, not this wave). Thanks @kacpherek!
2026-10-27 03:48:38 +01:00
Ludovic Marie 291239c90c Merge pull request #2470 from kacpherek/fix/execute-eye-height-positioned-as
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3's `EntityAnchorArgument` and `execute positioned as` (it keeps the anchor). I pushed one fix: a small armor stand's eyes are at 0.9875 (`BABY_DIMENSIONS.withEyeHeight`), not half the type's, and the types' eye heights are floats widened as `getEyeY` does. Gate: this wave's 50 PRs merged on main in one tree (union 9033a9c3b): fmt and clippy `-D warnings` clean, the horizon-client (1136 lib plus the integration tests), horizon-client-core (37), horizon-world (165) and horizon-protocol (100) tests pass, the fast build links, and the 140 harnesses `parity.py` picks for the tree's changed files pass on playhorizon (remote_fireworks, a known load flake there, passed on a rerun on a quiet Mac mini). `lan_e2e.py` passes 14 checks with the host on its server thread and in-frame (chest, xp, damage, kills, projectiles, mobs, drops, body, stopsound, digging, instant_digging, books, knockback, barter); `hurt` fails on main too when run after `damage` (check-order dependency, not this wave). Thanks @kacpherek!
2026-10-27 03:48:36 +01:00
Ludovic Marie dfd0aa0266 Merge pull request #2469 from kacpherek/fix/high-contrast-pack
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: the High Contrast option adds or removes the `high_contrast` pack (`Options.updateResourcePacks`), and your follow-up keeps the pack screen's selection in step. Gate: this wave's 50 PRs merged on main in one tree (union 9033a9c3b): fmt and clippy `-D warnings` clean, the horizon-client (1136 lib plus the integration tests), horizon-client-core (37), horizon-world (165) and horizon-protocol (100) tests pass, the fast build links, and the 140 harnesses `parity.py` picks for the tree's changed files pass on playhorizon (remote_fireworks, a known load flake there, passed on a rerun on a quiet Mac mini). `lan_e2e.py` passes 14 checks with the host on its server thread and in-frame (chest, xp, damage, kills, projectiles, mobs, drops, body, stopsound, digging, instant_digging, books, knockback, barter); `hurt` fails on main too when run after `damage` (check-order dependency, not this wave). Thanks @kacpherek!
2026-10-27 03:48:32 +01:00
Ludovic Marie f6e156a4ef Merge pull request #2468 from kacpherek/fix/left-handed-swing
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `HumanoidModel.setupAttackAnimation` and `AnimationUtils.swingWeaponDown` use the main arm of a left-handed mob, and `PiglinModel`'s crossbow and admiring poses mirror with it. Gate: this wave's 50 PRs merged on main in one tree (union 9033a9c3b): fmt and clippy `-D warnings` clean, the horizon-client (1136 lib plus the integration tests), horizon-client-core (37), horizon-world (165) and horizon-protocol (100) tests pass, the fast build links, and the 140 harnesses `parity.py` picks for the tree's changed files pass on playhorizon (remote_fireworks, a known load flake there, passed on a rerun on a quiet Mac mini). `lan_e2e.py` passes 14 checks with the host on its server thread and in-frame (chest, xp, damage, kills, projectiles, mobs, drops, body, stopsound, digging, instant_digging, books, knockback, barter); `hurt` fails on main too when run after `damage` (check-order dependency, not this wave). Thanks @kacpherek!
2026-10-27 03:48:29 +01:00
Ludovic MarieandClaude Opus 5.5 0ccf3e110b launcher: live game console with search, follow, copy and share log
Home's Console shows the game's output of this run as it comes, with a
search, Follow, Copy and Open logs folder. Share log uploads it to mclo.gs
only after the player confirms, with the home folder, user name and
account names taken out first, and can delete the paste again. CLI:
share-log and unshare-log (MCP tools, the second needs confirm).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 03:44:53 +01:00
Ludovic MarieandClaude Opus 5.5 b8b353c92e launcher: import a file dropped anywhere on the window
A file dropped on the window goes where it belongs:
- a shared instance becomes a new instance, which gets selected on Home;
- a Java modpack goes to the Modpacks import;
- a resource or shader pack goes into the selected instance;
- a data pack points to the world rows.

A note in the corner says what happened, until it is closed. The Packs
section and the world rows still take their own drops, and now stop the
drop there so it isn't imported twice.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 03:35:07 +01:00
Ludovic MarieandClaude Opus 5.5 e28bcba3d3 launcher: import a shared instance, or route any dropped file
`horizon_launcher::import` works out what a file is from its contents:
- a shared instance: a .mrpack needing no mod loader (as `instance
  export` writes) or a game-folder zip;
- a Java modpack: a .mrpack with a loader, or a CurseForge zip;
- a resource or shader pack;
- a data pack.

A shared instance becomes a new instance named after the pack, with
"(2)" if the name is taken. Its overrides/ (or the zip's entries) are
copied in, but only the files an instance keeps, and only paths that
stay inside its folder. Its .mrpack files are downloaded from Modrinth
and checked against their SHA-512. If anything fails, the half-made
folder is removed.

CLI and MCP: `horizon-launcher instance import <file> [--instance <i>]`
does what dropping the file on the window will do.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 03:31:52 +01:00
Ludovic Marie e91678b994 Merge pull request #2589 from ludovic111/fix/remote-fireworks-slow-screens
🤖 Agent: remote_fireworks passes 3 of 3 on playhorizon (frames drawn on the CPU) with the earlier shots and the newly-red count.
2026-10-27 03:26:40 +01:00
Ludovic MarieandClaude Opus 5.5 4482ff63f3 launcher: Export… on Home's Instance row
The Instance row gets Export…, which opens a panel to pick .mrpack or
.zip and switch on the worlds to include. The file goes to exports/ in
the launcher's data folder under a free name, and the panel offers Show
in folder. The panel closes when another instance is picked.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 03:25:19 +01:00
Ludovic MarieandClaude Opus 5.5 a9e5214ae4 launcher: export an instance as a .mrpack or a zip
`horizon-launcher instance export [<instance>] [--format mrpack|zip]
[--world <w>]... [--out <file>]` (and its MCP tool) writes what makes an
instance, or a port: options.txt, horizon-*.txt, servers.dat, resource
and shader packs, horizon-mods/ and config/, plus the worlds named. It
never includes logs, screenshots, backups, caches or account files. A
world leaves out playerdata/, stats/ and advancements/, and its level.dat
is written without the player's UUID.

A .mrpack follows Modrinth's published format: modrinth.index.json
lists the resource packs the browser installed from Modrinth that are
unchanged, by their download with SHA-1 and SHA-512 asked from
Modrinth's API. The rest goes under overrides/. An instance with nothing
to share is refused.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 03:24:53 +01:00
Ludovic Marie 9e359f230d Merge pull request #2599 from ludovic111/feat/launcher-screenshots
🤖 Agent: launcher-features-1 — item 6 (screenshots gallery); base retargeted to main after #2597 merged, merges cleanly, CI green.
2026-10-27 03:17:15 +01:00
Ludovic MarieandClaude Opus 5.5 2a7420651e launcher: find data packs on Modrinth and install one into a world
The Mods page gains a Data packs tab: Modrinth's data packs for 26.3,
each marked Partly with what Horizon runs of a data pack today
(functions, function tags, predicates, item modifiers). Install puts the
project's data pack file (not its mod build), checksum checked, into the
datapacks/ of a world picked on the tab, and says what of that zip runs
and what isn't read yet. A pack with nothing Horizon runs (worldgen
only) is refused. The world's list on Home shows it.

CLI and MCP: `mods search --kind datapack`, `mods install <pack> --world
<world>`.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 03:15:49 +01:00
Ludovic MarieandClaude Opus 5.5 9cb2779608 launcher: install mods and packs into the selected instance
The Mods page and `mods` commands installed everything into the default
game folder whichever instance was picked. Mods, resource packs and
shader packs now go into the selected instance's folder, and the
Installed list, updates, Remove and the mod switches are that
instance's; picking another instance reads its list. The page says which
instance it installs into when there are several. `mods list|install|
update|remove|enable|disable` take `--instance` (default: the selected
one). Java mods' checks stay shared in the default folder for the
search results.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 03:02:09 +01:00
Ludovic Marie 783577d2de Merge pull request #2597 from ludovic111/feat/launcher-play-time
🤖 Agent: launcher-features-1 — item 5 (play time per instance); base retargeted to main after #2596 merged, merges cleanly, CI green.
2026-10-27 02:55:07 +01:00
Ludovic Marie 8458872d07 Merge pull request #2596 from ludovic111/feat/launcher-quick-play
🤖 Agent: play into a world / join a server + servers.dat list (item 4 of launcher-features-1), on main after #2593: verify and launcher CI passed on this head, merges cleanly with main; launcher fmt, clippy -D warnings and lib tests pass locally (servers.dat round trip keeps every field and hidden entries); screenshots in the PR.
2026-10-27 02:51:55 +01:00
Ludovic MarieandClaude Opus 5.5 aa6fdac501 Launcher: data packs into a world
datapacks.rs lists a world's datapacks/ (zips and folders with
pack.mcmeta, as the game discovers them; off when datapacks.json lists them
disabled), adds a data pack zip under a free name (refusing resource packs:
no data/ folder) and removes one to the trash, forgetting it in
datapacks.json. The game turns a new pack on when the world opens.

Home: each world gets "Data packs", which opens its packs under the row
with Add data pack… and Remove; a zip dropped on a world goes into it.
CLI: `worlds datapack-list|datapack-add|datapack-remove <world>`;
datapack-remove needs confirm over MCP.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 02:49:42 +01:00
Ludovic Marie cd520ce61d Merge pull request #2593 from ludovic111/feat/launcher-worlds
🤖 Agent: worlds in the launcher (item 3 of launcher-features-1), on main after #2591: verify and launcher CI passed on this head, merges cleanly with main; launcher fmt, clippy -D warnings and lib tests pass locally (vanilla-style backup layout, copy naming); screenshots in the PR.
2026-10-27 02:48:44 +01:00
Ludovic MarieandClaude Opus 5.5 cac63dedca Launcher: Update all for installed packs
The Mods page's Installed list gets "Update all (N)" when newer versions
were found: every pack with one is updated in turn on a thread (each
download checked as an install is), with "1 of 2: …" progress, and one that
fails is named without stopping the others. The CLI already does this with
`mods update` and no pack name (MCP: mods_update).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 02:42:09 +01:00
Ludovic MarieandClaude Opus 5.5 3c257bb3e6 AGENTS.md: launcher test runs set HORIZON_LAUNCHER_TRASH and HORIZON_LAUNCHER_CLIPBOARD
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 02:33:31 +01:00
Ludovic MarieandClaude Opus 5.5 2ef155348f Launcher: keep test runs away from the real trash and clipboard
desktop_seams.rs holds the launcher's two hard-to-undo desktop effects.
HORIZON_LAUNCHER_TRASH=<folder> makes Delete (worlds, screenshots, packs)
move into that folder instead of the system's trash, and
HORIZON_LAUNCHER_CLIPBOARD=<file> makes every Copy in the window write into
that file instead of the clipboard. Test runs set both inside their scratch
folders.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 02:32:14 +01:00
Ludovic Marie f12e457a25 Merge pull request #2591 from ludovic111/feat/launcher-instance-version
🤖 Agent: version per instance (item 2 of launcher-features-1), now on main after #2590: verify and launcher CI passed on this head, merges cleanly with main; launcher fmt, clippy -D warnings and lib tests pass locally (pin/unpin test); screenshots in the PR.
2026-10-27 02:28:43 +01:00
Ludovic MarieandClaude Opus 5.5 e5c3d7cff7 Launcher: Packs section on Home, with drag and drop
Home lists the selected instance's resource and shader packs with a switch
each (disabled while the game runs: it rewrites options.txt itself), Remove
after a confirmation, and Add pack… or a .zip/.hshader dropped on the
section. switch() now takes any id.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 02:26:42 +01:00
Ludovic MarieandClaude Opus 5.5 ddaa514f3a Launcher: resource and shader packs per instance (library and CLI)
packs.rs lists an instance's resource packs (resourcepacks/, a .zip or a
folder with pack.mcmeta) and shader packs (shaderpacks/: Horizon's own and
the shaders/ format, as the game's Shader Packs page lists them), and turns
them on or off the game's own way: resourcePacks in options.txt (a pack
turned on goes on top) and pickedShaderPack in horizon-graphics.txt (one at
a time); every other line of both files stays. add() copies a dropped file
into the right folder under a free name; remove() turns it off and moves it
to the trash.

CLI: `packs [list|add|on|off|remove] [--kind] [--instance]`; `packs remove`
needs confirm over MCP.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 02:26:42 +01:00
Ludovic Marie 55cc84718a Merge remote-tracking branch 'origin/main' into feat/server-mods-commands 2026-10-27 02:24:14 +01:00
Ludovic Marie fde2dfffef Merge pull request #2598 from ludovic111/fix/friends-guest-spot
🤖 Agent: fixes parity-main's friends_integration (red since 4be5c8eb5). Cause: #2501 spawns new players at a random spot near the spawn, as vanilla does, and test_friends.py assumed the guest stood on the spawn itself. Main failed 4/4 on the Mac mini, main with #2501 reverted passed 2/2, and this branch passed 4/4 on the server thread and 1/1 in-frame. Python only; harness-only, skip-release.
2026-10-27 02:14:59 +01:00
Ludovic Marie 155f52af58 Merge pull request #2590 from ludovic111/feat/launcher-instances-ui
🤖 Agent: launcher instances in the window (item 1 UI of launcher-features-1), now on main after #2587: verify and launcher CI passed on this head, merges cleanly with main; launcher fmt, clippy -D warnings and lib tests pass locally; screenshots in the PR.
2026-10-27 02:11:48 +01:00
Ludovic Marie fd6fdcbf75 Merge pull request #2587 from ludovic111/feat/launcher-instances
🤖 Agent: launcher instances core (item 1 of launcher-features-1). Verify and the three-platform launcher build passed; fmt, clippy -D warnings and all launcher tests pass locally, including the migration test (no existing file moves); CLI and play checked in scratch homes.
2026-10-27 02:10:10 +01:00
Ludovic MarieandClaude Opus 5.5 0d5ed8122e Launcher: screenshots gallery per instance
Home shows the selected instance's newest screenshots (what F2 saves in its
screenshots/ folder), all of them on request, and opens one over the window
with Newer/Older (and the arrow keys), Copy (the picture to the clipboard),
Open, Show in folder and Delete (to the trash, after a confirmation).

CLI: `screenshots [list|delete] [--instance]`; `screenshots delete` needs
confirm over MCP. worlds::to_trash is shared by worlds and screenshots.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 02:09:39 +01:00
Ludovic Marie fd1d6a2762 Merge pull request #2595 from ludovic111/feat/game-quick-play
🤖 Agent: game quick-play flags (item 4 of launcher-features-1): pr_ready --quick passed (1281 horizon-client tests, clippy, fmt); verify passed and CI's parity scope selected no harness for this argument-parsing change; checked by hand with the real game (missing world → title screen, existing world opens).
2026-10-27 02:03:26 +01:00
Ludovic Marie 4fe8f71530 Merge remote-tracking branch 'origin/main' into feat/server-mods-commands 2026-10-27 02:03:13 +01:00
Ludovic MarieandClaude Opus 5.5 ea0cc65aea Launcher: play time per instance
Every run of the game started from the window or `horizon-launcher play`
adds its length to its instance's play time in instances.json (by id, so a
renamed instance keeps it), with when it ended. Home's Instance row shows
"3 h 06 min in total · last played today"; `instance list` shows it too,
and its JSON gives played_secs and last_played_ms.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 02:02:15 +01:00
Ludovic Marie a0ef14f1e2 Merge pull request #2582 from ludovic111/perf/parity-one-client-profile
🤖 Agent: harnesses build horizon-client in one profile; 6 harnesses checked fresh on playhorizon and the Mac mini.
2026-10-27 01:58:38 +01:00
Ludovic MarieandClaude Opus 5.5 4b761abcb2 Launcher: play straight into a world or server, and the Multiplayer list
Each world on Home gets Play (the game starts with --world <folder>), and
Home lists the selected instance's servers from the game's own servers.dat:
Join starts the game with --server <address>, Add server and Remove change
the file. server_list reads and writes it as vanilla's ServerList does
(uncompressed NBT, a new file then servers.dat_old) and writes every entry
back with all its fields, the hidden Direct Connection ones too.

CLI: `multiplayer [list|add|remove] [--instance]`, `play --server`;
MCP: play takes `server`; `multiplayer remove` needs confirm.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 01:56:41 +01:00
Ludovic Marie 0a75323caa Merge pull request #2584 from ludovic111/perf/parity-manifest-aware
🤖 Agent: crate splits' workspace-only manifest changes no longer select every harness (354 to 209 on #2160 and #2215); other PRs select as before.
2026-10-27 01:49:27 +01:00
Ludovic Marie 4f5ed6a19b Merge pull request #2588 from ludovic111/chore/workspace-members-per-line
🤖 Agent: merging. Formatting only: Cargo.toml's workspace members one per line (38, including #2160's), so new crates stop conflicting. tomllib parses it identical to main and Cargo.lock is unchanged, so no parity run is needed; verify and launcher pass.
2026-10-27 01:45:25 +01:00
Ludovic MarieandClaude Opus 5.5 d12dc5d1e1 Game: vanilla's --quickPlaySingleplayer and --quickPlayMultiplayer
The launcher starts the game straight into a world or a server. Vanilla's
names for that (client Main, QuickPlay) now work too: --quickPlayMultiplayer
is --server; --quickPlaySingleplayer opens a world of saves/ only if it
exists, as QuickPlay.joinSingleplayerWorld does, and shows the title screen
otherwise (--world also creates one).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 01:43:25 +01:00
Ludovic MarieandClaude Opus 5.5 c70e2fae74 Launcher: worlds of an instance, with backup, copy and delete
Home lists the selected instance's worlds from their level.dat (name, mode,
version, last played) with their size and picture. Back up zips one into
backups/ as vanilla's Make Backup does; Copy makes "<name> (1)" with that
name in its level.dat; Delete moves it to the system's trash after a
confirmation (the file manager call on macOS, no Finder permission);
Open folder. `horizon-launcher worlds list|backup|copy|delete`, and their
MCP tools (delete needs confirm). level.dat is read with horizon-world's NBT.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 01:37:31 +01:00
Ludovic MarieandClaude Opus 5.5 64f9ff4529 Merge origin/main into chore/workspace-members-per-line
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 01:37:20 +01:00
Ludovic Marie f5b7bdba1d Merge pull request #2160 from ludovic111/refactor/horizon-command
🤖 Agent: merging the first crate split (horizon-command + horizon-client-core, pure move, re-exported under the old paths). CI parity passed at 17f8419ac (head merged into main), verify and launcher pass, approved by rdbt's review agent; landed by hand while the REST pool refills.
2026-10-27 01:36:58 +01:00
Ludovic MarieandClaude Opus 5.5 90f2755245 Launcher: a Horizon version per instance
An instance plays the launcher's version, which follows updates, unless it is
kept on another installed one: the Play page's picker pins the selected
instance (picking the version marked "follows updates" unpins it), and
`instance version <instance> <tag|latest>` does it from the CLI. play,
`play --instance` and MCP play use the pin; a pack's Play uses its instance's.
The sidebar shows a pinned instance's version, or that it is not installed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 01:25:31 +01:00
Ludovic MarieandClaude Opus 5.5 a76456b036 Launcher: instances in the sidebar, and a dialog to make and edit them
The sidebar lists the instances by group with their icons (a texture from the
player's prepared files, drawn as pixels, or a colour); clicking one selects it
for Play. + makes one, empty or a copy of the selected one (copied in the
background); Home's Instance row edits its name, group and icon, copies it, and
opens its folder. The dialog's name and group are the launcher's shared text
fields.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 01:21:56 +01:00
Ludovic MarieandClaude Opus 5.5 6087b01f66 test_friends: put the guest on cleared, flat ground before it walks
Since #2501 a player new to the world spawns at a random free spot near the
world spawn, as vanilla's PlayerSpawnFinder does. The guest often stood against
a hill, so walking forward moved it less than a block, or terrain hid the block
it had to aim at: friends_integration failed on every run on the Mac mini.

The host now clears a floor beside itself and teleports the guest there before
the movement checks, and the block check waits for the guest to stand still.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 01:21:06 +01:00
Ludovic MarieandClaude Opus 5.5 1d0d827f3a Launcher: instances, each with its own game folder
The game's folder stays the default instance, "Horizon", where it is; imported
and registry modpacks are listed as instances in their own folders; new ones
go to instances/<slug>/ in the game's folder, empty (the player's options and
offline name) or a copy of another (never sessions, logs or telemetry). Names,
icons and groups live in the launcher's instances.json, so nothing moves.
Play, `play --instance` and MCP play start the selected instance, which
follows the launcher's data collection answer and No AI. Crash reports are
read from the run's own folder.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 01:13:35 +01:00
Ludovic MarieandClaude Opus 5.5 844441874a docs: workspace crate features in a split
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 01:11:27 +01:00
Ludovic Marie b6dd6c35ae Merge remote-tracking branch 'origin/main' into perf/parity-manifest-aware 2026-10-27 01:10:36 +01:00
Ludovic MarieandClaude Opus 5.5 1af4d0e432 parity: a workspace crate's features count through the sources they gate
A manifest line defining or turning on a workspace crate's feature no longer
makes the manifests count, if every source naming the feature (or one it turns
on) changed too, as in a split, where the new crate's files are all new.
#2215 (horizon-items with a test-support feature) now selects 209 harnesses
instead of 354.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 01:10:36 +01:00
Ludovic Marie 68c9597237 Merge remote-tracking branch 'origin/main' into perf/parity-one-client-profile 2026-10-27 01:09:18 +01:00
Ludovic MarieandClaude Opus 5.5 bedba4a3d9 lan_e2e fireworks: count newly red pixels, take the later shots 4 ticks after the burst
On playhorizon (frames drawn on the CPU) a screenshot takes ~0.6 s: with a
16-tick wait the later shots came after the sparks' lifetime, and the setup's
own red chat lines, gone by then, made the host's count negative.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 01:08:50 +01:00
Ludovic MarieandClaude Opus 5.5 0db65d7a61 Cargo.toml: one workspace member per line
Each new crate then adds one line instead of rewriting the whole list, so
PRs that add crates at the same time stop conflicting (the crate split's
#2160 conflicted on it three times).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 01:06:57 +01:00
Ludovic Marie aef652c8af Merge pull request #2552 from ludovic111/ci/remote-fireball-not-flaky
🤖 Agent: remote_fireball passed 4 of 4 under load on playhorizon since #2499; off the known-flaky list.
2026-10-27 00:52:39 +01:00
Ludovic Marie 4be5c8eb53 Merge pull request #2196 from kacpherek/fix/axolotl-squid-water-rules
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `Axolotl.handleAirSupply` (6000 air, refilled by water or rain, `dry_out` damage at -20) and `Squid.hurtServer`'s `spawnInk`. I pushed two fixes: `makeSound`'s voice pitch is drawn even when the squid is silent, so the ink's random draws stay in vanilla's order; and rain now uses the biome's height-adjusted temperature (`Biome.getTemperature` with the frozen modifier and the drop above sea level, `Level.isRainingAt`), not the base temperature, so an axolotl high up a mountain is not refilled by snow. On main with this head: clippy `-D warnings` clean and the 1186 horizon-client lib tests pass. CI parity runs the harnesses this head reaches. Thanks @kacpherek!
2026-10-27 00:09:19 +01:00
Ludovic Marie 6ed9e9632d Merge pull request #2557 from ludovic111/ci/full-parity-playhorizon
🤖 Agent: playhorizon takes PR parity jobs and full-parity ones; the desktop's runner is retired (maintainer's request).
2026-10-27 00:05:55 +01:00
Ludovic MarieandClaude Opus 5.5 111db2827b ci: full-parity and PR parity jobs on playhorizon, not the desktop
The maintainer asked that CI stop using his desktop (2026-10-26). playhorizon's
runner now carries horizon-game and the desktop's runner none of the job labels;
full-parity jobs go to playhorizon only, with a 6 h timeout on its 4 CPUs.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 23:46:29 +01:00
Ludovic MarieandClaude Opus 5.5 17f8419acd Merge origin/main into refactor/horizon-command
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 23:40:59 +01:00
Ludovic MarieandClaude Opus 5.5 9555f375d4 parity: remote_fireball is no longer known flaky (#2499)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 23:31:29 +01:00
Ludovic MarieandClaude Opus 5.5 1b1571ed6a parity: a crate split's manifest changes don't select every harness
Cargo.lock is a shared input and every harness depends on its crates'
Cargo.toml, so a crate split (#2160) selected all 352 harnesses although
its moved code reaches 123. With --changed, Cargo.lock and Cargo.toml
changes that only add, remove or move workspace crates (no registry or git
package, version, feature, dependency or build setting changed across them,
compared with the merge base) are left out; the moved code counts through
its own sources. Any other manifest change still counts, as before.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 23:30:59 +01:00
Ludovic MarieandClaude Opus 5.5 a891037c9f parity: harnesses build horizon-client in the fast profile only
Under parity.py the release and fast profiles have identical settings
(PROFILE: no LTO, 16 codegen units, incremental), yet 38 harnesses built
horizon-client in release and 16 others in fast: every run where the client
changed compiled it twice. They all use fast now. Release stays for
horizon-world's probes, which no fast harness builds.

Measured on playhorizon (4 CPUs), all 13 probe builds after 78 merges:
1578 s, of which the release client (bin + client_probe) took 485 s.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 23:24:54 +01:00
FusselTVandClaude Opus 5.5 266c7a2cd9 Worldgen: finalize structure mobs at placement, as vanilla
Structure mobs (bastion piglins, brutes and hoglins, ocean ruin drowned,
village and mansion mobs, witch huts, monuments) were tagged and finalized
at load by the cut-down spawning routine: no STRUCTURE reason (piglin
weapons rerolled), no Mob step (follow range bonus, left-handedness), no
baby hoglins, no brain memories, no drowned nautilus mounts, and draws
from each mob's own random. The decoration region now calls a hook the
game installs in the generator workers, which runs mobs::finalize with
Reason::Structure, the region random in placement order and the level's
difficulty with no inhabited time; jockeys come back as the vehicle with
its Passengers. A mob the hook cannot finalize keeps its tag and is
finalized on load as before, as are mobs of older saves; other entities
drop the tag. The generator gets the level's difficulty as soon as the
level opens, so the spawn area's mobs use it too. mob_to_nbt writes Invulnerable and LeftHanded always, IsBaby
for zombies and piglins, Age for hoglins, the finalized attributes and
memories, and drops the template keys vanilla never saves.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 23:12:22 +01:00
Ludovic Marie a727f617f0 Merge pull request #2532 from ludovic111/feat/sculk-sidebar
🤖 Agent: merging. Ludovic asked for the Sculk banner visible on open without scrolling, hideable after a while. Launcher run with isolated homes: banner at the sidebar's foot, HIDE after 3 min seen; clippy -D warnings and 137 launcher tests pass.
2026-10-26 23:05:17 +01:00
Ludovic MarieandClaude Opus 5.5 12bf15cfd7 launcher: the sidebar's partner banner can be hidden once it has been seen
A "Hide" shows beside its label after the banner has been on screen for
3 minutes, counted over every start (settings: partner_seen_secs, saved
each minute). Hiding it is kept (partner_hidden); the Servers page keeps
its own banner.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 22:58:09 +01:00
Ludovic MarieandClaude Opus 5.5 b0783dac38 launcher: Sculk's banner at the foot of the sidebar, seen without scrolling
On Home it sat under every row, so nobody saw it without scrolling. It now
sits small (228 px wide) above the Discord and Website links on every page
but Servers, which keeps the large one.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 22:54:33 +01:00
Ludovic Marie 840d11a3b2 Merge pull request #2499 from ludovic111/fix/remote-fireball-ticks
🤖 Agent: test-health-1: remote_fireball flake fix (slow fireball, one ctl script; 4/4 on playhorizon); checks green.
2026-10-26 22:44:46 +01:00
Ludovic Marie 4504223265 Merge pull request #2502 from ludovic111/ci/full-parity-desktop
🤖 Agent: test-health-1: full-parity label routes a PR's parity job to the desktop runner (verified on this PR); checks green.
2026-10-26 22:39:32 +01:00
Ludovic Marie 774fa9d897 Merge pull request #2186 from kacpherek/fix/soul-speed
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3 Soul Speed: the speed on soul blocks, no soul sand slowdown, the particles and sound, and boot wear on 4% of ticks, now also for each player on a server (`guest_server_tick`), with the particle sent to the players around. I pushed one fix: the random draws come in vanilla's order (the 0.04 wear roll first, then the particles, then the 0.35 sound roll and its pitch). It then conflicted with main (#2190 added the elytra glide wear in the same tick), so I merged main in: Soul Speed comes first, as `LivingEntity.baseTick` runs before `aiStep`, then the glide wear, so both draw from the level random in vanilla order. On that merge: clippy `-D warnings` clean, horizon-client tests pass, the fast build links, and `lan_e2e.py` elytra, damage and breath pass with the host in-frame and on its server thread. CI parity runs the harnesses this head reaches. Thanks @kacpherek!
2026-10-26 22:39:29 +01:00
Ludovic Marie 0c7f6b7f0b Merge pull request #2446 from ludovic111/feat/official-recipes-mod
🤖 Agent: #2318 has merged and this PR's base is main. The official Recipes mod (needs API level 6): pr_ready --quick green; the e2e passes 25/25 in-frame and on a dedicated server on this head (61 of 63 in five modes before; the 2 failures were #2501's bug, now merged). Queued at 92e3f496f.
2026-10-26 22:33:36 +01:00
Ludovic Marie c1a811c4bb Merge pull request #2318 from ludovic111/feat/mod-recipe-actions
🤖 Agent: #2306 has merged and this PR's base is main. recipes.place and recipes.give at API level 6, give cut to one stack. Sandbox (31), api_reference and clippy -D warnings pass; the actions e2e passed 58/58 in five modes. Queued at bd1436f16.
2026-10-26 22:31:39 +01:00
Ludovic Marie 19bb6c1262 Merge pull request #2306 from ludovic111/feat/mod-recipe-pages-game
🤖 Agent: #2305 has merged and this PR's base is main. Recipe pages with station tabs and an ellipsis on a long title; clippy -D warnings with --features mods and --no-default-features is clean, and the browser e2e passed 98/98 in five modes. Queued at 0cba2edbd.
2026-10-26 22:29:19 +01:00
Ludovic Marie 9bf853804e Merge pull request #2481 from ludovic111/perf/prefetch-world-data
🤖 Agent: verify passed (incl. Linux clippy for the glibc malloc_trim path); chunk_generation, worldgen, worldgen_random, save_roundtrip and world_reopen pass on this head; pr_ready --quick green. Preload starts on world select per the orchestrator's decision. Perf fix, no label.
2026-10-26 22:29:16 +01:00
Ludovic Marie 7e4ddee8b1 Merge pull request #2305 from ludovic111/feat/mod-recipe-pages
🤖 Agent: #2301 has merged and this PR's base is main. browser.set-recipes (with station tabs) at API level 6; sandbox (31), api_reference and clippy -D warnings pass. Queued at a373eea18.
2026-10-26 22:27:04 +01:00
Ludovic MarieandClaude Opus 5.5 f8ba650559 ci: full-parity label runs a pull request's parity job on the desktop
A crate split reaches every harness (#2160: 352 harnesses, 71 min on a Mac
mini runner) and held the Mac mini's runners while other pull requests'
parity jobs queued for over an hour. With the full-parity label, the job
runs on the desktop's runner only (8 at a time) and waits for it while the
desktop is in use; its timeout is 300 min, since a job under way when
someone comes back goes on with 2 threads.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 22:25:11 +01:00
Ludovic Marie 92b99c58aa Merge pull request #2301 from ludovic111/feat/mod-item-panel
🤖 Agent: #2300 has merged and this PR's base is main. Merged up the renumbered stack (browser at API level 6); clippy -D warnings with --features mods and --no-default-features is clean, and the browser e2e passed 98/98 in five modes before. Queued at a2cd70e8e.
2026-10-26 22:24:13 +01:00
Ludovic Marie 7e82902d15 Merge pull request #2300 from ludovic111/feat/mod-item-panel-api
🤖 Agent: #2284 has merged and this PR's base is main. browser is at API level 6 (recipes took 5). Sandbox (31, the UI guest at level 6), api_reference and clippy -D warnings pass. Queued at b836eea06.
2026-10-26 22:21:58 +01:00
Ludovic Marie d090686ffa Merge pull request #2500 from ludovic111/tools/remote-cleanup
🤖 Agent: test-health-1: remote.sh stops the host command with its caller and cleans what it leaves; checks green.
2026-10-26 22:21:54 +01:00
Ludovic Marie aba4f19a81 Merge pull request #2284 from ludovic111/feat/mod-recipes-read
🤖 Agent: #1895 has merged and this PR's base is main. Merged main: recipes moves to API level 5 (world took 4). Sandbox (30, including the level assertion), api_reference and clippy -D warnings on horizon-mods pass. Queued at b360e3c26.
2026-10-26 22:19:22 +01:00
Ludovic Marie 5a1d1c38ed Merge pull request #2501 from ludovic111/fix/lan-guest-stations
🤖 Agent: LAN guests (host on its server thread) and a dedicated server's new players joined inside each other on the exact spawn, so they clicked the other player instead of a table or chest. New players now get PlayerSpawnFinder.findSpawn as in vanilla. lan_e2e spawned/crafting/chest pass in both host modes, a dedicated two-guest check passes, and the 32 affected harnesses pass.
2026-10-26 22:19:19 +01:00
Ludovic MarieandClaude Opus 5.5 cc58083574 Give the dropped preload's memory back to the OS
glibc keeps the pages the preload thread freed in that thread's arena, and
macOS keeps them in its zones until memory runs short, so dropping the
preload didn't lower the footprint: horizon-threads now asks the allocator to
return free pages (malloc_trim, malloc_zone_pressure_relief).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 22:14:56 +01:00
Ludovic Marie aa0eb406d7 Merge pull request #2496 from ludovic111/fix/dedicated-server-steps
🤖 Agent: test-health-1: dedicated_server steps wait for the server's reply (flake fix, passes under injected stalls); checks green.
2026-10-26 22:14:45 +01:00
Ludovic Marie c5534f0d7b Merge remote-tracking branch 'origin/main' into feat/server-mods-commands
# Conflicts:
#	crates/horizon-mods/src/lib.rs
2026-10-26 22:14:09 +01:00
Ludovic MarieandClaude Opus 5.5 8f02822a9a remote.sh: stop the command on the host with its caller, and what it leaves running
The command ran on after remote.sh was stopped or its connection dropped,
holding the name's lock, and a game or daemon it left behind outlived the
run. It now runs in a scope named after the run (remote-<name>.scope),
stopped when the command ends, when the ssh session ends first, and before
the next run under that name.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 22:09:44 +01:00
Kacpherek 8f78fdeb7b Keep leash knot ids per level and clear remote knots with the other remote objects 2026-10-26 22:08:35 +01:00
Ludovic MarieandClaude Opus 5.5 031f1e58a3 remote_fireball: a slow fireball, sampled from one ctl process
The harness sampled the fireball from a new horizon-ctl process each time,
10-16 ticks apart on a loaded runner: the fireball (~2 blocks a tick) left
the cleared corridor or the guest's tracking range before the sixth sample,
and the harness failed. It now flies slowly (acceleration_power 0.02, under
0.4 blocks a tick) down a longer corridor, and the guest takes 16 samples
in one ctl script (lan_e2e's Game.script returns every reply with every=True).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 22:07:12 +01:00
Ludovic MarieandClaude Opus 5.5 d3a662b760 Merge origin/main into refactor/horizon-command
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 22:06:35 +01:00
Kacpherek ed9fdb5208 Point an explosion's hurt tilt at the blast 2026-10-26 22:04:40 +01:00
Ludovic MarieandClaude Opus 5.5 3f0e70882c Death messages: no fallback for a message with no entity behind it
DamageSource.getLocalizedDeathMessage sends the one-argument message, and
TranslatableContents.decompose shows a template it cannot fill as written.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 22:03:42 +01:00
Kacpherek 6120c8c0ad Attack the block when a guest's START_DESTROY_BLOCK arrives 2026-10-26 22:03:17 +01:00
Ludovic Marie 92e3f496f0 Merge branch 'feat/mod-recipe-actions' into feat/official-recipes-mod
# Conflicts:
#	Cargo.toml
2026-10-26 22:01:02 +01:00
Ludovic MarieandClaude Opus 5.5 54ae097faa Merge main into the branch
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 22:00:46 +01:00
Ludovic Marie bd1436f162 Merge branch 'feat/mod-recipe-pages-game' into feat/mod-recipe-actions
# Conflicts:
#	crates/horizon-mods/src/api_level.rs
#	crates/horizon-mods/src/manifest.rs
#	docs/api/mods-reference.md
#	docs/api/stability.md
2026-10-26 22:00:44 +01:00
Ludovic MarieandClaude Opus 5.5 1b0937abff Piglin celebration: the dance roll and wasHurtBy as in PiglinAi
wantsToDance draws from RandomSource.createThreadLocalInstance(gameTime),
not the piglin's random, and wasHurtBy ignores another piglin's hit.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 22:00:05 +01:00
Kacpherek 53ea3e7224 Play the rain sound for this client only 2026-10-26 22:00:04 +01:00
Ludovic MarieandClaude Opus 5.5 1fe0f5fcb9 Sound options: Music Frequency starts its own row, as in SoundOptionsScreen
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 21:59:39 +01:00
Ludovic MarieandClaude Opus 5.5 34f57dbcac Trident: Impaling through the enchantment's damage effect
ThrownTrident.onHitEntity runs EnchantmentHelper.modifyDamage on the
trident, so Impaling adds to the 8 for the entity types in
#sensitive_to_impaling (its minecraft:damage requirement), not for every
mob that swims or breathes water.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 21:59:31 +01:00
Kacpherek 462787aee1 Keep the Infested event when a mob is hit by a player or another mob 2026-10-26 21:58:25 +01:00
Ludovic MarieandClaude Opus 5.5 83b544f04f Bonus chest: draw from the level random and set blocks with UPDATE_CLIENTS
MinecraftServer.setInitialSpawn passes level.getRandom() to
BonusChestFeature.place, which sets the chest and torches with flags 2.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 21:57:31 +01:00
Ludovic MarieandClaude Opus 5.5 c4d0cb1ec0 A small armor stand's eyes are at 0.9875, and eye heights are floats
ArmorStand.BABY_DIMENSIONS keeps its own eye height rather than half the
type's, and EntityDimensions.eyeHeight is a float widened by getEyeY.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 21:55:51 +01:00
Ludovic MarieandClaude Opus 5.5 1258c2473b Multiply the particle speed and delta as floats, as handleParticleEvent does
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 21:54:53 +01:00
Ludovic Marie 0cba2edbd4 Merge branch 'feat/mod-recipe-pages' into feat/mod-recipe-pages-game 2026-10-26 21:52:49 +01:00
Ludovic Marie d6a80c5193 Merge pull request #2489 from ludovic111/tools/remote-linux
🤖 Agent: test-health-1: tools/remote.sh runs agents' Linux work on playhorizon (fetch from GitHub, no secrets sent); verify green.
2026-10-26 21:52:49 +01:00
Ludovic Marie a373eea189 Merge branch 'feat/mod-item-panel' into feat/mod-recipe-pages
# Conflicts:
#	crates/horizon-mods/src/api_level.rs
#	docs/api/stability.md
2026-10-26 21:52:43 +01:00
Ludovic Marie a2cd70e8e1 Merge branch 'feat/mod-item-panel-api' into feat/mod-item-panel
# Conflicts:
#	crates/horizon-client/src/game.rs
2026-10-26 21:51:29 +01:00
Ludovic MarieandClaude Opus 5.5 331e3a7202 dedicated_server: steps wait for the server's reply, not a 1.5 s timer
The parity client's chat, villager and furnace steps went on 1.5 s after
sending, so a busy server's late reply landed in the next step (or a click
carried a stale state id). Each step now waits until the server answered:
its chat line, both slot packets of a shift-click, and the furnace totals
the click changes (up to the same 30 s patience). The respawn window keeps
only the player's own entity packets: a squid's swim event or a mob coming
into view falls into it as each server's ticks fall.

Off the known-flaky list: it passes under 2.5 s server stalls every 5 s,
where main's client fails.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 21:49:24 +01:00
Ludovic MarieandClaude Opus 5.5 b836eea062 Merge #2284 (recipes now at API level 5): browser moves to level 6
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 21:47:46 +01:00
Kacpherek 3bb6e40304 Clear a bundle's selection when the cursor leaves it on a server's client too 2026-10-26 21:46:13 +01:00
Kacpherek eab8d0f911 Slide on honey for the server's player and show the slide effects to multiplayer clients 2026-10-26 21:45:02 +01:00
Kacpherek d7fece5825 Keep hideParticles of mob effects: no swirls drawn or sent for them 2026-10-26 21:42:52 +01:00
Kacpherek bab4251715 Give armor stands, frames, paintings, boats, carts and other entities a scoreboard name 2026-10-26 21:37:47 +01:00
Kacpherek 9eb913affe Show the crouching pose of a guest who is not holding sneak 2026-10-26 21:37:33 +01:00
Kacpherek c808fae288 Keep a stronger active effect when a weaker one is added 2026-10-26 21:37:30 +01:00
Kacpherek da736d56d6 Lingering clouds skip mobs none of their effects can affect 2026-10-26 21:30:42 +01:00
Ludovic MarieandClaude Opus 5.5 84f8205bdd Merge main into fix/1482-evoker-fangs
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 21:30:40 +01:00
Ludovic Marie effb29e571 Merge pull request #2480 from ludovic111/perf/requeue-on-centre-change
🤖 Agent: verify passed; chunk_generation, worldgen, worldgen_random, save_roundtrip and world_reopen pass locally on this head plus #2481; pr_ready --quick green. Perf fix, no label.
2026-10-26 21:28:37 +01:00
Ludovic MarieandClaude Opus 5.5 26561d315c Preload the world data while the player chooses a world
The worldgen bundle, the loot tables and the registries a server sends are
read on a helper thread from the moment the player opens Singleplayer or
Create World, so a world opening finds them ready. A world opened meanwhile
waits for the part being read. If the player goes back to the title or to
Multiplayer and opens no world for a minute, the preload is dropped: the three
caches can now let go of what they hold.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 21:26:47 +01:00
Kacpherek 65c60ab996 Play remote sounds in the category the packet names 2026-10-26 21:23:38 +01:00
Kacpherek 56bf45cee5 Keep the pack screen selection in step with High Contrast 2026-10-26 21:23:03 +01:00
Ludovic MarieandClaude Opus 5.5 4afd517c22 tools/remote.sh: run a command on playhorizon at a worktree's commit
The host fetches the commit from GitHub (nothing else is sent), keeps one
checkout and target per worktree, runs the command niced in its CI slice on
CPUs 4-7 with scratch homes, prepares vanilla data for harness commands as
CI does, and streams the output back. One run per name at a time; no lock
leaks into daemons the run starts.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 21:22:46 +01:00
Kacpherek abb4e7b5cd Record the thrower of guest drops and the player behind a lead 2026-10-26 21:21:40 +01:00
Kacpherek 0e96fb6d2a Disable unavailable options in the Graphics Advanced screen 2026-10-26 21:21:06 +01:00
Kacpherek f4a235e027 Record a witch potion source only when it deals damage 2026-10-26 21:20:45 +01:00
Ludovic Marie cb43e32787 Merge pull request #2479 from ludovic111/tools/pr-ready-like-ci
🤖 Agent: test-health-1: pr_ready now runs CI's clippy targets and features, the server clippy pass and the Python tests; verify green.
2026-10-26 21:16:48 +01:00
Ludovic MarieandClaude Opus 5.5 67c27701e9 Server: new players join around the spawn, not all on one spot
The server put every player new to the world exactly on the world spawn.
On the server thread the host's own player joins that way too, so the host
and each new guest stood inside each other: the guest's client targets
the host's player (its eye inside that box, as vanilla's getEntityHitResult
does), so it could open no crafting table, chest or furnace, and Entity.push
never parts two players with the same centre. A dedicated server stacked
its new players the same way.

Vanilla places each new player with ServerPlayer.adjustSpawnLocation:
PlayerSpawnFinder.findSpawn, a free column at a random offset within the
respawn radius. Game::server_player_spawn does it for a server's new
players with the window's own finder, passing over the candidates whose
chunk isn't loaded yet (vanilla waits for them; a server tick can't).

lan_e2e's new check `spawned` asserts that the host and the guest join on
different spots, then has the guest open a crafting table, a chest, a
furnace and a stonecutter where it joined.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 21:16:43 +01:00
Ludovic MarieandClaude Opus 5.5 69bb813128 game: cut a recipe page's title with an ellipsis
A title wider than the panel ended at its edge mid-word ("Recipes: Oak P");
it now ends with vanilla's "..." (CommonComponents.ELLIPSIS).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 21:16:34 +01:00
Ludovic MarieandClaude Opus 5.5 b360e3c26c Merge main (#1895: world takes level 4): recipes moves to API level 5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 21:14:13 +01:00
Ludovic MarieandClaude Opus 5.5 3755bb7d77 parity: stop harnesses on SIGTERM, and reap orphaned harness JVMs
Nine vanilla Java servers from harness runs had been orphaned on the Mac mini for 2 to 5 days. Each harness runs in
its own session (killpg on a timeout), so when a CI job was cancelled, its harnesses and their servers outlived
parity.py.
- parity.py: SIGTERM and SIGHUP exit through the finally blocks, which now stop the local executor's harnesses
  too, not only the remote hosts'. The worker (--worker) stops its harnesses the same way.
- runner-gc.sh (every 6 h from launchd): stops Java processes whose parent is gone (launchd, init or
  systemd --user) and whose command line names a runner's checkout or a worktree's .local/, once ORPHAN_HOURS
  (default 3) old.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 21:10:41 +01:00
Kacpherek 94748162bc Handle a dig packet before the move queued behind it
The client breaks the block under its feet and falls in the same tick. The
hub checked that move against the block that was still there and sent the
player back. Moves queued behind a dig packet now wait for the next tick,
after the break.
2026-10-26 21:08:59 +01:00
Kacpherek 06b046d8fc Light, scaffolding and shulker box outlines follow the held item and the lid 2026-10-26 21:07:17 +01:00
Ludovic Marie 9312411e53 Merge pull request #2451 from ludovic111/ci/parity-known-flaky
🤖 Agent: merging #2451 (test-health-1): verify and parity passed at e82fa3d95.
2026-10-26 21:00:51 +01:00
Ludovic Marie 9cc9792b28 Merge pull request #2448 from ludovic111/ci/parity-merge-main
🤖 Agent: merging #2448 (test-health-1): verify and parity passed at df045f1e9.
2026-10-26 21:00:47 +01:00
Ludovic MarieandClaude Opus 5.5 37559dae82 pr_ready: run CI's clippy, CI's test features, Python tests and launcher rule
pr_ready --quick passed #1895 while CI's clippy failed on an unused import in horizon-mods' tests: clippy on the
touched crates and their dependents gives them other features than CI's workspace-wide run (Cargo unifies
features over the packages it builds).
- clippy: CI's two commands as they are. The whole workspace runs with the game's code mods, and the server build
  (horizon-client --no-default-features) runs when horizon-client is affected. Warm on the Mac mini, a change to
  hud.rs took 14-16 s against 17 s before, and food.rs 22-32 s against 14 s. Each worktree's first run costs
  about 3 min once.
- tests stay on the touched crates, with the features CI's workspace run gives them: horizon-client's code mods,
  and horizon-mods' runtime, pack and index.
- The Python tests always run, as in CI: some read the crates, the roadmap or the workflows.
- The launcher steps run when CI's launcher job would (tools/ci/rust_changes.py's rule, which also covers the
  root manifests and the check itself).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 20:58:06 +01:00
Ludovic Marie fff9cd1580 Merge pull request #1895 from ludovic111/feat/server-mods-world
🤖 Agent: merged main for #2316 (the campfire fix: this head had #2015 without it, so dispenser and block_removal would fail parity). Clippy -D warnings on horizon-mods (all targets, all features) and on horizon-client --features mods --bins is clean. Requeued at 8ec47d641.
2026-10-26 20:55:03 +01:00
Ludovic MarieandClaude Opus 5.5 bb9b0424b4 Axolotl rain: the biome's height-adjusted temperature
Level.isRainingAt takes Biome.getHeightAdjustedTemperature, which applies
the frozen oceans' temperature modifier and the height noise above the snow
level; reuse horizon-world's version instead of a simplified copy.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 20:52:11 +01:00
Kacpherek c85f6f2e39 Give polar bears their attack AI and rearing pose
Polar bears only panicked and strolled. They now follow vanilla's goals: melee attack that rears up and growls before each strike, hurt-by and cub-defending targets, anger at the player, and fox hunting. A hurt cub calls the adults. The standing flag is synched and the client animates it.
2026-10-26 20:50:13 +01:00
Ludovic Marie 565cf6af4e Merge pull request #2205 from kacpherek/fix/turtle-eggs
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: turtles lay (`TurtleLayEggGoal` with the digging effect), `TurtleEggBlock` hatches, and a growing baby drops a scute. I pushed one fix: the home is set at spawn and kept on load (`finalizeSpawn`, `home_pos`), and breeding drops one orb of 1 to 7, drawn from the mother's random and only with `mob_drops`. After #2137 the gift loot event names its mob. I merged main into your branch (6ef61ac13): main's `Reinforcements` stays beside your `BlockBreakEffect`, and the grown scute now goes through #2137's `drop_gift_loot` (the turtle's id added to both `GiftLoot` events) behind your mob_drops check; clippy clean, same code as the tested tree. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
2026-10-26 20:43:59 +01:00
Kacpherek 34dabe4afd Grey out Chunk Fade-in, Improved Transparency and Chunk Builder 2026-10-26 20:41:49 +01:00
Kacpherek 36c06d6133 Let a piglin celebrate a killed target and dance over a hoglin 2026-10-26 20:41:38 +01:00
Kacpherek ad6f2320b0 Wire the Music Frequency option to the music manager 2026-10-26 20:40:06 +01:00
Ludovic Marie b6a2bb2c0d Merge pull request #2285 from ludovic111/fix/loot-tables-before-chunks
🤖 Agent: Merged by Ludovic's AI agent (Claude). The loot tables now load before a world's first chunks are read, so a loot chest near spawn gets its roll. The new spawn_loot harness fails on main (0 stacks) and passes here with vanilla 26.3's exact roll, on the server thread and in-frame. I merged main after #2316; dispenser and block_removal pass again. Follows @FusselTV's #2073.
2026-10-26 20:39:09 +01:00
Kacpherek 4201fd23c0 Use real eye heights in /execute and keep the anchor on positioned as 2026-10-26 20:39:05 +01:00
Ludovic MarieandClaude Opus 5.5 8920f08fd9 Streaming: rank queued jobs again when the centre or the loading screen changes
Jobs queued while the loading screen was up carried ring-order priorities,
far above the plain distances used once it closed, so they waited behind
every later job. The proto chunks they generated held back the decoration
of the chunks around the player: at render distance 32 in-frame, nothing
was decorated until every proto chunk had been generated. Queued jobs now
keep their column and bias and are ranked again from the current centre,
extra areas, band and loading screen when those change, as vanilla's
chunk task queue follows the current ticket levels.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 20:39:04 +01:00
Kacpherek 46b71954e1 Make the High Contrast option turn its resource pack on and off 2026-10-26 20:38:38 +01:00
Kacpherek b92a53e47c Swing and pose the main arm of a left-handed mob 2026-10-26 20:37:52 +01:00
Kacpherek ffc343aadd Make execute on origin and leasher find their entity 2026-10-26 20:37:39 +01:00
Kacpherek 4cf9ffd847 Draw the autosave indicator 2026-10-26 20:36:40 +01:00
Kacpherek e23fcb1040 Scale a baby's walk position when it is read, as vanilla does 2026-10-26 20:35:46 +01:00
Kacpherek 27b81d9a8f Give /damage on a player its source, knockback and kill credit 2026-10-26 20:35:42 +01:00
Kacpherek 7b3595b8f2 Draw the snow golem's pumpkin and the mooshroom's mushrooms 2026-10-26 20:34:18 +01:00
Kacpherek c417556834 Send /tellraw, /title and /playsound to LAN guests 2026-10-26 20:33:36 +01:00
Kacpherek bf62ccdabc Place the bonus chest in new worlds 2026-10-26 20:33:16 +01:00
Kacpherek 41d8b9e48a Jigsaw structures: default max distance vertical and padding top like vanilla 2026-10-26 20:32:20 +01:00
Ludovic MarieandClaude Opus 5.5 6ef61ac13b Merge origin/main into the turtle branch
Keeps main's Reinforcements beside BlockBreakEffect, and the turtle's grown
scute goes through #2137's drop_gift_loot (the mob's id added to its two
GiftLoot events) behind the mob_drops check.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 20:31:43 +01:00
Kacpherek 0e658e3e62 Make drowned throw real tridents instead of arrows 2026-10-26 20:31:21 +01:00
Kacpherek 041ecb7b7a Structures: load the template's block entity NBT into every container and sign 2026-10-26 20:31:16 +01:00
Kacpherek 41ca489d4e Move /particle count 0 particles at speed times delta 2026-10-26 20:31:14 +01:00
Kacpherek 82d2b61a4a Swing a horse's tail now and then 2026-10-26 20:30:33 +01:00
Kacpherek f9b8b40a7d Show score components in /tellraw and /title 2026-10-26 20:30:13 +01:00
Kacpherek 08cd05ed6e Make the Entity Shadows option turn shadows off 2026-10-26 20:30:05 +01:00
Kacpherek f312aae7db Name the potion, cloud or thrower in magic death messages 2026-10-26 20:29:22 +01:00
Ludovic Marie 4d6875ecfb Merge pull request #2201 from kacpherek/fix/raider-and-zombie-doors
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `BreakDoorGoal` (the difficulty gate, 240 ticks, the crack progress that your last commit now sends to nearby players) and the vindicators' door rules in raids. I pushed one fix: zombies the game spawns now roll `canBreakDoors` (`random < special * 0.1`), as `Zombie.finalizeSpawn` does. One difference is left for later: crack packets go to players within 64 blocks, and vanilla's `destroyBlockProgress` uses 32. I merged main into your branch (ac4309d3b): #2162 moved the zombie attributes into a free function, and its leader bonus now goes through `set_can_break_doors`; clippy clean, door tests pass, same code as the tested tree. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
2026-10-26 20:28:41 +01:00
Ludovic MarieandClaude Opus 5.5 e82fa3d957 parity: retry a known flaky harness once, and say so
tools/parity_hosts.json's new `known_flaky` lists the harnesses that still flake for a cause not fixed yet,
each with its reason: dedicated_server (its Java client's 1.5 s step timer), remote_fireball (wall-clock
sampling) and world_reopen. One of them that fails runs once more from scratch, and a pass then counts.

Every retry prints a RETRIED line at the end of the run and in the CI job's summary, so a harness that
passes only on its retry stays visible on PR parity and parity-main alike. The existing fresh rerun of a
failure that reused oracle results is reported the same way. A known flaky harness skips that rerun and keeps
reusing oracle results, because flaking doesn't mean its cached oracle runs were wrong. Repeated runs
(--repeat, --flake-check) are never retried.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 20:27:34 +01:00
Ludovic MarieandClaude Opus 5.5 d026c6db3e Merge origin/main into fix/soul-speed
Keeps #2184's glide wear with Unbreaking after Soul Speed's tick, the order
of LivingEntity.baseTick and aiStep (both draw from the level random), and
both the can_glide_through and soul_speed_blocks tags.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 20:26:50 +01:00
Kacpherek e872d3ae54 Send isFlat in every dimension and use the dimension's min y for the horizon 2026-10-26 20:26:12 +01:00
Ludovic Marie e704a8dccb Merge pull request #2187 from kacpherek/fix/shulker-bullet-duplication
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `Shulker.hitByShulkerBullet` teleports an open shulker and may split it, less likely the more shulkers are near. I pushed one fix: the split roll uses the level's random, as vanilla's `level.getRandom()` does. Your update (d7ec7b607: the open state is read at hit time, the split chance from the level's random) checked too: clippy clean, and the 79 harnesses `parity.py --changed-files` picks pass apart from the campfire cases #2316 fixes. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
2026-10-26 20:24:29 +01:00
Ludovic Marie 1975fe486d Merge pull request #2190 from kacpherek/fix/piglin-loved-items
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `PiglinAi.mobInteract` takes only gold ingots from a hand, and other loved items go to its inventory. One difference is left for later: the comparison of two items it likes equally isn't ported yet. Your update (e60cbe8d4: whole nugget stacks stay in the inventory, picked-up enchantments are kept) checked too: clippy clean, the 79 harnesses `parity.py --changed-files` picks pass apart from the campfire cases #2316 fixes, and lan `barter` passes in both modes. One gap left for later: vanilla can put a nugget stack in an empty main hand, and Horizon's hand slot holds one item. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
2026-10-26 20:24:25 +01:00
Ludovic Marie 98e29159eb Merge pull request #2184 from kacpherek/fix/elytra-glide-rules
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `LivingEntity.handleFallFlyingCollisions` hurts on a horizontal hit with `fly_into_wall`. Gliding stops on climbable blocks and with Levitation, and the elytra wears through `hurtAndBreak`, so Unbreaking applies. Your update (a4407a4bc: Unbreaking for guests, wall hits kept on the server copy) checked too: clippy clean, the 79 harnesses `parity.py --changed-files` picks pass apart from the campfire cases in `dispenser` and `block_removal` that #2316 fixes, and lan `elytra` passes in both modes. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
2026-10-26 20:24:21 +01:00
Ludovic MarieandClaude Opus 5.5 df045f1e9a CI parity: test the head merged into main
A head tested alone keeps failing harnesses that main already fixed. Since 10-25 evening, 9 of 21 failed PR
parity jobs failed only on such fixes: server_knockback (heads without #2114, 7 times), mob_ai's oracle heap
(without #2270), save_roundtrip (without #2212). #2160 needed 4 parity runs this way.

The job now merges main's current tip into the head and selects harnesses against that main commit, so only
the pull request's own changes count. A branch that doesn't change the runner scripts or the selection gets
main's, which replaces the two steps that copied main's vanilla-data.sh and parity.py in by hand. A head that
conflicts with main is tested alone, with a warning. The job summary names the main commit, and the check is
still `parity / parity` on the head.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 20:22:13 +01:00
Ludovic Marie 55a71d3502 Merge pull request #2164 from kacpherek/fix/witch-drinking
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `Witch.aiStep` stands still while drinking (the -0.25 speed modifier) and only drinks an effect it doesn't already have, and `performRangedAttack` skips a potion the target already has. It merges after #2138, since both add fields to `Seen`. I merged main into your branch (f8415efe2): `Seen` keeps #2138's sitting/ridden fields beside your effects; clippy clean. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
2026-10-26 20:19:47 +01:00
Ludovic Marie 02cd2254d0 Merge pull request #2157 from kacpherek/fix/armor-wear-environment-damage
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: damage that doesn't bypass armor (cactus, fire, lava, campfire, berry bushes) wears the armor through `hurtArmor`. Here the wear lands when the damage events drain at the next player tick; vanilla applies it inside `actuallyHurt`. That is close enough for now. #2144 merged first and conflicted here, so I merged main into the branch: the hurt_armor call this PR takes out goes, and main's Thorns code stays. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
2026-10-26 20:19:43 +01:00
Ludovic Marie 1132d2986a Merge pull request #2199 from kacpherek/fix/xp-orbs
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: the `ExperienceOrb` pickup box, merging by count, and the lava bounce. Your last commit's fix for a merged orb picked up by a guest or a server's client (one count at a time, the client keeping the orb) matches what I found. I pushed one fix to the harness: whether orbs merge depends on entity ids, which Horizon doesn't share with vanilla, so a spawner case in `block_removal` merged here and not in vanilla. The oracle and the fixture now print a merged orb once per count, and that case passes. (`block_removal`'s campfire cases fail on main until #2316 lands.) Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
2026-10-26 20:19:39 +01:00
Ludovic Marie 1cdc8ca76b Merge pull request #2192 from kacpherek/fix/piglin-soul-fire-repellents
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `PiglinSpecificSensor` finds the nearest `#piglin_repellents` block (a soul campfire only while lit), and the piglin walks away from it. I pushed one rename: this PR and #2191 each added a `find_repellent` to piglin.rs, so this one is now `find_piglin_repellent`. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
2026-10-26 20:16:50 +01:00
Ludovic Marie a1098c0769 Merge pull request #2210 from kacpherek/fix/2174-empty-hand-place-sound
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `BlockItem.place` plays the place sound only when a block item places a block; an empty hand or another item plays none. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
2026-10-26 20:16:46 +01:00
Ludovic Marie 73993e7cda Merge pull request #2204 from kacpherek/fix/conduit-power
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `ConduitBlockEntity.serverTick` (the frame every 40 ticks, Conduit Power in range, the attack on the nearest enemy), and your last commit's server ticker, which runs without a renderer. One difference is left for later: the conduit's target isn't synced to remote clients yet. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
2026-10-26 20:16:42 +01:00
Ludovic Marie 53a95a6e1c Merge pull request #2203 from kacpherek/fix/illagers-spare-baby-villagers
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: the illagers' target goals skip baby villagers. One difference is left for later, older than this PR: the illagers' shared hurt-by list still includes Vex. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
2026-10-26 20:16:39 +01:00
Ludovic Marie 3ec6c73037 Merge pull request #2202 from kacpherek/fix/evoker-and-vex-rules
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: the evoker's vex summon and cooldowns, the fang lines and circle, the vex limits, and the wololo spell (blue sheep only, with `mob_griefing`). Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
2026-10-26 20:16:35 +01:00
Kacpherek 591b7b74da Predict the sort and tweak clicks on a server like an ordinary click 2026-10-26 20:15:10 +01:00
Kacpherek d78238f765 Show the enchanted hit when a melee weapon's enchantments add damage
Player.attack shows magic crit particles whenever the enchantments added
damage. Horizon showed only the critical hit, for the window's player and
for a guest's attack.
2026-10-26 20:14:32 +01:00
Ludovic Marie 159f2bf890 Merge pull request #2200 from kacpherek/fix/item-drop-direction-and-creative-stand
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `Player.drop` throws leftovers from the eyes, and `ArmorStand.swapItem` lets a creative player keep their stack. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
2026-10-26 20:13:01 +01:00
Ludovic Marie 6628d8b950 Merge pull request #2198 from kacpherek/fix/cactus-destroys-items
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `CactusBlock.entityInside` hurts any entity with `cactus` damage, and that destroys item entities. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
2026-10-26 20:12:57 +01:00
Ludovic Marie ce5be7ffa1 Merge pull request #2197 from kacpherek/fix/chest-blocked-above
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `ChestBlock.isChestBlockedAt`: a block above that is a redstone conductor, or a sitting cat on top. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
2026-10-26 20:12:53 +01:00
Ludovic Marie 2806902c93 Merge pull request #2195 from kacpherek/fix/guardian-beam-rules
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: the guardian's beam deals 2 more on Hard and 2 more from an elder, and squid and axolotl targets must be over 3 blocks away (`GuardianAttackSelector`). Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
2026-10-26 20:12:49 +01:00
Ludovic Marie 8839f0bc72 Merge pull request #2137 from kacpherek/fix/chicken-eggs
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `Chicken.aiStep` lays through `dropFromGiftLootTable(gameplay/chicken_lay)` with the chicken as `this`, which the table's conditions need, and snow and chorus flowers drop their loot when a player breaks them. One difference is left for later: an explosion's drops don't pass the exploding entity as `this` yet (snow broken by TNT). Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
2026-10-26 20:12:45 +01:00
Kacpherek 303281ed09 Puff a poof where a dead mob's body goes
A mob removes its body twenty ticks after dying and tells everyone with
entity event 60, which draws twenty poofs about it. Horizon removed dead
mobs silently.
2026-10-26 20:12:28 +01:00
Kacpherek 18f8a6868b Burst a breaking potion into item crumbs and coloured spell particles
A thrown potion or experience bottle that breaks sends level events 2002 or
2007 with its colour and the splash sound (1053, 1054). Horizon spawned only
eight terrain crumbs, on the server's own side, so a server player saw
nothing.
2026-10-26 20:09:23 +01:00
Ludovic Marie 0f726a9f7c Merge pull request #2194 from kacpherek/fix/turtle-helmet-water-breathing
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: the turtle helmet gives Water Breathing for 200 ticks while the eyes are out of water (`Player.turtleHelmetTick`). Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
2026-10-26 20:08:08 +01:00
Ludovic Marie f73cfff300 Merge pull request #2193 from kacpherek/fix/fish-buckets
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `Bucketable.bucketMobPickup` for cod, salmon, pufferfish, tropical fish, tadpoles and axolotls, with the data each bucket carries (salmon size, tropical fish pattern and colours, the age only for axolotls and tadpoles) and the pattern names in enum order. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
2026-10-26 20:08:03 +01:00
Ludovic Marie 6ac5d148ec Merge pull request #2178 from kacpherek/fix/lightning-rod-attraction
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `ServerLevel.findLightningRod` takes the closest lightning rod POI within 128 blocks, at the top of its column. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
2026-10-26 20:07:59 +01:00
Ludovic Marie 5689e63500 Merge pull request #2169 from Domanffe/perf/hotbar-no-clone
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked: the hotbar now draws its stacks by reference. The output is the same, without a clone per stack each frame. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @Domanffe!
2026-10-26 20:07:55 +01:00
Ludovic Marie d721ea2a44 Merge pull request #2163 from kacpherek/fix/charged-creeper-heads
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: a charged creeper's explosion makes a mob it kills drop its head, once per creeper (`Creeper.canDropMobsSkull` / `increaseDroppedSkulls`). Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
2026-10-26 20:07:51 +01:00
Kacpherek efd6c60a27 Axolotl air: refill only where it rains locally, not in deserts or snowy biomes 2026-10-26 20:07:36 +01:00
Kacpherek 43d1f4b801 Draw the swirls of potion effects around players and mobs
A client draws the particles of an entity's visible effects itself, from
the synched effect particle list (LivingEntity.tickEffects). Horizon drew
none and sent no such list: now the window's own player, a server's mobs
and players, and the mobs and guests of a game in the frame all swirl.
2026-10-26 20:06:26 +01:00
Ludovic MarieandClaude Opus 5.5 ac4309d3bf Merge origin/main into fix/raider-and-zombie-doors
#2162 moved the zombie attributes into a free function; its leader bonus now
goes through set_can_break_doors like this branch's other door flags.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 20:03:32 +01:00
Kacpherek a08f3eb2fa Select a bundle's item with the mouse wheel 2026-10-26 20:02:36 +01:00
Ludovic Marie 6e01dac2f9 Merge pull request #2162 from kacpherek/fix/zombie-reinforcements
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `Zombie.hurtServer` on Hard rolls `spawnReinforcementsChance` and tries 50 spots around the target (`checkSpawnRules`, no player close, unobstructed), and both zombies lose 0.05 of the chance. Two small differences are left for later: the roll is drawn at the end of the tick rather than inside the hurt, and the new zombie draws one extra yaw. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
2026-10-26 20:02:35 +01:00
Kacpherek 96b538d48f Throw up landing dust where a hard fall lands
A landing that hurts sends the block's crumbs from the server
(LivingEntity.checkFallDamage). Horizon built the event but dropped it, for
the window's player and for every server player.
2026-10-26 19:59:59 +01:00
Ludovic Marie f9b2f949ee Merge pull request #2159 from kacpherek/fix/infinity-plain-arrows-only
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: Infinity's `ammo_use` effect only covers `minecraft:arrow`, so tipped and spectral arrows are used up. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
2026-10-26 19:59:37 +01:00
Kacpherek 29eae3a1af Throw a burst of spit particles when a llama spits 2026-10-26 19:58:51 +01:00
Ludovic MarieandClaude Opus 5.5 d241c54e05 Official mod: Recipes, a recipe viewer
Every item sits in the item panel beside the inventory and container
screens, with a search box (words in the name or id, @namespace, #tag).
R or a click shows an item's recipes, U or a right click its uses, with a
tab per station; Back returns through the pages. + beside a crafting
recipe the player knows fills the grid as the recipe book does. In
creative mode, with "Click gives items" on, a click gives one item and a
right click a stack. The keys are rebindable in Controls.

Written from vanilla 26.3's recipe book and screens and Horizon's design
system only. On servers that are not Horizon it shows the game's own
26.3 recipes; known recipes and + follow the server's recipe book.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 19:58:45 +01:00
Kacpherek 4efa49ae82 Raise sprint dust under running players
A sprinting player kicks up dust of the block under their feet every tick
(Entity.spawnSprintParticle). Horizon drew none, for the window's player
or for the other players it sees.
2026-10-26 19:58:22 +01:00
Kacpherek 85db9a23a9 Use the small explosion particle when an explosion keeps blocks 2026-10-26 19:57:54 +01:00
Kacpherek 00cb813dc8 Show the missing item tooltip lines in vanilla order
Custom names are italic. Unbreakable, dyed, written book author and
generation, music disc songs, goat horn instruments, loaded crossbow
projectiles, hive bees and honey, the potion "When Applied" block and
enchantment attribute modifiers (Efficiency V gives +26 Mining Efficiency)
now appear, and advanced tooltips (F3+H) end with the item id and the
component count. Attribute lines use the attribute's sentiment colour.
2026-10-26 19:57:18 +01:00
Kacpherek 9e848670db Send swimming, gliding, fire, invisible and glowing for LAN players 2026-10-26 19:57:11 +01:00
Kacpherek dbd24e2e16 A creaking heart blown up by an explosion makes its creaking die 2026-10-26 19:57:02 +01:00
Ludovic Marie 15b38ba030 Merge pull request #2158 from kacpherek/fix/fishing-hook-items
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `FishingHook.canHitEntity` hooks item entities too, its luck is the owner's luck attribute plus Luck of the Sea, and `fish_caught` counts the catch. One difference is left for later: the hook takes the first overlap rather than the closest one on its path. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
2026-10-26 19:56:48 +01:00
Kacpherek 5a70691d63 Show the note particle above a note block when it plays 2026-10-26 19:56:39 +01:00
Kacpherek e7e6e5bc07 Scale entity tracking range by entity-broadcast-range-percentage
The property was parsed and ignored. The tracking range of each entity type is now scaled by it on a dedicated server, like DedicatedServer.getScaledTrackingDistance.
2026-10-26 19:56:22 +01:00
Kacpherek 4ebfaef0d3 Slide slowly down the sides of honey blocks 2026-10-26 19:56:07 +01:00
Kacpherek 7112d0cd64 Mobs standing in a wither rose get wither, bees in an open eyeblossom get poison 2026-10-26 19:55:27 +01:00
Kacpherek 818b377c02 Add honey drips, poplar and azalea leaves, falling dust and campfire food smoke 2026-10-26 19:54:32 +01:00
Ludovic Marie 46150ba08c Merge pull request #2156 from kacpherek/fix/buttons-pressed-by-arrows
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `ButtonBlock.checkPressed` presses a wooden button while an arrow is inside its shape and keeps it pressed as long as one is there. One difference is left for later: wooden pressure plates and tripwires don't count arrows yet. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
2026-10-26 19:54:07 +01:00
Ludovic Marie 72e678be8c Merge pull request #2155 from kacpherek/fix/target-block-projectiles
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `TargetBlock.onProjectileHit` gives a signal of 1 to 15 from how close the hit is to the face's centre, for 20 ticks from an arrow and 8 from anything else, with the `target_hit` trigger and stat. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
2026-10-26 19:51:09 +01:00
Ludovic Marie 7674ef77cc Merge pull request #2154 from kacpherek/fix/daylight-detector
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `DaylightDetectorBlock.updateSignalStrength` (sky light minus darkening, the sun-angle curve, the inverted mode) and its update when used. Main conflicted with its new campfire block entity, so I merged main into the branch, keeping both block entities. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
2026-10-26 19:48:14 +01:00
Ludovic Marie cddb07c9db Merge pull request #2153 from kacpherek/fix/crossbow-piercing
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: Piercing sets the arrow's `pierceLevel`, and `AbstractArrow.onHitEntity` remembers the entities it went through and goes on until the level runs out. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
2026-10-26 19:45:21 +01:00
Ludovic Marie a99e31d01f Merge pull request #2152 from kacpherek/fix/trident-unbreaking-riptide
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: a thrown trident wears through `hurtAndBreak`, so Unbreaking applies, and `TridentItem.releaseUsing` doesn't launch a riding player with Riptide. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
2026-10-26 19:42:31 +01:00
Ludovic MarieandClaude Opus 5.5 b28b499135 mods: recipes.give hands out one stack at most
As the creative inventory does: a count over the item's stack size (64, 16
or 1) is cut to it, so a mod can ask for a full stack without knowing it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 19:41:35 +01:00
Ludovic Marie 97ddd2c6f9 Merge pull request #2151 from kacpherek/fix/trident-channeling
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: channeling.json summons lightning at the trident's hit while it thunders and the target sees the sky. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
2026-10-26 19:39:42 +01:00
Ludovic Marie f01a4e2119 Merge pull request #2150 from kacpherek/fix/villager-bed-search-radius
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `VillagerMakeLove.takeVacantBed` asks the POI manager for a free home within 48 blocks (a sphere, closest first) and keeps only a bed a path reaches. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
2026-10-26 19:36:14 +01:00
FusselTVandClaude Opus 5.5 61df807b33 Structures: a chunk saves its starts also when no piece reaches it
A chunk's STRUCTURE_STARTS are the starts made in it
(StructureManager.getAllStarts), whether or not one of their pieces
reaches it. own_starts filtered the chunk's references, which only hold
starts with a piece in the chunk, so a start whose pieces all lie next
door (seed 8675309's giant ruined portal in chunk -26, 23) was not saved.
The proto chunk now keeps the starts made at its position.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 19:32:44 +01:00
Ludovic Marie 7f8a6680a8 Merge pull request #2149 from kacpherek/fix/farmer-composter-bread
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `WorkAtComposter` composts the farmer's seeds through `ComposterBlock.insertItem` and makes bread from wheat. I pushed one small fix: the `ComposterBlock.addLayer` doc comment had moved off its function. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
2026-10-26 19:32:30 +01:00
FusselTVandClaude Opus 5.5 ec81bea5bf Worldgen: blocks with a block entity get vanilla's default one
WorldGenRegion.setBlock gives every block that has a block entity a new
one, so vanilla saves the sculk sensors, catalysts and shriekers of the
deep dark and potent sulfur with their block entities. Horizon only
saved block entities that a structure or feature placed data for, and
these kinds have none, so a Horizon-made world had none of them (3,035
sculk sensors, 753 catalysts, 329 shriekers and 21 potent sulfur over
three 1,024-block seeds). A finished column now adds vanilla's defaults
(block_entity_nbt::canonical) for every such block: as the modelled
block entity where Horizon has one (sculk), as raw NBT otherwise.
Potent sulfur gets its block entity type.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 19:31:53 +01:00
FusselTVandClaude Opus 5.5 1eb7a04a4e End: no exit portal while nobody is in the fight
With force-loaded levels ticking without players (#1525), Horizon placed
the exit portal as soon as the End's main island was loaded. Vanilla
only places it from scanState and the dragon's death, both with players
in the fight, so a pre-generated End has no portal until someone
arrives.

test_forced_end_parity force-loads the End's arena on vanilla's and
Horizon's dedicated servers with nobody online and compares what is
around 0, 0.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 19:31:44 +01:00
FusselTVandClaude Opus 5.5 55a066e88a End: spike crystals are saved with the chunk that generates them
SpikeFeature.placeSpike adds each crystal to the WorldGenRegion, so a
pre-generated End holds its ten crystals before anyone ticks it.
Horizon's decoration dropped spawn_end_crystal and the game placed the
crystals later, once the arena was loaded and ticking. An End generated
with ticks frozen, as a seed generator does, had none. The decoration
region now writes the crystal like any placed entity; the game's own
placement still skips a spike that has its crystal.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 19:31:06 +01:00
FusselTVandClaude Opus 5.5 8c2ef15171 Falling suspicious sand and gravel break where they land
BrushableBlock.tick makes the falling block entity with disableDrop, so
vanilla breaks it on landing (onBrokenAfterFall: level event 2001 and
the block_destroy game event) and
places nothing. Horizon landed it as a plain block without its
archaeology block entity. Seen in pre-generated worlds: suspicious gravel
of cold ocean ruins that sits over water fell during the first ticks and
landed one block lower without its block entity, so with no loot.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 19:31:06 +01:00
FusselTVandClaude Opus 5.5 705e3d10cf Force-loaded chunks decorate in vanilla's order
Chunks no player ticket reached were decorated nearest the centre first.
Vanilla's ForceLoadCommand adds a FORCED ticket per chunk, x-outer and
z-inner, and the FULL tasks run in that order, so a chunk decorates
before its neighbour at higher x or z. Both orders agree only in the +x,
+z quarter of the map. Elsewhere features crossing chunk borders
overwrote each other the other way round: a geode next door ate a trial
chamber's dispensers, sculk or a clay pool closed a dungeon so it was
never placed, and ore, stone and tree blobs swapped.

The forceload command now ranks the chunks it adds in vanilla's order
(decoration_order::forced_order, which shares its scheduler with
vanilla_order); chunks without a rank still go last, nearest first.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 19:31:06 +01:00
Ludovic Marie 68db8847e7 Merge pull request #2147 from kacpherek/fix/creeper-fire-charge
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `Creeper.mobInteract` ignites with any `#creeper_igniters` item. A fire charge is used up, flint and steel takes damage, and both play their own sound. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
2026-10-26 19:29:08 +01:00
Ludovic Marie 3a5aa2b3e7 Merge pull request #2146 from kacpherek/fix/skeleton-attack-interval-difficulty
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `AbstractSkeleton.reassessWeaponGoal` sets the bow goal's interval to 20 ticks on Hard and 40 otherwise. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
2026-10-26 19:25:43 +01:00
Kacpherek 484cc945aa Add the rockets list to the remote motion test fixture 2026-10-26 19:23:52 +01:00
Kacpherek 0c127ec1d9 Stub breaking_block_effects in the remote mining test fixture 2026-10-26 19:23:34 +01:00
Kacpherek a14da6bb70 Merge origin/main into fix/1302-speed-after-travel 2026-10-26 19:23:02 +01:00
Ludovic MarieandClaude Opus 5.5 edc4a58397 Merge origin/main into refactor/horizon-command
Brings #2316 (the campfire block entity fix): CI's parity checks out this
branch's head, so dispenser and block_removal need it here.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 19:22:41 +01:00
Ludovic Marie c2e4eee251 Merge pull request #2145 from kacpherek/fix/mob-griefing-explosions-endermen
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `ServerLevel.explode` turns `ExplosionInteraction.MOB` into KEEP when `mob_griefing` is off, and `EnderMan`'s take and leave block goals need the rule too. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
2026-10-26 19:22:10 +01:00
Kacpherek 3114fc66e9 Merge origin/main into fix/2067-deflect-fireballs 2026-10-26 19:22:08 +01:00
Ludovic Marie bc13a67e74 Merge pull request #2143 from kacpherek/fix/sweep-attack-damage
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `Player.doSweepAttack` hits the living entities in the target's box inflated by (1, 0.25, 1), not the player, the target or allies, and not marker armor stands. I pushed one fix: the distance bound is vanilla's fixed `distanceToSqr(entity) < 9.0`, not the player's reach. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
2026-10-26 19:21:26 +01:00
Kacpherek 452fca8bdd Merge origin/main into fix/1938-ghast-dismount 2026-10-26 19:21:07 +01:00
Kacpherek ab40cd63e6 Merge origin/main into fix/remote-leash-knots 2026-10-26 19:20:16 +01:00
Ludovic Marie f6ff2319a5 Merge branch 'feat/mod-recipe-pages-game' into feat/mod-recipe-actions 2026-10-26 19:19:55 +01:00
Ludovic MarieandClaude Opus 5.5 5d7097877b game: station tabs on the recipe pages
A row of tabs above the recipe pages shows `set-recipes`' stations (their
item in a slot, a white frame on the selected one). A click sends `tab(i)`
to the mod, R/U over a tab works like over any slot, and horizon-ctl lists
them as "<name> tab". Changing tab resets the page.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 19:19:30 +01:00
Ludovic MarieandClaude Opus 5.5 f8415efe29 Merge origin/main into fix/witch-drinking
Keeps main's sitting/ridden fields on Seen beside this branch's effects.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 19:19:22 +01:00
Kacpherek 00f2663e7b Merge origin/main into fix/harvest-full-beehive 2026-10-26 19:19:20 +01:00
Ludovic Marie c35575f462 Merge pull request #2138 from kacpherek/fix/animal-breeding-rules
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: rabbits breed (`Rabbit.isFood`), damage ends love (`Animal.hurtServer` resets `inLove`), the horse and wolf `canMate` checks, and the breeding orb only with `mob_drops` (`Animal.finalizeSpawnChildFromBreeding`). Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
2026-10-26 19:17:45 +01:00
Ludovic Marie 8ec47d6419 Merge remote-tracking branch 'origin/main' into feat/server-mods-world 2026-10-26 19:14:31 +01:00
Ludovic Marie 5ecce8a7a3 Merge pull request #2142 from kacpherek/fix/mob-damage-difficulty-scaling
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `Player.hurtServer` scales damage that `scalesWithDifficulty` from a living non-player: none on Peaceful, `min(d / 2 + 1, d)` on Easy, `d * 3 / 2` on Hard (so a creeper's blast scales, and TNT a player lit doesn't). Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
2026-10-26 19:14:06 +01:00
Kacpherek 91d6bdba85 Fly firework rockets on by themselves between server updates 2026-10-26 19:11:21 +01:00
Ludovic MarieandClaude Opus 5.5 e57faff4a3 Merge main
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 19:10:55 +01:00
Ludovic Marie badceed27c Merge pull request #2188 from kacpherek/fix/loot-set-stew-effect
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `SetStewEffectFunction` keeps the effect it rolled, with the duration times 20 unless the effect is instant. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
2026-10-26 19:10:53 +01:00
Ludovic Marie 45159841bc Merge pull request #2316 from ludovic111/fix/campfire-not-a-container
🤖 Agent: Merging ahead of the queue. verify, launcher and CI parity pass, and dispenser and block_removal are back to 4000/4000 and 6000/6000. This turns main green again after #2015.
2026-10-26 19:10:13 +01:00
Kacpherek 4248048ab6 Merge origin/main into fix/elytra-firework-trail 2026-10-26 19:08:50 +01:00
Kacpherek 7f5e3384b3 Play mining hit sounds once, from the server's destroy level events
The server now sends level events 2019 and 2020 every tick a player digs
(ServerPlayerGameMode.tick), and clients handle them like LevelEventHandler:
a crack on the face, plus the hit sound for 2020. A remote client plays only
the first hit itself and no longer adds its own sound every fourth tick, so
the hit was heard twice, out of phase, for every block.
2026-10-26 19:07:11 +01:00
Ludovic Marie cfc55bb6c6 Merge pull request #2185 from kacpherek/fix/slime-block-step-slowdown
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `SlimeBlock.stepOn` slows a walker that isn't sneaking when `|dy| < 0.1`, by `0.4 + |dy| * 0.2`. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
2026-10-26 19:06:12 +01:00
Ludovic Marie 6af65a142b Merge pull request #2182 from kacpherek/fix/weather-command-without-cycle
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `/weather` sets the weather whether or not `advance_weather` is on. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
2026-10-26 19:00:56 +01:00
Ludovic Marie 60109e0d91 Merge pull request #2181 from kacpherek/fix/sleep-rules
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `ServerPlayer.startSleepInBed` (creative skips the monster check; the block above must be free; the reach box), and `ServerLevel.tick`'s morning skip with the weather reset. One difference is left for later: the monster box is centred on the player, and vanilla centres it on the bed. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
2026-10-26 18:56:09 +01:00
Kacpherek d7ec7b6077 Shulker bullet split: record whether the shell was open at hit time 2026-10-26 18:55:45 +01:00
Ludovic Marie 1f88194b61 Merge pull request #2180 from kacpherek/fix/fire-burnout-difficulty-radius
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `FireBlock.tick`: burn-out chances, ignite odds scaled by difficulty, and the spread box (-1..1 sideways, -1..4 up). Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
2026-10-26 18:53:10 +01:00
Ludovic Marie 17fa57db3d Merge pull request #2179 from kacpherek/fix/lightning-fire-rules
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `LightningBolt.spawnFire` only on Normal and Hard and with fire spread on, as here. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
2026-10-26 18:49:47 +01:00
Ludovic MarieandClaude Opus 5.5 dae701a3c7 Merge main into fix/armor-wear-environment-damage
Main's thorns (#2144) sit where this branch took out hurt_armor; the armor
now wears through the damage pipeline and main's thorns stay.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 18:49:18 +01:00
Kacpherek e2b1dc6691 Soul Speed: run its effects for each server-side player 2026-10-26 18:49:12 +01:00
Kacpherek e60cbe8d4d Keep whole nugget stacks and picked-up enchantments on piglins 2026-10-26 18:48:03 +01:00
Kacpherek a4407a4bcc Elytra: Unbreaking for remote players, keep wall hit events on the server copy 2026-10-26 18:47:34 +01:00
Ludovic Marie 0b98555009 Merge pull request #2189 from kacpherek/fix/dragon-crystal-heal-reach
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `EnderDragon.checkCrystals` looks for crystals in its box grown by 32 and heals 1 every 10 ticks, capped at max health. Main moved the crystal search into `nearest_crystal_to`, which the server thread's client uses too, so I merged main into the branch and put the grown box there. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
2026-10-26 18:46:47 +01:00
Ludovic Marie d6a7f7f32c Merge pull request #2176 from kacpherek/fix/2161-shared-key-bindings
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `KeyMapping.click` and `set` act on every mapping bound to the key, as here, and your second commit stops a key from replaying a press when two mappings share it by default. #2207 fixes the same issue; I kept this one because it came first and changes less. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
2026-10-26 18:46:14 +01:00
Ludovic Marie 2ca383f36d Merge pull request #2183 from kacpherek/fix/campfire-shovel-douse
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `ShovelItem.useOn` puts out a lit campfire (`CampfireBlock.dowse`) with its sound and smoke. One difference is left for later: clients of the server thread don't see that smoke yet. Main conflicted with its new `campfire` module (cooking), so I merged main into the branch, keeping both modules. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
2026-10-26 18:43:00 +01:00
Ludovic Marie 5812071a1b Merge pull request #2172 from rdbtCVS/fix/1525-forced-dimension-ticking
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `ServerLevel.tick` keeps ticking a level that has forced chunks, even with no players and past the 300-tick empty timeout. `lan_e2e.py --server-thread --check forced-levels` passes. Main conflicted in lan_e2e.py, so I merged main into the branch, keeping both checks. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @rdbtCVS!
2026-10-26 18:42:08 +01:00
Ludovic MarieandClaude Opus 5.5 eb66e42e38 Merge feat/mod-recipe-pages into feat/mod-recipe-pages-game
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 18:39:55 +01:00
Ludovic MarieandClaude Opus 5.5 6a62ad67cc mods: set-recipes takes a row of tabs, and the tab event says which one the player picked
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 18:39:46 +01:00
Ludovic Marie 1a43fa5859 Merge pull request #2206 from socksprox/fix/piston-moving-block-anim
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `ServerLevel.runBlockEvents` broadcasts each handled block event, and `ClientLevel.blockEvent` re-runs `triggerEvent` on the client, which builds the moving piston it ticks and draws (`deathTicks`, client-side `finalTick`, the 2001 effect for destroyed blocks). I pushed one fix with a test. The client's re-run also ran the server's neighbour logic: a block it moved could start another piston on that client alone, and vanilla calls `checkIfExtend` only server-side. Its `playSound(null, ...)` also played the piston and note sounds a second time, next to the server's; `ClientLevel.playSeededSound` plays only when the excluded player is the local one. In game on the server thread, a piston that pushes a redstone block under a second piston leaves both clients' blocks as the server has them. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @socksprox!
2026-10-26 18:39:19 +01:00
Ludovic Marie 212775a11f Merge pull request #2144 from kacpherek/fix/thorns-hits-attacker
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: Thorns' `post_attack` effect damages the attacker (the shooter for a projectile). Main conflicted in `lan_pvp.rs` because it already had `Strike.attacker`, so I merged it, keeping main's critical-hit code and then your Thorns loop. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
2026-10-26 18:37:46 +01:00
Ludovic MarieandClaude Opus 5.5 a4764f5b0d Merge origin/main into refactor/horizon-command
Brings #2212 (namespaced furnace RecipesUsed), which save_roundtrip needs:
CI's parity checks out this branch's head, not a merge with main.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 18:36:14 +01:00
Ludovic Marie edd7f712b6 Merge pull request #2293 from ludovic111/ci/thinkpad-off
Tools: the ThinkPad no longer runs harnesses or builds; playhorizon is the Linux host
2026-10-26 18:35:33 +01:00
Ludovic Marie cbfb8845d2 Merge pull request #2191 from kacpherek/fix/hoglin-repellents
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `HoglinSpecificSensor` finds the nearest `#hoglin_repellents` block (8 sideways, 4 up and down), and `HoglinAi` walks away and stays passive for 200 ticks. I pushed one rename: this PR and #2192 each added a `find_repellent` to piglin.rs, so this one is now `find_hoglin_repellent` and the two can merge together. Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @kacpherek!
2026-10-26 18:34:59 +01:00
Ludovic Marie 0b1ed3cdf5 Merge pull request #2175 from rdbtCVS/t3/fix-breaking-crack-jitter
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked: the crack overlay keeps its UVs aligned from one stage to the next. Its ignored GPU test passes here (macOS, Metal). Gate: this wave's 55 PRs merged on main in one tree: clippy `-D warnings` clean, 1186 horizon-client and 163 horizon-world tests pass, and of the 101 harnesses `parity.py --changed-files` picked, 100 pass (`block_removal` failed on one spawner case from #2199's orb merging, fixed there). `lan_e2e.py` passes 24 checks with the host on its server thread and in-frame (chest, xp, pvp, doors, villagers, beds, frames, projectiles, arrows, fishing, damage, mobs, blast, drops, elytra, breath, predict, cracks, barter, kills, hurt, contacts, underwater, plates), plus forced-levels on the server thread. Thanks @rdbtCVS!
2026-10-26 18:33:51 +01:00
Ludovic MarieandClaude Opus 5.5 1825b3991c Campfires are no Container for droppers and hoppers
Since campfires got their block entity (#2015), the container lookup took
it for a zero-slot container: a dropper facing one kept its item instead of
dropping it, as vanilla's HopperBlockEntity.getContainerAt finds no Container
there. The dispense fixture also filled campfires through the generic slots,
which a campfire doesn't have, so block_removal saw them empty.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 18:30:56 +01:00
Ludovic MarieandClaude Opus 5.5 db13c68585 mods: recipes.place and recipes.give, a recipe viewer's actions
recipes.place(book-id, all) is the recipe book's click: in the open
inventory's or crafting table's grid it sends ServerboundPlaceRecipePacket
on a server, or places it in this world, refused as handlePlaceRecipe
refuses it (no crafting screen, a recipe the player's book doesn't hold, an
impossible one); a recipe missing items shows as a ghost. recipes.give(stack)
gives a creative player the stack as the creative inventory does; a server
hears it as SetCreativeModeSlot through the inventory's sync. Both answer at
once from the tick's snapshot (placing, creative, known) and take at most 36
calls a hook call.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 18:28:26 +01:00
Ludovic Marie 9422f698ee Merge pull request #2107 from kacpherek/fix/1959-pack-reload-surface
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked: the renderer that replaces this one shares its instance, adapter and device, so it can take the configured surface instead of creating a second one for the same window. Wayland allows only one tearing-control object per surface, and the reporter's log shows the fresh surface coming back lost. I can't reproduce the Wayland/RADV crash here (macOS), so this is checked from the code and the log. On macOS the game still draws after the renderer is rebuilt (screenshots before and after). Fixes #1959. Main's HDR output conflicted in recreate_surface, so I merged main into the branch: the reused surface now goes through main's Renderer::configure too. Thanks @kacpherek! Gate: this batch merged with wave 1 in one tree: clippy `-D warnings` clean, 1164 horizon-client lib tests pass, and of the 88 harnesses `parity.py --changed-files` picked, 87 pass (`block_entity_render` fails from #2099, which is held).
2026-10-26 18:21:17 +01:00
Kacpherek 0300ad8922 Keep the current attack target regardless of invisibility 2026-10-26 18:13:33 +01:00
Ludovic MarieandClaude Opus 5.5 64f4e9b6ff parity_hosts: playhorizon skips mob_ai, lan_integration, world_reopen (failed in its trial)
Full trial (run 40341431094): 342/348 in 96.9 min; save_roundtrip,
remote_fireball and server_player_sculk passed on a rerun with vscreen and
fewer jobs; these three did not.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 18:12:19 +01:00
Ludovic MarieandClaude Opus 5.5 61ff050927 mods: drop the sandbox test's unused LEVEL import
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 18:11:56 +01:00
Ludovic Marie 800b91567a Merge pull request #2303 from ludovic111/fix/setblock-loot-table
🤖 Agent: Merged by Ludovic's AI agent (Claude). setblock and data now keep a container's LootTable and LootTableSeed until its items change, as vanilla does. command_exec matches the 26.3 oracle (870/870), and in game the chest opens with vanilla's exact roll, in-frame and on the server thread.
2026-10-26 18:11:16 +01:00
Kacpherek e8ae26c270 Carry the exploding mob's kind with its blast and anger bees at guests too 2026-10-26 18:09:01 +01:00
Ludovic MarieandClaude Opus 5.5 ad557fef47 game: a mod's recipe pages in the item panel
browser.set-recipes replaces the panel's items with the mod's recipes,
laid out with the game's slot, furnace arrow and flame sprites and small
buttons: a crafting grid, one input over a flame, one input, three inputs
or two, then an arrow and the result, a note under it and the action
button beside the result (under it when the panel is narrow). Slots cycle
through their stacks once a second. As many recipes as fit the panel's
height make a page, with the recipe book's arrows; a title row holds a
Back button and the title. The same title sent again keeps the page.

Clicks on the slots, the action button, the arrows and Back reach the mod;
tooltips and the mods' keys work on the recipes' stacks as on the items.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 18:03:42 +01:00
Ludovic MarieandClaude Opus 5.5 fc9305bb15 mods: browser.set-recipes, recipes in the item panel's place
A mod gives the recipes to show (a layout: crafting grid, cooking, single
input, smithing or brewing; the slots, each cycling through its stacks;
the results; a note; and an optional action button, enabled or greyed)
under a title; an empty list shows the items again. The game pages them
and sends the slot-clicked, action, page and back events.

At most 256 recipes, 9 slots each and 64 stacks a slot. Same level as the
rest of browser (5): none of it has shipped yet.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 18:03:42 +01:00
Ludovic MarieandClaude Opus 5.5 fbbed63f04 setblock and data keep a container's loot table
A command's tag went through decode_block_entity, which has nowhere to put
LootTable, so `setblock ... chest{LootTable:...}` made an empty chest
(`data get` showed `Items: []`). An empty container given a LootTable is
now rolled as a loaded chunk's are, and the roll is kept with its chunk
(RolledLoot), so it saves, and `data get` shows, LootTable and
LootTableSeed until its items change (tryLoadLootTable / trySaveLootTable).
`data merge` and `data modify` go through the same path.

command_exec: setblock a seeded loot chest, then data get it whole and its
LootTableSeed (870/870 match the 26.3 oracle).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 18:02:34 +01:00
Ludovic Marie 21a89c2e7f Merge pull request #2046 from ESHAYAT102/fix/remote-squid-animation
🤖 Agent: Gate check by Ludovic's AI agent (Claude). The client branch of Squid.aiStep (body pitch/roll from the server's velocity, the tentacle cycle, entity event 19 resetting it) matches the 26.3 bytecode. The new `squid_client` harness passes against the real Squid oracle on an integration tree with the other open contributor PRs. I merged main into the branch and stubbed in test_remote_motion's fixture what remote_motion now also reaches (dragon, fireballs, thrown potions, eyes of ender), so that test runs again. Fixes #1684 is right. Thanks @ESHAYAT102!
2026-10-26 18:00:12 +01:00
Ludovic Marie e24ed85036 Merge main into fix/1525-forced-dimension-ticking 2026-10-26 17:59:02 +01:00
Ludovic MarieandClaude Opus 5.5 06a45d86a1 Merge main into fix/1959-pack-reload-surface
Main's HDR output configures the surface through Renderer::configure; the
reused spare surface goes through it too.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 17:58:15 +01:00
Ludovic Marie 64fab2cc83 Merge pull request #2060 from kacpherek/fix/1953-creative-menu-fps
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Before/after on the Mac mini (M4 Pro) with the creative inventory open, two runs each, fast profile, `horizon-ctl bench`. Main: CPU frame 0.81 to 0.91 ms on the building/combat tabs and 1.32 to 1.47 ms while searching (664 to 742 fps). This PR: 0.44 to 0.49 ms on building/combat and 0.49 ms while searching (1122 to 1125 fps). The panel pixels match main. After #1990 made the search an EditBox, I merged main in, and the shown items now key on its value; in game, typing "diamond" narrows the search tab from 45 to 17 stacks. `item_model`, `creative`, `container_screen` and `hud` pass, and so do the creative unit tests (18). Fixes #1953 is right. Thanks @kacpherek!
2026-10-26 17:57:51 +01:00
Ludovic Marie 57f5e1fb8e Merge pull request #2299 from ludovic111/fix/macos-python-tests
🤖 Agent: Merged by Ludovic's AI agent (Claude). Two tests in tests/ errored only on macOS (a python3-less PATH for the oracle shim, and /var vs /private/var); the suite now passes on the Mac mini, 311/311.
2026-10-26 17:53:36 +01:00
Ludovic MarieandClaude Opus 5.5 763fc284ed Merge feat/mod-item-panel-api into feat/mod-item-panel
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 17:51:36 +01:00
Ludovic MarieandClaude Opus 5.5 b8da45f5ef mods: browser's page event names the first recipe shown, set-recipes' indexes are its list's
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 17:51:32 +01:00
Ludovic MarieandClaude Opus 5.5 823941756f block_removal: print a merged orb once per count on both sides
Whether two orbs merge (ExperienceOrb.tryMergeToExisting) depends on entity
ids, and the fixture's ids aren't the oracle's: a spawner case merged in
Horizon and not in vanilla (3 orbs against 4). Both sides now print an orb
once per count, which compares what was awarded.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 17:46:53 +01:00
Ludovic MarieandClaude Opus 5.5 487a461a8a game: the item panel of a mod's browser beside container screens
The panel sits right of the inventory or container window, from the top of
the screen to a search box at its foot, in up to 12 columns of the game's
own item slots, with the recipe book's page arrows. It shows on the
survival inventory and every container screen (not the creative one) while
a running mod has items in it, and hides when there is no room for two
columns. Clicks on it never throw the carried stack; the carried stack and
tooltips are drawn again above it.

The mod hears clicks on items, the search text as it changes, and its own
keys pressed with the pointer on an item, the panel's or a slot's. The
search box takes every key but Escape while focused, as the recipe book's.
Each set-items is turned into stacks once.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 17:46:29 +01:00
Ludovic MarieandClaude Opus 5.5 aa839fa99a mods: browser, a recipe viewer's item panel (API level 5)
A new interface under capability ui. browser.set-items lists the items of
a panel the game shows right of the inventory and container screens (an
empty list hides it); browser.poll returns what the player did since the
last poll: a click on an item, the search text, or one of the mod's own
keys pressed over an item (a slot's or the panel's), with that stack. The
slot-clicked, action, page and back events are there now for the recipe
pages to come, since adding variant cases later would break mods.

At most 8192 items and 256 waiting events. The game side is the next PR.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 17:46:29 +01:00
Ludovic MarieandClaude Opus 5.5 d01a621fec tests: two parity-runner tests pass on macOS
OracleReuseTests ran the shim through `#!/usr/bin/env python3` with a PATH
whose Python folder is Homebrew's, which names it only python3.13: the
shim wasn't found (127). The test now gives that PATH a python3 of its own.

test_report_rejects_sources_changed_during_a_run compared a resolved source
path with an unresolved temp root, and macOS's /var is /private/var.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 17:46:06 +01:00
Ludovic MarieandClaude Opus 5.5 0f22a274fa test_remote_motion: stub what remote_motion reaches on main
remote_motion now also hands motion to the dragon, its fireballs, thrown
potions and eyes of ender; the extracted fixture needs those to compile.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 17:44:05 +01:00
Ludovic Marie a42857ab48 Merge pull request #2050 from rdbtCVS/rdbt/fix-1527-forced-chunk-save
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Forced chunks are now kept in each dimension's data/minecraft/chunk_tickets.dat as 26.3's TicketStorage writes them, and the persistent tickets Horizon doesn't simulate are kept too. A world with /forceload areas round-trips with vanilla 26.3, and the save harnesses pass on an integration tree with the whole save series. I merged main into the branch after #2070 and #2288 and moved the new test to the end of save.rs's tests, so the other save PRs don't collide with it (save:: 27/27). Fixes #1527 is right. Thanks @rdbtCVS!
2026-10-26 17:34:12 +01:00
Ludovic MarieandClaude Opus 5.5 eee7253b1b Merge main
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 17:33:02 +01:00
Ludovic Marie 5963e6cc01 Merge pull request #2294 from ludovic111/fix/recipe-command-targets
🤖 Agent: Merged by Ludovic's AI agent (Claude). /recipe now changes each target's own recipe book and tells that player's client, as RecipeCommand does through ServerPlayer.awardRecipes/resetRecipes. lan_e2e recipe_targets fails on main and passes here, in-frame and on the server thread; recipe_removal and command_exec still pass. Thanks to platform-2 for finding it.
2026-10-26 17:31:33 +01:00
Ludovic Marie d26252bff5 Merge pull request #2291 from ludovic111/carry/2074-poi-files
🤖 Agent: Merged by Ludovic's AI agent (Claude). This lands @FusselTV's #2074 (write points of interest), verified by a vanilla 26.3 round trip; the save harnesses pass. The only additions are merges of main (after #2070 and #2288); save:: passes 29/29. Thanks @FusselTV!
2026-10-26 17:26:38 +01:00
Ludovic MarieandClaude Opus 5.5 f69d23019b Merge main (the probe list keeps squid_client_probe beside the new probes)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 17:26:12 +01:00
Kacpherek b62095dfcb Run the trident player_hurt_entity trigger as the thrower 2026-10-26 17:25:37 +01:00
Ludovic MarieandClaude Opus 5.5 2fc2ee1e13 Merge main (creative.rs: the search is an EditBox since #1990; the shown items key on its value)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 17:23:53 +01:00
Ludovic Marie abd2d9f3fe Merge pull request #2283 from ludovic111/ci/launcher-check
🤖 Agent: Merging. verify passes, and the new launcher job passes on this head (fmt, clippy -D warnings, lib tests). On the earlier head it had caught main's E0061 before #2281. The Python tests show the same 2 environment errors as main.
2026-10-26 17:23:37 +01:00
Ludovic MarieandClaude Opus 5.5 b8e90da1c2 Merge main (save.rs tests: the forced-chunk test moves to the end of the module, clear of the tests other PRs add at its top)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 17:22:07 +01:00
Ludovic MarieandClaude Opus 5.5 d8e68148be /recipe gives and takes each target's own recipes
cmd_recipe changed self.level.recipe_book for every target, so
`/recipe give|take <other player>` changed the book of whoever ran it (or
the dedicated server's own) and the target's client heard nothing. Each
target now runs with its state swapped in (for_player), as
ServerPlayer.awardRecipes/resetRecipes do on the player's own
ServerRecipeBook: the guest's book changes and its client gets
ClientboundRecipeBookAdd/RemovePacket.

lan_e2e recipe_targets: the host gives and takes the guest's recipes and,
on the server thread, the guest gives the host one; each open book must
show exactly its own.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 17:18:47 +01:00
Ludovic MarieandClaude Opus 5.5 7e5fa6f91b Merge main (save.rs tests: the POI tests beside #2073's loot test)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 17:18:19 +01:00
Kacpherek ba3b567f54 Predict a full beehive reacting to shears and glass bottles on the client 2026-10-26 17:15:29 +01:00
Ludovic Marie e0423bba77 Merge remote-tracking branch 'origin/main' into ci/launcher-check
# Conflicts:
#	tools/ci/rust_changes.py
2026-10-26 17:15:21 +01:00
Ludovic Marie 04cdf22527 Merge pull request #2288 from ludovic111/carry/2073-keep-loot-tables
🤖 Agent: Merged by Ludovic's AI agent (Claude). This lands @FusselTV's #2073 (unopened containers keep their loot table), verified against vanilla 26.3: seeded loot 48/48 in both single-player paths, and the save harnesses pass. The only addition is a merge of main after #2070. Thanks @FusselTV!
2026-10-26 17:15:02 +01:00
Ludovic MarieandClaude Opus 5.5 94e8b184bc Merge main
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 17:14:48 +01:00
Ludovic MarieandClaude Opus 5.5 9f1784dc0c rust_changes: append the repository root to sys.path instead of prepending tools/
Prepending tools/ made unittest discovery import tools/test_decoration_parity.py
in place of tests/test_decoration_parity.py.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 17:14:46 +01:00
Ludovic MarieandClaude Opus 5.5 9aba251de5 Merge #2288 (#2073 with main merged)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 17:14:34 +01:00
Ludovic MarieandClaude Opus 5.5 3ae2aad690 Merge main (save.rs: SaveDir's shape beside the forced chunks; both tests kept)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 17:13:59 +01:00
Ludovic MarieandClaude Opus 5.5 0bd6a576a9 Tools: the ThinkPad no longer runs harnesses or builds; playhorizon is the Linux host
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 17:12:10 +01:00
Ludovic Marie 5f1c25c226 Merge pull request #2171 from ludovic111/feat/mod-run-quiet
🤖 Agent: merged main to resolve a conflict in game/remote.rs (main's boss-overlay reset beside this PR's QuietCommands event; both kept). Clippy with mods on is clean. Requeued at the new head.
2026-10-26 17:11:50 +01:00
Ludovic Marie f7170d6b9f Merge pull request #2045 from ESHAYAT102/fix/creative-tool-block-breaking
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Item.canDestroyBlock in creative (swords, maces and tridents can't break blocks) matches the 26.3 bytecode, on the player's prediction and on the server. On the integration tree, `lan_e2e --check creative_tools` passes in-frame and on the server thread, and the new `creative_mining` harness passes against its oracle. I merged main into the branch twice; the second time, lan_e2e.py and the probe list keep both sides, every def checked against its source branch, and client_probe and the bins compile. Fixes #2035 is right. Thanks @ESHAYAT102!
2026-10-26 17:09:09 +01:00
Ludovic MarieandClaude Opus 5.5 43ae7a0974 Merge main (save.rs: LevelShape beside the POI encoder; both sets of tests kept)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 17:08:19 +01:00
Ludovic MarieandClaude Opus 5.5 96984300d3 mods: the UI guest's level is 3 now that world is level 4
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 17:07:20 +01:00
Ludovic Marie 91295ab88c Merge pull request #2109 from kacpherek/fix/2083-remote-effect-clouds
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `AreaEffectCloud` syncs `DATA_RADIUS` (8), `DATA_WAITING` (9) and `DATA_PARTICLE` (10), waits 20 ticks (dragon clouds too), and `clientTick` is ported draw for draw. `DragonFireball`'s trail is `dragon_breath`. One fix pushed: `AbstractHurtingProjectile.tick` calls `applyInertia` before the move, so the client's fireball now accelerates and then moves by the new velocity, with `Vec3.normalize`'s zero for a tiny vector. Main conflicted with #2086 (thrown projectiles between server updates), so I merged it, keeping both: the thrown and eye path first, then the dragon fireball's. `lan_e2e.py --check particles` and `objects` pass in both modes. Part of #2083, as it says. Thanks @kacpherek! Gate: this batch merged with wave 1 in one tree: clippy `-D warnings` clean, 1164 horizon-client lib tests pass, and of the 88 harnesses `parity.py --changed-files` picked, 87 pass (`block_entity_render` fails from #2099, which is held).
2026-10-26 17:06:31 +01:00
Kacpherek 5d3e9d218d Clean copper for a skeleton trap's visual lightning too 2026-10-26 17:05:35 +01:00
Ludovic Marie bb81cb4586 Merge pull request #2078 from RoyalPixelsDE/fix/structure-starts
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Chunks now write the structure starts and references they hold, so vanilla keeps the structures (and their spawns) after the round trip. Checked with a vanilla save of a generated world (overworld, Nether, End) taken through Horizon and back to vanilla 26.3 on the server thread and in-frame. On an integration tree with the whole stack, `save_roundtrip`, `world_reopen`, `chunk_generation`, `structure_start_nbt`, `structure_blocks`, `structure_layout`, `trial_spawner` and `vault` all pass. Thanks @FusselTV!
2026-10-26 17:03:35 +01:00
Ludovic MarieandClaude Opus 5.5 bf39e5d49e Merge main (save.rs: keep both new tests; the loot test encodes with LevelShape::OVERWORLD after #2070)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 17:01:49 +01:00
Ludovic Marie 8b792957f7 Merge pull request #2286 from ludovic111/chore/site-repo-split
🤖 Agent: merging. playhorizon.dev already deploys from ludovic111/horizon-site (verified page by page); this drops website/ and moves tokens.css, roadmap.json, the benchmark fixtures and the web icons where the site's build and the tests read them. verify passed; pr_ready --quick and the telemetry server's tests pass locally.
2026-10-26 17:00:07 +01:00
Ludovic Marie 2c6b60f3fb Merge pull request #2105 from socksprox/fix/fire-smoke-no-break-particles
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `BaseFireBlock.animateTick` (three large smoke above, or two per burnable side when the block below neither burns nor is sturdy up, in that order and with the `* 0.1F` float edge), `SoulFireBlock.canBurn` always true, and `Level.destroyBlock` → `spawnDestroyParticles` → `BaseFireBlock`'s empty `spawnDestroyByEntityParticles`. `fire.igniteOdds` is in block-behaviours.json at the top level, as read. I added one rule: `FireBlock.getIgniteOdds` is 0 for a waterlogged state, so such a neighbour no longer counts as burnable. This and #2082 (the player's predicted break) cover different paths, so both stay; #2092 was already closed in favour of this one. In game, fire and soul fire show their large smoke above them. Thanks @socksprox, and @kacpherek for #2092! Gate: this batch merged with wave 1 in one tree: clippy `-D warnings` clean, 1164 horizon-client lib tests pass, and of the 88 harnesses `parity.py --changed-files` picked, 87 pass (`block_entity_render` fails from #2099, which is held).
2026-10-26 16:59:27 +01:00
Ludovic Marie 8f827db52d Merge pull request #2102 from kacpherek/fix/1516-vehicles-in-nether-portals
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `NetherPortalBlock.entityInside` takes any entity that `canUsePortal(false)`, and boats and minecarts travel through `teleportCrossDimension` with their data, as items and mobs already did here. A ridden vehicle stays out of this PR's scope (the player-and-vehicle case is #1486). `lan_e2e.py --check vehicles`, `boats` and `minecarts` pass in both modes. Fixes #1516. Thanks @kacpherek! Gate: this batch merged with wave 1 in one tree: clippy `-D warnings` clean, 1164 horizon-client lib tests pass, and of the 88 harnesses `parity.py --changed-files` picked, 87 pass (`block_entity_render` fails from #2099, which is held).
2026-10-26 16:56:54 +01:00
Ludovic Marie 0e0b0c947f Merge pull request #2100 from kacpherek/fix/1436-item-frame-light
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `ItemFrameRenderer.getBlockLightLevel` gives a glow frame's model at least block light 5 (the item and map inside stay full bright), and the frame model takes the entity shader's directional shading from its face normals. My screenshot run couldn't place a frame, so this rests on the code and on `lan_e2e.py --check frames`, which passes in both modes. Fixes #1436. Thanks @kacpherek! Gate: this batch merged with wave 1 in one tree: clippy `-D warnings` clean, 1164 horizon-client lib tests pass, and of the 88 harnesses `parity.py --changed-files` picked, 87 pass (`block_entity_render` fails from #2099, which is held).
2026-10-26 16:54:21 +01:00
Ludovic Marie e065f2af6f Merge pull request #2097 from kacpherek/fix/1507-server-end-credits
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `EndPortalBlock.entityInside` calls `showEndCredits` on a player in the End without `seenCredits`. That sends `WIN_GAME` with 0.0 once (`wonGame`), and `handleClientCommand(PERFORM_RESPAWN)` while `wonGame` respawns the player in the Overworld keeping its data. `seenCredits` round-trips through playerdata. One difference is left for later: vanilla takes the player out of the End level (`removePlayerImmediately`) while the poem rolls, and here it stays in the level. `lan_e2e.py --check portals` passes with the host on its server thread (I pushed one fix to the check: it closes the credits on the host's first End exit). Fixes #1507. Thanks @kacpherek! Gate: this batch merged with wave 1 in one tree: clippy `-D warnings` clean, 1164 horizon-client lib tests pass, and of the 88 harnesses `parity.py --changed-files` picked, 87 pass (`block_entity_render` fails from #2099, which is held).
2026-10-26 16:51:57 +01:00
Ludovic MarieandClaude Opus 5.5 8bb27b44ca Merge origin/feat/mod-run-quiet into feat/server-mods-world; world at API level 4
#2171 takes level 3 (command.run-quiet), so the world interface's functions
take level 4. The workspace members keep main's horizon-hdr and this branch's
beacons example.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 16:51:29 +01:00
Kacpherek 36ef05b0b9 Keep infinite mob effects through the effect tick 2026-10-26 16:51:22 +01:00
Ludovic Marie 8ccf20d909 Merge pull request #2077 from RoyalPixelsDE/fix/structure-starts-3-legacy
🤖 Agent: Gate check by Ludovic's AI agent (Claude). The remaining legacy structure pieces now save their vanilla fields. Checked with a vanilla save of a generated world (overworld, Nether, End) taken through Horizon and back to vanilla 26.3 on the server thread and in-frame. On an integration tree with the whole stack, `save_roundtrip`, `world_reopen`, `chunk_generation`, `structure_start_nbt`, `structure_blocks`, `structure_layout`, `trial_spawner` and `vault` all pass. Thanks @FusselTV!
2026-10-26 16:51:18 +01:00
Ludovic Marie c5f9e3404c Merge remote-tracking branch 'origin/main' into feat/mod-run-quiet
# Conflicts:
#	crates/horizon-client/src/game/remote.rs
2026-10-26 16:49:16 +01:00
Ludovic Marie c3f5e9f79c Merge remote-tracking branch 'origin/feat/mod-run-quiet' into feat/server-mods-world
# Conflicts:
#	crates/horizon-client/src/game/remote.rs
2026-10-26 16:49:04 +01:00
Ludovic Marie 7ace1e9e8f Merge pull request #2076 from RoyalPixelsDE/fix/structure-starts-2-templates
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Template pieces now save as vanilla's TemplateStructurePiece does. Checked with a vanilla save of a generated world (overworld, Nether, End) taken through Horizon and back to vanilla 26.3 on the server thread and in-frame. On an integration tree with the whole stack, `save_roundtrip`, `world_reopen`, `chunk_generation`, `structure_start_nbt`, `structure_blocks`, `structure_layout`, `trial_spawner` and `vault` all pass. Thanks @FusselTV!
2026-10-26 16:48:38 +01:00
Kacpherek 49ad61035b Count shield damage only for accepted hits and skip kills saved by a totem 2026-10-26 16:47:11 +01:00
Ludovic Marie ebf2132885 Merge pull request #2103 from rdbtCVS/fix/1948-merchant-cost-components
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `ItemCost.STREAM_CODEC` is item, count, then `DataComponentExactPredicate.STREAM_CODEC`, a list of typed components with no removal count, as now written and read. Its verify failed only on rustfmt (item_sync.rs), which I pushed. Main conflicted in item_sync.rs's tests (the new bundle test), so I merged it, keeping both tests. I also took the codec round trip back out of the trading probe: the probe's output is what the harness compares with vanilla, the round trip is already covered by your two unit tests, and its `unwrap` would abort the probe on any cost whose components name an entry the stub registry lacks. `lan_e2e.py --check hero-trading` passes in both modes. Fixes #1948. Thanks @rdbtCVS! Gate: this batch merged with wave 1 in one tree: clippy `-D warnings` clean, 1164 horizon-client lib tests pass, and of the 88 harnesses `parity.py --changed-files` picked, 87 pass (`block_entity_render` fails from #2099, which is held).
2026-10-26 16:44:53 +01:00
Ludovic MarieandClaude Opus 5.5 4fe648b674 Move the website to its own repository (ludovic111/horizon-site)
playhorizon.dev now deploys from the private ludovic111/horizon-site, which holds website/'s history. The files
its build still reads from here move out of website/:

- the design tokens to crates/horizon-theme/tokens.css (xtask tokens writes them, tokens_css_is_up_to_date checks them);
- the roadmap's JSON to roadmap/roadmap.json (tools/roadmap.py build);
- the benchmark fixtures to crates/horizon-telemetry-server/tests/fixtures/benchmarks/ (the site keeps a copy);
- the web icons to docs/media/brand/web/ (xtask icons).

pr_ready loses its website area and the tokens special case; rust_changes, the roadmap's links and every doc
that pointed into website/ follow.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 16:43:32 +01:00
Ludovic Marie 747c22a5c2 Merge pull request #2088 from rdbtCVS/rdbt/fix-1491-guardian-beams-20261026
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `Guardian.DATA_ID_MOVING` (16) and `DATA_ID_ATTACK_TARGET` (17, the target's entity id, 0 for none) are synced, `onSyncedDataUpdated` zeroes `clientSideAttackTime`, and the client's `aiStep` advances it up to `getAttackDuration` (80, elder 60), which drives the beam. The in-frame tick now shares `client_tick`. Main conflicted (the bat flags from #2003, the sonic and cushion checks), so I merged it into the branch, keeping both sides. `lan_e2e.py --check guardian-beams` passes with the host in-frame and on its server thread (I pushed one fix to the check: the host reads the guardian from the level that ticks it). Fixes #1491. Thanks @rdbtCVS! Gate: this batch merged with wave 1 in one tree: clippy `-D warnings` clean, 1164 horizon-client lib tests pass, and of the 88 harnesses `parity.py --changed-files` picked, 87 pass (`block_entity_render` fails from #2099, which is held).
2026-10-26 16:41:53 +01:00
Ludovic MarieandClaude Opus 5.5 4471e59132 Load the loot tables before a world's first chunks are read
A loaded container with a loot table is rolled as its chunk is decoded, but
the loot tables were loaded lazily by the first level tick. The spawn chunks
are decoded while the world opens, so a loot chest in them came out empty and
lost its table on the next save. WorldRequest::prepare, which every world
opens through (in-frame, server thread, dedicated server), now loads them
first.

spawn_loot: vanilla writes a flat world with a seeded loot chest by the
spawn and rolls it; Horizon opens it in a fresh process, in-frame and on the
server thread, and must hold the same stacks in the same slots. Without the
fix both modes hold nothing.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 16:41:23 +01:00
Ludovic Marie cab57a158f Merge pull request #2072 from RoyalPixelsDE/fix/proto-block-entity-id
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Block entities placed in proto chunks now save with their id, as vanilla's ProtoChunk does, so vanilla loads them. Checked with a vanilla save of a generated world (overworld, Nether, End) taken through Horizon and back to vanilla 26.3 on the server thread and in-frame. On an integration tree with the whole stack, `save_roundtrip`, `world_reopen`, `chunk_generation`, `structure_start_nbt`, `structure_blocks`, `structure_layout`, `trial_spawner` and `vault` all pass. Thanks @FusselTV!
2026-10-26 16:41:09 +01:00
Ludovic MarieandClaude Opus 5.5 402b8dc01e Merge main (lan_e2e.py and the probe list keep both checks and probes)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 16:39:28 +01:00
Ludovic Marie 9cbf93a193 Merge pull request #2070 from RoyalPixelsDE/fix/dimension-height
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Nether and End chunks now save with their own min_y/height and sky light, as ChunkSerializer does per dimension: vanilla reads them back with every section in place. Checked with a vanilla save of a generated world (overworld, Nether, End) taken through Horizon and back to vanilla 26.3 on the server thread and in-frame. On an integration tree with the whole stack, `save_roundtrip`, `world_reopen`, `chunk_generation`, `structure_start_nbt`, `structure_blocks`, `structure_layout`, `trial_spawner` and `vault` all pass. Thanks @FusselTV!
2026-10-26 16:38:29 +01:00
Kacpherek 3842d851d8 Match the brewed_potion trigger's potion condition as a PotionsPredicate 2026-10-26 16:38:07 +01:00
Ludovic Marie b1afe112a4 Merge pull request #2281 from ludovic111/fix/launcher-proxy-redact
🤖 Agent: Merged by Ludovic's AI agent (Claude). This keeps proxy credentials out of the launcher's messages and fixes the launcher build that broke on main (download::agent call in browser/horizon.rs). Launcher lib tests 137/137, horizon-port-pack 68/68. Thanks again to @zhykowo for the proxy support in #2059.
2026-10-26 16:36:08 +01:00
Ludovic Marie b107b8539d Merge pull request #2101 from kacpherek/fix/server-thread-chest-minecart
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `ContainerEntity.isChestVehicleStillValid` is `!isRemoved() && player.isWithinEntityInteractionRange(getBoundingBox(), 4.0)`, which is entity interaction range (3, creative 5) plus 4 measured from the eye to the box, as here. The menu had closed on the tick it opened because the placeholder key was checked as a block. This is also the Discord report of a chest minecart that plays its sound but opens no screen. `lan_e2e.py --check minecarts` passes in both modes, and through horizon-ctl a chest minecart's screen is still open 60 ticks after it opens. Thanks @kacpherek! Gate: this batch merged with wave 1 in one tree: clippy `-D warnings` clean, 1164 horizon-client lib tests pass, and of the 88 harnesses `parity.py --changed-files` picked, 87 pass (`block_entity_render` fails from #2099, which is held).
2026-10-26 16:35:45 +01:00
Ludovic MarieandClaude Opus 5.5 aa8a78e1b7 Merge main into fix/daylight-detector
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 16:35:36 +01:00
Ludovic MarieandClaude Opus 5.5 0c8e8037df Merge main into fix/campfire-shovel-douse
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 16:35:19 +01:00
Ludovic Marie df518c1683 Merge pull request #2071 from RoyalPixelsDE/fix/trial-spawner-spawn-data
🤖 Agent: Gate check by Ludovic's AI agent (Claude). A trial spawner with nothing to spawn no longer writes an empty spawn_data, matching TrialSpawnerStateData's codec. Checked with a vanilla save of a generated world (overworld, Nether, End) taken through Horizon and back to vanilla 26.3 on the server thread and in-frame. On an integration tree with the whole stack, `save_roundtrip`, `world_reopen`, `chunk_generation`, `structure_start_nbt`, `structure_blocks`, `structure_layout`, `trial_spawner` and `vault` all pass. Thanks @FusselTV!
2026-10-26 16:35:11 +01:00
Kacpherek c987778e97 Decorated pot: keep every quad and cull back faces like entitySolid 2026-10-26 16:34:24 +01:00
Kacpherek b28108128d Keep a hopper minecart's items in sync with a leftover menu copy 2026-10-26 16:33:08 +01:00
Ludovic Marie 2aa41f867e Merge pull request #2068 from lazy-importer/fix/2039-custom-item-name-tooltip
🤖 Agent: Gate check by Ludovic's AI agent (Claude). The item_name component now sits on items and tooltips as vanilla's Item.getName/getStyledHoverName read it (custom_name first, then item_name, then the translation key). I pushed d983ef89b so item_name stays a component and is not folded into custom_name. `tooltip_display`, `lore_tooltip` and `firework_tooltip` pass on an integration tree with the other open contributor PRs. Fixes #2039 is right. Thanks @lazy-importer!
2026-10-26 16:32:32 +01:00
Ludovic Marie 8741f4d590 Merge pull request #2015 from vlesmentie/feat/campfire-cooking
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Campfire cooking (CampfireBlockEntity.cookTick/cooldownTick, placing food, dropping the result, the arm swing) matches 26.3, and food cooks in game on the server thread and in-frame. Since campfires now tick, the save round trip saw the fixture's lit campfire advance its CookingTimes between load and save, as the lit furnace already did. I added that one ignore rule (ba797f60a); `save_roundtrip` then passes with 0 unexplained differences. Thanks @vlesmentie!
2026-10-26 16:29:52 +01:00
Ludovic MarieandClaude Opus 5.5 dbbe3a45d9 CI: check the launcher's workspace on every push to main and on pull requests that touch it
verify's cargo runs never build launcher/ (its own workspace), so #2059 and the
registry ratings commit each passed alone and left main's launcher failing to
compile (E0061). The new launcher job runs its rustfmt, clippy -D warnings and lib
tests when a pull request changes the launcher, a crate it builds or the root
manifests, and on every push to main.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 16:29:09 +01:00
Kacpherek c50557ea0b Credit the projectile owner for explosion damage to mobs 2026-10-26 16:29:05 +01:00
Ludovic MarieandClaude Opus 5.5 7ac90ff0b8 Merge main into fix/1482-evoker-fangs
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 16:28:40 +01:00
Kacpherek f364c4f9c6 Leave wandering traders out of the spawn egg baby check 2026-10-26 16:28:28 +01:00
Ludovic MarieandClaude Opus 5.5 8df89ca4d6 Merge main into fix/dragon-crystal-heal-reach
Main moved the crystal search into nearest_crystal_to, which the server
thread's client uses too; the dragon's grown box now lives there.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 16:28:09 +01:00
Ludovic MarieandClaude Opus 5.5 81c8e0e815 Merge main into fix/1525-forced-dimension-ticking
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 16:27:56 +01:00
Kacpherek 9841ce7824 Revert "Decorated pot: draw one face of its top and bottom planes"
This reverts commit 408a75aeb3.
2026-10-26 16:27:13 +01:00
Kacpherek 83a6f5c7b9 Revert "Decorated pot: say why only the outer faces stay"
This reverts commit d338bcd070.
2026-10-26 16:27:13 +01:00
Ludovic Marie d880b23fec Merge pull request #2278 from ludovic111/feat/sculk-banner-launcher
🤖 Agent: Sculk partner banner in the launcher (Servers and Home), as Ludovic asked. Launcher fmt, clippy -D warnings and tests pass; checked in dark and light on the Mac mini; verify passed on this head.
2026-10-26 16:26:53 +01:00
Ludovic Marie c97be88ae5 Merge pull request #2094 from rdbtCVS/rdbt/fix-1478-firework-bursts-20261026
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `FireworkRocketEntity.explode` broadcasts entity event 17 before damage and `discard`, and `handleEntityEvent(17)` calls `createFireworks` with the rocket's movement and its item's explosions, so sending the event before the tracker drops the rocket is the right order. Main conflicted (fireworks.rs, lan_e2e.py), so I merged it, keeping main's rocket damage and your `remote_firework_explosion`. `lan_e2e.py --check fireworks` passes in both modes. Fixes #1478. Thanks @rdbtCVS! Gate: this batch merged with wave 1 in one tree: clippy `-D warnings` clean, 1164 horizon-client lib tests pass, and of the 88 harnesses `parity.py --changed-files` picked, 87 pass (`block_entity_render` fails from #2099, which is held).
2026-10-26 16:26:25 +01:00
Ludovic MarieandClaude Opus 5.5 600b2a1dda Launcher proxy: keep a proxy's credentials out of messages; build again
An address the launcher can't use as a proxy is printed to stderr by
horizon-port-pack and shown under the Settings field or by `settings set`:
a user name and password in it now read `***`.

The registry's star ratings called download::agent() without the address
it now takes (#2059 and the ratings landed side by side), so the launcher
didn't build.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 16:25:16 +01:00
Ludovic MarieandClaude Opus 5.5 4b89d0cd85 Merge main into fix/2083-remote-effect-clouds
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 16:24:55 +01:00
Kacpherek 18f4dd92ae Play container open and close sounds only when the opener count crosses zero 2026-10-26 16:24:42 +01:00
Kacpherek 47654b62a3 Save the armadillo scute timer and reset it only when a scute can drop 2026-10-26 16:24:26 +01:00
Ludovic Marie 03b52db24e Merge pull request #2096 from kacpherek/fix/2091-recipe-book-tabs
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `AbstractRecipeBookScreen.mouseClicked` offers the click to `recipeBookComponent` before anything else, and the tabs sit left of the book, outside the strip `hasClickedOutside` counts, so the outside branch has to ask the book first. `lan_e2e.py --check recipes` passes in both modes. Fixes #2091. Thanks @kacpherek! Gate: this batch merged with wave 1 in one tree: clippy `-D warnings` clean, 1164 horizon-client lib tests pass, and of the 88 harnesses `parity.py --changed-files` picked, 87 pass (`block_entity_render` fails from #2099, which is held).
2026-10-26 16:23:43 +01:00
Ludovic Marie 27f9120510 Merge pull request #2021 from senjinthedragon/hdr-output
🤖 Agent: Gate check by Ludovic's AI agent (Claude). I merged main and added `crates/horizon-hdr` to the workspace, and pushed caae02641 so that native material packs keep a light-emitting translucent face (nether portal) at its own alpha. HDR stays off by default. With it off, screenshots match main within noise, and the frame time does too (A/B, two runs each: p50 0.326 to 0.329 ms vs 0.317 to 0.345 ms). `section_mesh` passes. HDR on was not tested here (no HDR display on the Mac mini). Thanks @senjinthedragon!
2026-10-26 16:23:13 +01:00
Ludovic MarieandClaude Opus 5.5 37825b750a Piglin AI: name the piglin repellent finder for its mob
#2191 and #2192 each add a `find_repellent` to piglin.rs, one for
hoglins and one for piglins; merged together they would collide.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 16:23:06 +01:00
Ludovic MarieandClaude Opus 5.5 68aa70108f Piglin AI: name the hoglin repellent finder for its mob
#2191 and #2192 each add a `find_repellent` to piglin.rs, one for
hoglins and one for piglins; merged together they would collide.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 16:23:05 +01:00
Ludovic Marie 0a699d34f5 Merge pull request #2049 from vlesmentie/fix/remote-ender-dragon
🤖 Agent: Gate check by Ludovic's AI agent (Claude). With the default server thread, the End's dragon is now drawn (screenshots in the End: invisible on main, visible with this PR), and attacks on its parts reach the server. `server_dragon` passes on an integration tree with the other open contributor PRs. Fixes #1530 is right. Thanks @vlesmentie!
2026-10-26 16:22:28 +01:00
Kacpherek 2f3dade42f Show the firework trail on a boosting rocket and hide its sprite
On a server-thread client an elytra boost rocket drew no spark trail and
showed up as a static item sprite. The client now follows the entity the
rocket is attached to, spawns the trail each tick like
FireworkRocketEntity.tick, and does not draw an attached rocket.
2026-10-26 16:19:53 +01:00
Ludovic MarieandClaude Opus 5.5 96afc48ce2 Merge main into fix/1948-merchant-cost-components
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 16:18:40 +01:00
Ludovic MarieandClaude Opus 5.5 804a288214 Merge main into rdbt/fix-1491-guardian-beams-20261026
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 16:18:38 +01:00
Ludovic Marie 1851dc87a7 Merge pull request #2086 from kacpherek/fix/thrown-eye-and-pearl
🤖 Agent: Gate check by Ludovic's AI agent (Claude). rdbt's approval counts as the first review (trusted reviewer). In the integration tree with the other open PRs, `lan_e2e --check thrown` and `--check projectiles` pass in-frame and on the server thread, and the `pearl_death` harness passes. Fixes #1826 is right. Thanks @kacpherek!
2026-10-26 16:17:59 +01:00
Ludovic MarieandClaude Opus 5.5 ba797f60a1 save_roundtrip: the lit campfire keeps cooking while Horizon runs
Campfires now tick, so the fixture's campfire advances its CookingTimes
between load and save, as the lit furnace does.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 16:17:25 +01:00
Ludovic Marie 95f9ad56b2 Merge pull request #2270 from ludovic111/fix/mob-ai-oracle-heap
🤖 Agent: verify passed; this only raises the mob_ai oracle's heap (tools only). The oracle passed alone at 4g.
2026-10-26 16:17:23 +01:00
Kacpherek 6f4c2cfbbc Count the hurt tilt down while the player is dead or riding 2026-10-26 16:16:19 +01:00
Ludovic MarieandClaude Opus 5.5 528d65ab12 launcher: Sculk partner banner on Servers and Home
Sculk, a server host, sponsors Horizon. Its banner (a 50 KB WebP,
1200x441, bundled with include_bytes) sits under a muted "Partner"
label at the end of the Servers page and of Home, below Play. A click
opens https://my.sculkhosting.com/ref/horizon; hovering lifts it 2 px
and turns the label to ink; it is reachable with Tab.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 16:15:57 +01:00
Ludovic MarieandClaude Opus 5.5 bf2c867966 Zombies: roll canBreakDoors when the game spawns them
Zombie.finalizeSpawn calls setCanBreakDoors(nextFloat() < special * 0.1)
after the baby roll; the game's spawn finalize never rolled it, so only
the finalize probe's zombies could break doors.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 16:15:39 +01:00
Ludovic Marie f8eb2f086e Merge pull request #1990 from Joansitoh/fix/server-text-caret
🤖 Agent: Gate check by Ludovic's AI agent (Claude). This needs no decision from Ludovic: every field now edits at the caret, as vanilla's EditBox does. rdbt's line-break review: the book page is a MultiLineEditBox in 26.3 (javap), so I pushed MultilineTextField's own word rule. getPreviousWord/getNextWord split words at any Character.isWhitespace and start from the cursor clamped to the last character. The single-line EditBox.getWordPosition keeps counting spaces only. I also merged main in twice (multiplayer.rs labels, stations.rs). Integration tree: `lan_e2e --check books`, `signs` and `anvil` pass in-frame and on the server thread, the `creative` and `container_screen` harnesses pass, and the ui/creative/title/clipboard unit tests pass. Thanks @Joansitoh!
2026-10-26 16:15:00 +01:00
Ludovic MarieandClaude Opus 5.5 2394d6e7dc Pistons: a client's replay starts no piston and plays no sound
A client's copy of a block event (`client_block_event`) ran the server's
neighbour logic: a block it moved could start another piston on the client
alone (`PistonBaseBlock.checkIfExtend` is only called server-side), and the
re-run's `playSound(null, ...)` played the piston and note sounds a second
time next to the server's (`ClientLevel.playSeededSound` plays only for the
local player as the excluded one).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 16:14:21 +01:00
Ludovic Marie 26a2aa2fdc Merge pull request #2047 from ESHAYAT102/fix/cushion-placement
🤖 Agent: Gate check by Ludovic's AI agent (Claude). I checked CushionItem.useOn and Cushion.interact against the 26.3 bytecode with rea_vanilla and pushed three leftovers. (1) An occupied cushion now returns PASS, as Cushion.interact does. (2) A cushion placed in fire burns at once (destroyIfInFire). (3) Placing always posts the entity_place game event before consuming the item. I also merged main into the branch (lan_e2e.py). The suffocation check matches vanilla's block definitions except closed shulker boxes, which are approximated as always suffocating (noted in a comment). Integration tree: `lan_e2e --check cushions` passes in-frame and on the server thread, and the `cushion_placement` and suffocation harnesses pass. Thanks @ESHAYAT102!
2026-10-26 16:14:15 +01:00
Ludovic MarieandClaude Opus 5.5 409a828128 mods: capability recipes (API level 4), the game's items and recipes for recipe viewers
recipes.items() lists every item in registry order, with its name in the
player's language and its tags. recipes.all() lists every recipe: crafting
and furnace ones from the recipe book's own entries (vanilla's displays and
display ids), then campfire, stonecutting, smithing and brewing (display
only). Stacks point into the item table by index. recipes.known() gives the
book ids the player's recipe book holds, from the client book in every mode,
so on a server it follows the server's book.

The game builds the tables once, the first time a running mod reads them.
Experimental, new functions only. Smithing::Transform now keeps its recipe
id.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 16:11:29 +01:00
Ludovic MarieandClaude Opus 5.5 625c4c559e Merge main into the creative tool mining fix
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 16:10:15 +01:00
Ludovic Marie 006006f1d5 Merge pull request #2059 from zhykowo/feat/launcher-proxy
🤖 Agent: Gate check by Ludovic's AI agent (Claude). The launcher contacts no new host: the existing requests (releases, Mojang files, Modrinth, CurseForge, the mod registry, pictures, Microsoft sign-in, Tailscale's API, modpack jars) now go through the player's HTTP proxy, or HTTPS_PROXY/HTTP_PROXY/ALL_PROXY when unset. Loopback and NO_PROXY hosts stay direct, and the Servers page's local API never uses the proxy. Tokens (Microsoft, Tailscale) only cross the proxy inside TLS (CONNECT), so the proxy can't read them, and none are logged. rdbt's NO_PROXY review is addressed: `horizon-port-pack`'s agents apply the same rules (`no_proxy_lists_hosts_and_their_subdomains`). On Linux at da54f988d: launcher lib tests 132 passed, `cargo clippy --all-targets -D warnings` clean, `horizon-port-pack` 68 tests passed and clippy clean. Labelled feature. One small note for later: an unusable proxy address is printed to stderr as typed, so a `user:pass@` in it would show in the log. Thanks @zhykowo!
2026-10-26 16:10:03 +01:00
Kacpherek e157c5a0a5 Tick conduits on the server without a renderer 2026-10-26 16:07:31 +01:00
Kacpherek 92e59024f2 Give merged orbs their value once per count for LAN guests and dedicated players 2026-10-26 16:07:23 +01:00
Kacpherek af49fd7dc3 Use a collision-shape raycast for the spyglass line-of-sight check 2026-10-26 16:07:14 +01:00
Kacpherek c75854a968 Send mob door cracks to nearby players 2026-10-26 16:07:11 +01:00
Kacpherek af6faa88e5 Report the hook's open-water flag to the fishing_rod_hooked trigger 2026-10-26 16:06:30 +01:00
Kacpherek e71d4281ac Spawn a baby when a spawn egg is used on its own mob kind
Mob.checkAndHandleImportantInteractions: a spawn egg of the clicked mob's
kind spawns a baby at the mob's position (SpawnEggItem.spawnOffspringFromSpawnEgg),
inheriting the parent's variant, and uses one egg outside creative.

Fixes #2269.
2026-10-26 16:04:35 +01:00
Ludovic MarieandClaude Opus 5.5 453ee22376 Merge main into fix/server-text-caret
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 16:01:24 +01:00
Ludovic Marie db4ea8a200 Merge pull request #2038 from levzzz5154/fix/bundle-tooltip
🤖 Agent: Gate check by Ludovic's AI agent (Claude). `tools/bundle_tooltip_e2e.py` passes in-frame. On the server thread (what players run) it showed every bundle full, holding more bundles, so I pushed two commits: (1) BundleContents' stream codec (component 52, a list of ItemStackTemplate, checked with javap) plus sending it to clients in `network_stack`; (2) `Hub::stack` gave every stack inside an item the outer item's id, which also turned a crossbow's charged projectiles into crossbows on clients. It is fixed now: each inner stack looks up its own id. With both commits the e2e passes on the server thread and in-frame (306 px tooltip frame; main shows 117 px and fails). The screenshot shows 32 stone with a half-full bar, as in vanilla. Unit tests: `bundle_contents_are_item_stack_templates` and `a_bundle_keeps_its_contents_across_the_wire`. Thanks @levzzz5154!
2026-10-26 15:58:10 +01:00
Ludovic MarieandClaude Opus 5.5 864ba24c3d Merge main into fix/thorns-hits-attacker
Main's critical-hit change added the same Strike.attacker field; keep
one, then the crit particles before the victim's thorns.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 15:56:14 +01:00
Ludovic Marie d48e56778e Merge pull request #2037 from levzzz5154/fix/loot-enchant-functions
🤖 Agent: Gate check by Ludovic's AI agent (Claude). I checked the loot functions against vanilla 26.3 directly. A vanilla server placed 48 chests with fixed LootTableSeed values: 16 tables (end city, stronghold library, ancient city, mansion, dungeon, trial chamber rewards and more) times 3 seeds, then rolled them. Horizon then opened the same world. Every chest got the same items, counts, slots and enchantments as vanilla, enchant_randomly and enchant_with_levels included: 48/48 on the server thread and 48/48 in-frame. The only difference is the order of keys inside the enchantments map, which vanilla doesn't read. EnchantRandomly and EnchantWithLevels match the bytecode, and rdbt's single-level review point is covered by `a_single_level_enchantment_draws_no_level`. It is also built and run in an integration tree with the other open PRs. Thanks @levzzz5154!
2026-10-26 15:51:13 +01:00
Ludovic MarieandClaude Opus 5.5 608a6d9bee Turtles: a fixed home from spawn or load, one breeding orb
Turtle.finalizeSpawn sets homePos to where it spawns, and loading falls
back to the block it stands on; the home no longer moves with a turtle
that had none, which let it lay eggs anywhere. TurtleBreedGoal.breed adds
one ExperienceOrb of 1 to 7 drawn from the mother's random.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 15:50:47 +01:00
Ludovic MarieandClaude Opus 5.5 75f7029d01 Squid ink: draw the squirt's voice pitch even when silent
Mob.makeSound calls getVoicePitch before Entity.playSound checks
isSilent, so the squid's random advances either way.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 15:48:37 +01:00
Ludovic MarieandClaude Opus 5.5 1d51f9d873 Soul Speed: draw the random chances in vanilla's order
Both random_chance terms come first in their all_of conditions, so the
level random is drawn before the block and movement checks: the boots'
4% on every location change, and the sound's 35% on every tick the
boots are worn.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 15:48:30 +01:00
Ludovic MarieandClaude Opus 5.5 7d8b6f3b90 Composter: keep addLayer's doc on composter_add_layer
The extracted composter_extract_produce took the doc comment of the
function below it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 15:47:37 +01:00
Ludovic Marie aed8b571c4 Merge remote-tracking branch 'origin/main' into feat/server-mods-world 2026-10-26 15:46:47 +01:00
rdbtCVSandOpenCode GPT-6.1 Sol 5eef2bea0c Clean up evoker summons after fang integration check
Remove the test arena's surviving vexes so later checks receive no stray attacks.

Co-Authored-By: OpenCode GPT-6.1 Sol <[email protected]>
2026-10-26 14:45:59 +00:00
Kacpherek 26626e8c27 Give frogs vanilla's smooth swimming move control 2026-10-26 15:45:51 +01:00
Ludovic MarieandClaude Opus 5.5 17e4d547c7 parity: give the mob_ai oracle a 4g heap
VanillaMobAiOracle ran out of its 3g heap in PoiManager.tick on a busy CI
runner. Each scenario forces 9x9 chunks at a new origin; the live heap after
GC swings between 0.3 and 1 GB over a run, with peaks near 2 GB before GC.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 15:45:13 +01:00
Kacpherek 545370b023 Key binds: press a shared default key once 2026-10-26 15:41:11 +01:00
Ludovic MarieandClaude Opus 5.5 b242a5ac15 Give the stacks inside a sent item their own ids
Hub::stack resolved every item id of a stack to the outer item's, so a
bundle's contents and a crossbow's charged projectiles reached a server's
clients as more bundles and crossbows. The tooltip then drew a bundle of
bundles, full. Each stack inside now looks its own id up.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 15:40:50 +01:00
Ludovic Marie 8975e2370b Merge pull request #2217 from ludovic111/ci/linux-runner-ubuntu
CI: Linux runner on Ubuntu (playhorizon), tokens without gh, CPU and memory knobs
2026-10-26 15:40:06 +01:00
socksproxandDevin 48811b12f1 Pistons animate their moves on the clients, as Java's do
A remote client (single player's window on its server thread, a LAN
guest, a dedicated server's client) never ran the client side of a
piston's move: the block event reached block_entity_event, which has no
piston ticker, so moving_piston blocks arrived with no moving block
entity and pushed blocks teleported (#1704).

Vanilla's runBlockEvents broadcasts a handled event to the clients, whose
own level re-runs triggerEvent (PistonBaseBlock.moveBlocks) to build the
PistonMovingBlockEntity it then ticks and renders interpolated. Do the
same:

- run_block_events emits a BlockEntityEvent for a handled event; the LAN
  and integrated hubs send it as ClientboundBlockEventPacket, before the
  tick's block changes, as vanilla's broadcast precedes
  ChunkHolder.broadcastChanges.
- The remote client re-runs triggerEvent for pistons, note blocks and
  bells on its own level (dropResources stays server-side, as
  popResource checks !isClientSide), and ticks the resulting moving
  pistons each client tick so they animate and collide between the
  server's states. Block events a client-side move queues run right
  away, as ClientLevel.blockEvent does.

Note blocks and bell rings reach the guests through the same packet,
which they did not get before.

Co-Authored-By: Devin <[email protected]>
2026-10-26 15:37:36 +01:00
Ludovic Marie 9a7f75b75d Merge pull request #1894 from ludovic111/feat/server-mods-game
🤖 Agent: merged main (aec52c85e). pr_ready --quick passes (1308 horizon-client tests, clippy, 507 LOC). The welcome server mod e2e passes 35/35 in every mode: in-frame, server thread, LAN host in-frame and on its thread with a guest, dedicated with a guest. Verify is green; parity now runs on main (parity-scope). Queued.
2026-10-26 15:30:36 +01:00
Ludovic MarieandClaude Opus 5.5 a4839d322b CI: Linux runner on Ubuntu (playhorizon), tokens without gh, CPU and memory knobs
linux-runner.sh takes its packages from apt where there is no Nix, the
registration token from RUNNER_TOKEN (a machine where gh must not be logged
in), and RUNNER_CPUS / RUNNER_MEMORY for a machine that does nothing else.
playhorizon, a partner's 8-thread server, joins parity_hosts.json.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 15:28:21 +01:00
Ludovic MarieandClaude Opus 5.5 7fbfbb5e2b Shulker bullet split: draw the failure chance from the level's random
Shulker.hitByShulkerBullet tests level().getRandom().nextFloat() against
the crowding chance, not the shulker's own random.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 15:27:12 +01:00
Ludovic MarieandClaude Opus 5.5 6bdf5fe368 Sweep: hit mobs within 3 blocks in creative too
Player.doSweepAttack keeps nearby entities with distanceToSqr < 9.0; it
doesn't use the entity interaction range.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 15:26:32 +01:00
Ludovic Marie c5a2259d92 Merge pull request #2022 from mc070309/cursor/sprint-jump-impulse-68db
🤖 Agent: Gate check by Ludovic's AI agent (Claude). The server fix in this PR landed first in #1779 (levzzz's PR, same change to `hub.rs` and `agent.rs`, with the same `lan_bot` test). I merged main into the branch and kept main's side, so what this PR now adds is its two unit tests: `sprint::tests::sprint_survives_releasing_the_key_through_a_jump` (the client sprint state survives the key coming up on the jump tick) and `living_motion::tests::sprint_jump_adds_look_direction_impulse` (LivingEntity.jumpFromGround's 0.2 impulse along the look, and none for a walk). Both pass, and `lan_e2e --check sprint-fov` passes in-frame and on the server thread in the integration tree. Labelled skip-release, because players see no change. Thanks @mc070309 for tracking the cause down and for the tests.
2026-10-26 15:25:12 +01:00
Kacpherek 7102727197 Play the container close sound when a server-thread player closes it 2026-10-26 15:24:26 +01:00
Kacpherek 70868f3208 Give Oozing, Infested, Weaving and Wind Charged their effects
These four 26.x effects only existed as potion entries. Oozing spawns two slimes, Weaving places cobwebs and Wind Charged makes a gust when the bearer dies. Infested sometimes releases silverfish when the bearer is hurt.
2026-10-26 15:20:34 +01:00
Ludovic Marie 7ec3fd2b4c Merge pull request #2212 from ludovic111/fix/furnace-recipes-used-namespace
🤖 Agent: Merging. verify and parity passed; the new unit test covers the merge of both key forms, and save_roundtrip no longer sees duplicate RecipesUsed keys.
2026-10-26 15:20:16 +01:00
Kacpherek aff673a08d Give the Ender Dragon its ambient sound 2026-10-26 15:20:10 +01:00
Kacpherek 2e642857fc Fire the levitation and using_item advancement triggers 2026-10-26 15:20:03 +01:00
Kacpherek 147e5269c7 Play the rain sound the way vanilla picks it 2026-10-26 15:19:20 +01:00
Kacpherek b3a499d85d Play chest, barrel and shulker box sounds with a random pitch from the right spot 2026-10-26 15:18:05 +01:00
Kacpherek 3c013dac75 An axe turns an unoxidized copper golem statue back into a golem
The golem stands where the statue was, facing the same way, the axe
loses durability and the statue block is removed.
2026-10-26 15:18:03 +01:00
Kacpherek b913dab5e5 Walk, follow and flee at the camel's real AI speeds 2026-10-26 15:17:53 +01:00
Kacpherek e87e839c97 Fire the construct_beacon trigger when a beacon activates 2026-10-26 15:17:44 +01:00
Kacpherek 8c8f857313 Fire item_used_on_block when an axe, honeycomb, disc or vault key is used on a block 2026-10-26 15:17:42 +01:00
Kacpherek 9c427dfae0 Mobs killed by a wither leave a wither rose 2026-10-26 15:16:54 +01:00
Kacpherek 13144a82a1 Pull the third-person camera back further when riding a happy ghast 2026-10-26 15:16:51 +01:00
Kacpherek ef711f8b1f Fire the brewed_potion trigger when a potion is taken from a brewing stand 2026-10-26 15:16:44 +01:00
Kacpherek 601e7918a8 Lightning scrapes one oxidation step off a copper golem
A copper golem hit by a bolt goes back one weathering step, and its
weathering timer (or wax) is reset.
2026-10-26 15:16:43 +01:00
Kacpherek dd3697bb4d Hopper minecarts take from the container above, one item per tick 2026-10-26 15:16:43 +01:00
Kacpherek 273729aac8 Count the combat statistics
damage_resisted, damage_absorbed and damage_blocked_by_shield now add up when
Resistance, absorption hearts or a shield take damage, and a player who lands
the killing blow on another player gets player_kills.
2026-10-26 15:16:34 +01:00
Kacpherek 94592a35ac Use the flat world's horizon height for the dark disc and void fog 2026-10-26 15:16:33 +01:00
Kacpherek 728bf14aa7 Use vanilla ambient sound intervals for guardians, hoglins, shulkers and allays
A happy ghast with a passenger waits six times as long.
2026-10-26 15:16:06 +01:00
Kacpherek 53501ab94e Merge a mob's repeated effects like vanilla
A stronger effect now replaces the old one with its own duration, instead of taking the longer duration and the higher level separately.
2026-10-26 15:15:55 +01:00
Kacpherek 581dc08cf5 Fire shot_crossbow and killed_by_arrow advancement triggers 2026-10-26 15:15:43 +01:00
Kacpherek 85a6ce0921 Lightning scrapes oxidation off copper
A struck copper block goes back to unoxidized, and 3 to 5 random walks
take one step of oxidation off the weathering copper around it. Waxed
blocks stay as they are but still start the walks.
2026-10-26 15:15:30 +01:00
Kacpherek 1b4fe0f067 Wither rose and open eyeblossom give bees an effect instead of breeding them 2026-10-26 15:15:28 +01:00
Kacpherek fd0f09d6f6 End the Absorption effect once the absorption is used up
The effect used to stay in the list until its timer ran out, even with no absorption hearts left.
2026-10-26 15:15:20 +01:00
Kacpherek 06fb03567e Count the block interaction statistics
Eating a cake slice, using a cauldron (fill, use, clean armor, banner or shulker
box), tuning a note block, ringing a bell, opening an ender chest and putting a
record in a jukebox now add to their statistics, which stayed at 0 before.
2026-10-26 15:15:11 +01:00
Kacpherek c418034f10 Let goats breed, follow wheat and be milked
Goats only strolled and panicked, so wheat did nothing after the love
hearts and a bucket gave no milk. Give them the GoatAi core and idle
behaviours that exist here (panic 2.0, breed, tempt 1.25, baby follow
1.25) and Goat.mobInteract's milking with the goat milk sounds.
2026-10-26 15:15:07 +01:00
Kacpherek c273a7271e Make breeze shots follow the world difficulty and aim higher at riders 2026-10-26 15:14:59 +01:00
Kacpherek 5b9a66089e Give a partial item pickup the random pitch of a full pickup 2026-10-26 15:14:43 +01:00
Kacpherek 4c72a3a44f Make a detector rail's comparator read the minecart on it 2026-10-26 15:14:42 +01:00
Kacpherek 8691d9a8e8 Let players harvest a full beehive with shears or a glass bottle 2026-10-26 15:14:34 +01:00
Kacpherek d2105fe66c Round instant potion effects and measure splash range from the body
Instant health and harm round to the nearest point instead of truncating. A splash potion measures the distance to the entity's box, not its centre, so a potion landing next to a player hits harder and reaches farther.
2026-10-26 15:14:30 +01:00
Kacpherek 79bfb8fcb8 Let a brush take a scute off an armadillo
Right-clicking an adult armadillo with a brush did nothing. Follow
Armadillo.mobInteract: drop a scute from the brush loot table, play the
brush sound, post the entity_interact game event and wear the brush by 16.
2026-10-26 15:14:26 +01:00
Kacpherek 20630b1230 Fire the enchanted_item trigger when an enchanting table enchants 2026-10-26 15:14:25 +01:00
Kacpherek 8d77913ea5 Note blocks play on a left click, and take mob heads on top
Mining a note block now plays its note and counts the play_noteblock
statistic. A mob head used on the top face is placed instead of tuning
the block.
2026-10-26 15:14:10 +01:00
Kacpherek 66637220ef Play the step sound of the block above the floor for the player
Powder snow, lily pads, carpets, snow layers and similar blocks now sound like vanilla when walked on: inside blocks replace the floor's step, combination blocks play their own step with the floor's step muffled.
2026-10-26 15:13:58 +01:00
Kacpherek 5fa423169f Fire player_hurt_entity and entity_hurt_player advancement triggers 2026-10-26 15:13:51 +01:00
Kacpherek 183382cd2b Fire the fishing_rod_hooked trigger when a hook is reeled in 2026-10-26 15:13:34 +01:00
Kacpherek 84aba7b3d4 Count talking to villagers under vanilla's statistic name
The statistic was written as talk_to_villager, but vanilla's key is
talked_to_villager, so the statistics screen and scoreboard criteria always
showed 0. Wandering traders already count through the same code.
2026-10-26 15:13:27 +01:00
Kacpherek 329ce1b724 Make armadillos shed scutes on their own
Armadillos never dropped scutes because nothing ran their shed timer.
Follow Armadillo.customServerAiStep: count down 5 to 10 minutes, then
drop a scute, play the sound and post the entity_place game event.
2026-10-26 15:13:27 +01:00
Kacpherek e2a399aff0 Reopen signs on right click and apply dyes, ink sacs and honeycomb
Right-clicking a placed sign opened an empty inventory screen. It now
opens the editor for the side the player faces, or plays the waxed-sign
fail sound. Dyes, glow ink sacs, ink sacs and honeycomb change the
colour, glow or wax of the side the player faces and are used up.
2026-10-26 15:13:19 +01:00
Kacpherek 2a0824cf5f Invisible players are harder for mobs to notice
Mobs now scale their detection range by 0.7 times the worn armor fraction (at least 0.1) when the player has Invisibility, like vanilla's getVisibilityPercent.
2026-10-26 15:13:15 +01:00
Kacpherek a4d0f76edd Count movement statistics as vanilla does
Fall distance is counted once per landing from 2 blocks or more instead of on
every downward tick. Riding counts only for the mounts vanilla lists (pig,
horse family by full distance, strider, happy ghast, nautilus, boat, minecart),
and airborne sideways moves over 25 cm count as flying.
2026-10-26 15:13:08 +01:00
Kacpherek 00e2cfec29 Let tadpoles be fed slime balls to grow up faster 2026-10-26 15:12:57 +01:00
Kacpherek a5fa40d2ee Fire the villager_trade trigger so What a Deal! can be earned 2026-10-26 15:12:48 +01:00
Kacpherek cc39d57e07 Explosions empty and anger the bees of destroyed hives 2026-10-26 15:12:42 +01:00
Kacpherek 0a5e80de7b Step out of a minecart in vanilla's order 2026-10-26 15:12:33 +01:00
Kacpherek b7c47ccbf9 Let a sneaking player open chest and hopper minecarts 2026-10-26 15:11:52 +01:00
Kacpherek 9c3450622b Update comparators when a copper golem moves items in or out of a chest 2026-10-26 15:11:49 +01:00
Kacpherek 3615c65d18 Make allays drop what they carry on death and use the default pickup delay when throwing 2026-10-26 15:11:01 +01:00
Ludovic MarieandClaude Opus 5.5 be73f4ac6a Keep a furnace's RecipesUsed keys namespaced, so vanilla reads Horizon's furnaces
bf81ae635 started saving recipes_used, but Horizon's recipe ids lack the
minecraft: namespace, so a furnace could hold both forms of one id and
vanilla refused it with "Duplicate entry for key" (save_roundtrip).
recipe_used_key gives the full id; loading merges the two forms from saves
written in between.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 15:10:58 +01:00
Kacpherek 7cc8def23f Let a lit TNT minecart explode when it hits a wall 2026-10-26 15:10:57 +01:00
Kacpherek e553db9bb5 Let frogs take fall damage five points below the base amount 2026-10-26 15:10:09 +01:00
Kacpherek 790b191ed1 Bees let out of a sedated hive stay out for 400 ticks 2026-10-26 15:09:20 +01:00
Kacpherek d136dda092 Bee stings only poison on Normal and Hard 2026-10-26 15:08:48 +01:00
Ludovic Marie 8027cb4176 Merge pull request #2177 from ludovic111/feat/launcher-mod-ratings
🤖 Agent: Merging. verify passed, along with launcher tests (11 registry tests), the horizon-theme icon test and clippy -D warnings. #2173, which it was stacked on, is merged.
2026-10-26 15:05:13 +01:00
Kacpherek 7d5a052261 Target blocks react to projectile hits
A target block never changed its power when something hit it. Every
projectile that hits a block now asks the block how to react, as vanilla
does: a target block takes its power from where the projectile landed
(1 to 15 by the distance from the face centre) and resets after 20
ticks for an arrow or 8 for any other projectile. A player's hit also
counts the target_hit statistic.
2026-10-26 15:02:57 +01:00
Kacpherek 84b621096a Satisfy clippy 2026-10-26 15:02:46 +01:00
Kacpherek f0d2a2ae74 Satisfy clippy 2026-10-26 15:02:43 +01:00
Kacpherek 0c22d75a2e Satisfy clippy 2026-10-26 15:02:41 +01:00
Ludovic Marie 91400aac5b Merge pull request #2173 from ludovic111/feat/mod-index-downloads
🤖 Agent: Merging. verify and mod-registry passed, and pr_ready --quick passed (clippy, horizon-mods and launcher tests). The index's downloads field is optional, so the schema doesn't change.
2026-10-26 15:02:11 +01:00
Ludovic Marie 26894ab3fd Merge pull request #2170 from ludovic111/feat/mods-site-stats
🤖 Agent: Merging. verify passed and the website tests (44) pass. Ratings appear once #2167's service is live; until then the pages show downloads only.
2026-10-26 14:59:28 +01:00
Kacpherek 4abf5c0ea2 Play no place sound after using an empty hand or a non-block item on a block 2026-10-26 14:58:57 +01:00
Ludovic Marie ae17e26009 Merge pull request #2140 from ludovic111/feat/quiet-commands-server
🤖 Agent: verify and parity pass (109 harnesses affected; parity green). The dedicated-server e2e (tools/quiet_commands_e2e.py) passes 7/7. Queued. #2171 (the mod API, stacked on it) follows once this lands.
2026-10-26 14:57:05 +01:00
Ludovic MarieandClaude Opus 5.5 ae3b844d74 Merge main into fix/server-text-caret
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 14:56:10 +01:00
rdbtCVSandOpenCode GPT-6.1 Sol 02897ac731 Merge main into fix/1482-evoker-fangs
Preserve both fang and sonic-wave integration checks.

Co-Authored-By: OpenCode GPT-6.1 Sol <[email protected]>
2026-10-26 13:54:53 +00:00
Ludovic Marie 03c8594494 Merge pull request #2061 from lazy-importer/fix/2034-bossbar-vanish-reconfigure
🤖 Agent: contrib-gate-17 verified this head (26.3 sources, integration tree build, clippy, tests, parity; details in the PR comment). Thanks @lazy-importer!
2026-10-26 14:53:47 +01:00
Ludovic MarieandClaude Opus 5.5 9540734e29 Merge main into fix/cushion-placement
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 14:50:57 +01:00
Ludovic Marie 601df6c22d Merge pull request #2052 from levzzz5154/fix/chest-shift-click
🤖 Agent: contrib-gate-17 verified this head (26.3 sources, integration tree build, clippy, tests, parity; details in the PR comment). Thanks @levzzz5154!
2026-10-26 14:50:55 +01:00
Ludovic MarieandClaude Opus 5.5 3dd17ad7d7 Launcher: registry mods show their 1-5 star rating
Horizon results and project pages show a project's rating from the
registry service (GET playhorizon.dev/api/mods/ratings, fetched with the
results, kept ten minutes): five pixel stars, the mean and how many, or
"No ratings yet". Nothing shows when the service doesn't answer.

The launcher has no GitHub account, so a project's page offers "Sign in
with GitHub to rate": it opens the site's sign-in, which comes back to the
project's page and its stars (one rating per GitHub account).

HORIZON_MOD_RATINGS (a file or https:// address) points a test registry at
its own ratings. The star icon is drawn for Horizon on the icons' grid.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 14:50:40 +01:00
Ludovic Marie 394ce4928c Merge pull request #2044 from Domanffe/docs/roadmap-help-wanted-links
🤖 Agent: contrib-gate-17 verified this head (26.3 sources, integration tree build, clippy, tests, parity; details in the PR comment). Thanks @Domanffe!
2026-10-26 14:47:47 +01:00
Ludovic MarieandClaude Opus 5.5 0e4965845a Book pages: move by words as MultilineTextField does
BookEditScreen's page is a MultiLineEditBox. Its getPreviousWord and
getNextWord split words at any whitespace (Character.isWhitespace, line breaks
included) and start from the cursor clamped to the last character. The
single-line EditBox.getWordPosition keeps looking at spaces only, so the line
break rule moves to the multiline box alone.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 14:47:17 +01:00
Ludovic Marie 8fa967ac07 Merge remote-tracking branch 'origin/main' into feat/server-mods-game 2026-10-26 14:45:29 +01:00
Ludovic Marie aec52c85e9 Merge pull request #2041 from Domanffe/perf/uniform-values-reuse
🤖 Agent: contrib-gate-17 verified this head (26.3 sources, integration tree build, clippy, tests, parity; details in the PR comment). Thanks @Domanffe!
2026-10-26 14:44:52 +01:00
Ludovic MarieandClaude Opus 5.5 a0a170064e Merge main into sprint-jump-impulse
Main's #1779 already keeps the sprint through PlayerInput (hub.rs, agent.rs)
and has the same lan_bot test, so this keeps main's side: what the branch adds
is the two unit tests.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 14:44:08 +01:00
Ludovic MarieandClaude Opus 5.5 caae026416 HDR: keep native material packs' vertex tint opaque
The mesher now keeps a face's light emission in its colour's alpha byte. The
terrain shader drops it, but the native material path (merged since) passed
the colour through as the tint, so a light-emitting translucent face such as
a nether portal drew a little more transparent with HDR off.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 14:42:00 +01:00
Ludovic MarieandClaude Opus 5.5 92ed365a19 Merge main into hdr-output
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 14:41:59 +01:00
Ludovic Marie c65338b3cb Merge pull request #2029 from Domanffe/perf/subtitle-width-once
🤖 Agent: contrib-gate-17 verified this head (26.3 sources, integration tree build, clippy, tests, parity; details in the PR comment). Thanks @Domanffe!
2026-10-26 14:41:27 +01:00
rdbtCVSandCodex GPT-6.1 Sol 6e0a49029f Keep block-breaking cracks aligned between stages
Restore integer atlas origins and sample local texel centers before nearest filtering. Cover the packed stage origins with a GPU sampling regression.

Fixes #1882.

Co-Authored-By: Codex GPT-6.1 Sol <[email protected]>
2026-10-26 13:39:51 +00:00
Kacpherek bd59cdc25c Let turtles lay eggs that hatch into baby turtles
A bred turtle now carries an egg instead of the baby appearing at once.
She walks home, digs into sand for a while and lays a turtle egg there.
A hatching egg gives one baby turtle per egg with the egg as its home,
and a baby that grows up drops a scute.
2026-10-26 14:39:16 +01:00
Ludovic MarieandClaude Opus 5.5 590ca775c2 Mod registry: download counts in the index and the launcher's browser
The registry's publish job (ludovic111/horizon-mods#7) writes each
project's `downloads` into index.json, summed from GitHub's counts of its
release assets every hour. `index::Project` reads it (absent: 0, never
written when 0, so older readers and the schema are unchanged).

The launcher's Horizon results show "12.3k downloads" like Modrinth's, and
list the most downloaded first.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 14:37:12 +01:00
Ludovic Marie 38bea19137 Merge pull request #2167 from ludovic111/feat/mod-ratings-api
🤖 Agent: Merging. The verify and mod-registry checks (fmt, clippy -D warnings, tests) passed. The registry-data volume is already mounted at /data.
2026-10-26 14:36:12 +01:00
Kacpherek 4ae514d0ae Throw leftover items from the eyes, and creative players keep their stack on an armor stand 2026-10-26 14:36:10 +01:00
Kacpherek 628ff01a44 Only zombies that break doors and raiding vindicators get through doors
Mobs no longer open or break doors on their own except villagers and copper
golems. Zombies that rolled canBreakDoors break doors on hard, and
vindicators break doors on normal and hard and open them during a raid, as
BreakDoorGoal, VindicatorBreakDoorGoal and RaiderOpenDoorGoal do. A zombie's
CanBreakDoors value is now saved and loaded.
2026-10-26 14:35:52 +01:00
Kacpherek c7bacfb293 Water buckets pick up fish and tadpoles, keeping their data 2026-10-26 14:35:50 +01:00
Ludovic MarieandClaude Opus 5.5 282556870f Cushions: pass when occupied, burn when placed in fire, entity_place
Checked against the 26.3 bytecode (tools/rea_vanilla.py):
- Cushion.interact passes (PASS) when the cushion already has a rider;
  CONSUME is only for a startRiding that fails.
- CushionItem.useOn calls destroyIfInFire right after addFreshEntity, and
  posts GameEvent.ENTITY_PLACE from the player.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 14:34:55 +01:00
Kacpherek ee19d2c00c Experience orbs: wider pickup area, merging, and lava bounce 2026-10-26 14:33:48 +01:00
Ludovic Marie a03940e0a8 Merge pull request #2166 from ludovic111/ci/parity-main-flake-green
🤖 Agent: parity-main counts a failure that passes again at HEAD as a pass; tests assert each exit code, YAML parses.
2026-10-26 14:33:41 +01:00
Kacpherek f24ca405a8 Piglins walk away from soul fire, soul torches, soul lanterns and lit soul campfires
Piglins now sense the nearest block from the piglin repellents tag within 8 blocks sideways and 4 up or down (a soul campfire only while lit). When one is closer than 8 blocks and the piglin is idle with nowhere to go, it walks away from it.
2026-10-26 14:33:25 +01:00
Ludovic Marie 4f70efcfa9 Merge pull request #2027 from Domanffe/perf/sign-first-line
🤖 Agent: contrib-gate-17 verified this head (26.3 sources, integration tree build, clippy, tests, parity; details in the PR comment). Thanks @Domanffe!
2026-10-26 14:33:10 +01:00
Kacpherek 091a98656e Dragon: find healing crystals in its grown box and cap healing at max
The search now uses the dragon's 16 by 8 box grown by 32 blocks like
EnderDragon.checkCrystals, and healing no longer goes above max health.
2026-10-26 14:32:46 +01:00
Kacpherek fd1d857f1b Hoglins keep away from warped fungus, portals and respawn anchors
Hoglins now sense the nearest block from the hoglin repellents tag within 8 blocks sideways and 4 up or down. While one is near they become passive for 200 ticks (the attack target is dropped and no new one is picked), walk away from it, and avoid picking random walk spots within 8 blocks of it. Being hit ends the passive state.
2026-10-26 14:32:02 +01:00
Kacpherek fd0cb00296 Axolotls dry out on land, squid squirt ink when a living entity hurts them 2026-10-26 14:31:58 +01:00
Ludovic MarieandClaude Opus 5.5 a83d6f33be Campfire: a full campfire's use does not swing the arm
CampfireBlock.useItemOn returns SUCCESS_SERVER (a swing) only when
placeFood took the item, and CONSUME (no swing) when all four slots are
taken.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 14:31:50 +01:00
Kacpherek 3e78846975 Soul Speed: speed, no soul sand slowdown, boots wear, soul particles
The enchantment's location_changed effects now run for the player: the
movement_speed and movement_efficiency modifiers turn on over soul sand and
soul soil and stay on while airborne, the boots wear with a 4% chance per
block change, and walking on those blocks spawns soul particles and the
soul escape sound.
2026-10-26 14:31:28 +01:00
Kacpherek 8bfa7a5b7b Loot: suspicious stew keeps the effect set_stew_effect rolled
The function drew an effect and a duration but dropped them. The stack
now carries the effect and it is stored in suspicious_stew_effects.
2026-10-26 14:31:03 +01:00
rdbtCVSandCodex b1136ac287 Keep forced server levels ticking without players
Fixes #1525. Respect forced tickets when scheduling levels and bound playerless integrated-server ticks to their ticket sources.

Co-Authored-By: Codex (GPT-6.1 Sol) <[email protected]>
2026-10-26 13:30:54 +00:00
Kacpherek 199178907c Shovels put out a lit campfire 2026-10-26 14:30:49 +01:00
Kacpherek 7abb092089 Wire fire burnout, difficulty and spread radius into the fire tick 2026-10-26 14:30:34 +01:00
Kacpherek fcfd425b94 Give the conduit its power and its attack
A conduit with enough prismarine around it now gives conduit power to
players in water or rain in range, hurts an enemy in water or rain when
it has 42 frame blocks, and plays its activate, deactivate and ambient
sounds, as ConduitBlockEntity.serverTick does. Its target also feeds the
client's attack particles.
2026-10-26 14:30:32 +01:00
Kacpherek d8fcebdf71 Match vanilla difficulty and survival rules for lightning fire 2026-10-26 14:30:31 +01:00
Kacpherek 551a9ee9c5 Make lightning strike the closest lightning rod 2026-10-26 14:30:29 +01:00
Ludovic MarieandClaude Opus 5.5 fb86430813 mods: command.run-quiet (API level 3), a batch of commands with nothing in the chat
A mod hands `run-quiet` a list of lines (at most 256 a call). In single
player they run as the player typing them would, with the player's source
made silent; on a Horizon server (it announces horizon:quiet_commands) they
go as one payload that the server runs the same way; on any other server
they fall back to chat commands, whose feedback shows. command.run is
unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 14:30:23 +01:00
Ludovic Marie 9c26db87e6 Merge pull request #2024 from levzzz5154/fix/remote-cooldown-tick
🤖 Agent: contrib-gate-17 verified this head (26.3 sources, integration tree build, clippy, tests, parity; details in the PR comment). Thanks @levzzz5154!
2026-10-26 14:30:19 +01:00
Ludovic MarieandClaude Opus 5.5 f49d5d91e6 Site: downloads and 1-5 star ratings on /mods
- The list shows each project's rating (stars, mean and count) and its
  downloads ("12.3k downloads"), ordered by downloads by default; an Order
  menu also sorts by rating or name (?sort= too).
- A project's page shows the same, then a rating control: five stars when
  signed in (the account's own rating lit), "Sign in with GitHub to rate"
  (coming back to the page) when not, a note for its owners.
- Ratings come from the registry service's GET /api/mods/ratings; a rating
  is POSTed with the session's CSRF token. Until the service answers, the
  pages show downloads only.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 14:29:31 +01:00
Kacpherek 6875c44bbe Piglins accept only gold ingots by hand, keep other loved items, pick up more
Right-clicking a piglin with any loved item used to consume it and the piglin threw it back. Now only piglin currency (gold ingots) is accepted from the hand, and anything else a piglin admires is worn if it can be or stored in its inventory, which saves, and drops when it dies. Piglins also pick up gold nuggets when they have room, food when they have not eaten lately, and gear that fits an empty slot.
2026-10-26 14:28:40 +01:00
Kacpherek bcdf769f8d Illagers do not attack baby villagers
AbstractIllager.canAttack refuses a baby villager or wandering trader.
Skip them when an illager picks a nearest target and drop them from an
illager's current target.
2026-10-26 14:28:20 +01:00
Kacpherek 54fe73854e Cactus destroys dropped items that touch it 2026-10-26 14:28:06 +01:00
Kacpherek 4aa7838d1b Shulker bullets make open shulkers teleport and duplicate
An open shulker hit by a shulker bullet teleports and may leave a new
shulker of its colour behind, like Shulker.hitByShulkerBullet. Levitation
from the bullet now needs the hit to have hurt the player.
2026-10-26 14:27:48 +01:00
Ludovic Marie ec7a2e22a7 Merge pull request #2012 from Domanffe/perf/poses-layer-reuse
🤖 Agent: contrib-gate-17 verified this head (26.3 sources, integration tree build, clippy, tests, parity; details in the PR comment). Thanks @Domanffe!
2026-10-26 14:27:29 +01:00
Kacpherek 991406d0e0 Turtle helmet: Water Breathing while the eyes are out of the water 2026-10-26 14:27:10 +01:00
Kacpherek 0ba73d0a7e Match vanilla evoker, fangs and vex rules
Count vexes and look for blue sheep within a 16 block radius, spare only
illagers and their vexes with evoker fangs, and let vexes ignore raiders
(including ravagers) instead of other vexes.
2026-10-26 14:27:05 +01:00
Kacpherek 05fd9c3795 Chests with a block or a sitting cat on top no longer open 2026-10-26 14:26:59 +01:00
Kacpherek 71981a95a2 Weather: the rain and thunder fade runs even with the weather cycle off 2026-10-26 14:26:34 +01:00
Kacpherek af6b375626 Guardians: damage scales with Hard and elders, squid and axolotls must be beyond 3 blocks 2026-10-26 14:25:55 +01:00
Kacpherek 19c7a4a008 Slime block slows a walking player (SlimeBlock.stepOn)
While standing on slime with a vertical speed under 0.1 and not sneaking, the
horizontal speed is scaled by 0.4 + |vertical speed| * 0.2 every tick.
2026-10-26 14:25:37 +01:00
Kacpherek 23d546c6d5 Sleep: creative skips the monster check, bed reach and obstruction match vanilla, morning follows the time and weather rules 2026-10-26 14:24:20 +01:00
Kacpherek 3353f1b6e4 Elytra: hurt on wall hits, stop on climbables and levitation, Unbreaking wear, real gravity
Gliding into a wall now hurts like LivingEntity.handleFallFlyingCollisions
(fly_into_wall, big or small fall sound). A glide no longer starts on a
climbable or under Levitation, ends there, and a climbable (not vines) ends a
glide in progress. The glide's elytra wear goes through Unbreaking, and the
glide uses the effective gravity instead of 0.08.
2026-10-26 14:24:16 +01:00
Ludovic MarieandClaude Opus 5.5 3caf8f7014 lan_e2e portals: close the credits on the host's first End exit
The first exit from the End now rolls the poem and credits, and the host
goes home once they close (PERFORM_RESPAWN); the check asserts they show.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 14:24:09 +01:00
Ludovic MarieandClaude Opus 5.5 8d6d1f26a4 Send a bundle's contents to a server's clients
BundleContents had no stream codec here: the host never sent what a bundle
holds, so on the server thread, LAN and a dedicated server the new tooltip
showed every bundle empty, and a vanilla server's bundle made its packet
undecodable. Its STREAM_CODEC is a list of ItemStackTemplate, the same as
ChargedProjectiles' (component 52).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 14:20:25 +01:00
Alexander KrylovandHermes Agent 13e946a89c HUD: the hotbar draws its stacks without cloning them
`draw_item` and `draw_item_decorations` only read the game, so they take
`&self`; the hotbar loop can then hand them `self.inventory[slot].as_ref()`
and `self.offhand.as_ref()` instead of cloning the stack once per slot per
frame.

Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
2026-10-26 16:20:17 +03:00
Ludovic Marie b78daab11c Merge pull request #2081 from kacpherek/fix/performance-keeps-animated-textures
🤖 Agent: contrib-gate-17 verified this head (26.3 sources, integration tree build, clippy, tests, parity; details in the PR comment). Thanks @kacpherek!
2026-10-26 14:18:21 +01:00
zhykowoandWorkBuddy AI da54f988d4 Proxy: one copy of the rules, none of it per request
Co-Authored-By: WorkBuddy AI (DeepSeek-V4.1-Flash) <[email protected]>
2026-10-26 21:17:28 +08:00
Ludovic MarieandClaude Opus 5.5 455817204f Mod registry: 1-5 star ratings, one per GitHub account
The registry service keeps players' ratings of published projects in one
SQLite file on a volume (REGISTRY_DB, /data on Railway):
- GET /api/mods/ratings: every rated project's average and count, cached a
  minute; never who rated.
- GET /api/mods/{id}/rating: the signed-in account's own rating.
- POST /api/mods/{id}/rating: sets or changes it, checked like a submission
  (origin, session, CSRF token), 30 per account and 600 in all per hour, only
  for projects on the registry's main, never by their owners.
- The sign-in takes ?back=/mods/<id> to come back to a project's page.

The image hands the volume to nobody before running as nobody.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 14:15:46 +01:00
Ludovic Marie aadb8b6727 Merge pull request #2079 from nirajkhetre/fix/enderman-death-message
🤖 Agent: contrib-gate-17 verified this head (26.3 sources, integration tree build, clippy, tests, parity; details in the PR comment). Thanks @nirajkhetre!
2026-10-26 14:14:44 +01:00
Ludovic Marie 7b94de404e Merge pull request #2010 from Domanffe/perf/skip-lookup-once
🤖 Agent: contrib-gate-17 verified this head (26.3 sources, integration tree build, clippy, tests, parity; details in the PR comment). Thanks @Domanffe!
2026-10-26 14:13:29 +01:00
Kacpherek 7c16e83fdf Key binds: a key bound to several mappings drives all of them 2026-10-26 14:13:03 +01:00
Ludovic MarieandClaude Opus 5.5 9e76f8c4f0 parity-main: a failure that passes again at HEAD counts as a pass
Twice today (runs 40301239508, 40311712302) mob_ai failed in parity-main, passed when bisect ran it again at
the same HEAD, and the run still ended red, so no game release could follow main for three hours. The
harness step now continues on error and the bisect step gives the verdict: 0 when every failed harness
passes again at HEAD, 1 when they fail at the base too or a merge is named.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 14:12:04 +01:00
Ludovic Marie 8087d8bf58 Merge pull request #2009 from vlesmentie/fix/villager-new-profession-trades
🤖 Agent: contrib-gate-17 verified this head (26.3 sources, integration tree build, clippy, tests, parity; details in the PR comment). Thanks @vlesmentie!
2026-10-26 14:10:43 +01:00
Ludovic Marie 659e6d7896 Merge pull request #1993 from Domanffe/perf/no-double-cracks-and-ticker-copy
🤖 Agent: contrib-gate-17 verified this head (26.3 sources, integration tree build, clippy, tests, parity; details in the PR comment). Thanks @Domanffe!
2026-10-26 14:09:57 +01:00
Ludovic MarieandClaude Opus 5.5 ae63b23dd6 lan_e2e guardian-beams: the host reads the guardian's own level
horizon-ctl's mobs answers from the server's level when the host runs on
its server thread, where the targeted player is Player, as in-frame.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 14:08:15 +01:00
Ludovic Marie a3f5615ec7 Merge pull request #2108 from ludovic111/perf/worldgen-data-parallel-load
🤖 Agent: pr_ready passed (horizon-world and client tests, affected harnesses), lan_e2e passed in-frame and on the server thread. GeneratorData::load on the ThinkPad 0.64 → 0.45 s warm. Queued for merge.
2026-10-26 14:07:33 +01:00
Ludovic Marie b244c5d26e Merge pull request #2042 from Domanffe/perf/tooltip-component-compare
🤖 Agent: contrib-gate-17 verified this head (26.3 sources, integration tree build, clippy, tests, parity; details in the PR comment). Thanks @Domanffe!
2026-10-26 14:06:20 +01:00
Ludovic MarieandClaude Opus 5.5 d983ef89b4 Keep item_name as its component: translate it when drawn
The decoded plain string replaced the component on save and on the way to
LAN guests, so a translatable name (explorer maps' filled_map.*, the
ominous banner in tests without a language) came back as its key, and
spill's ominous banner test failed. The component now stays in the patch
as loaded and is resolved in the current language when a name is drawn.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 14:05:17 +01:00
Ludovic Marie 9af6d0be5c Merge pull request #2118 from ludovic111/fix/localize-server-screens
🤖 Agent: contrib-gate-17 verified this head (integration tree build, clippy, tests, parity; details in the PR comment). Re-opened from kohad1ck's PR; thanks to kohad1ck.
2026-10-26 14:03:06 +01:00
Kacpherek d0509e2bee Witches stand still while drinking and skip potions a target already has
A drinking witch now gets the -0.25 drinking speed modifier, so it does
not walk for the 32 ticks. When it throws a potion, slowness, poison and
weakness are only picked if the target (player or mob) does not already
have that effect, as Witch.performRangedAttack does.
2026-10-26 14:02:39 +01:00
Ludovic Marie 034c0f0228 Merge pull request #2117 from ludovic111/fix/font-glyphs-atlas-rebuild
🤖 Agent: contrib-gate-17 verified this head (integration tree build, clippy, tests, parity; details in the PR comment). Re-opened from kohad1ck's PR; thanks to kohad1ck.
2026-10-26 14:00:01 +01:00
Kacpherek d057c89164 Mobs killed by a charged creeper drop their head
A creeper's explosion hurt mobs with no attacker, and the creeper is
already gone from the mob list when its blast is applied, so the
charged_creeper loot never matched. The creeper now announces itself
(ExplosionSource) before it explodes, its victims name it as the damage
source, and it stays known until they have been looted. One head drops
per charged explosion.
2026-10-26 13:59:33 +01:00
Ludovic Marie dec94b6c61 Merge pull request #2053 from rdbtCVS/rdbtcvs/fix-2019-recipe-screen-cleanup
🤖 Agent: contrib-gate-17 verified this head (26.3 sources, integration tree build, clippy, tests, parity; details in the PR comment). Thanks @rdbtCVS!
2026-10-26 13:59:13 +01:00
Ludovic Marie c1e7a86622 Merge pull request #2020 from levzzz5154/fix/guest-chat-visibility
🤖 Agent: contrib-gate-17 verified this head (26.3 sources, integration tree build, clippy, tests, parity; details in the PR comment). Thanks @levzzz5154!
2026-10-26 13:58:21 +01:00
Ludovic Marie 6ee319c607 Merge pull request #2018 from rdbtCVS/fix/peaceful-mob-despawn-1942
🤖 Agent: contrib-gate-17 verified this head (26.3 sources, integration tree build, clippy, tests, parity; details in the PR comment). Thanks @rdbtCVS!
2026-10-26 13:55:47 +01:00
Ludovic Marie fc5cd8060d Merge pull request #2007 from rdbtCVS/fix/1962-passenger-animation
🤖 Agent: contrib-gate-17 verified this head (26.3 sources, integration tree build, clippy, tests, parity; details in the PR comment). Thanks @rdbtCVS!
2026-10-26 13:55:12 +01:00
Kacpherek d622030988 Zombies call reinforcements when hurt on Hard
A hurt zombie with a target now tries up to 50 spots 7 to 40 blocks away
for a copy of itself, as Zombie.hurtServer does. The copy takes the
target, and both zombies get the charge that lowers their chance.

New zombies also roll their reinforcement chance and a leader's bonus
(Zombie.handleAttributes); before this the chance stayed at 0.
2026-10-26 13:53:34 +01:00
Kacpherek b752a821b1 Wear armor when cactus, fire, lava, campfires and other environment damage hurt the player 2026-10-26 13:53:21 +01:00
Ludovic Marie 0555120244 Merge pull request #2048 from rdbtCVS/fix/1490-sonic-boom-delivery
🤖 Agent: contrib-gate-17 verified this head (26.3 sources, integration tree build, clippy, tests, parity; details in the PR comment). Thanks @rdbtCVS!
2026-10-26 13:52:47 +01:00
Kacpherek a489d65ab0 Arrows press wooden buttons and keep them pressed
Wooden buttons ignored arrows: a pressed button always released after
its delay. A button whose type allows arrows is now pressed when an
arrow (or a thrown trident) is inside its shape, and it stays pressed,
scheduling the release again, while one still is. Arrows now also run
the entityInside check on the blocks they pass through each tick.
2026-10-26 13:52:43 +01:00
Ludovic Marie 9feb4da17c Merge pull request #2003 from rdbtCVS/fix/bat-wing-animation-2000
🤖 Agent: contrib-gate-17 verified this head (26.3 sources, integration tree build, clippy, tests, parity; details in the PR comment). Thanks @rdbtCVS!
2026-10-26 13:51:59 +01:00
Kacpherek 688d86db7c Villagers look for a child's bed in a full 48 block sphere and only take one they can reach
The search only covered 8 blocks up and down. It now uses the POI index over
the whole sphere, and a bed only counts when the villager has a path to it,
as VillagerMakeLove.takeVacantBed does.
2026-10-26 13:51:58 +01:00
Kacpherek 5d30776b07 Make Piercing crossbow arrows pass through entities
Crossbow arrows now get their pierce level from the Piercing enchantment, go through that many entities plus one without sticking, and are discarded at the next one. Arrows without Piercing behave as before. Pillager arrows now use the pierce level their weapon already carried.
2026-10-26 13:51:56 +01:00
Kacpherek 8178ec3dae Let a fire charge light a creeper
A creeper is now lit by any item in the creeper_igniters tag, not only
flint and steel. A fire charge is used up and plays its own sound; flint
and steel still loses durability.
2026-10-26 13:51:46 +01:00
Ludovic Marie f8f49a033f Merge pull request #1992 from DanielGodriNeto/fix/benchmark-wait-loaded
🤖 Agent: contrib-gate-17 verified this head (26.3 sources, integration tree build, clippy, tests, parity; details in the PR comment). Thanks @DanielGodriNeto!
2026-10-26 13:51:04 +01:00
Kacpherek 02e5c728c3 Pick the skeleton bow interval by difficulty
Skeletons now shoot every 20 ticks on Hard and every 40 otherwise
(bogged and parched: 50 and 70) instead of always using the normal
interval.
2026-10-26 13:49:57 +01:00
Kacpherek 5b0ae25ff8 Make thorns hurt the entity that hit the player
Thorns picked the nearest living mob in the world. They now hurt the mob
that caused the hit (or its projectile's owner), or the attacking player
in a player-versus-player hit.
2026-10-26 13:49:39 +01:00
Ludovic Marie 245206203d Merge pull request #1999 from rdbtCVS/t3/fix-critical-hit-particles
🤖 Agent: contrib-gate-17 verified this head (26.3 sources, integration tree build, clippy, tests, parity; details in the PR comment). Thanks @rdbtCVS!
2026-10-26 13:48:23 +01:00
Kacpherek 48c0f39e96 Farmers make bread and compost seeds when they work at a composter
A farmer working at its composter turns wheat into bread (three wheat for one
loaf, up to three loaves, while it holds at most 36 bread) and puts spare wheat
and beetroot seeds into the composter, emptying a full one first, as
WorkAtComposter does. Other professions are unchanged.
2026-10-26 13:48:15 +01:00
Kacpherek 02f222728d Respect mobGriefing for mob explosions and endermen
Mob explosions (creepers, ghast fireballs, wither skulls and the wither
spawn blast) now keep blocks when mobGriefing is off, and follow the
mob_explosion_drop_decay rule when it is on. Endermen no longer pick up
or put down blocks with mobGriefing off.
2026-10-26 13:48:13 +01:00
Kacpherek 4d0116762c Apply Unbreaking to thrown tridents and stop Riptide while riding
Throwing a trident now wears it through the same durability path as Riptide, so Unbreaking applies, and it still never breaks the trident. Riptide no longer launches a player who is riding something.
2026-10-26 13:48:08 +01:00
Kacpherek 48c289f775 Make Channeling summon lightning when a trident hits
A thrown trident with Channeling now calls down a lightning bolt in a thunderstorm, both on a mob it hurts (if the mob can see the sky) and on a lightning rod it hits (if the rod can see the sky), with the thunder sound.
2026-10-26 13:47:53 +01:00
Kacpherek d68ab989c0 Let the fishing bobber hook dropped items, add owner luck, count only fish as fish caught 2026-10-26 13:47:50 +01:00
Kacpherek 65bebdfc49 Make a sword sweep hurt the mobs next to the target
The sweep only drew the particle and played the sound. Mobs in the sweep
box now take the sweep damage (with Sweeping Edge) and 0.4 knockback.
2026-10-26 13:47:18 +01:00
Ludovic Marie 1da8c57e72 Merge pull request #1987 from socksprox/fix/world-text-parity
🤖 Agent: contrib-gate-17 verified this head (26.3 sources, integration tree build, clippy, tests, parity; details in the PR comment). Thanks @socksprox!
2026-10-26 13:47:04 +01:00
Kacpherek c4b9067d6d Infinity only saves plain arrows, not tipped or spectral ones 2026-10-26 13:45:54 +01:00
Ludovic Marie 11635c3bdf Merge pull request #2085 from kacpherek/fix/2084-enderman-teleport
🤖 Agent: contrib-gate-17 verified this head (26.3 sources, integration tree build, clippy, tests, parity; details in the PR comment). Thanks @kacpherek!
2026-10-26 13:44:44 +01:00
Kacpherek d0af843094 Daylight detectors follow the sky and update when used
A daylight detector never changed its power: nothing ran its update.
It now has a block entity with the ticker vanilla has, and updates its
power every 20 game ticks from the sky light, the sky darkening and the
sun angle. Using it to flip inverted mode recomputes the power at once.
2026-10-26 13:44:16 +01:00
Ludovic Marie ba6ca28577 Merge pull request #2082 from kacpherek/fix/fire-extinguish-no-break-particles
🤖 Agent: contrib-gate-17 verified this head (26.3 sources, integration tree build, clippy, tests, parity; details in the PR comment). Thanks @kacpherek!
2026-10-26 13:44:08 +01:00
Kacpherek aec3b7e262 Scale mob damage to players with the difficulty
Damage types marked when_caused_by_living_non_player now scale when a mob
(or the owner of its projectile, or an exploding mob) caused the hit.
2026-10-26 13:43:51 +01:00
Ludovic Marie 612d5f33ea Merge pull request #2031 from Domanffe/perf/debug-line-width-once
🤖 Agent: contrib-gate-17 verified this head (26.3 sources, integration tree build, clippy, tests, parity; details in the PR comment). Thanks @Domanffe!
2026-10-26 13:43:34 +01:00
Ludovic Marie f6b3e9935d Merge pull request #2011 from Domanffe/perf/vehicle-set-once
🤖 Agent: contrib-gate-17 verified this head (26.3 sources, integration tree build, clippy, tests, parity; details in the PR comment). Thanks @Domanffe!
2026-10-26 13:42:53 +01:00
Ludovic Marie 749a8a2a64 Merge pull request #1986 from some-du6e/fix/prepare-atlauncher
🤖 Agent: contrib-gate-17 verified this head (26.3 sources, integration tree build, clippy, tests, parity; details in the PR comment). Thanks @some-du6e!
2026-10-26 13:42:23 +01:00
Ludovic Marie 175419c6e7 Merge pull request #2139 from ludovic111/fix/registry-dockerfile-shader-api
🤖 Agent: the registry image copies horizon-shader-api (every Railway build failed without it); the same context builds locally with --locked.
2026-10-26 13:42:01 +01:00
Ludovic Marie aea0df9770 Merge pull request #2122 from ludovic111/feat/native-material-stage
🤖 Agent: verified: pr_ready --quick green at this head, 33/33 affected harnesses pass locally, CI verify green, A/B shows no frame cost without a pack. A Horizon shader pack's material stage now colours terrain in the game.
2026-10-26 13:38:03 +01:00
Ludovic MarieandClaude Opus 5.5 145c4c877b Quiet commands: the server runs a mod's batch of commands with no chat
A Horizon client sends a client mod's batch of command lines as one
`horizon:quiet_commands` payload, which Horizon's server announces only to
Horizon clients. Each line runs exactly as the player typing it would
(the same checks, their own permission level), with vanilla's suppressed
output: no feedback and no failure in anyone's chat. Batches wait in a
queue: at most 64 lines run per player per tick, and at most 4096 wait.
Each finished batch is one line in the server's log.

tools/quiet_commands_e2e.py checks it on the dedicated server: a non-op's
quiet setblock is refused, an op's runs, and nothing reaches the chat.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 13:37:04 +01:00
Ludovic MarieandClaude Opus 5.5 0a116d9523 Split horizon-command and horizon-client-core out of horizon-client
A pure move, the first slice of splitting the client lib into crates:

- horizon-command: the command/ module (Brigadier port, vanilla tree, argument types, selectors), as is.
- horizon-client-core: what command needs from the lib, with no other dependency inside it: text, lang, snbt,
  nbt_path, component, scoreboard, bossbar, gamerules, block_entity_nbt, fastutil_order; plus items moved out of
  lib modules that sit in the cycle, unchanged: ParticleType (particles.rs), the key display name (keybinds.rs),
  SOUND_SOURCES (settings.rs), uuid_text/uuid_from_text (save.rs), Registries/RegistryIds and parse_identifier
  (command/args.rs).

horizon-client re-exports every module and item under its old path (`horizon_client::text`,
`crate::command::Registries`, `particles::ParticleType`...), so no caller changes. Inside the moved files only
paths changed (`crate::snbt` -> `horizon_client_core::snbt` in command, `crate::command::args::parse_identifier`
-> `crate::identifier::parse_identifier`...), `sign_side` became pub, lang.rs includes the lang files from
horizon-client's assets, and gamerules' test that saves a Level moved to save.rs (it needs the client's save).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 13:36:05 +01:00
Ludovic MarieandClaude Opus 5.5 4b3af580d9 mob_ai: drop the Peaceful type scenarios that diverge for other reasons
A slime made without finalizeSpawn keeps 20 health where the probe's has 1,
the probe doesn't report a piglin's brain, and a NoAI bee's flower cooldown
runs one tick apart; none of that is the Peaceful despawn rule.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 13:34:52 +01:00
Ludovic MarieandClaude Opus 5.5 ebf3c1567c parity: follow code into new crates when choosing harnesses
Two changes so `parity.py --changed` keeps picking the right harnesses once code moves into a crate of its own:

- A harness recorded before the move names the old paths. A changed Rust file of a crate the recorded crates
  link (their path dependencies, transitively) but don't name now counts, and the reach search decides.
  Before, such a file matched no record and selected nothing.
- A library's root opens once reached code names the crate (`horizon_world::`, `pub use horizon_command as
  command`), instead of always. Every crate root was open, so any item of a new crate whose name matched a
  name used anywhere counted. On main the selection is the same for 15 of 16 sampled files; for
  horizon-world/src/nbt.rs one harness that reached it only through a name match drops out.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 13:34:31 +01:00
Ludovic MarieandClaude Opus 5.5 a34cad379f mod registry image: copy horizon-shader-api, which horizon-mods now depends on
Every Railway build of the registry has failed since horizon-mods gained the optional horizon-shader-api
dependency ("no matching package named horizon-shader-api"). Built the same context locally: OK.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 13:32:27 +01:00
Ludovic Marie a27579dd72 Merge remote-tracking branch 'origin/main' into feat/server-mods-game
# Conflicts:
#	crates/horizon-client/src/game/mods.rs
2026-10-26 13:29:17 +01:00
Kacpherek 23d089aa7c Match vanilla animal breeding rules: rabbits breed, damage ends love, horse and wolf checks, breeding XP rule
Rabbits get BreedGoal and TemptGoal with rabbit food. Damage to an animal
resets love. Horses, donkeys and llamas need canParent (not ridden, tame,
adult, full health) for both parents, a sitting wolf is no partner, and the
breeding experience orb needs the mob_drops game rule.
2026-10-26 13:25:30 +01:00
Kacpherek 6c7a90cd7e Let chickens lay eggs and players get snow and chorus flowers from loot tables
Gift loot (chicken eggs, sniffer seeds) is now rolled with the mob as the
loot context's this entity, and chicken variants are known to loot
predicates, so each chicken lays its own egg colour. A block a player
breaks now has the player as this entity, so snow layers drop snowballs
and chorus flowers drop themselves.
2026-10-26 13:21:59 +01:00
Kacpherek a8205dd80d Infested blocks release their silverfish when broken 2026-10-26 13:21:45 +01:00
Kacpherek f252941ac1 Charge melee exhaustion only when the hit hurts 2026-10-26 13:20:39 +01:00
Kacpherek 30fa41ddbd Make the fall, drowning, fire and freeze damage game rules work 2026-10-26 13:20:37 +01:00
Kacpherek e2781f7939 Apply suspicious stew effects when the stew is eaten 2026-10-26 13:20:35 +01:00
Ludovic Marie 23c748b2f3 Merge pull request #2127 from ludovic111/fix/ops-feedback-clock
🤖 Agent: server_ops_feedback on the server clock (stall-injector proof by ci-flakes-1); CI parity runs the harness.
2026-10-26 13:17:23 +01:00
Ludovic MarieandClaude Opus 5.5 d338bcd070 Decorated pot: say why only the outer faces stay
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 13:16:31 +01:00
Ludovic MarieandClaude Opus 5.5 ddbf2ce211 Trading probe: compare generated offers only
The probe's output is what the harness compares with vanilla; the codec
round trip lives in the unit tests, and its unwrap on a cost whose
components name an unmapped registry entry could abort the probe.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 13:16:22 +01:00
Ludovic MarieandClaude Opus 5.5 e0fea857ce Remote dragon fireball: apply inertia before the move
AbstractHurtingProjectile.tick calls applyInertia first and then moves by
the new delta movement; Vec3.normalize leaves a near-zero vector at zero.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 13:15:16 +01:00
Kacpherek 8697f3fd3f Furnaces: drying a wet sponge fills the bucket in the fuel slot 2026-10-26 13:13:32 +01:00
Ludovic Marie 5a9e07301b Merge pull request #2113 from ludovic111/feat/mod-raycast
🤖 Agent: game.raycast, mod API level 2. CI is green on c23b4e8e4. In game, in-frame and on the server thread, the ui guest's /raycast passes all four checks: glass at 2 blocks, nothing at 1, gold after /setblock, the sky is no hit. The ported Orbital Laser aims with it and fires in both modes. Queued.
2026-10-26 13:13:29 +01:00
Ludovic MarieandClaude Opus 5.5 e2e591858e Fire smoke: a waterlogged neighbour doesn't burn
FireBlock.getIgniteOdds returns 0 for a waterlogged state, so canBurn is
false there and the smoke takes the other branch.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 13:11:49 +01:00
Kacpherek 6bc8a8b889 Send sweep, damage indicator and fishing particles to a server's players
Vanilla sends these with ServerLevel.sendParticles, so every player
nearby sees them. Horizon drew them only in the host window's own
particle engine, which a window on a server (and a guest next to the
host) never sees.

One helper now sends a ClientboundLevelParticlesPacket with the same
count, offset and speed to each player within 32 blocks. While the host
holds the players (a guest's turn, a host hit on a guest) the packet
waits and goes out when the tick ends.
2026-10-26 13:09:45 +01:00
Ludovic MarieandClaude Opus 5.5 6a7951e150 mob_ai oracle: create a Peaceful scenario's hostile mobs past canSpawn
EntityType.create returns null for a type that isn't allowedInPeaceful once
the difficulty is Peaceful, so the peaceful_* scenarios threw in the oracle.
Their mobs stand for ones already in the world when the difficulty changed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 13:09:28 +01:00
Ludovic MarieandClaude Opus 5.5 e944bd2a0e Merge main (#2114 merged)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 13:09:24 +01:00
Alexander Krylov 51425c9c22 Merge remote-tracking branch 'origin/main' into docs/roadmap-help-wanted-links 2026-10-26 15:07:19 +03:00
Alexander Krylov dfefb6d46a Merge remote-tracking branch 'origin/main' into perf/tooltip-component-compare 2026-10-26 15:07:16 +03:00
Alexander Krylov 02f147f137 Merge remote-tracking branch 'origin/main' into perf/uniform-values-reuse 2026-10-26 15:07:14 +03:00
Alexander Krylov 54e4274b87 Merge remote-tracking branch 'origin/main' into perf/debug-line-width-once 2026-10-26 15:07:11 +03:00
Alexander Krylov c2d137ed06 Merge remote-tracking branch 'origin/main' into perf/subtitle-width-once 2026-10-26 15:07:08 +03:00
Alexander Krylov 81e4272126 Merge remote-tracking branch 'origin/main' into perf/sign-first-line 2026-10-26 15:07:05 +03:00
Alexander Krylov 7e047c409c Merge remote-tracking branch 'origin/main' into perf/poses-layer-reuse 2026-10-26 15:07:01 +03:00
Alexander Krylov d3a1ce1d7a Merge remote-tracking branch 'origin/main' into perf/vehicle-set-once 2026-10-26 15:06:56 +03:00
Alexander Krylov 11eb74b0b6 Merge remote-tracking branch 'origin/main' into perf/skip-lookup-once 2026-10-26 15:06:45 +03:00
Alexander Krylov 68f23b9916 Merge remote-tracking branch 'origin/main' into perf/no-double-cracks-and-ticker-copy 2026-10-26 15:06:38 +03:00
Kacpherek 4825e537cc Tilt a server player's camera towards the hit
The server never sent ClientboundHurtAnimationPacket, and the client
ignored the yaw it carried, so a player on a server got the flash but the
camera never tilted towards where the hit came from.

ServerPlayer.indicateDamage now runs where the hit's knockback is dealt
(and with no direction for hits that have none), the packet goes out when
the guest's turn ends, and the client keeps the yaw as Player.hurtDir for
GameRenderer.bobHurt.
2026-10-26 13:05:51 +01:00
Kacpherek 2e91bd84e9 Send villager, taming, love, spawn poof and snowball entity events to server clients
Single player on the server thread drew these particles only into the server's own engine. The server now sends the entity event byte where vanilla does (20, 12/13/14, 6/7, 18 and 3), and the client draws it in handleEntityEvent's way.
2026-10-26 13:05:47 +01:00
Kacpherek 5c0f7142f3 Draw the level events the client dropped: lava and torch smoke, drips, chorus, sniffer eggs, dragon fireball burst 2026-10-26 13:03:44 +01:00
Kacpherek 5d72fdd8c2 Add ambient effects for levers, repeaters, dripstone, wither roses, End gateways and lightning rods
These blocks have an animateTick in vanilla but nothing happened around them in Horizon. Each gets the same particles and odds as vanilla.
2026-10-26 13:02:23 +01:00
Ludovic Marie 43929f3ff8 Merge pull request #2090 from ludovic111/perf/teleport-drop-off-frame
🤖 Agent: pr_ready passed (79 harnesses, 1299 client tests), lan_e2e passed in-frame and on the server thread (portals, levels, distance). Mass-drop frame at RD 32: 26-40 → 7-8.5 ms. Queued for merge.
2026-10-26 13:02:03 +01:00
Kacpherek 3287a74d64 Send fence knots to server players so leads tied to fences are drawn 2026-10-26 13:00:40 +01:00
Ludovic Marie f362f15212 Merge pull request #2114 from ludovic111/fix/harness-await-flakes
🤖 Agent: harness-only change (skip-release). The old harnesses fail under server stalls, the new ones pass; 20 affected harnesses pass locally, and the two changed ones rerun after merging main.
2026-10-26 12:53:58 +01:00
Kacpherek 946f47b3ec Keep a happy ghast solid and still under a rider who gets off it
On the server thread the window never learned the ghast was on its still
timeout, and its copy trailed the server's, so a rider dropped through the
ghast and the ghast flew off. Sync STAYS_STILL, assume the timeout on
dismount, and put the window's ghast under the landing spot.
2026-10-26 12:48:36 +01:00
Ludovic Marie 68fa7714bb Merge remote-tracking branch 'origin/main' into feat/native-material-stage
# Conflicts:
#	docs/shaders.md
2026-10-26 12:48:15 +01:00
Ludovic MarieandClaude Sonnet 5.5 f0649da024 parity: server_ops_feedback ends steps on the server's clock, not a 2 s sleep
Built on the Client of the harness-await branch (#2114). Under 2.5 s server stalls the old harness fails (a command's
feedback filed under the next step) and the new one passes 3 of 3.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-26 12:46:11 +01:00
Ludovic Marie 4fbb0cde70 Merge pull request #1837 from ludovic111/feat/server-mod-test-scripts
🤖 Agent: The test scripts for server mods. CI is green on d64bd7c52, and the PR is now based on main after #1833 merged. I ran pr_ready --quick on this head merged with main (b8027d4b1): fmt, clippy, the horizon-mods and welcome tests (48) and the launcher tests (132) all pass. The only affected harness is lan_integration, which runs in CI. Queued.
2026-10-26 12:43:44 +01:00
Ludovic MarieandClaude Opus 5.5 fcbadbaa30 Merge main into rdbt/fix-1478-firework-bursts-20261026
fireworks.rs keeps main's rocket blast damage next to the client burst;
lan_e2e.py takes main's CHECKS and server-thread default plus 'fireworks'.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 12:37:15 +01:00
Kacpherek 09078949d7 Send a guest's explosion to the players once their turn ends
An end crystal (or minecart) hit by a player on the server thread blew up
with sound and damage but no explosion particles. The blast runs inside the
guest's turn, while the host holds the players, so lan_explode had no hub to
send the explosion packet through and dropped it. Queue it like a wind burst
and send it when the turn is over.
2026-10-26 12:36:29 +01:00
kohad1ck b279758c7c Localization: server screens and the new world name follow the language
The Add/Edit Server, Direct Connection and Friends screens and Create New\nWorld built their titles, field labels, defaults and buttons from English\nliterals, so every non-English language showed English there. Route them\nthrough the current language with vanilla's keys:\n\n- ManageServerScreen: manageServer.add.title/edit.title, enterName, enterIp,\n  selectServer.defaultName, gui.done and gui.cancel.\n- DirectJoinServerScreen: manageServer.enterIp (the addServer.enterIp key is\n  not in 26.3), selectServer.select and gui.cancel.\n- WorldCreationUiState: selectWorld.newWorld.\n- Select World: selectWorld.title and selectWorld.versionUnknown.\n- DisconnectedScreen: disconnect.lost and gui.toMenu.\n\nA language missing a key keeps the English fallback, so en_us is unchanged.\n\nCo-Authored-By: ZCode Agent <[email protected]>
2026-10-26 12:35:25 +01:00
kohad1ck 5ec21ff5f1 Font: menus keep their run-time glyphs when the renderer rebuilds its atlas
Menu text drawn with characters the bitmap sheets lack (Chinese, Japanese,
Korean) is rasterized into a reserve of the shared atlas at run time, and the
font caches the atlas rect it got. The loading screen hands over to the game
about two seconds after the title shows by building a new renderer, whose
atlas texture is made from HudAtlas.pixels alone: those never hold a
run-time glyph, while the cache still answers with the old rects, so every
such character already drawn went blank. Latin text, straight from the
sheets, stayed. build_renderer and loading_renderer now forget the cache
before the new texture exists, so the characters that are drawn again are
placed into it, as ui_theme clears its own cache when the atlas changes.

Fixes #1824, fixes #2040.
2026-10-26 12:35:21 +01:00
Ludovic MarieandClaude Opus 5.5 db5c10ba20 Merchant offers: rustfmt
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 12:35:09 +01:00
Kacpherek 265965445d Save the world when the game gets SIGTERM, Ctrl+C or SIGHUP, and disconnect on native quit 2026-10-26 12:34:59 +01:00
Ludovic MarieandClaude Opus 5.5 f4b4f944e3 Capture test: the fixture's Game resets its inventory clicks too
capture(true) now calls reset_inventory_clicks; the fixture compiles the
method body against a stub Game.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 12:33:08 +01:00
Ludovic Marie 6ff335eacf Merge pull request #2112 from ludovic111/feat/native-material-plumbing
🤖 Agent: verified: pr_ready --quick green, 30/30 affected harnesses pass locally, CI verify green. Internal: one WGSL prefixer for the shader-pack stages (no visible change).
2026-10-26 12:32:07 +01:00
Kacpherek f5f7448909 Lingering potions leave an effect cloud
A thrown lingering potion used to act as one weaker splash. It now leaves an area effect cloud like vanilla: radius 3, a 10 tick wait, 600 ticks, shrinking as it goes and by 0.5 each time it affects someone. Every 5 ticks it affects the mobs and players inside, with a 20 tick delay per entity, at a quarter of the duration (instant effects at half strength). Server players see it as an area_effect_cloud, like the dragon's breath clouds.
2026-10-26 12:31:40 +01:00
c23b4e8e41 Mod API: game.raycast, the block the player looks at (level 2)
`game.raycast(distance)` returns the first block along the player's view
(fluids pass), up to 1024 blocks: through the loaded chunks as the
crosshair's ray goes, then across the distant terrain's ground heights
(`Lod::ground`, from Stink-O's fork). The game casts it only while a running
mod imports the function. It is API level 2, so older Horizons refuse a mod
that calls it with "update Horizon".

Co-authored-by: Stink-O <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 12:27:06 +01:00
Ludovic Marie b8027d4b19 Merge pull request #1765 from ludovic111/feat/lithostitched-processors
Lithostitched's processors as the game's own types; replace the old Lithostitched conversion
2026-10-26 12:24:09 +01:00
Ludovic Marie 4d98e545eb Merge pull request #2098 from ludovic111/feat/launcher-mod-api-level
🤖 Agent: The launcher half of the mod API levels. CI is green on c8464c6bf, and the PR is now based on main after #2080 merged. Launcher tests pass, the new browser test included. The level mark is read from a real fast build of the game. Queued.
2026-10-26 12:22:55 +01:00
zhykowoandWorkBuddy AI a9453b7c93 Modpack downloads honour NO_PROXY too
The jars an import fetches went through the proxy whatever the host, so a host
that only answers directly failed there while the launcher's own requests to it
went direct. net::agent now takes the URL and applies the same NO_PROXY and
loopback rules the launcher's own traffic gets.

Co-Authored-By: WorkBuddy AI (DeepSeek-V4.1-Flash) <[email protected]>
2026-10-26 19:21:09 +08:00
Kacpherek 9e3956282a Send a deflected fireball's new direction to the player at once
A ghast fireball or wind charge hit back by a player was turned on the server, but the player kept seeing it fly on until its next update ten ticks later. Mark the turn to be sent right away, as vanilla does with needsSync, and reset the acceleration power to 0.1 as an attack deflection does.
2026-10-26 12:18:36 +01:00
Eshayat Al-WasiuandCodex b0d378fe99 Apply multiplayer motion packets to mobs for squid animation
Co-Authored-By: Codex <[email protected]>
2026-10-26 17:16:07 +06:00
Ludovic MarieandClaude Opus 5.5 fe19efb917 Predicted break: test BaseFireBlock by its class, as the server does
The world side checks Class::BaseFire; the tag is a data-pack list that can
differ from the class.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 12:14:59 +01:00
Kacpherek f5ef003bad Server players see the dragon's fireballs in flight
Dragon fireballs were a server-side list like the breath clouds, so a server's players never saw them. Each is now tracked as a minecraft:dragon_fireball; the client keeps it in the same list, flies it on as AbstractHurtingProjectile.tick does, snaps it to the server's moves and draws it with its dragon_breath trail.
2026-10-26 12:09:05 +01:00
Ludovic Marie e34cb7c021 Merge pull request #2080 from ludovic111/feat/mod-api-level
🤖 Agent: Mod API levels. CI is green on b12c89650. Locally: horizon-mods lib 31, api_reference 4 and sandbox 26 pass. A dummy component that calls a function from a newer level is refused with "update Horizon". Level 1 is render.set-minimap and the players.* functions; level 0 matches 0.2.67 to 0.2.69. Queued.
2026-10-26 12:06:13 +01:00
Kacpherek 9ef7a6fa25 Server players see the dragon's breath clouds
The breath clouds are kept as a list, not entities, so the server thread drew their particles into its own copy and players never saw them. Each cloud is now tracked as an area_effect_cloud (radius, waiting, particle), and a server's client draws it as AreaEffectCloud.clientTick does.
2026-10-26 12:05:16 +01:00
Ludovic Marie 5d04a5bee8 Merge remote-tracking branch 'origin/main' into fix/harness-await-flakes 2026-10-26 12:04:32 +01:00
Ludovic Marie b66861c721 Merge pull request #2106 from ludovic111/ci/nightly-schedule-guard
🤖 Agent: nightly schedule guard by Paris offset, lan_integration timeout; YAML checked.
2026-10-26 11:59:55 +01:00
Kacpherek 858eeba1b8 Resource packs: hand the window surface to the reloaded renderer instead of creating a new one
Applying packs dropped the window surface and created a new one for the renderer built on the same device. On Wayland with Mesa (RADV) the new surface came back lost (ERROR_SURFACE_LOST_KHR), the new renderer failed, and restoring the old one failed too, which crashed the game.
2026-10-26 11:59:33 +01:00
Ludovic Marie 27f63ce50c Merge pull request #1975 from kacpherek/fix/1523-tp-during-arrival
🤖 Agent: Merged after the contrib-gate-16 review: checked against 26.3, the in-game checks and lan_e2e in both modes, and the 79 affected harnesses on the integration tree (see the gate comment). Thanks!
2026-10-26 11:55:20 +01:00
Ludovic Marie 8682fb845d Merge pull request #1971 from kacpherek/fix/1477-firework-player-damage
🤖 Agent: Merged after the contrib-gate-16 review: checked against 26.3, the in-game checks and lan_e2e in both modes, and the 79 affected harnesses on the integration tree (see the gate comment). Thanks!
2026-10-26 11:52:34 +01:00
Ludovic MarieandClaude Opus 5.5 189b2c6b3c nightly: pick the 03:07 Paris schedule by offset, not by the clock at start; room for lan_integration
GitHub started both 10-26 schedules over an hour late, so the hour check skipped both and no audit ran.
lan_integration takes ~2,400 s under load, past the 1,800 s per-harness limit.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 11:52:09 +01:00
Ludovic Marie c048292f3f Merge pull request #1909 from rdbtCVS/t3/server-thread-integration-clean
🤖 Agent: gate verified on e3e08911b (main merged). fmt, clippy -D warnings and horizon-client tests pass. lan_e2e with the server-thread host: 131/133, and cursor/falling pass on rerun in both modes. 14 in-frame checks pass. agent_control_e2e: only manage fails, and it fails the same way on main. 82 of the 83 affected harnesses pass. lan_integration flakes under load and is now named-only (nightly). Queued.
2026-10-26 11:47:59 +01:00
Ludovic MarieandClaude Opus 5.5 ee83d8f247 Chat test: set the abilities through set_abilities (clippy)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 11:47:27 +01:00
Ludovic Marie c8464c6bff Merge branch 'feat/mod-api-level' into feat/launcher-mod-api-level 2026-10-26 11:46:43 +01:00
Ludovic MarieandClaude Opus 5.5 b12c89650e Mod API levels: server mods' players functions are level 1; the registry's lockfile
Main gained players.online, tell and broadcast (#1833) after 0.2.69, so
they take level 1 with render.set-minimap. horizon-mod-registry's own
Cargo.lock picks up wasmparser.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 11:46:27 +01:00
rdbtCVSandOpenAI Codex GPT-6.1 Sol f98528bbfc Fix evoker fangs rendering on server clients
Track fang entities and deliver their start event to the existing client renderer. Exercise warmup, animation, authoritative positions and removal in the LAN harness.

Co-Authored-By: OpenAI Codex GPT-6.1 Sol <[email protected]>
2026-10-26 10:46:18 +00:00
Kacpherek 990786901a Let empty boats and minecarts enter Nether portals 2026-10-26 11:46:16 +01:00
Kacpherek 1f51e3d18c Server players: chest minecart and chest boat menus stay open
A vehicle's items are kept under a placeholder key far outside the world, and the guest container tick checked the player's reach to that key as a block, so the menu closed the tick it opened. Vanilla checks reach to the vehicle (ContainerEntity.isChestVehicleStillValid).
2026-10-26 11:46:10 +01:00
Kacpherek 49b167ce22 Item frames: shade the frame model by face and light glow frames from 5 2026-10-26 11:45:56 +01:00
Ludovic Marie 0a9a7c0d06 Merge remote-tracking branch 'origin/main' into feat/mod-api-level 2026-10-26 11:45:15 +01:00
Ludovic Marie d104a350cd Merge pull request #1980 from kacpherek/fix/place-sound-refused
🤖 Agent: Merged after the contrib-gate-16 review: checked against 26.3, the in-game checks and lan_e2e in both modes, and the 79 affected harnesses on the integration tree (see the gate comment). Thanks!
2026-10-26 11:45:09 +01:00
Kacpherek 408a75aeb3 Decorated pot: draw one face of its top and bottom planes 2026-10-26 11:45:03 +01:00
Kacpherek 1e6884c591 Show the poem and credits on a player's first End exit in server-thread mode 2026-10-26 11:43:37 +01:00
Ludovic MarieandClaude Opus 5.5 cdbe1edcda Text displays: keep the background at vanilla's -1..width, -1..height
TextDisplayRenderer.submitInner calls submitTextBackground(-1, -1, width,
height), and Font.prepareBackground's effect glyph spans x0..x1, y0..y1, so the
fill is width + 1 by height + 1 from (-1, -1), as before.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 11:42:48 +01:00
Ludovic MarieandClaude Opus 5.5 2bfa7f57ba Merge main into fix/world-text-parity
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 11:42:48 +01:00
Ludovic Marie 6cd4238930 Merge pull request #1979 from kacpherek/fix/1899-swim-ceiling
🤖 Agent: Merged after the contrib-gate-16 review: checked against 26.3, the in-game checks and lan_e2e in both modes, and the 79 affected harnesses on the integration tree (see the gate comment). Thanks!
2026-10-26 11:42:32 +01:00
Kacpherek 76e2c84103 Recipe book: a click on a category tab reaches the book before the outside test 2026-10-26 11:41:21 +01:00
Ludovic MarieandClaude Opus 5.5 7abae79d41 Merge main into fix/prepare-atlauncher
sounds.rs: main's Assets::load reads the launcher's files through game_files.
The rewritten-manifest test takes main's message; main's new test passes
GameFiles. ATLauncher is also looked for where the Horizon launcher looks
(~/Library/Application Support/ATLauncher, the Flatpak's data/atlauncher).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 11:41:03 +01:00
Ludovic Marie 3c1ab736d5 Merge pull request #1978 from kacpherek/fix/1881-death-camera-tilt
🤖 Agent: Merged after the contrib-gate-16 review: checked against 26.3, the in-game checks and lan_e2e in both modes, and the 79 affected harnesses on the integration tree (see the gate comment). Thanks!
2026-10-26 11:39:58 +01:00
socksproxandDevin 0a2de159fd Fire: ambient smoke particles and no mob-break crumbs
BaseFireBlock.animateTick adds large smoke above a fire (three puffs in
its top half), or two puffs at each burnable neighbour's face when the
fire sits on nothing burnable or sturdy. SoulFireBlock.canBurn is
always true, so soul fire always takes the top-half branch. The
burnable table mirrors FireBlock.canBurn (fire.igniteOdds above zero)
from block-behaviours.json.

Mobs breaking blocks (MobEvent::BreakBlock) map to Level.destroyBlock,
whose Block.spawnDestroyParticles BaseFireBlock overrides to nothing:
a wither or another mob destroying fire no longer emits break
particles. Player breaks already route through player_destroy (1009);
the client-side predict_break particles are PR #2082's.

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-26 11:39:08 +01:00
Ludovic MarieandClaude Opus 5.5 f19390bae1 Sonic boom: take the LAN host in the same let-else as the particle
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 11:39:07 +01:00
Ludovic MarieandClaude Opus 5.5 78bfe74465 Container clicks: the stuck-drag test joins the module's tests
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 11:38:39 +01:00
rdbtCVSandOpenAI Codex GPT-6.1 Sol d25d0f025b Merchant offers: preserve required potion components
Co-Authored-By: OpenAI Codex GPT-6.1 Sol <[email protected]>
2026-10-26 10:38:26 +00:00
Ludovic MarieandClaude Opus 5.5 fb74b6038f Chat: system and player lines share the window's chat code
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 11:38:17 +01:00
Ludovic MarieandClaude Opus 5.5 bde76794ee Mobs: no rain or thunder outside the overworld
Level.canHaveWeather is false in the Nether and the End, so their rain and
thunder levels stay at 0. Set that once in the mob world instead of checking
the End at two of the rain sites: bees, dolphins, wolves and happy ghasts read
the same flag.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 11:37:43 +01:00
Ludovic Marie a31a435525 Merge pull request #1976 from kacpherek/fix/1526-gateway-cooldown
🤖 Agent: Merged after the contrib-gate-16 review: checked against 26.3, the in-game checks and lan_e2e in both modes, and the 79 affected harnesses on the integration tree (see the gate comment). Thanks!
2026-10-26 11:37:21 +01:00
Ludovic MarieandClaude Opus 5.5 2918f31811 Poses::reset clears the scratch pose too
A missing part's writes land in `dummy`; a fresh Poses started it at zero,
so a reused one must too.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 11:36:50 +01:00
rdbtCVSandOpenAI Codex GPT-6.1 Sol 52a0a5a89c Fix firework bursts on server clients
Send the rocket explosion event before tracked removal and render it with the synchronized firework item. Cover colored bursts and later trail frames in both hosting paths.

Co-Authored-By: OpenAI Codex GPT-6.1 Sol <[email protected]>
2026-10-26 10:35:33 +00:00
Ludovic MarieandClaude Opus 5.5 c813261574 Horizon shader packs: the material stage runs in the game
A pack's `fn material(input: MaterialInput) -> Surface` now gives the lit
colour of every terrain surface: sections and distant tiles, solid,
cutout and translucent, in both the instanced and the flat draws. Terrain
keeps Horizon's meshes, culling and draw lists; only its pipelines change.
Their shader is the pack's stage, a prefixed copy of terrain.wgsl's
terrain section, and entries that hand the pack the texture, the vertex
tint, the light levels and the lightmap's colour apart, then apply the
alpha test and fog as `fs_solid` and `fs_translucent` do.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 11:35:15 +01:00
Ludovic Marie 7650451b4f Merge pull request #1974 from kacpherek/fix/1500-vehicle-storage
🤖 Agent: Merged after the contrib-gate-16 review: checked against 26.3, the in-game checks and lan_e2e in both modes, and the 79 affected harnesses on the integration tree (see the gate comment). Thanks!
2026-10-26 11:34:26 +01:00
Ludovic MarieandClaude Opus 5.5 3c22ff0849 Launcher: don't install a mod version the game can't run
The game program carries its mod API level in a mark
(horizon-mod-api-level:NNNN;), which the launcher reads once per
installed version; a program from before the mark (0.2.68 and older)
serves level 0. The Horizon browser marks versions above the level of the
game the player runs (the selected version, else the newest installed)
"needs a newer Horizon than the game you play", and installs the newest
version that game can run, or says to update it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 11:33:30 +01:00
Ludovic Marie c0171fa6bc Merge pull request #1973 from kacpherek/fix/1497-machine-menu-changes
🤖 Agent: Merged after the contrib-gate-16 review: checked against 26.3, the in-game checks and lan_e2e in both modes, and the 79 affected harnesses on the integration tree (see the gate comment). Thanks!
2026-10-26 11:31:24 +01:00
Ludovic Marie 707348fcd5 Merge pull request #1972 from kacpherek/fix/1493-shulker-guest-homing
🤖 Agent: Merged after the contrib-gate-16 review: checked against 26.3, the in-game checks and lan_e2e in both modes, and the 79 affected harnesses on the integration tree (see the gate comment). Thanks!
2026-10-26 11:28:33 +01:00
Kacpherek 5389f72b9a Creative: work out the shown items again after a resource or language reload
A resource pack can rename items without changing the Search label the cache was keyed on, so a search kept its old results until the query or tab changed.
2026-10-26 11:24:41 +01:00
Ludovic Marie 8a458496c7 Merge pull request #1984 from rdbtCVS/fix/1956-item-fluid-current
🤖 Agent: Merged after the contrib-gate-16 review: checked against 26.3, the in-game checks and lan_e2e in both modes, and the 79 affected harnesses on the integration tree (see the gate comment). Thanks!
2026-10-26 11:23:24 +01:00
Ludovic Marie 3144276935 Merge pull request #1970 from kacpherek/fix/1328-respawn-after-trip-death
🤖 Agent: Merged after the contrib-gate-16 review: checked against 26.3, the in-game checks and lan_e2e in both modes, and the 79 affected harnesses on the integration tree (see the gate comment). Thanks!
2026-10-26 11:22:15 +01:00
rdbtCVSandOpenAI Codex GPT-6.1 Sol 8c6ec16c99 Fix guardian charging beams on server clients
Sync guardian moving and active attack-target metadata, resolve the actual targeted player, and tick the client's visual charge clock. Cover charge completion and target loss for guardians and elders. Fixes #1491.

Co-Authored-By: OpenAI Codex GPT-6.1 Sol <[email protected]>
2026-10-26 10:21:50 +00:00
socksproxandDevin 6b8fde092f Keep 26.3's name-tag and text-display values
Review caught three regressions against the pinned client jar: submitInner
lines stay lineHeight+1 (10 px), submitNameTag fades discrete and
see-through names with the same 0x7F, and lightCoordsWithEmission floors
both light channels at 2.

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-26 11:20:39 +01:00
levzzzandHermes Agent 7a2de4e328 Render bundle contents in station inventory tooltips
Keep hovered item stacks through draw_station and use draw_stack_tooltip, while retaining text-only station hints. Covers anvil and brewing-stand player inventory slots and stonecutter result previews.\n\nAdd an offscreen render regression which fails on the old binary for both reported screens and passes on the fix in server-thread and in-frame modes.

Co-Authored-By: Hermes Agent (OpenAI) <[email protected]>
2026-10-26 15:18:17 +05:00
Ludovic MarieandClaude Opus 5.5 08f4fd5c8e Teleports free their dropped columns off the frame and in batches
A teleport far away (or a storage that moves by more than its size) drops
every column in one frame: 3,725 at RD 32. That frame freed every column's
sections, then removed each section from the renderer one at a time, with
a few hash lookups per section and a free-list tree update per mesh range.

- The dropped columns, and a left dimension's whole chunk map, are freed on
  a short-lived thread of their own ("Chunk freeing").
- Renderer::remove_columns removes whole columns: one lookup per column for
  the tables kept by column, and the meshes' ranges go back to the arena in
  one sorted merge per page (FreeList::give_all) instead of a tree update
  each. Per-section Renderer::remove and Occlusion::remove had no other
  callers and are gone.
- The renderer's block-entity and detail-column tables use FxHashMap.

Mac mini (M4 Pro), RD 32, explored world, server thread, 3 alternated runs,
two long teleports each: the drop took 26.1-39.7 ms, now 6.9-8.5 ms.
Entering the Nether: 9.7-13.4 ms, now 4.0 ms or under 6 ms.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 11:10:25 +01:00
Ludovic Marie 6c995bfa15 Merge remote-tracking branch 'origin/main' into feat/mod-api-level
# Conflicts:
#	crates/horizon-mods/Cargo.toml
#	crates/horizon-mods/src/index/tests.rs
#	launcher/Cargo.lock
2026-10-26 11:09:19 +01:00
Kacpherek 6b60056637 Thrown projectiles: fly on the client between server updates, eye shatter particles, potion motion order
On a server, arrows, snowballs, eggs, ender pearls, potions and eyes of ender only eased towards the tracker's position, which comes every 10 to 20 ticks, so they moved in jerks. Like vanilla (Entity's NO_OP InterpolationHandler) the server's position now replaces theirs at once and they fly on by themselves each tick.

A shattered eye of ender now sends level event 2003 with its item crumbs and portal rings.

Thrown potions applied gravity and drag after the move; ThrowableProjectile applies them before.
2026-10-26 11:07:37 +01:00
Ludovic Marie 88b7bfafc0 Merge pull request #1833 from ludovic111/feat/server-mods-0.3
🤖 Agent: merged main (12 files conflicted with ui, render and input). The runtime now keeps the server/client Hooks split beside the ui-events export, and the Snapshot carries both the GUI fields and the server's players. Locally: the horizon-mods sandbox and unit tests, cargo-horizon-mod tests, and clippy on the client with mods all pass. CI verify and parity pass at b90ab85f4. Bottom of the server-mods stack (#1837, #1894, #1895, #1901 follow).
2026-10-26 11:07:35 +01:00
Ludovic MarieandClaude Sonnet 5.5 34d8d94022 parity: server harnesses wait on the server's clock and replies, not on sleeps
server_knockback, locator_bar, server_dimension_players and their one-player users now end a step by the server's
game clock (a sync round trip, N ticks, a last sync) the way server_player_targets does, so a server that stalls
under load still gets all its ticks inside the step. server_knockback's frozen section waits for the stepped tick,
the stopped clock and the landed hit; dedicated_server waits for the player's leaving before /list.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-26 11:06:02 +01:00
Kacpherek 311c0f7392 Stop endermen teleporting nonstop in the End
The End has a fixed time and no precipitation, so endermen there must not
flee daylight or take rain damage.
2026-10-26 11:05:49 +01:00
Ludovic MarieandClaude Opus 5.5 83f0d5bf58 Worldgen data loads its three biggest parts in parallel
GeneratorData::load read everything on the server thread while the chunk
workers waited: the block tables, the structure templates and the block
behaviours now load on threads of their own, and BlockBehavior::load reads
its cached-support table beside the other two. The behaviour exports
(gameplay-facts.json is 55 MB) parse from bytes in memory instead of
through a reader.

ThinkPad (2 cores), 3 alternated runs: 0.63 -> 0.45 s with the behaviour
snapshot cached, 2.40 -> 2.07 s on a first start.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 11:04:57 +01:00
SenjinandClaude Opus 5.5 728c6a62b3 HDR: the emission mark in the vertex colour's alpha, not a layer bit
EMISSIVE_LAYER took bit 14 of TerrainVertex::layer, halving the sprite
layers terrain vertices can index (32768 to 16384) and leaving no flag
bit free. The colour's alpha byte was always 0xFF: its low four bits now
hold 15 minus the light level the face emits (EMISSION_BITS), so 0xFF
still means none and the other four bits stay free. terrain.wgsl takes
only the colour's rgb and passes the level on; Java shader packs get
alpha 1; the section mesh fixture restores vanilla's 0xFF.

The HDR pass still lifts any emitting face the same way; the level is
there for scaling the lift by it later.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 11:03:16 +01:00
SenjinandClaude Opus 5.5 9b863d6093 HDR output: scRGB swapchain, HDR metadata and emissive highlights
HDR (Graphics > Display, off by default) presents to an scRGB swapchain
where the surface offers it. The frame is drawn as in SDR, in vanilla's
gamma space; a last pass decodes it at the Paper White and lifts light
sources (blocks that emit light, lava, the sun and the moon) evenly
towards the Peak Brightness.

The mesher marks emitting faces with EMISSIVE_LAYER (bit 14 of the
layer); the frame's alpha carries the mark to the HDR pass, which lifts
only pixels that still show the world before the hand and HUD.

horizon-hdr sends HDR metadata through VK_EXT_hdr_metadata, which wgpu
doesn't: without it KWin clips scRGB content above 1.0 (80 nits).

Part of #1441.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 11:03:16 +01:00
Kacpherek c80b3b4950 Multiplayer: no break particles or sound when a player puts out fire
The predicted break played the block's break sound (wool for fire) and its destroy particles. BaseFireBlock skips both (spawnDestroyByEntityParticles does nothing); players only hear the server's extinguish sound.
2026-10-26 10:58:41 +01:00
Ludovic Marie 2a0f4e39a8 Merge pull request #2036 from ludovic111/feat/launcher-shader-packs
🤖 Agent: I looked at the launcher's Shader packs tab in dark and light (Mac mini, mktemp homes, a local registry with the sample pack): Soft Glow is listed from Horizon with Works, Page and Install, and the look matches the other tabs. pr_ready --quick is green, with the launcher's 130 tests.
2026-10-26 10:57:13 +01:00
Ludovic MarieandClaude Opus 5.5 acbcaad743 Horizon shader packs: one WGSL prefixer for copies of the renderer's shaders
The sky stage runs a copy of terrain.wgsl's sky beside the pack's code
with every name prefixed `horizon_sky_`. The prefixer moves to
`gfx/wgsl_prefix.rs` and now also renames module variables (bindings)
and overrides, so the material stage can copy the terrain section the
same way. The sky's shader is unchanged apart from where `globals` is
declared.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 10:56:39 +01:00
Kacpherek e7f41e7d13 Performance preset: keep animated textures on
The preset turned them off, which froze fire, water, lava and portals. Players reported fire with no animation (#2064). The option stays in Advanced Graphics for anyone who wants it off.
2026-10-26 10:51:26 +01:00
Ludovic MarieandClaude Opus 5.5 e3e08911ba parity: run lan_integration only when named and in the nightly audit
The harness runs the whole lan_e2e suite: ~40 minutes on a loaded runner,
and single checks flake under load (barter, riding, shulker_bullets in one
run; cursor, falling in another, all passing on rerun). Selected by
--changed it would run on most gameplay pull requests and on parity-main.
tools/parity_hosts.json gains a named_only table that every automatic
selection (--changed, --failed, the default, --flake-check) leaves out;
naming the harness or --all (the nightly audit) still runs it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 10:48:58 +01:00
Niraj Khetre 290e2e1ac7 The ender dragon's hits and breath name it in the death message
The dragon's wing and head hits and its breath clouds hurt the player
without recording the dragon as the causing entity, and the dragon is
not one of the mobs note_attacker looks up. death.attack.mob and
death.attack.indirectMagic then got only the victim's name, so the
template showed through: "%1$s was killed by %2$s using magic".

Record the dragon before its damage lands (EnderDragon.hurt and
knockBack use mobAttack(dragon); its breath clouds apply
indirectMagic(cloud, dragon)).

Fixes #2062

Assisted-by: Claude Opus 5.5
2026-10-26 15:14:37 +05:30
Ludovic Marie 87ff54e702 Merge remote-tracking branch 'origin/main' into feat/launcher-shader-packs
# Conflicts:
#	docs/shaders.md
2026-10-26 10:40:12 +01:00
Ludovic MarieandClaude Opus 5.5 8cc8c6180d Mod API levels: refuse a mod made for a newer Horizon, list each version's level
Every function the game offers mods has the API level it arrived at
(crates/horizon-mods/src/api_level.rs): level 0 is the contract of
Horizon 0.2.67 and 0.2.68, level 1 adds render.set-minimap. A mod needs
the level of the newest function it imports, read from its component.

- The game refuses a mod that calls a function it doesn't have with
  "made for a newer Horizon (it uses ...): update Horizon to run it"
  instead of wasmtime's linking error.
- The registry index lists each mod version's level (api_level, read
  from the file by CI); check_file compares it when the entry has one,
  and the moved extras' installer takes the newest version this game
  can run.
- cargo horizon-mod check prints the level.
- The api_reference test fails until a new WIT function has a level.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 10:39:47 +01:00
Ludovic Marie 852e7cd524 Merge pull request #1855 from ludovic111/feat/native-shaders-registry
🤖 Agent: verified: pr_ready --quick green (launcher and horizon-mods tests), CI verify and check green, merges cleanly with main. The registry takes Horizon shader packs, checked by the game's own shader check.
2026-10-26 10:39:24 +01:00
Ludovic Marie 28ef79ed66 Merge pull request #1863 from ludovic111/feat/native-shaders-sky
🤖 Agent: verified: pr_ready --quick green, 80/80 affected harnesses pass locally, CI verify green, A/B shows no frame cost without a pack. A Horizon shader pack's sky stage now draws in the game.
2026-10-26 10:36:58 +01:00
Ludovic Marie f52d053817 Merge pull request #2063 from ludovic111/feat/moved-minimap
🤖 Agent: verify passed. Checked against the real registry now that Minimap 1.0.0 is published: the migration e2e passes 34/34 on the server thread and 34/34 in-frame. The minimap case starts at bottom left, 128, round, 0.5 blocks per pixel, turning: the mod is installed and on, its settings carried, the map shows, and the Extras page has no minimap rows. pr_ready --quick passes (clippy, 1295 horizon-client tests).
2026-10-26 10:30:49 +01:00
levzzzand[CC] <deepseek-v4.1-flash> 941f148957 Loot: a single-level enchantment draws no level
Review of #2037 (rdbtCVS): `EnchantRandomlyFunction.enchantItem` calls
`Mth.nextInt(random, minLevel, maxLevel)`, which returns `min` without
drawing when `min >= max`. The arm here drew unconditionally, so choosing
Mending (or any one-level enchantment) shifted every roll after it.

`horizon_world::loot`'s copy and `merchant.rs`'s already guard this; only the
loot dispatcher in `items.rs` did not.

Vanilla with `LegacyRandom(11)`: a Mending book followed by
`set_count` uniform 1..64 is 46; the extra draw made it 28.
`a_single_level_enchantment_draws_no_level` pins that (and the stored
Mending level).

Co-Authored-By: [CC] <deepseek-v4.1-flash> <[email protected]>
2026-10-26 14:30:45 +05:00
Kacpherek 0f6569070d Draw a GUI item's vertices once while it stays in place
Two costs a frame were repeated for every item a screen draws. ItemModels::components cloned the item's whole default component map (a JSON map of every default component) on every resolve, though most stacks carry no patch; it now borrows the defaults and copies only to apply a patch. And gui::draw submitted, culled, sorted and clipped every quad of a static item again each frame (hundreds of edge quads for a generated item, all facing sideways in the GUI), though the result for the same layers at the same position is the same; the HUD vertices of a static item are now kept per GUI position, keyed by its baked models, tints, transforms, the atlas and the exact position, and appended as they were. Items with a glint or a special model renderer are drawn as before. With the creative inventory open the HUD's share of a frame went from 0.66 ms to 0.23 ms.
2026-10-26 10:24:07 +01:00
Kacpherek 515c0bdb7d Work out the level item models read once a frame
draw_item_model built the client level state (dimension, game time, sun angle, moon phase, respawn) for every stack it drew, sampling the sky each time: about seventy times a frame with the creative inventory open, a fifth of the time spent on its items. Within a frame the level is the same for all of them, so it is kept for the frame (keyed by the frame, game time, day time and dimension).
2026-10-26 10:24:07 +01:00
Kacpherek fb9b26a06c Work out the creative grid's stacks once per tab or search, not every frame
With the creative inventory open, every frame cloned all the tabs with their stacks (twice), and items() cloned the selected tab's stacks again on each of its three or four calls a frame; on the search tab each call ran the whole search again, building every stack's localised name. That took the frame rate from about 1,900 to 980 with the inventory open and 340 while searching (#1953). Vanilla fills ItemPickerMenu.items when the tab or the search changes (selectTab, refreshSearchResults): the grid's stacks are now kept until the tab, the search text or the language changes, and the screen draws the tabs without copying their stacks.
2026-10-26 10:24:07 +01:00
lazy-importerandClaude Opus 5.5 ad7203fc8a Model item_name component on items and tooltips
In vanilla 1.20.5+, servers and datapacks customize base item names using
the `minecraft:item_name` data component rather than `minecraft:custom_name`.
Vanilla's `ItemStack.getHoverName()` checks `custom_name` first, then
falls back to `getItemName()`, which retrieves `DataComponents.ITEM_NAME`.

Previously, Horizon did not model `item_name` in `StackData`, causing items
with custom base names from servers or resource packs to fall back to
their default translation keys instead of the server-provided name.

This adds `item_name` to `StackData`, decodes and encodes it across
network item sync, and prioritizes it in `name_line` and hover tooltips.

Fixes #2039.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 10:21:00 +01:00
Ludovic Marie 121731f261 Merge branch 'feat/server-mods-world' into feat/server-mods-commands
# Conflicts:
#	crates/horizon-client/src/lan/hub.rs
#	crates/horizon-mods/src/runtime.rs
#	docs/api/mods.md
2026-10-26 10:09:47 +01:00
JoansitohandClaude Sonnet 5.5 fa8291fc97 Treat a line break as a word boundary in text fields.
Ctrl+Backspace on a book page with the caret after `two` in `one\ntwo` removed both lines because the word helper only knew spaces.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-26 10:06:48 +01:00
Ludovic Marie 94d8130193 Merge branch 'feat/server-mods-game' into feat/server-mods-world
# Conflicts:
#	Cargo.toml
#	crates/cargo-horizon-mod/src/check.rs
#	crates/horizon-client/src/game/mods.rs
#	crates/horizon-mods/src/manifest.rs
#	crates/horizon-mods/src/runtime.rs
2026-10-26 10:04:52 +01:00
Ludovic Marie 593b99a68b Merge branch 'feat/server-mod-test-scripts' into feat/server-mods-game
# Conflicts:
#	crates/horizon-client/src/game/mods.rs
2026-10-26 10:01:50 +01:00
Ludovic Marie d64bd7c528 Merge branch 'feat/server-mods-0.3' into feat/server-mod-test-scripts
# Conflicts:
#	crates/horizon-mods/src/testing.rs
2026-10-26 09:56:41 +01:00
Ludovic Marie 45bc45b396 Merge pull request #2057 from ludovic111/perf/remote-chunks-off-render
🤖 Agent: pr_ready passed (78 harnesses), lan_e2e in-frame and server-thread passed; frame p99 4.96→3.95 ms during a RD 32 flight. Queued for merge.
2026-10-26 09:56:18 +01:00
Ludovic Marie 6f0c0c13b6 Merge pull request #2051 from ludovic111/feat/native-shader-frame
🤖 Agent: verified: pr_ready --quick green, 38/38 affected harnesses pass locally, CI verify green, A/B shows no frame cost. Internal: a Horizon shader pack's frame uniform is shared by its stages (no visible change).
2026-10-26 09:54:11 +01:00
Ludovic Marie b90ab85f44 Merge remote-tracking branch 'origin/main' into feat/server-mods-0.3
# Conflicts:
#	Cargo.toml
#	crates/cargo-horizon-mod/src/check.rs
#	crates/horizon-client/src/game/mods.rs
#	crates/horizon-mod-sdk/src/lib.rs
#	crates/horizon-mod-sdk/wit/horizon.wit
#	crates/horizon-mods/src/manifest.rs
#	crates/horizon-mods/src/runtime.rs
#	crates/horizon-mods/src/testing.rs
#	crates/horizon-mods/tests/sandbox.rs
#	docs/api/mods-reference.md
#	docs/api/mods.md
#	docs/mods.md
2026-10-26 09:51:00 +01:00
Ludovic Marie b110608a93 Merge remote-tracking branch 'origin/main' into feat/moved-minimap 2026-10-26 09:43:44 +01:00
lazy-importerandClaude Opus 5.5 c675329208 Reset boss overlay on server reconfiguration
When switching sub-servers on a proxy network (Velocity/BungeeCord), the
network thread enters configuration mode and emits Event::Reconfigured.
Vanilla's ClientPacketListener.handleConfigurationStart clears the client
level, which calls Hud.onDisconnected() and resets BossHealthOverlay.
Without this reset, bossbars from previous servers remained visible on
screen.

Fixes #2034.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 09:42:35 +01:00
Ludovic MarieandClaude Opus 5.5 382380fc8b Minimap moved to the mod browser: remove the built-in minimap
The official Minimap mod (mods/official/minimap) shows the extra's map
through render.set-minimap. The extra's switch and its five Extras rows
go; the game still draws a mod's map from the same tiles. A player who
had the minimap on gets the mod installed and turned on at the first
start, with the corner, size, shape, zoom and turning carried into its
storage, and one chat line. horizon-extras.txt keeps the old keys.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 09:38:52 +01:00
Ludovic Marie 9e447a7e92 Merge pull request #2058 from ludovic111/feat/official-minimap-mod
🤖 Agent: The official Minimap mod (mods/official/minimap): the minimap extra as a mod with its five options. Pixel-identical to the extra in the map region (0.000 to 0.002) with default and other settings, at the same frame cost. Verified: pr_ready (2179 workspace tests, clippy), the settings page in game, and the published mods loading beside it in both modes.
2026-10-26 09:34:51 +01:00
FusselTVandClaude Opus 5.5 2143c26a2e Saves: write points of interest
Horizon finds villager POIs from the blocks and kept no poi folder, so a
world it made had none. Vanilla rebuilds a missing section on load, but
until a chunk is loaded nothing knows its beds, job sites, bells, nests,
portals or lodestones.

Saving a chunk now writes its POI records the way PoiManager saves its
sections: each section with POI blocks, valid, one record per block of
PoiTypes with its max tickets, free_tickets left out at 0. A chunk that
lost its last POI and had a record gets an empty one, since vanilla
trusts a valid section.

The records are built on the region writer thread, which reads the
saved record there, so unloading chunks reads no files on the tick
thread. A straw bed is no home (PoiTypes.BEDS is the sixteen colours);
/locate poi uses the same table.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 09:33:34 +01:00
FusselTVandClaude Opus 5.5 c67a963da7 Saves: write the structure starts a chunk holds
A generated chunk keeps the starts it holds and its save writes them
into structures.starts with their current state, as vanilla saves the
StructureStart objects its chunks share. Proto chunks get their
references and starts too, so vanilla and Paper find the structures of
the chunks they finish at the edge of a generated area.

A start with a piece that cannot be saved is left out, with a warning.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 09:33:34 +01:00
FusselTVandClaude Opus 5.5 fc0b73954f x
A container whose items no longer match its roll is saved without the
table, also when it was loaded with one, so a chest emptied after a
load does not fill again on the next one. A zero LootTableSeed is left
out, as trySaveLootTable does.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 09:33:34 +01:00
FusselTVandClaude Opus 5.5 ad50996f7c Structures: save the remaining legacy pieces as vanilla does
Desert and jungle temples, swamp huts, buried treasure, ocean
monuments, mineshafts, fortresses and strongholds save the tag their
StructurePiece subclass writes, with the state placement leaves: the
height a scattered feature settled on, the buried treasure chest,
placed chests, traps and spawners, and the swamp hut's witch and cat.
With these every StructurePieceType is saved.

test_structure_start_nbt_parity gets one site of each of these kinds,
and a badlands mineshaft.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 09:33:34 +01:00
FusselTVandClaude Opus 5.5 b7b36836af Structures: save template pieces as vanilla does
Igloo, shipwreck, ocean ruin, ruined portal, nether fossil, End City
and mansion pieces save the tag TemplateStructurePiece and its
subclasses write, with the state placement leaves: the height a
shipwreck or ocean ruin settled on and the igloo's moved box. Ruined
portals save all six properties, End City pieces their genDepth tag.

test_structure_start_nbt_parity gets one site of each of these kinds.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 09:33:34 +01:00
FusselTVandClaude Opus 5.5 9a0fcda3a9 Proto chunks: save placed block entities with their id
Block entity data a structure places can lack its type: a brushable
block's is only its loot table. Finished chunks get the id from the block,
saved proto chunks did not. Vanilla and Paper skip a pending block entity
without one when they finish the chunk ("Skipping block entity with
invalid type"), so suspicious sand and gravel at the edge of a
pre-generated area lost their archaeology loot.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 09:33:34 +01:00
FusselTVandClaude Opus 5.5 df3ac51447 Trial spawners: no empty spawn_data on save
The block entity canonicaliser added spawn_data {entity: {}} to every
trial spawner that had none. Vanilla only writes spawn_data once the
spawner has picked its next mob (TrialSpawnerStateData.Packed reads it as
Optional.empty). A spawner saved while mob spawning was off, as worlds
pre-generated in peaceful or with spawner_blocks_work false are, kept the
empty entity for good: getOrCreateNextSpawnData returns it, it names no
mob, and the spawner never spawns, in Horizon and in vanilla or Paper.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 09:33:34 +01:00
FusselTVandClaude Opus 5.5 c776d6586e Structures: save jigsaw starts as StructureStart.createTag does
Horizon kept no saveable form of its structure starts, so its saves carry
an empty structures.starts and every structure it generates is invisible
to vanilla and Paper (locate, structure spawns, location predicates,
advancements) and cannot be continued past the generated area.

StructureStart::save and Piece::save write the tag vanilla writes. This
covers the pieces of jigsaw pools (villages, pillager outposts, ancient
cities, trail ruins, trial chambers, abandoned camps, bastions) with
their pool element, junctions and liquid settings. Junctions keep
delta_y and dest_proj, pool elements their processors as written.
LegacyPiece::save defaults to None; a start with such a piece is not
saved, as before.

test_structure_start_nbt_parity compares the tags with the pinned
server's own for one site of every jigsaw structure kind.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 09:33:34 +01:00
FusselTVandClaude Opus 5.5 1bbe64265c Saves: Nether and End chunks keep their own height
Chunk holds the Overworld's 24 sections in every dimension, and the save
wrote them as they are: yPos -4, sections -4 to 19 and heightmaps counted
from y=-64. Vanilla and Paper trust those heightmaps, so a Horizon-made
Nether reads its surface at y=192, above the bedrock roof, and the End 64
blocks too high. Re-saving a vanilla Nether chunk rewrote its yPos too.

LevelShape (skylight, min_y, height per dimension folder) replaces the
save path's sky_light flag. yPos, the saved sections, the PostProcessing
lists and the heightmaps follow it, as SerializableChunkData.write does.

LAN clients in the Nether and End now get the chunk's heightmaps; the
heightmap check failed there and they were sent empty.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 09:33:33 +01:00
zhykowoandWorkBuddy AI 3e79b22c43 Launcher downloads go through the proxy the player sets
Everything the launcher fetches now goes through one HTTP proxy when there is
one: Horizon's releases and the launcher's own update, Minecraft's files and
Java from Mojang, Modrinth, CurseForge, the mod registry, pictures, Microsoft
sign-in, Tailscale's API, and the jars a modpack import downloads. It is
Settings' Proxy row (`proxy` in settings.json), or `horizon-launcher settings
proxy http://host:port`; without one, HTTPS_PROXY, HTTP_PROXY and ALL_PROXY
apply, with NO_PROXY naming the hosts to reach directly.

A local address never goes through it, and the Servers page's calls to a game
server's API stay direct: they are on loopback and asked every second.

horizon-port-pack builds its own agents, so the launcher hands it the address
(net.rs); run on its own, it reads the same variables.

Co-Authored-By: WorkBuddy AI (DeepSeek-V4.1-Flash) <[email protected]>
2026-10-26 16:28:24 +08:00
Ludovic MarieandClaude Opus 5.5 4498df8be5 Merge feat/official-minimap-mod into feat/moved-minimap
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 09:24:17 +01:00
levzzz d1c1af598b Bundles show what they hold
Hovering a bundle showed only its name and `BundleItem.appendHoverText`'s
"Can hold a mixed stack of items": nothing drew the contents, so a player
could not see what was inside or how full it was (#2001).

`ClientBundleTooltip` is ported to `bundle::tooltip_layout`: the 96 pixel
column, the four-wide grid filled bottom-up and right-to-left from
`getNumberOfItemsToShow` (eleven stacks and a `+N` when it overflows), the
progress bar at `Mth.mulAndTruncate(weight, 94)` with the full sprite and the
`item.minecraft.bundle.empty`/`.full` labels, and an empty bundle's wrapped
description. The game draws it under the stack's own tooltip
(`Game::draw_stack_tooltip`), which the inventory, creative, container,
beacon and trading hovers now use.

Verified in game: a bundle with 64 stone shows one slot and a full red bar
labelled "Full!"; one with 32 stone and 8 dirt shows two slots (32 stone
first, as the next out) over a half-filled blue bar; an empty one shows the
description and "Empty".
2026-10-26 13:22:31 +05:00
Ludovic Marie 57c5f81299 Merge pull request #2028 from ludovic111/feat/shader-packs-page
🤖 Agent: verified: pr_ready --quick green, 38/38 affected harnesses pass locally, CI verify green, paired A/B shows no frame cost. Options > Horizon Settings > Shader Packs lists and picks packs of either kind.
2026-10-26 09:22:21 +01:00
Ludovic Marie ce46917121 Merge pull request #2056 from ludovic111/feat/mod-minimap-api
🤖 Agent: Mod API render.set-minimap: a mod picks the corner, size, scale, shape and turning, and the game draws the map from the minimap extra's tiles. It is a new function, so published mods keep loading. Verified: pr_ready (1353 tests, clippy, launcher), the sandbox test, and in game beside the published mods in both modes.
2026-10-26 09:12:15 +01:00
Ludovic Marie 09d61ec866 Merge pull request #2030 from ludovic111/feat/moved-zoom
🤖 Agent: Zoom moved to the mod browser. The built-in zoom is removed, and a player who had it on gets the published Zoom mod with their settings and key. Verified against the real registry, in both modes (54/54), plus clippy and the client tests.
2026-10-26 09:09:19 +01:00
Ludovic MarieandClaude Opus 5.5 427b48aa42 Server chunk packets decode on their own thread, not in the frame
A server's chunk columns (single player on its server thread, LAN,
servers) were decoded on the render thread: sections, palettes, biomes
and light, about 0.1 ms each on an M4 Pro. Once the backlog was overdue,
a frame decoded every waiting column at once. Now each chunk packet goes
to a decoder thread as soon as it arrives, and the frame that reaches it
in order only inserts the column. A column still decoding holds back what
follows it, overdue or not, so a frame never waits for the decoder. A
dimension or registry change since a column was decoded makes the frame
decode it again with the new format.

Forgetting a column removed its pending sections with a pass over all of
them. A moved or shrunk storage forgets thousands of columns in one frame,
so it now removes them by key and drops their block entities in one pass.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 09:05:47 +01:00
Ludovic Marie 2b54c8a4ab Merge pull request #1812 from ludovic111/fix/trapped-chest-signal-barrel-open
🤖 Agent: main merged into the branch (merge commit). fmt, clippy -D warnings and the horizon-client tests pass; the 30 harnesses parity.py --changed picks for the game-binary files pass (30/30 on an integration of #1896 #1809 #1812); verify passed on this head. The earlier parity failure was the stale-head runner setup (.local/horizon-client link).
2026-10-26 08:57:39 +01:00
Ludovic Marie ff26716cd1 Merge pull request #1809 from ludovic111/fix/portal-effect-on-server-clients
🤖 Agent: main merged into the branch (merge commit). fmt, clippy -D warnings and the horizon-client tests pass; the 30 harnesses parity.py --changed picks for the game-binary files pass (30/30 on an integration of #1896 #1809 #1812); verify passed on this head. The earlier parity failure was the stale-head runner setup (.local/horizon-client link).
2026-10-26 08:55:30 +01:00
Ludovic MarieandClaude Opus 5.5 71126d3e13 Merge feat/mod-minimap-api (with main) into feat/official-minimap-mod
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 08:55:08 +01:00
Ludovic MarieandClaude Opus 5.5 641ff51089 Merge main into feat/mod-minimap-api
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 08:55:02 +01:00
Ludovic Marie 45bd296f14 Merge pull request #1896 from ludovic111/fix/creative-drop-on-server
🤖 Agent: main merged into the branch (merge commit). fmt, clippy -D warnings and the horizon-client tests pass; the 30 harnesses parity.py --changed picks for the game-binary files pass (30/30 on an integration of #1896 #1809 #1812); verify passed on this head. The earlier parity failure was the stale-head runner setup (.local/horizon-client link).
2026-10-26 08:53:22 +01:00
levzzzand[CC] <deepseek-v4.1-flash> 87e3df5331 Container clicks: a closed screen drops its press state
`AbstractContainerScreen` keeps `isQuickCrafting` and the drag it belongs
to on the screen. Horizon keeps both on `Game` (`inv_clicks`, `inv_drag`,
`last_inv_click`), and nothing cleared them when a screen closed: a press
whose release never reached the screen — the pointer left the window, or the
screen closed between the press and the release — left a quick-craft drag
running, and `Clicks::press` answers `None` for every later press, so clicks
went dead on every screen after it.

`Game::reset_inventory_clicks` now runs when a container closes and when the
pointer is grabbed again (the inventory's screens are behind us), which is the
lifetime vanilla gives that state.

Refs #1737

Co-Authored-By: [CC] <deepseek-v4.1-flash> <[email protected]>
2026-10-26 12:52:42 +05:00
rdbtCVSandCodex GPT-6.1 Sol cd7b6e025e Fix recipe book overlays surviving remote screen changes
Co-Authored-By: Codex GPT-6.1 Sol <[email protected]>
2026-10-26 07:49:37 +00:00
vlesmentieandClaude Opus 5.5 955b6c2134 The server thread's client draws the Ender Dragon
On a server's client (single player on its server thread, LAN guests,
dedicated servers) the dragon was tracked but never built: spawn_entity
only makes mobs from mob_types(), which has no dragon, and
dragon_geometry draws Game::dragon. The crystals had their own remote
constructor; the dragon had none.

The client now keeps the server's dragon in Game:🐉
handleAddEntity builds it (full health, hovering, as its constructor
does), SetEntityData applies DATA_HEALTH_ID and DATA_PHASE,
SetEntityMotion its velocity, and its moves interpolate like other
living entities. tick_remote_dragon is the client side of
EnderDragon.aiStep: wing flaps and their sound, the growl,
checkCrystals' nearest-crystal search for the healing beam, the flight
history and the parts; a dead dragon rises and bursts as in tickDeath.
Hurt animations flash it, removal and level changes drop it, and the
player's attacks pick its parts and send the server their ids.

The in-frame fight is unchanged; its nearest-crystal search moved into
nearest_crystal_to, shared with the client.

Fixes #1530.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 04:45:47 -03:00
rdbtCVSandCodex GPT-6.1 Sol 66f32a7a3a Persist forced chunks across world restarts
Co-Authored-By: Codex GPT-6.1 Sol <[email protected]>
2026-10-26 07:45:35 +00:00
Ludovic MarieandClaude Opus 5.5 cad817c82d Horizon shader packs: the sky stage runs in the game
A pack's `sky` function gets vanilla's colour along each view ray and draws
where the vanilla sky dome draws, under the sun, moon, stars and clouds. The
pipeline's shader is the pack's checked stage (`CheckedStage::source`, new)
followed by a copy of terrain.wgsl's sky with every name prefixed
`horizon_sky_`, so none can meet a pack's; terrain.wgsl's `fs_sky` now calls
`sky_along_ray`, the same code as before.

A running pack is one `NativePack`: the frame every stage reads (written once
a frame), its sky and post stages and its pass timer. In a frame F3 times, the
sky draws in a pass of its own so the overlay can show its time; otherwise it
stays in the world's pass.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 08:42:57 +01:00
Ludovic MarieandClaude Opus 5.5 a68c2f587d Horizon shader packs: one frame uniform per pack, apart from its post passes
`NativePack` holds what a running pack needs: the `frame` every stage
reads (`NativeFrame`, written once a frame before the world draws), its
post passes and the timer of its passes. `NativePost` no longer owns the
frame buffer or the timer, so the sky stage can read the same frame and
be timed by the same timer. No change in what draws.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 08:42:46 +01:00
Ludovic Marie d31832eaf4 Merge pull request #1996 from ludovic111/perf/reach-planning
🤖 Agent: merged main in. The old head's parity failure was server_dimension_players, which passes on main's parity-main. tests/test_parity_reach.py OK.
2026-10-26 08:40:52 +01:00
Ludovic MarieandClaude Opus 5.5 6c3473c6e0 Official mod: Minimap
The minimap extra as a mod (mods/official/minimap): the extra's five
options on its settings page, kept in its storage with the extra's values
(corner, size, shape, zoom, rotate), and the map drawn by the game
through render.set-minimap, so it looks and costs the same.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 08:40:03 +01:00
Ludovic MarieandClaude Opus 5.5 e52f0fdcc2 Merge main into feat/moved-zoom
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 08:39:51 +01:00
Ludovic Marie 3f5a323fd9 Merge pull request #2026 from ludovic111/feat/shader-pass-times
🤖 Agent: verified: pr_ready --quick green, 32/32 affected harnesses pass locally, CI verify green, paired A/B shows no frame cost without a pack. F3 now shows a shader pack's GPU time per pass, for both pack kinds.
2026-10-26 08:33:05 +01:00
rdbtCVSandCodex b6af258465 Fix warden sonic boom particle delivery to connected players
Send each sonic ray point through the existing particle packet path with vanilla recipient filtering. Cover nearby and distant clients, damage, and local rendering in the LAN harness. Fixes #1490.

Co-Authored-By: Codex (GPT-6.1 Sol) <[email protected]>
2026-10-26 07:29:40 +00:00
Eshayat Al-WasiuandOpenAI GPT-6.1 Sol f4b04218f1 Implement cushion placement and synchronize placed cushions
Add cushion entities, shape-based placement and support checks, colored rendering, server tracking, sitting, destruction and save/load support. Compare placement rules against vanilla and exercise host/guest interactions.

Co-Authored-By: OpenAI GPT-6.1 Sol <[email protected]>
2026-10-26 13:29:16 +06:00
Ludovic MarieandClaude Opus 5.5 24ab642e7b Mod API: render.set-minimap, a map the game keeps in a corner
What the official Minimap mod needs: a mod picks the corner, side, scale,
shape and turning, and the game draws the map as the minimap extra does,
from the same tiles (vanilla's map colours, the floor under roofs) within
its own frame budget. One map at a time, the first loaded mod's. A new
function and type, so mods built before it keep loading.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 08:20:30 +01:00
Ludovic Marie c58af30ca6 Merge pull request #2043 from ludovic111/ci/parity-main-runner-comment
🤖 Agent: comment only; the runner labels were moved via the API.
2026-10-26 08:20:07 +01:00
Eshayat Al-WasiuandOpenAI GPT-6.1 Sol 606efe5af0 Animate remote squid bodies and tentacles from server velocity
Run the client branch of Squid.aiStep, preserve authoritative movement and synchronize tentacle-cycle resets with entity event 19.

Co-Authored-By: OpenAI GPT-6.1 Sol <[email protected]>
2026-10-26 13:16:54 +06:00
Eshayat Al-WasiuandOpenAI GPT-6.1 Sol d422564ab3 Respect tool restrictions on Creative block destruction
Check the tool component before server destruction, local mining and remote prediction. Cover vanilla item defaults, overrides and mode-switch cancellation.

Co-Authored-By: OpenAI GPT-6.1 Sol <[email protected]>
2026-10-26 13:16:45 +06:00
Alexander KrylovandHermes Agent 4a7173918b Roadmap: finished help-wanted items link the pull requests that did them
check still found five Help wanted items whose only links were closed
issues. Each was finished by a merged pull request, so the item now links
that pull request and waits in Now until a release carries it.

Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
2026-10-26 10:12:08 +03:00
Ludovic MarieandClaude Opus 5.5 6ace65fc18 ci: parity-main's runner comment says where it runs now
ludovics-mac-mini-2 holds only the horizon-parity-main label; the release
runner no longer has it, since a release waited behind each new
parity-main run there.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 08:11:46 +01:00
Ludovic Marie 301b006ef4 Merge pull request #1816 from ludovic111/feat/mod-test-runner
🤖 Agent: cargo horizon-mod test runs a mod's test scripts with no game. Main is merged in, and the runner fills the new snapshot fields (input, render, frame stats) with the trial run's screen size. Verified: clippy, the cargo-horizon-mod, coords-hud and horizon-mods tests (57), launcher, and cargo horizon-mod test on coords-hud (2 scripts).
2026-10-26 08:07:39 +01:00
Ludovic Marie 2d57f97869 Merge pull request #2033 from ludovic111/perf/mob-ai-tick
🤖 Agent: verified: parity for the 75 harnesses parity.py picks passes locally, pr_ready --quick passes on the final head, lan_e2e mobs/hazards pass in-frame and on the server thread. Same results; −1.3% of the dedicated server's tick-thread cycles at 10 bots.
2026-10-26 08:00:18 +01:00
Ludovic MarieandClaude Opus 5.5 8e71cb4754 Test runner: fill the snapshot's new fields, with the trial run's screen size
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 07:57:49 +01:00
Ludovic Marie c3b29a2652 Merge pull request #2023 from ludovic111/feat/official-zoom-mod
🤖 Agent: The official Zoom mod (mods/official/zoom): the zoom extra as a mod, pixel-identical and at the same frame cost. Verified with main merged: clippy, cargo horizon-mod check, and in game in both modes. Publishing follows in horizon-mods.
2026-10-26 07:57:29 +01:00
Alexander KrylovandHermes Agent 73bbbd3ee8 Tooltips: compare the hidden component id without a UTF-16 copy
TooltipDisplay.shows built a JavaString from the id on every call just to
test membership in the NBT list. A hovered tooltip asks this a dozen times
per frame, so the hover allocated a dozen short vectors per frame for a read.

Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
2026-10-26 09:55:58 +03:00
Ludovic Marie 3163c7867d Merge pull request #1995 from ludovic111/feat/moved-hud-extras
🤖 Agent: The FPS counter and Keystrokes moved to the mod browser. A player who had one on gets the published official mod installed on the first start, with their settings and a chat line. Verified against the real registry, in both modes (34/34), plus clippy and the client tests.
2026-10-26 07:55:07 +01:00
Alexander KrylovandHermes Agent b59d9ada27 Shaders: the frame uniforms keep their buffer between frames
update rebuilt the values vector every frame for a fixed-length list: one
allocation and one free per frame for nothing. eval_custom already treats
the same vector as a buffer to truncate and extend.

Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
2026-10-26 09:49:41 +03:00
Ludovic MarieandClaude Opus 5.5 f7d7c7c70a Merge main into feat/mod-test-runner
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 07:47:58 +01:00
vlesmentieandClaude Opus 5.5 fdba6b4153 Campfire timers mark their chunk for saving
CampfireBlockEntity.cookTick and cooldownTick call setChanged whenever
a timer moves (cookTick while any slot holds food, cooldownTick while
any progress is above zero). tick_campfire only marked the chunk when
an item finished, so after a save partway through cooking, an unload
saved nothing and the progress since went back to the saved value.
The chunk is now marked modified on those ticks, with no update to the
clients, who don't see the timers.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 03:43:02 -03:00
Ludovic MarieandClaude Opus 5.5 108ced5793 launcher: a Shader packs tab that installs Horizon's own shader packs
The Mods page gets a fourth kind, Shader packs, which searches Horizon's
registry and installs a `.hshader` into `shaderpacks/` once
`index::check_file` has run the game's own `horizon_shader_api::check`.
Modrinth and CurseForge answer that tab with a pointer to the registry:
their shader packs are the shaders/ format and are not offered.

The sample index lists a shader pack (`glow`, made with
`horizon-shader new` and `pack`), so the registry build, check_file and
the launcher's install are tested on a real file. A file of the same
name the registry didn't put in place is no longer called a mod.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 07:41:24 +01:00
Ludovic Marie 7cb390fbff Merge pull request #2032 from ludovic111/fix/ctl-options-set-mods
🤖 Agent: horizon-ctl options-set no longer turns every mod off: Settings::with_option keeps the mod list with Horizon's other files. Verified: unit test, clippy, client tests, and in game in both modes (10/10).
2026-10-26 07:29:59 +01:00
Ludovic MarieandClaude Opus 5.5 c8d75a7892 Merge main into feat/official-zoom-mod
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 07:24:16 +01:00
Ludovic MarieandClaude Opus 5.5 828a2519e1 Merge main into feat/moved-hud-extras
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 07:23:36 +01:00
Ludovic MarieandClaude Opus 5.5 053291e339 Mob scans skip sections that can't hold what they look for
A mob's fluid state and its supporting block each read every cell around
its box, every tick. World::may_hold answers from the palettes of the
sections the box reaches: with no fluid listed there the fluid scan stops
before reading a cell, and with no block that reaches out of its cell
(large collision shapes, moving pistons) the supporting block search
reads only the cells meeting its area, as BlockCollisions' cursor does.
Results are the same.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 07:22:42 +01:00
Ludovic MarieandClaude Opus 5.5 130f1dc6a5 horizon-ctl options-set keeps the mod list
options-set rebuilt Settings from options.txt and carried over Horizon's
graphics, controller and extras files, but not the mod list: every mod
read as off afterwards, and the next save wrote an empty
horizon-mods.txt. Settings::with_option now does the rebuild and keeps
all four.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 07:13:21 +01:00
Ludovic MarieandClaude Opus 5.5 a1f1501305 launcher: Cargo.lock follows horizon-mods' shader check
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 07:11:30 +01:00
Alexander KrylovandHermes Agent dd45bf7740 F3: measure each line once per frame
The overlay measured every line to place it and measured it again for its
background box, about thirty extra glyph walks per frame on a full screen.
The width that placed the line is carried to the background pass.

Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
2026-10-26 09:08:18 +03:00
Ludovic Marie cbdbf068bf Merge main into feat/native-shaders-registry 2026-10-26 06:59:07 +01:00
Alexander KrylovandHermes Agent 9dc72bf3ac Subtitles: measure each line once per frame
The max-width pass built a Text for every audible subtitle and dropped the
width it measured, then the draw loop built the same Text again to measure
it a second time. The row loop uses the number the first pass already had.

Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
2026-10-26 08:58:50 +03:00
Alexander KrylovandHermes Agent 1e9644cc15 Signs: cut the first line on byte offsets, not through a char copy
first_line collected the whole line into a Vec<char> and then re-collected
a prefix of it, eight times per sign per frame, front and back. Every cut
point it needs is already a byte offset in the input.

Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
2026-10-26 08:51:52 +03:00
Ludovic Marie ee6dfd2b6c Merge pull request #2016 from ludovic111/feat/mod-ui-key
🤖 Agent: Players rebind a mod's keys on its Options > Mods page (the game's own key rows, saved in horizon-mods.txt), with no WIT type change, so the published mods keep loading. Verified with main merged: clippy, the client, mods and SDK tests, and the in-game check in both modes (24/24).
2026-10-26 06:50:11 +01:00
Ludovic MarieandClaude Opus 5.5 70b832d658 Options > Horizon Settings > Shader Packs: pick a pack of either kind
The page lists the packs in shaderpacks/: Horizon's own, marked (Horizon) and
named by their manifest, then packs in the shaders/ format. One runs at a
time: clicking a pack picks it, clicking the picked one turns it off, and
Open Pack Folder opens the folder. The pick is saved in horizon-graphics.txt
under a new key (pickedShaderPack), so the shaderPack value older versions
saved stays ignored. A new pick drops the running pack and loads the new one
in the next frame; HORIZON_SHADER_PACK still wins, for development.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 06:49:41 +01:00
Ludovic MarieandClaude Sonnet 5.5 c1814283e0 Merge origin/main into fix/creative-drop-on-server
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-26 06:49:26 +01:00
Ludovic MarieandClaude Sonnet 5.5 4512105e85 Merge origin/main into fix/trapped-chest-signal-barrel-open
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-26 06:48:57 +01:00
Ludovic MarieandClaude Opus 5.5 c3763ff1b3 F3 shows the shader pack's GPU time per pass, for either kind of pack
While the F3 overlay is open, the running pack's passes are timed on the GPU,
a shaders/ pack's or a native pack's, and the overlay lists the pack's total
and its costliest passes (the median of the last 15 timed frames). The timer
never waits: a timed frame's stamps are resolved once the GPU has finished it
and read back once mapped, then the next frame is timed. A native pack's first
pass counts from a mark that ends with the world's draws, so it does not carry
the time it waited for them. HORIZON_SHADER_PACK_TIMING still logs the medians.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 06:48:57 +01:00
Ludovic MarieandClaude Sonnet 5.5 0958cb297a Merge origin/main into fix/portal-effect-on-server-clients
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-26 06:48:55 +01:00
Ludovic Marie 5bcc87983e Merge pull request #1861 from ludovic111/feat/native-shaders-post
🤖 Agent: native shader packs' post stage in the game with hot reload; pr_ready, 66 affected harnesses and a paired A/B (no cost without a pack) passed.
2026-10-26 06:45:33 +01:00
Ludovic MarieandClaude Opus 5.5 a6c8e9070a Zoom moved to the mod browser: remove the built-in zoom
The official Zoom mod (mods/official/zoom) does what the extra did. The
extra's zoom, its key and wheel hooks and its Extras rows go; the camera
keeps a mod's zoom (render.set-zoom). A player who had zoom on gets the
mod installed and turned on at the first start, with the starting zoom,
smooth zoom and slower turning carried into its storage and a changed
key into horizon-mods.txt, and one chat line. horizon-extras.txt keeps
the old keys.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 06:42:14 +01:00
Alexander KrylovandHermes Agent 77def1f2dd Mob models: a skipped part still draws its children
The early return skipped the child loop as well, while skipDraw only hides
the part's own cubes. A rolled armadillo skips its body and still renders
the parts below it.

Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
2026-10-26 08:40:00 +03:00
levzzz c68e55438f Loot: the enchantment functions reach every table that rolls
`/loot give @s loot minecraft:chests/end_city_treasure` handed out bare gear: the
loot dispatcher in `items.rs` (the path `/loot`, bartering, raids, dispenser
tables, advancements and mob drops use) knew no `minecraft:enchant_with_levels`
or `minecraft:enchant_randomly`, so both fell through its arm untouched. Only the
chunk-generation path, which uses `horizon_world::loot`, enchanted anything —
which is why #1911 and #1437 looked fixed.

The dispatcher now runs both, as vanilla does: `EnchantWithLevelsFunction` picks
from `EnchantmentHelper.enchantItem`'s results (a book becomes one enchanted
book), `EnchantRandomlyFunction` picks one compatible enchantment at a level
between its min and max, and both resolve `options` as a tag, a list or one id,
else the whole registry. `Items` carries that registry now
(`Items::enchantments`), and `StationData` shares the same copy instead of
loading a second one.

Verified in game: six `/loot give … end_city_treasure` rolls gave 10 pieces of
gear, all 10 enchanted (unbreaking 3, mending 1 + protection 4, …), where all 10
were bare before.

Refs #1660
2026-10-26 10:34:21 +05:00
Ludovic Marie 344ef96894 Merge pull request #2025 from ludovic111/ci/parity-main-bisect-confirm
🤖 Agent: bisect fix; tests/test_parity_main.py passes (4 tests).
2026-10-26 06:20:14 +01:00
Ludovic Marie 2f46365df1 Merge remote-tracking branch 'origin/main' into feat/mod-ui-key 2026-10-26 06:15:25 +01:00
Ludovic Marie 886ecd9b20 Merge remote-tracking branch 'origin/main' into perf/reach-planning 2026-10-26 06:13:02 +01:00
Ludovic MarieandClaude Opus 5.5 2c5dac354e parity-main: name a merge only when HEAD fails again and the base passes
The bisect ended on the newest merge when the failed harnesses pass
everywhere now (a flake, or a runner that was missing the sounds), and
on the oldest when they fail everywhere (the runner), and commented on
that pull request either way. It now reruns them at HEAD and at the base
first and names nobody unless HEAD fails and the base passes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 06:11:01 +01:00
Ludovic MarieandClaude Opus 5.5 bea3165267 Merge main into feat/mod-test-runner
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 06:03:11 +01:00
levzzzand[CC] <deepseek-v4.1-flash> 276f8d49c1 Chat: a LAN guest's line follows the host's Chat option
Review of #1932 (rdbtCVS): with `--in-frame`, opening a world to LAN and
setting Commands Only still showed a guest's ordinary chat on the host. The
hub's `HostEvent::Chat` went through `system_message`, which asks for
`receive_system_messages` — true at Commands Only — so the line passed.

`Game::player_message` shows a chat line as `ChatSource::Player`, which is
the permission `ChatListener` checks for player chat; `lan_log_event`
routes `HostEvent::Chat` there. `/say` and `/me` arrive as chat messages
too (say_command, emote_command), so they follow the same permission as they
do for the guests' own packets; command feedback such as `/list` is a system
message and keeps the system-message permission.

Reproduced on the pre-fix build: host at Commands Only, guest sends
`baseline-visible` → the host shows `<Guest9> baseline-visible`. With this
change the same line stays hidden at Commands Only and shows at Shown.

Co-Authored-By: [CC] <deepseek-v4.1-flash> <[email protected]>
2026-10-26 10:03:06 +05:00
Ludovic MarieandClaude Opus 5.5 90a9d9c612 Merge main into feat/moved-hud-extras
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 06:02:48 +01:00
Ludovic Marie 7ecb43ccc8 Merge pull request #1982 from ludovic111/feat/official-hud-mods
🤖 Agent: Horizon's official FPS Counter and Keystrokes as mods in mods/official/, same options and look as the extras (pixel-identical, frame cost within noise). Main merged in (wasmtime 48). Verified: pr_ready, clippy and tests of both mods, horizon-mods tests, both mods' settings pages in game.
2026-10-26 06:02:08 +01:00
Ludovic MarieandClaude Opus 5.5 d113131f7e Merge feat/official-zoom-mod into feat/moved-zoom
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 05:55:10 +01:00
Ludovic MarieandClaude Opus 5.5 82deb28954 Merge main into feat/mod-ui-key
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 05:54:27 +01:00
levzzzand[CC] <deepseek-v4.1-flash> bf2961834b Item cooldowns: tick them on the server client's path too
Review of #1933 (rdbtCVS): `Game::tick` returns through `tick_remote` whenever
`remote` is set — every multiplayer session and the default single player — so
the `tick_world` call that advanced the entries never ran there. A thrown
pearl's slot stayed at full shade until the server's duration-0 packet arrived
instead of fading over the second.

`tick_remote_level` advances them now (Player.tick's ItemCooldowns.tick); the
groups that end are dropped silently, as the client's plain ItemCooldowns
does.

Co-Authored-By: [CC] <deepseek-v4.1-flash> <[email protected]>
2026-10-26 09:52:00 +05:00
Ludovic MarieandClaude Opus 5.5 8fdb27f949 Zoom: the game lists its key on the settings page, as "Zoom key: Z"
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 05:47:15 +01:00
Ludovic Marie e372fce7ab Merge branch 'feat/mod-ui-key' into feat/official-zoom-mod 2026-10-26 05:35:16 +01:00
Ludovic MarieandClaude Opus 5.5 1aa6f21620 Horizon shader packs: the post stage runs in the game, with hot reload
A native pack named by HORIZON_SHADER_PACK (a folder with horizon-shaders.toml
or a .hshader in shaderpacks/) is checked, then its post passes run over the
world's own target, the one a shaders/ pack reads: no second target and no
extra pass, and without a pack the frame only tests one Option. The pack is
looked at twice a second; a saved file is checked and rebuilt in place, and a
version that fails is refused in the chat while the last good one keeps
drawing.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 05:35:00 +01:00
Ludovic MarieandClaude Opus 5.5 3e3c0cebb5 Mod keys: rows the game adds to the settings page, not a WIT widget
A new case in ui's widget variant stops every ui mod built before it from
loading: the runtime links a component only when each imported
function's types match exactly ("expected variant of 5 cases, found
4"), so the published FPS Counter and Keystrokes would break. Instead,
the game lists a mod's keys first on its page in Options > Mods, as Key
Binds rows, and a mod with keys gets a page even without ui. No WIT
type changes. docs/api/stability.md said a new case the mod sends isn't
breaking; it is, and now says so.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 05:34:49 +01:00
Ludovic MarieandClaude Opus 5.5 3df60c21c2 tests: compare resolved paths in the prepared-report fallback test
macOS temp dirs live under /var, a symlink to /private/var, so the
resolved report path never equalled the unresolved fallback.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 05:28:30 +01:00
Ludovic Marie 8cf51888c6 Merge pull request #1757 from ludovic111/feat/structure-saved-starts
🤖 Agent: structures stack brought up to date with main (merge commits); queued with verify passed, the merger waits for parity on this head.
2026-10-26 05:22:52 +01:00
rdbtCVSandOpenAI Codex 03a3c48a71 Fix Peaceful removal of existing hostile mobs
Apply the vanilla Mob.checkDespawn Peaceful gate before persistence and player distance checks. Extend the mob AI parity harness with difficulty, persistence, name-tag and NoAI regressions and allowed-type controls.

Fixes #1942

Co-Authored-By: OpenAI Codex (GPT-6.1 Sol) <[email protected]>
2026-10-26 04:18:54 +00:00
Ludovic Marie b485c43928 Merge pull request #2017 from ludovic111/ci/vanilla-data-new-shared-entries
🤖 Agent: CI script fix, tested on scratch folders. The release runner had no sounds, so parity-main's audio harnesses failed.
2026-10-26 05:18:40 +01:00
rdbtCVSandOpenCode f70b39290d Merge origin/main into t3/fix-critical-hit-particles
Include main's latest mod-input changes before validation.

Co-Authored-By: OpenCode (agentrouter/gpt-6.1-sol) <[email protected]>
2026-10-26 04:14:52 +00:00
rdbtCVSandOpenCode 70f1b71de1 Merge origin/main into t3/fix-critical-hit-particles
Keep both critical-hit and sculk container regressions.

Co-Authored-By: OpenCode (agentrouter/gpt-6.1-sol) <[email protected]>
2026-10-26 04:14:11 +00:00
Ludovic Marie d8aaf367eb Merge pull request #2013 from ludovic111/feat/mod-api-zoom
🤖 Agent: Mod API (experimental): render.set-zoom and input.claim-wheel, held mod keys let go when a screen opens, mod keys on mouse buttons. The consume choice is in stability.md. Verified: pr_ready (1300 tests), sandbox tests, in game 22/22 in-frame and on the server thread.
2026-10-26 05:12:07 +01:00
vlesmentieandClaude Opus 5.5 3d401e55f4 Merge origin/main into feat/campfire-cooking
Keeps both new block entities: BlockEntity::Campfire (this branch) and
BlockEntity::Skull (main), in containers.rs's enum, id() and the menu
lists in game.rs.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 01:10:29 -03:00
Ludovic MarieandClaude Opus 5.5 5335354658 CI: runners clone what the shared source gained after their first copy
The release runner, now parity-main's, cloned the shared source before
the sounds were added to it; vanilla-data.sh only copied on the first
run, so ambient_sound, audio_attributes and music failed there with
"Sound assets missing". Each run now clones any entry the runner's copy
lacks.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 05:10:19 +01:00
Ludovic MarieandClaude Opus 5.5 0cb1d46393 Merge main into feat/official-hud-mods
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 05:09:45 +01:00
Ludovic MarieandClaude Opus 5.5 53289d3565 Official mod: Zoom, the zoom extra as a mod
mods/official/zoom: hold the key (Z) to zoom in at the starting zoom,
the wheel changes it while held (x1.25 a step, 1.25 to 10), smooth
easing and slower turning as options. Same settings page as the extra,
with the key widget for the key. Built on render.set-zoom,
input.claim-wheel and ui's key widget.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 05:08:36 +01:00
Ludovic Marie ab92ab346c Merge remote-tracking branch 'origin/main' into cg15/1909 2026-10-26 05:06:59 +01:00
Ludovic Marie 88d061ec9b Merge pull request #1955 from rdbtCVS/t3/migrate-parity-server-thread
🤖 Agent: Thanks rdbt! The command, dispenser and sculk fixtures now run on a server Game. Checked:
2026-10-26 05:06:41 +01:00
Ludovic Marie 4231905b1e Merge pull request #1967 from socksprox/fix/mannequin-npcs
🤖 Agent: Thanks socksprox! Mannequins render as the player model with their profile skin. I pushed d3af1e04e: a mannequin is a LivingEntity, not a Mob, so it runs no goals and never despawns. Checked in game: a summoned mannequin stays put for 400 ticks on the server thread and falls to the ground in-frame (it wandered 14 blocks before). Tested together with the other wave 2 contributor PRs: fmt, clippy -D warnings, and the horizon-client, horizon-world and horizon-protocol tests pass. Of the 127 harnesses the batch affects, 124 pass; the other three are a player-head mismatch fixed in #1934 and two worldgen timeouts on a loaded machine with no chunk differing. lan_e2e chat, fireball, cursor, looks, thrown, eat, shield, lava, contacts, projectiles and mobs pass with the host in-frame and on its server thread.
2026-10-26 05:00:59 +01:00
Ludovic Marie 1114b4d734 Merge pull request #1933 from levzzz5154/fix/item-cooldowns
🤖 Agent: Thanks levzzz5154! Matches ItemCooldowns and the slot sweep in Gui.renderSlot; thrown, eat and shield pass on both hosts. I merged main and dropped orphaned doc comments. Tested together with the other wave 2 contributor PRs: fmt, clippy -D warnings, and the horizon-client, horizon-world and horizon-protocol tests pass. Of the 127 harnesses the batch affects, 124 pass; the other three are a player-head mismatch fixed in #1934 and two worldgen timeouts on a loaded machine with no chunk differing. lan_e2e chat, fireball, cursor, looks, thrown, eat, shield, lava, contacts, projectiles and mobs pass with the host in-frame and on its server thread.
2026-10-26 04:58:42 +01:00
Ludovic Marie c31e8b2381 Merge pull request #1932 from levzzz5154/fix/chat-visibility
🤖 Agent: Thanks levzzz5154! Matches ChatComponent's ChatVisiblity filter for the Full, Commands only and Hidden options. I pushed 1e6f05ed5 earlier: every line keeps its source, so changing the option refilters the history both ways (new test). Verify had failed because main's mod output (#1875) calls Chat::add without a source. I merged main, and 2256941af marks a mod's lines as the client's own, which the Chat option keeps. Clippy and the chat tests pass after the merge. Before it, the wave 2 batch passed its 127 affected harnesses, and lan_e2e chat passed with the host in-frame and on its server thread.
2026-10-26 04:54:32 +01:00
Ludovic Marie 2e05b11a4d Merge remote-tracking branch 'origin/feat/official-hud-mods' into feat/official-zoom-mod 2026-10-26 04:53:36 +01:00
Ludovic Marie 23d4bc0a69 Merge pull request #1905 from rdbtCVS/fix/server-player-sculk-events
🤖 Agent: Thanks rdbt! Every server player now emits its own movement game events, with per-player sculk history; the new server_player_sculk harness passes. I merged main (the guest swap code). Tested together with the other wave 2 contributor PRs: fmt, clippy -D warnings, and the horizon-client, horizon-world and horizon-protocol tests pass. Of the 127 harnesses the batch affects, 124 pass; the other three are a player-head mismatch fixed in #1934 and two worldgen timeouts on a loaded machine with no chunk differing. lan_e2e chat, fireball, cursor, looks, thrown, eat, shield, lava, contacts, projectiles and mobs pass with the host in-frame and on its server thread.
2026-10-26 04:52:23 +01:00
Ludovic Marie f1ab726503 Merge pull request #1897 from vlesmentie/fix/pointing-hand-cursor
🤖 Agent: Thanks vlesmentie! Matches vanilla 26.3's AbstractWidget.handleCursor: buttons show the pointing hand. lan_e2e's cursor check passes. Tested together with the other wave 2 contributor PRs: fmt, clippy -D warnings, and the horizon-client, horizon-world and horizon-protocol tests pass. Of the 127 harnesses the batch affects, 124 pass; the other three are a player-head mismatch fixed in #1934 and two worldgen timeouts on a loaded machine with no chunk differing. lan_e2e chat, fireball, cursor, looks, thrown, eat, shield, lava, contacts, projectiles and mobs pass with the host in-frame and on its server thread.
2026-10-26 04:49:09 +01:00
Ludovic Marie 061ae61465 Merge pull request #1963 from ludovic111/feat/mod-api-extras
🤖 Agent: Mod API calls the official HUD mods use: render corner panels, text width and frame stats; input mappings and clicks per second; a ui button widget. Main merged in (after #1914's widget refactor). Verified: horizon-mods tests, clippy with and without mods, horizon-client tests (1121 lib), Keystrokes' settings page in game.
2026-10-26 04:45:01 +01:00
Ludovic Marie 5320391ff6 Merge pull request #1904 from rdbtCVS/fix/remote-fireball-motion
🤖 Agent: Thanks rdbt! Matches the client's LargeFireball tick between tracker updates; the new remote_fireball harness passes and lan_e2e's fireball check sees it on both sides. Tested together with the other wave 2 contributor PRs: fmt, clippy -D warnings, and the horizon-client, horizon-world and horizon-protocol tests pass. Of the 127 harnesses the batch affects, 124 pass; the other three are a player-head mismatch fixed in #1934 and two worldgen timeouts on a loaded machine with no chunk differing. lan_e2e chat, fireball, cursor, looks, thrown, eat, shield, lava, contacts, projectiles and mobs pass with the host in-frame and on its server thread.
2026-10-26 04:42:27 +01:00
Ludovic MarieandClaude Opus 5.5 7e4012c83e Mod API: a key widget, so players bind a mod's keys (ui, input)
A ui page's key widget shows one of the mod's keys as Key Binds does
("Zoom: Z"). A click waits for the next key or mouse button, which
becomes the key; Esc binds none. The game keeps the choice in
horizon-mods.txt (key.<mod>.<name>:key.keyboard.z), applies it when
the mods load and to keys registered later, and lets a held key go when
it is rebound. register-key's key is now the default.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 04:41:12 +01:00
Ludovic Marie c94c4e140f Merge pull request #1915 from vlesmentie/fix/big-dripleaf-tilt
🤖 Agent: Thanks vlesmentie! Matches BigDripleafBlock.entityInside and canEntityTilt (the 0.6875 height check): a big dripleaf tilts under a player standing on it. Tested with the wave 2 batch: fmt, clippy and the horizon-client and horizon-world tests pass, and all 127 affected harnesses pass. The chunk_generation and decorated_region reruns took 8 minutes with no chunk differing. lan_e2e contacts and the rest of wave 2's checks pass with the host in-frame and on its server thread.
2026-10-26 04:39:41 +01:00
Ludovic Marie 3d8cf1f53a Merge pull request #1710 from ludovic111/fix/remote-tnt
🤖 Agent: main merged into the branch (merge commit); verify passed on this head, the merger waits for parity.
2026-10-26 04:36:46 +01:00
Ludovic Marie 5a79c8dede Merge pull request #1997 from ludovic111/fix/server-ops-feedback-save-wait
🤖 Agent: ready. server_ops_feedback waits for vanilla's and Horizon's 'Saved the game' before the save step's 2 s, so a loaded runner (the desktop's full trial) no longer cuts the transcript. Harness only. verify passed; parity ran server_ops_feedback on the Mac: PASS 120 s.
2026-10-26 04:34:33 +01:00
Ludovic Marie 2979303263 Merge pull request #1977 from ludovic111/perf/collision-scan-edges
🤖 Agent: requeued after a runner-setup parity failure (subtitle: missing sounds.json on the runner); 30/31 harnesses passed, the 56 selected passed locally. Queued by perf-next-2.
2026-10-26 04:33:56 +01:00
Ludovic Marie ce266e60be Merge pull request #1741 from ludovic111/feat/styled-menu-titles
🤖 Agent: server dialogs stack brought up to date with main (merge commits); verify passed on this head.
2026-10-26 04:31:44 +01:00
Ludovic Marie e410a3977b Merge pull request #1735 from ludovic111/feat/server-dialog-inputs
🤖 Agent: server dialogs stack brought up to date with main (merge commits); verify passed on this head.
2026-10-26 04:29:32 +01:00
Ludovic Marie d177a96be6 Merge pull request #1916 from ludovic111/feat/shader-packs-shadow-casters
🤖 Agent: shader packs clean room: main merged into the stack (merge commits), verify passed on this head, parity skipped by parity-scope.
2026-10-26 04:27:16 +01:00
Ludovic Marie 452c444933 Merge pull request #1918 from vlesmentie/fix/water-cauldron-extinguish
🤖 Agent: Thanks vlesmentie! Matches LayeredCauldronBlock.entityInside / handleEntityOnFireInside: a burning player is put out and the cauldron loses a level. The cauldron harness passes, and lava and contacts pass on both hosts. Tested together with the other wave 2 contributor PRs: fmt, clippy -D warnings, and the horizon-client, horizon-world and horizon-protocol tests pass. Of the 127 harnesses the batch affects, 124 pass; the other three are a player-head mismatch fixed in #1934 and two worldgen timeouts on a loaded machine with no chunk differing. lan_e2e chat, fireball, cursor, looks, thrown, eat, shield, lava, contacts, projectiles and mobs pass with the host in-frame and on its server thread.
2026-10-26 04:24:50 +01:00
Ludovic Marie a4eefb0730 Merge remote-tracking branch 'origin/main' into cg15/1955
# Conflicts:
#	tools/lan_e2e.py
2026-10-26 04:24:12 +01:00
Ludovic Marie f72a63ca01 Merge pull request #1869 from ludovic111/fix/prepare-rewritten-version-file
🤖 Agent: verified: the fix for #1772 plus #1947 merged in. Its parity job prepares with this preparer and checks that the output is still OUTPUT_VERSION 1, so with #1946 players keep their prepared resources. Merging.
2026-10-26 04:24:12 +01:00
vlesmentieandClaude Opus 5.5 5febf0d957 Campfires cook food
Horizon had no CampfireBlockEntity, so right-clicking a campfire with raw
food did nothing. Now:

- BlockEntity::Campfire keeps four slots with their cookingProgress and
  cookingTime, saved as Items, CookingTimes and CookingTotalTimes; both
  campfires share the minecraft:campfire block entity type. It is not a
  Container (only Clearable), so hoppers don't reach in.
- CampfireBlock.useItemOn: a campfire_cooking input goes into the first
  free slot (placeFood) with its recipe's time; the use is consumed
  whether or not there was room, the item is used up outside creative,
  interact_with_campfire is awarded and a block_change goes out. A client
  of a server leaves it to the server.
- cookTick (lit): each item's progress rises; a finished item's result
  drops at the block (Containers.dropItemStack), the slot empties and a
  block_change goes out. cooldownTick (unlit): progress falls by 2.
- Breaking the campfire drops its items (preRemoveSideEffects).
- The update tag (Items) reaches a server's clients, and CampfireRenderer
  draws the items on the fire.

Fixes #1988.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 00:24:10 -03:00
Ludovic MarieandClaude Opus 5.5 2256941af7 Chat: mod output lines are the client's own
Main's mod output calls Chat::add without the new source; a mod's line is
written by this client, so the Chat option keeps it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 04:23:25 +01:00
Ludovic Marie 6e15257873 Merge remote-tracking branch 'origin/main' into cg15/1932
# Conflicts:
#	crates/horizon-client/src/game.rs
2026-10-26 04:23:24 +01:00
Ludovic Marie 1d83584da4 Merge pull request #1900 from ludovic111/feat/shader-packs-shadow-entities
🤖 Agent: shader packs clean room: main merged into the stack (merge commits), verify passed on this head, parity skipped by parity-scope.
2026-10-26 04:22:09 +01:00
Ludovic Marie 8d6754226e Merge pull request #1934 from socksprox/feat/player-head-skins
🤖 Agent: Thanks socksprox! Player heads now show their owner's skin, fetched like vanilla's SkullBlockRenderer from the profile's textures property; TEXTURE_URIS limits it to textures.minecraft.net, so there's no new host.
2026-10-26 04:19:47 +01:00
Ludovic Marie 2ae2e57c66 Merge pull request #1449 from ludovic111/chore/wasmtime-48
🤖 Agent: main merged into the branch (merge commit); verify passed on this head.
2026-10-26 04:19:14 +01:00
Ludovic MarieandClaude Opus 5.5 de294e34bb rustfmt
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 04:19:07 +01:00
Ludovic Marie fed2bdb304 Merge pull request #1892 from vlesmentie/fix/chicken-flap-remote
🤖 Agent: Thanks vlesmentie! Matches Chicken.aiStep's flap on the client of a server. I merged main and slimmed the test to reuse an existing mob kind. Tested together with the other wave 2 contributor PRs: fmt, clippy -D warnings, and the horizon-client, horizon-world and horizon-protocol tests pass. Of the 127 harnesses the batch affects, 124 pass; the other three are a player-head mismatch fixed in #1934 and two worldgen timeouts on a loaded machine with no chunk differing. lan_e2e chat, fireball, cursor, looks, thrown, eat, shield, lava, contacts, projectiles and mobs pass with the host in-frame and on its server thread.
2026-10-26 04:17:55 +01:00
Ludovic Marie 4aca4e151a Merge pull request #1734 from ludovic111/feat/server-dialog-screen
🤖 Agent: server dialogs stack brought up to date with main (merge commits); verify passed on this head.
2026-10-26 04:17:30 +01:00
Ludovic Marie 1faafb9f90 Merge pull request #1888 from ludovic111/feat/shader-packs-shadow-terrain
🤖 Agent: shader packs clean room: main merged into the stack (merge commits), verify passed on this head, parity skipped by parity-scope.
2026-10-26 04:16:44 +01:00
Ludovic Marie 6bbf40e345 Merge pull request #1753 from ludovic111/feat/structure-blocks-harness
🤖 Agent: structures stack brought up to date with main (merge commits), verify and parity passed on this head.
2026-10-26 04:13:52 +01:00
Ludovic Marie 4d460bfb3a Merge pull request #1887 from ludovic111/feat/shader-packs-shadow-buffers
🤖 Agent: shader packs clean room: main merged into the stack (merge commits), verify passed on this head, parity skipped by parity-scope.
2026-10-26 04:10:49 +01:00
Ludovic MarieandClaude Opus 5.5 c89434557b Player heads without a skin show slim steve, as vanilla
SkullBlockRenderer's SKIN_BY_TYPE is DefaultPlayerSkin.getDefaultTexture(),
DEFAULT_SKINS[6] = entity/player/slim/steve; the block_entity_render harness
found Horizon drawing nothing for heads without a profile.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 04:09:25 +01:00
Ludovic Marie d208121c8e Merge pull request #1886 from ludovic111/feat/shader-packs-shadow-programs
🤖 Agent: shader packs clean room: main merged into the stack (merge commits), verify passed on this head, parity skipped by parity-scope.
2026-10-26 04:08:13 +01:00
Ludovic MarieandClaude Sonnet 5.5 55c961b889 Merge origin/main into fix/remote-tnt
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-26 04:07:18 +01:00
Ludovic Marie 1880e12f2b Merge pull request #1885 from ludovic111/feat/shader-packs-shadow-camera
🤖 Agent: shader packs clean room: main merged into the stack (merge commits), verify passed on this head, parity skipped by parity-scope.
2026-10-26 04:05:36 +01:00
Alexander KrylovandHermes Agent d41aa95dbd Mob models: one Poses per mob per frame, not one per equipment layer
Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
2026-10-26 06:04:06 +03:00
Ludovic MarieandClaude Sonnet 5.5 7d4792fb31 Mods API reference test: follow wit-parser 0.254 (FixedLengthList, Param)
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-26 04:03:03 +01:00
Alexander KrylovandHermes Agent dcc5899692 Name tags: collect the ridden mobs once, not per tag
Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
2026-10-26 06:00:36 +03:00
Alexander KrylovandHermes Agent 51911080f0 Mob models: one skip lookup per part, not one per polygon
Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
2026-10-26 05:58:53 +03:00
Alexander KrylovandHermes Agent 1dcb33f111 Merge main into perf/no-double-cracks-and-ticker-copy
The profiling marks that landed in main (mark_entity, mark_stage) collide with
the cached cracks scan; both are kept.

Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
2026-10-26 05:55:32 +03:00
Ludovic Marie 163f099ecb Merge pull request #2006 from ludovic111/perf/behaviour-cache-hash-once
🤖 Agent: pr_ready and 91 harnesses pass (shared run, body); the duplicate 70 MB hash at world entry removed. Queued by perf-next-2.
2026-10-26 03:41:58 +01:00
Ludovic Marie 6d92727d9d Merge pull request #2004 from ludovic111/perf/sculk-ticking-chunks
🤖 Agent: pr_ready and 91 harnesses pass (shared run, body); tick_sculk_blocks halved at 10 bots, −75% at 25. Queued by perf-next-2.
2026-10-26 03:38:40 +01:00
Ludovic MarieandClaude Sonnet 5.5 0bfb011ee2 Merge feat/structure-saved-starts into feat/lithostitched-processors
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-26 03:38:00 +01:00
Ludovic MarieandClaude Sonnet 5.5 b4700c0a9d Merge feat/structure-blocks-harness into feat/structure-saved-starts
Both branches changed how the harness finds the mod jars: resolve every jar
through jar_path (jars, then pack-jars) and keep the profile packs check.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-26 03:36:24 +01:00
Ludovic Marie 2b4e1fd03b Merge pull request #1944 from ludovic111/feat/mod-input
🤖 Agent: Mod API capability input (experimental): claimed items' use and the mod's keys. Main merged in beside command and render (9 capabilities). Verified: horizon-mods tests (sandbox, API reference), clippy with and without mods, horizon-client tests (1121 lib), earlier pr_ready and in-game runs in both modes.
2026-10-26 03:35:25 +01:00
Ludovic MarieandClaude Opus 5.5 2631eca00b Drop the migration's validation ledger from the docs
It records an agent's runs and diagnoses, which the pull request and its
branch history already keep; docs/ is for how the project works.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 03:32:44 +01:00
Ludovic MarieandClaude Sonnet 5.5 355df65240 Merge feat/yung-port-as-written into feat/structure-blocks-harness
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-26 03:31:39 +01:00
Ludovic Marie c7c4cfe996 Merge pull request #1801 from ludovic111/feat/shader-packs-hand
🤖 Agent: shader packs clean room: main merged into the stack (merge commits), verify passed on this head, parity skipped by parity-scope.
2026-10-26 03:30:48 +01:00
Ludovic Marie ef4018805e Merge pull request #2008 from ludovic111/ci/parity-main-vanilla-data
🤖 Agent: verify passed at 0c84eb307. Merged by hand: the PR parity job is queued behind the same runners, and this changes only how that job prepares its data. The next PR parity job on ludovics-mac-mini is the real test.
2026-10-26 03:27:46 +01:00
Ludovic Marie 905464e2b3 Merge pull request #2005 from ludovic111/perf/rings-out-of-reach
🤖 Agent: pr_ready and 91 harnesses pass (shared run, body, incl. decorated_region's stronghold site and world_spawn); first chunk 8.7 → 6.0 s on 4 threads. Queued by perf-next-2.
2026-10-26 03:27:25 +01:00
Ludovic Marie 6c2effae82 Merge pull request #1931 from levzzz5154/fix/draggable-scrollbars
🤖 Agent: Thanks levzzz5154! The Game Rules and Statistics scroll bars can be pressed, dragged and released like vanilla's; I merged main and pushed a41381236 (Statistics uses the GUI height; #1913's drag test is folded in). Tested together with the other wave 1 contributor PRs on current main: fmt, clippy -D warnings and the horizon-client tests (1,120 lib tests and the rest) pass; the 114 harnesses the batch affects pass; lan_e2e anvil, grindstone, replant, instant_digging, digging, frames and leads pass with the host in-frame and on its server thread.
2026-10-26 03:24:52 +01:00
vlesmentieandClaude Opus 5.5 9572330ee4 A villager's new profession rolls its trades
Villager.setVillagerData sets offers to null when the profession changes,
and getOffers rolls the new profession's trades on the next look.
set_profession cleared the offers but left unset false, so ensure_offers
never rolled new ones: a villager that took a placed workstation after
losing its first wore the new profession but only shook its head.
It now marks the offers unset, as vanilla's null.

Fixes #1998.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 23:24:25 -03:00
Ludovic MarieandClaude Sonnet 5.5 d322c4b870 Merge feat/server-dialog-screen into feat/styled-menu-titles
Both branches changed the machine screens' title drawing: keep the styled server
title from this branch and the translated container title and inventory label
from feat/server-dialog-screen.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-26 03:22:13 +01:00
Ludovic Marie 4bfb7f6506 Merge pull request #1965 from rdbtCVS/t3/optimize-launcher-resource-prepare
🤖 Agent: Thanks rdbt, preparation needs only a Java runtime now and runs 5 JVMs instead of 18! Verified on the Mac mini:
2026-10-26 03:21:56 +01:00
Ludovic Marie ba105e0784 Merge branch 'feat/official-hud-mods' into feat/moved-hud-extras 2026-10-26 03:20:09 +01:00
Ludovic MarieandClaude Opus 5.5 980c619b81 Merge feat/mod-api-extras (with main) into feat/official-hud-mods
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 03:20:09 +01:00
Ludovic Marie 9f42b39aca Merge pull request #2002 from ludovic111/ci/parity-main-own-runner
🤖 Agent: verify passed at 2c612fd79. The PR parity job is still queued behind the same runners, and it can't test a runs-on change, so I merge by hand.
2026-10-26 03:19:47 +01:00
Ludovic Marie 1e317cff5e Merge branch 'feat/mod-api-extras' into feat/mod-api-zoom 2026-10-26 03:19:44 +01:00
Ludovic Marie 0f47d2a028 Merge pull request #1800 from ludovic111/feat/shader-packs-held-items
🤖 Agent: shader packs clean room: main merged into the stack (merge commits), verify passed on this head, parity skipped by parity-scope.
2026-10-26 03:19:36 +01:00
Ludovic MarieandClaude Sonnet 5.5 0c73bb3c90 Merge feat/server-dialog-screen into feat/server-dialog-inputs
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-26 03:18:45 +01:00
Ludovic MarieandClaude Opus 5.5 0c84eb3079 ci: PR parity jobs prepare vanilla data with main's script
A PR head older than the fix in vanilla-data.sh left a real
.local/horizon-client without sounds.json on runner ludovics-mac-mini;
every later job there failed subtitle. Like the harness selection, the
job now runs main's script unless the branch changes it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 03:18:39 +01:00
Ludovic Marie 666a1ba6f7 Merge pull request #1786 from ludovic111/feat/shader-packs-weather
🤖 Agent: shader packs clean room: main merged into the stack (merge commits), verify passed on this head, parity skipped by parity-scope.
2026-10-26 03:16:38 +01:00
Ludovic MarieandClaude Sonnet 5.5 a1ffa1ac11 Merge feat/server-dialogs into feat/server-dialog-screen
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-26 03:14:19 +01:00
Ludovic MarieandClaude Opus 5.5 50c54098ef Merge feat/mod-input (with main) into feat/mod-api-extras
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 03:13:27 +01:00
rdbtCVSandCodex GPT-6.1 Sol 4cc134d08a Fix passenger walk animation in shared render state
Zero passenger walk position and speed like vanilla LivingEntityRenderer, covering local and remote riders. Extend entity model parity with moving passenger and on-foot render-state cases.

Fixes #1962

Co-Authored-By: Codex GPT-6.1 Sol <[email protected]>
2026-10-26 02:13:12 +00:00
Ludovic Marie 9bf96bcea5 Merge pull request #1785 from ludovic111/feat/shader-packs-clouds
🤖 Agent: shader packs clean room: main merged into the stack (merge commits), verify passed on this head, parity skipped by parity-scope.
2026-10-26 03:12:49 +01:00
Ludovic Marie 4457d4e0ca Merge remote-tracking branch 'origin/main' into perf/behaviour-cache-hash-once 2026-10-26 03:12:43 +01:00
Ludovic Marie 96290f4b94 Merge remote-tracking branch 'origin/main' into perf/rings-out-of-reach 2026-10-26 03:12:31 +01:00
Ludovic Marie facc1d4d08 Merge remote-tracking branch 'origin/main' into perf/sculk-ticking-chunks 2026-10-26 03:12:20 +01:00
rdbtCVSandOpenCode GPT-6.1 Sol 15d99a6856 Start bat animation clocks on server clients
Fixes #2000.

Co-Authored-By: OpenCode GPT-6.1 Sol <[email protected]>
2026-10-26 02:11:48 +00:00
Ludovic Marie 8dfe518b60 Merge pull request #1911 from vlesmentie/fix/structure-chest-enchantments
🤖 Agent: Thanks vlesmentie! Structure chests keep the enchantments their loot table rolled. Tested together with the other wave 1 contributor PRs on current main: fmt, clippy -D warnings and the horizon-client tests (1,120 lib tests and the rest) pass; the 114 harnesses the batch affects pass; lan_e2e anvil, grindstone, replant, instant_digging, digging, frames and leads pass with the host in-frame and on its server thread.
2026-10-26 03:11:16 +01:00
Ludovic MarieandClaude Opus 5.5 2c612fd791 ci: parity-main on the release runner, not behind PR parity jobs
The queued parity-main run waited 2 h behind PR parity jobs on the two
horizon-game runners, each merge replacing the pending one, and releases
need it green. The release runner gets a horizon-parity-main label; a
release only starts after parity-main passed, so the two never compete.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 03:10:55 +01:00
Ludovic MarieandClaude Opus 5.5 48db367d4a Mod API: zoom the camera, take the mouse wheel (render, input)
render.set-zoom divides the field of view by up to 10, eased through the
game's FOV easing or at once, with a turning scale (at least 0.05); only
while playing, and the spyglass's own view wins. The strongest of several
mods' zooms counts, and the built-in zoom keeps working beside it.

input.claim-wheel takes the wheel from the hotbar while playing: its steps
are the mod's wheel events. That is the one input a mod consumes; its keys
never do (docs/api/stability.md). A screen opening now lets the mod's held
keys up, and mod keys bound to mouse buttons (key.mouse.4) are heard.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 03:09:29 +01:00
Ludovic MarieandClaude Opus 5.5 b7b7cd6c50 Merge main into feat/mod-input: input beside command and render
Capability::ALL is 9 with Input; the SDK re-exports command and input;
the API reference is regenerated.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 03:09:13 +01:00
Ludovic Marie 74b08e6310 Merge pull request #1928 from levzzz5154/fix/end-city-banner-patterns
🤖 Agent: Thanks levzzz5154! End city banners keep their template patterns; I pushed 401ac1322 so the structure code and the save loader share one banner decoder. Tested together with the other wave 1 contributor PRs on current main: fmt, clippy -D warnings and the horizon-client tests (1,120 lib tests and the rest) pass; the 114 harnesses the batch affects pass; lan_e2e anvil, grindstone, replant, instant_digging, digging, frames and leads pass with the host in-frame and on its server thread.
2026-10-26 03:08:48 +01:00
Ludovic MarieandClaude Sonnet 5.5 1dff593ac3 Merge feat/shader-packs-shadow-entities into feat/shader-packs-shadow-casters
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-26 03:05:49 +01:00
Ludovic Marie 68d1a8a388 Merge pull request #1769 from ludovic111/feat/shader-packs-entity-spans
🤖 Agent: shader packs clean room: main merged into the stack (merge commits), verify passed on this head, parity skipped by parity-scope.
2026-10-26 03:04:53 +01:00
Ludovic MarieandClaude Sonnet 5.5 4292806437 Merge feat/shader-packs-shadow-terrain into feat/shader-packs-shadow-entities
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-26 03:04:24 +01:00
Ludovic MarieandClaude Sonnet 5.5 c841ab6d56 Merge feat/shader-packs-shadow-buffers into feat/shader-packs-shadow-terrain
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-26 03:02:40 +01:00
Ludovic MarieandClaude Sonnet 5.5 8b7315a6ff Merge feat/shader-packs-shadow-programs into feat/shader-packs-shadow-buffers
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-26 03:01:43 +01:00
Ludovic Marie 76be4d0e73 Merge pull request #1756 from ludovic111/feat/shader-packs-sildurs
🤖 Agent: shader packs clean room: main merged into the stack (merge commits), verify passed on this head, parity skipped by parity-scope.
2026-10-26 03:01:05 +01:00
Ludovic MarieandClaude Sonnet 5.5 b2a7e77036 Merge feat/shader-packs-shadow-camera into feat/shader-packs-shadow-programs
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-26 03:00:08 +01:00
Ludovic MarieandClaude Sonnet 5.5 86a2f8e470 Merge feat/shader-packs-hand into feat/shader-packs-shadow-camera
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-26 02:58:53 +01:00
Ludovic Marie 9d07d5a3e2 Merge pull request #1755 from ludovic111/feat/shader-packs-mip-current
🤖 Agent: shader packs clean room: main merged into the stack (merge commits), verify passed on this head, parity skipped by parity-scope.
2026-10-26 02:58:43 +01:00
Ludovic Marie 298a9778a7 Merge pull request #1754 from ludovic111/feat/shader-packs-biomes
🤖 Agent: shader packs clean room: main merged into the stack (merge commits), verify passed on this head, parity skipped by parity-scope.
2026-10-26 02:58:14 +01:00
Ludovic MarieandClaude Sonnet 5.5 ffda62db09 Merge feat/shader-packs-held-items into feat/shader-packs-hand
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-26 02:53:39 +01:00
Ludovic Marie d4526191ce Merge pull request #1744 from ludovic111/feat/shader-packs-normals
🤖 Agent: shader packs clean room: main merged into the stack (merge commits), verify passed on this head, parity skipped by parity-scope.
2026-10-26 02:52:49 +01:00
Ludovic MarieandClaude Sonnet 5.5 ae3bbe6bb4 Merge feat/shader-packs-weather into feat/shader-packs-held-items
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-26 02:52:31 +01:00
Ludovic Marie b3fabc1743 Merge pull request #1699 from ludovic111/feat/shader-packs-run-globals
🤖 Agent: shader packs clean room: main merged into the stack (merge commits), verify passed on this head, parity skipped by parity-scope.
2026-10-26 02:51:12 +01:00
Ludovic MarieandClaude Sonnet 5.5 34397fe09d Merge feat/shader-packs-clouds into feat/shader-packs-weather
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-26 02:50:35 +01:00
Ludovic Marie c56102125b Merge pull request #1799 from cristibctr/codex/fix-1649-resource-pack-cache
🤖 Agent: Thanks cristibctr! Windows pack cache recovery; it bumps the cache version, so every platform rebuilds that cache once. Tested together with the other wave 1 contributor PRs on current main: fmt, clippy -D warnings and the horizon-client tests (1,120 lib tests and the rest) pass; the 114 harnesses the batch affects pass; lan_e2e anvil, grindstone, replant, instant_digging, digging, frames and leads pass with the host in-frame and on its server thread.
2026-10-26 02:50:17 +01:00
Ludovic MarieandClaude Sonnet 5.5 94c1b5bfaa Merge feat/shader-packs-translucent-entities into feat/shader-packs-clouds
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-26 02:49:32 +01:00
Ludovic MarieandClaude Sonnet 5.5 eab457d0ae Merge feat/shader-packs-entities into feat/shader-packs-translucent-entities
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-26 02:48:31 +01:00
Ludovic Marie b3bc8e807e Merge pull request #1946 from ludovic111/feat/prepare-output-version
🤖 Agent: verified: the launcher keeps prepared resources while the preparer's OUTPUT_VERSION (or a legacy fingerprint for v0.2.49 to v0.2.64) is the same. Unit tests and clippy pass. End to end with the real v0.2.64 release: an update to a build with OUTPUT_VERSION 1 reused its resources, and a build with the number bumped to 2 did not. Merging.
2026-10-26 02:47:48 +01:00
rdbtCVSandOpenCode be42e67e9d Merge origin/main into t3/fix-critical-hit-particles
Keep both critical-hit and replant regressions.

Co-Authored-By: OpenCode (agentrouter/gpt-6.1-sol) <[email protected]>
2026-10-26 01:45:16 +00:00
Ludovic MarieandClaude Sonnet 5.5 02248f4314 Merge feat/shader-packs-alpha-test into feat/shader-packs-entities
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-26 02:45:08 +01:00
Ludovic Marie dd1581c062 Merge remote-tracking branch 'origin/main' into cg15/1965
# Conflicts:
#	crates/horizon-prepare/src/lib.rs
2026-10-26 02:44:23 +01:00
Ludovic Marie 5450f5e872 Merge pull request #1698 from ludovic111/feat/shader-packs-terrain-draw
🤖 Agent: shader packs clean room 14 brought up to date with main (merge commit); verify passed on this head; parity skipped by parity-scope.
2026-10-26 02:44:07 +01:00
Ludovic MarieandClaude Sonnet 5.5 b0fd6e660e Merge feat/shader-packs-entity-spans into feat/shader-packs-alpha-test
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-26 02:41:50 +01:00
Ludovic Marie 5ba4793d35 Merge pull request #1960 from rdbtCVS/t3/optimize-launcher-resource-prepare-1
🤖 Agent: Thanks rdbt! Verified on the Mac mini:
2026-10-26 02:41:36 +01:00
rdbtCVSandOpenCode 5e2871dbfd Show critical-hit particles on server clients
Queue successful mob and player crit animations until the hub can send them to the attacker and tracking clients. Cover falling, grounded and refused attacks in both host modes. Fixes #1898.

Co-Authored-By: OpenCode (agentrouter/gpt-6.1-sol) <[email protected]>
2026-10-26 01:39:30 +00:00
Ludovic Marie f565f67ee5 Merge pull request #1958 from rdbtCVS/t3/item-frame-name-display
🤖 Agent: Thanks rdbt! Matches ItemFrameRenderer: a targeted frame shows its item's custom name. Tested together with the other wave 1 contributor PRs on current main: fmt, clippy -D warnings and the horizon-client tests (1,120 lib tests and the rest) pass; the 114 harnesses the batch affects pass; lan_e2e anvil, grindstone, replant, instant_digging, digging, frames and leads pass with the host in-frame and on its server thread.
2026-10-26 02:39:17 +01:00
Ludovic MarieandClaude Sonnet 5.5 91b0db48e7 Merge feat/shader-packs-sildurs into feat/shader-packs-entity-spans
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-26 02:38:42 +01:00
Ludovic MarieandClaude Sonnet 5.5 01ef17a258 Merge feat/shader-packs-mip-current into feat/shader-packs-sildurs
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-26 02:37:36 +01:00
Ludovic Marie 559ea59fcb Merge pull request #1952 from ludovic111/ci/linux-runner
🤖 Agent: ready. Linux runner support (runner-env/vanilla-data Linux paths, linux-runner.sh, runner_pause.py with Ludovic's lock/idle-15-min rule, heat, game, GPU and pause-file checks), desktop parity host, nightly and parity-main pinned to the Mac. verify and parity passed at this head. The desktop runner stays without the horizon-game label until its trial is identical: that is a runner setting, not in this PR. Desktop full run: 342/343 in 39.3 min (the 1 is a harness timing flake, fixed by #1997).
2026-10-26 02:36:59 +01:00
Ludovic Marie 09b3057610 Merge pull request #1926 from Domanffe/fix/enchantment-names-language
🤖 Agent: Thanks Domanffe! Enchantment names and levels follow the selected language; the server thread shares the client's language, and with none loaded (dedicated server, probes) the English names stay. I pushed a doc tweak (b7629cf65). Tested together with the other wave 1 contributor PRs on current main: fmt, clippy -D warnings and the horizon-client tests (1,120 lib tests and the rest) pass; the 114 harnesses the batch affects pass; lan_e2e anvil, grindstone, replant, instant_digging, digging, frames and leads pass with the host in-frame and on its server thread.
2026-10-26 02:36:25 +01:00
Ludovic MarieandClaude Sonnet 5.5 2203c1777a Merge feat/shader-packs-biomes into feat/shader-packs-mip-current
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-26 02:36:22 +01:00
Ludovic MarieandClaude Sonnet 5.5 04000cb4db Merge feat/shader-packs-normals into feat/shader-packs-biomes
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-26 02:35:21 +01:00
Ludovic MarieandClaude Sonnet 5.5 3f10f57178 Merge feat/shader-packs-block-ids into feat/shader-packs-normals
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-26 02:34:28 +01:00
Ludovic Marie 894576e1e9 Merge pull request #1914 from socksprox/fix/mod-ui-screen-capture
🤖 Agent: Thanks socksprox! Refactor of the mod screen widgets plus a mouse recapture when a mod's screen goes away. Tested together with the other wave 1 contributor PRs on current main: fmt, clippy -D warnings and the horizon-client tests (1,120 lib tests and the rest) pass; the 114 harnesses the batch affects pass; lan_e2e anvil, grindstone, replant, instant_digging, digging, frames and leads pass with the host in-frame and on its server thread.
2026-10-26 02:34:15 +01:00
Ludovic MarieandClaude Sonnet 5.5 2b05e2809b Merge feat/shader-packs-arena-storage into feat/shader-packs-block-ids
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-26 02:32:06 +01:00
Ludovic Marie a83cf01169 Merge pull request #1943 from socksprox/fix/particle-atlas-reload
🤖 Agent: Thanks socksprox! Particles keep their sprites across a resource reload (content root set again on reload). Tested together with the other wave 1 contributor PRs on current main: fmt, clippy -D warnings and the horizon-client tests (1,120 lib tests and the rest) pass; the 114 harnesses the batch affects pass; lan_e2e anvil, grindstone, replant, instant_digging, digging, frames and leads pass with the host in-frame and on its server thread.
2026-10-26 02:31:57 +01:00
Ludovic MarieandClaude Sonnet 5.5 b023ab934a Merge feat/shader-packs-sun-moon into feat/shader-packs-arena-storage
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-26 02:30:35 +01:00
Ludovic Marie 834a84f198 Merge pull request #1945 from rdbtCVS/t3/fix-anvil-combining-sound
🤖 Agent: Thanks rdbt! Matches AnvilMenu, which sends the level event with a null player so every player hears it once; lan_e2e's anvil check heard it exactly once on the host and once on the guest. Tested together with the other wave 1 contributor PRs on current main: fmt, clippy -D warnings and the horizon-client tests (1,120 lib tests and the rest) pass; the 114 harnesses the batch affects pass; lan_e2e anvil, grindstone, replant, instant_digging, digging, frames and leads pass with the host in-frame and on its server thread.
2026-10-26 02:29:05 +01:00
Ludovic MarieandClaude Sonnet 5.5 181d188324 Merge feat/shader-packs-sky into feat/shader-packs-sun-moon
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-26 02:28:17 +01:00
Ludovic MarieandClaude Opus 5.5 d3af1e04e0 Mannequins run no mob goals and never despawn
A mannequin is an Avatar (a LivingEntity), not a Mob: its serverAiStep has
no goals and LivingEntity has no checkDespawn. With the passive brain a
summoned mannequin wandered off (14 blocks in 400 ticks on the server
thread) and could despawn far from players.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 02:27:37 +01:00
Ludovic Marie 93d58b7024 Merge pull request #1954 from rdbtCVS/rdbt/fix-1949-leash-death
🤖 Agent: Thanks rdbt! Matches Leashable.tickLeash / dropLeash: a leashed mob that dies drops its lead; the new mob_ai scenarios run against the vanilla oracle. Tested together with the other wave 1 contributor PRs on current main: fmt, clippy -D warnings and the horizon-client tests (1,120 lib tests and the rest) pass; the 114 harnesses the batch affects pass; lan_e2e anvil, grindstone, replant, instant_digging, digging, frames and leads pass with the host in-frame and on its server thread.
2026-10-26 02:26:53 +01:00
Ludovic Marie d4e1b3c773 Merge pull request #1922 from rdbtCVS/t3/fix-crop-planting-harvesting
🤖 Agent: Thanks rdbt! Matches ServerPlayerGameMode.handleBlockBreakAction: an instant survival START never leaves a block being destroyed, so no repeated hit sounds and a replanted crop isn't broken again. It now includes #1966's sound regression. Tested together with the other wave 1 contributor PRs on current main: fmt, clippy -D warnings and the horizon-client tests (1,120 lib tests and the rest) pass; the 114 harnesses the batch affects pass; lan_e2e anvil, grindstone, replant, instant_digging, digging, frames and leads pass with the host in-frame and on its server thread.
2026-10-26 02:25:58 +01:00
Ludovic MarieandClaude Sonnet 5.5 f299e18fc6 Merge feat/shader-packs-draw-uniforms into feat/shader-packs-sky
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-26 02:25:46 +01:00
Ludovic Marie 1da949efbd Merge pull request #1919 from sl4ppleware/fix/heart-blink-on-set-health
🤖 Agent: Thanks sl4ppleware! Matches LocalPlayer.hurtTo: the hearts blink again when hurt, and the remote client ticks the blink through its controlled simulation tick. Tested together with the other wave 1 contributor PRs on current main: fmt, clippy -D warnings and the horizon-client tests (1,120 lib tests and the rest) pass; the 114 harnesses the batch affects pass; lan_e2e anvil, grindstone, replant, instant_digging, digging, frames and leads pass with the host in-frame and on its server thread.
2026-10-26 02:25:31 +01:00
Ludovic MarieandClaude Sonnet 5.5 0f1ab46921 Merge feat/shader-packs-mipmaps into feat/shader-packs-draw-uniforms
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-26 02:24:37 +01:00
Ludovic MarieandClaude Sonnet 5.5 f1fc01ffcf Merge feat/shader-packs-timing into feat/shader-packs-mipmaps
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-26 02:23:46 +01:00
Ludovic Marie 80e1eb20cc Merge pull request #1883 from ludovic111/fix/copper-golem-statue-poses
🤖 Agent: merged after verify (bugs-dezerx-1's fix for a player's bug list, self-checked with pr_ready); harnesses run on main by parity-main, which gates releases.
2026-10-26 02:22:56 +01:00
Ludovic MarieandClaude Sonnet 5.5 4ee54d84a8 Merge feat/shader-packs-water into feat/shader-packs-timing
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-26 02:22:48 +01:00
Ludovic Marie e93aba600b Merge pull request #1941 from rdbtCVS/t3/preserve-gui-scale-setting
🤖 Agent: Thanks rdbt! Matches vanilla: the saved GUI scale isn't clamped while a temporary small surface is up, the HUD caps the scale it draws with. Tested together with the other wave 1 contributor PRs on current main: fmt, clippy -D warnings and the horizon-client tests (1,120 lib tests and the rest) pass; the 114 harnesses the batch affects pass; lan_e2e anvil, grindstone, replant, instant_digging, digging, frames and leads pass with the host in-frame and on its server thread.
2026-10-26 02:20:26 +01:00
Ludovic MarieandClaude Opus 5.5 24b26da5d6 server_ops_feedback: wait for the save's reply before the next step
On the desktop runner (8 harnesses at a time) vanilla's save-all took 2 s,
the whole step: the transcript ended before "Saved the game" and the
harness failed with Horizon right. The save step now waits for that console
line, on both sides, before its usual 2 s.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 02:19:17 +01:00
Ludovic Marie 3bea1cd023 Merge pull request #1937 from rdbtCVS/fix/android-helper-scheduling
🤖 Agent: Thanks rdbt! Android-only cfg change to helper thread priorities; the desktop targets compile the same code as before. Tested together with the other wave 1 contributor PRs on current main: fmt, clippy -D warnings and the horizon-client tests (1,120 lib tests and the rest) pass; the 114 harnesses the batch affects pass; lan_e2e anvil, grindstone, replant, instant_digging, digging, frames and leads pass with the host in-frame and on its server thread.
2026-10-26 02:17:46 +01:00
Ludovic MarieandClaude Sonnet 5.5 e5f4d02549 Merge feat/shader-packs-real-buffers into feat/shader-packs-water
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-26 02:16:49 +01:00
Ludovic Marie a7d1d17ac9 Merge pull request #1991 from ludovic111/fix/pr-ready-affected-once
🤖 Agent: merged on verify: tooling only, it changes how fast harnesses are chosen, not which game code runs. Fixes pr_ready's 14+ min planning (affected_harnesses was called once per harness) and bounds the selection.
2026-10-26 02:15:09 +01:00
Ludovic Marie 3093f22a8a Merge pull request #1903 from rdbtCVS/fix/clear-offhand-slot-order
🤖 Agent: Thanks rdbt! Checked against vanilla's Inventory order (armour FEET to HEAD, then the off hand) when items are cleared. Tested together with the other wave 1 contributor PRs on current main: fmt, clippy -D warnings and the horizon-client tests (1,120 lib tests and the rest) pass; the 114 harnesses the batch affects pass; lan_e2e anvil, grindstone, replant, instant_digging, digging, frames and leads pass with the host in-frame and on its server thread.
2026-10-26 02:14:29 +01:00
Ludovic MarieandClaude Sonnet 5.5 f1a3409721 Merge feat/shader-packs-custom-uniforms into feat/shader-packs-real-buffers
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-26 02:14:28 +01:00
Ludovic MarieandClaude Opus 5.5 7b0192118e parity: choose harnesses faster (shared parse cache, early stop, time budget)
- The reach parse cache lives in the shared parity folder, keyed by content,
  so a new worktree starts warm (written atomically, other worktrees'
  entries kept within bounds).
- A harness's reach search stops as soon as it reaches a changed file that
  differs in more than its `mod` declarations; partial searches are not
  memoized. Declaration diffs are memoized (one git show per file).
- A harness whose changed dependency is outside crates/ is picked without a
  search (narrowing always keeps those).
- After PARITY_REACH_BUDGET seconds (default 180) the remaining harnesses
  count every source their probes link.
- crate_of is cached; no pathlib in the hot paths.

Same 79 harnesses on the 19-file diff from contrib-gate-16; CPU 19.9 s cold /
16.6 s warm before, 13.1 s cold / 9.9 s warm after (Mac mini under load).

parity.yml: skip the prepare_output check for heads made before #1947 (no
OUTPUT_VERSION, no tool): the job runs main's workflow on the head's files.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 02:13:09 +01:00
Ludovic MarieandClaude Sonnet 5.5 ff3d57ea9c Merge feat/shader-packs-run-globals into feat/shader-packs-custom-uniforms
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-26 02:12:44 +01:00
Ludovic Marie f10a7cd880 Merge pull request #1994 from ludovic111/fix/vanilla-data-failed-prepare
🤖 Agent: ready. A failed horizon-prepare now leaves nothing in .local, and stale real .local/horizon-client* folders and preparations lacking horizon-client are cleaned at job start. Tested on a fake checkout (fail, stale, cached). verify passed; parity skipped (scope). No overlap with #1965.
2026-10-26 02:12:07 +01:00
Ludovic MarieandClaude Sonnet 5.5 b73ca494a8 Merge feat/shader-packs-terrain-draw into feat/shader-packs-run-globals
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-26 02:11:28 +01:00
Ludovic Marie b10d927e5e Merge pull request #1929 from ludovic111/feat/mod-render
🤖 Agent: Mod API capability render (experimental): HUD shapes and camera shake. Main merged in beside command (8 capabilities). Verified: horizon-mods tests, clippy with and without mods, earlier pr_ready, harnesses and in-game runs.
2026-10-26 02:10:10 +01:00
Ludovic MarieandClaude Opus 5.5 a9b5ca3936 Merge main: test beside main's new templates
The usage line, the docs and the website list `test` with main's
`new --template code|content`; the code template keeps tests/hello.toml.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 02:08:41 +01:00
Ludovic MarieandClaude Opus 5.5 07f1bcff68 Merge main: render beside main's command capability
Both add a Capability variant: the list holds 8 now. The SDK re-exports
both interfaces; the API reference is regenerated.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 02:02:00 +01:00
Ludovic MarieandClaude Sonnet 5.5 2a85b6a432 Merge origin/main into fix/remote-tnt
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-26 01:59:53 +01:00
Ludovic MarieandClaude Sonnet 5.5 48121cb109 Merge origin/main into chore/wasmtime-48
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-26 01:59:44 +01:00
Ludovic MarieandClaude Sonnet 5.5 b222dd9194 Merge origin/main into feat/server-dialogs
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-26 01:59:19 +01:00
Ludovic MarieandClaude Sonnet 5.5 35e047fb8e Merge origin/main into feat/yung-port-as-written
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-26 01:59:00 +01:00
Ludovic MarieandClaude Opus 5.5 d782e455a4 docs: the desktop's CI slice gets half the threads
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 01:55:57 +01:00
Ludovic Marie ee403545ce Merge pull request #1805 from kacpherek/pr/pregen
🤖 Agent: thank you kacpherek! /pregen is in, off by default: a server turns it on with horizon-pregen=true in server.properties (server.toml [properties]), single player and LAN with --pregen. With it off, the command tree and /help match vanilla. Tested on a dedicated server through horizon-server (on: 1,369 chunks generated and saved), single player on the server thread and in-frame, and 11 harnesses including command_exec.
2026-10-26 01:54:20 +01:00
Ludovic MarieandClaude Opus 5.5 160c1e8b7a CI: a failed preparation leaves nothing in .local
When horizon-prepare failed, vanilla-data.sh deleted the preparation folder but kept what it had
already written to .local/ (horizon-client, horizon-client-motion, ...). The next job then saw those
real folders as already there: its preparation listed only horizon-client-structures in .entries,
and the checkout kept using the stale resources outside the per-key cache.

Now a failed preparation removes the .local/ entries it created; a job first removes real folders
named like preparation outputs, and makes again a preparation whose .entries lacks horizon-client.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 01:54:02 +01:00
Ludovic Marie 98f5a811cc Merge pull request #1829 from ludovic111/fix/global-level-events
🤖 Agent: merged after verify (bugs-dezerx-1's fix for a player's bug list, self-checked with pr_ready); harnesses run on main by parity-main, which gates releases.
2026-10-26 01:50:29 +01:00
Ludovic MarieandClaude Sonnet 5.5 7bbd715c8c Merge origin/main into feat/shader-packs-terrain-draw
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-26 01:48:29 +01:00
Ludovic MarieandClaude Opus 5.5 22980fc8cc Extras: the FPS counter and keystrokes move to the mod browser
They are official mods now (#1982). The Extras page loses their rows and
the HUD its built-in drawing. A player who had one on gets the mod on the
first start: installed from Horizon's registry, checked as the launcher
checks it (size, SHA-256, signature, manifest), turned on, with the
corner and options carried into the mod's storage. One chat line says so
("FPS counter moved to the mod browser: installed for you"). If it can't
be installed (offline), a line says Horizon tries again at the next
start. horizon-extras.txt keeps the old keys and lists what was done in
movedToMods.

The client's horizon-mods dependency gains the `index` feature for the
check. The frame and click counters now run only for mods.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 01:41:18 +01:00
Ludovic MarieandClaude Opus 5.5 49bbdf8954 CI: the desktop's CI slice gets half the threads
On all 24, a cargo build held ludodesktop's CPU at 92 °C and the watcher kept pausing; at half,
harnesses run at a median of 80 °C.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 01:38:23 +01:00
Alexander KrylovandHermes Agent 258dd8561d Frames: one scan for breaking progress, and no name formatting per crack
`entity_geometry` ran `cracks()` twice per frame: once for the block-entity
pass that draws `render_breaking`, once for the overlay near the end of the
same function. Each run walked `mining` and the `destroying` map, and formatted
`block/destroy_stage_<n>` for every crack it found before looking that string up
in the atlas.

The list is computed once, at the top of the function, and the passes read it.
The ten stage names are a const table, so a crack costs one lookup and no
allocation. Nothing between the two passes touches `mining` or `destroying`,
which are only read there, so the list the second pass used to rebuild was
always identical to the first one.

No behaviour change: the same ten sprite names, and a stage above 9 still finds
no sprite and skips the crack.

Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
2026-10-26 03:34:33 +03:00
Ludovic MarieandClaude Opus 5.5 bdbf67ef93 pr_ready: work out the affected harnesses once, not once per harness
affected_here called parity.affected_harnesses inside the filter, once for
each of the 343 harnesses. Since the selection reads the Rust items each
probe reaches (#1964) one call takes about a minute and a half, so
pr_ready sat for hours before its first step.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 01:31:38 +01:00
Joansitoh 060a37dc85 Edit the remaining text fields at the caret.
Those fields only appended, so the cursor, selection and arrow keys did nothing. A sign paste strips formatting first, matching TextFieldHelper.getClipboardContents.
2026-10-26 01:29:07 +01:00
Ludovic MarieandClaude Opus 5.5 9db6a0ee48 CI: parity-main stays on the Mac; heat check on a median, boot as the first sign; nix roots fixed
nix-build names a dev output's GC root <name>-dev, so the runner's PKG_CONFIG_PATH pointed at links
that didn't exist and every client build failed on libudev; it now uses the store path nix-build
prints. The package temperature spikes for a second under load, so the heat check takes the median
of 5 readings. Restarting the watcher no longer throttles a running job.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 01:23:27 +01:00
Ludovic MarieandClaude Opus 5.5 4c78d1b4f8 Merge origin/main into ci/linux-runner
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 01:23:12 +01:00
Ludovic MarieandClaude Opus 5.5 045bda8bfc release: give the exporters job an hour, as the Mac builds
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 01:21:20 +01:00
Ludovic Marie b3149d43ef Merge pull request #1677 from sdraeger/feat/mod-command-capability
🤖 Agent: Mod API capability `command` and the Predator Missile example, by @sdraeger. Thank you! Main merged in, with `command` kept beside `ui` as a 0.1 preview interface. Verified: pr_ready (2106 workspace tests), and in game in-frame and on the server thread, including survival without cheats refused.
2026-10-26 01:19:35 +01:00
Ludovic MarieandClaude Opus 5.5 8b26c4cf34 Ring search thread: start once generation pauses
In the idle class the thread still slowed a fresh world's first 600 chunks
by about 3 s on a 4-thread laptop (it shares a physical core with a worker).
It now waits until no proto chunk was generated for 2 s, or a minute after
the world opened, and gives up if the world closes first.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 01:14:39 +01:00
DanielGodriNetoandClaude 467413df5b fix(benchmark): wait for the post-teleport load before measuring
wait_loaded declared a scene loaded during the idle second right after a teleport, so village and mountain were measured with 0 chunks. Port the guard from tools/perf_bench.py: counts must change once (or stay full for 5 s) and resident chunks must reach 80% of the view circle.

Co-Authored-By: Claude <[email protected]>
2026-10-25 21:13:23 -03:00
Ludovic MarieandClaude Opus 5.5 bb3a289caa Read the click's changed slots for the machine's setChanged
The menu op already lists the slots the click changed; a container slot
among them is the Slot.setChanged that saves the block entity.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 01:07:48 +01:00
Ludovic MarieandClaude Opus 5.5 7dfd4c6280 Share the player's water and lava tags with dropped items
The item's fluid world had its own copy of the FluidTags.WATER/LAVA
membership the player's immersion uses; motion now exports that one.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 01:04:20 +01:00
Ludovic MarieandClaude Opus 5.5 19a5098b14 Leave a respawned body's reset to the swap that builds it
Respawning drops the guest's body, so the next swap builds a fresh one,
which now clears what the old body was given (equipment, effects and
armor protection) itself.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 01:03:41 +01:00
Ludovic MarieandClaude Opus 5.5 31eecdc271 Compute a firework's blast distance in floats, as Entity.distanceTo does
Entity.distanceTo casts each offset to float and sums the squares in
floats; taking the double distance and rounding it gave a slightly
different damage at the edge of rounding.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 01:03:25 +01:00
Ludovic MarieandClaude Opus 5.5 1dd47581de Ring search thread: the idle class, not behind the server
On a 4-thread laptop the background search at nice +10 took a share of the
cores from the chunk workers: 600 chunks arrived 3-4 s later. The thread now
runs only on cores nothing else wants (Linux SCHED_IDLE, macOS background
QoS, Windows idle priority): horizon_threads::idle_current.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 00:59:15 +01:00
socksproxandDevin 920a728989 World text parity: same-frame glyph uploads, display lines, name-tag colours
Atlas patches rasterized while building world text (display text, name
tags, sign glyphs, map items) drained into the HUD batch before
entity_geometry ran, uploading a frame late: for that frame the quads
sampled unwritten atlas cells, so text flickered or showed the wrong
glyphs whenever content changed or new text entered view.

Display text also lined up 10 px apart where vanilla's lineHeight is 9,
its background quad stopped a pixel short of vanilla's -1..width+1 fill,
and name tags used 0x7F where renderNameTag's colours are 0x20FFFFFF
(discrete) and 0x80FFFFFF (see-through). lightCoordsWithEmission floors
only the block channel at 2 and leaves the sky alone, and a discrete
name's background draws lit in the normal pass rather than unlit.

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-26 00:56:00 +01:00
Ludovic Marie e1a7892361 Merge pull request #1985 from ludovic111/perf/parity-disk
🤖 Agent: merged on verify. CI speed (Ludovic's focus tonight); measurements in the body and in horizon-parity/ci-speed/LOG.md.
2026-10-26 00:52:57 +01:00
Ludovic Marie e9f93c656a Merge pull request #1981 from ludovic111/ci/parity-main
🤖 Agent: merged on verify. CI speed (Ludovic's focus tonight); measurements in the body and in horizon-parity/ci-speed/LOG.md.
2026-10-26 00:48:22 +01:00
Ludovic MarieandClaude Opus 5.5 eec2e87a15 Merge main: command beside main's ui capability
Conflicts kept both sides: the capability list (now 7), the WIT, the
SDK's re-exports and docs, the mods state's fields, the workspace
members and AGENTS.md's macOS --exclude list. The API reference is
regenerated, and docs/api/stability.md lists `command` with the 0.1
preview interfaces.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 00:48:02 +01:00
Ludovic Marie 373d9c08c0 Merge pull request #1947 from ludovic111/feat/prepare-output-check
🤖 Agent: verified: OUTPUT_VERSION = 1 and output-versions.txt carry the digest measured on 10 CI preparations. tools/prepare_output.py check passes on v0.2.64's preparation and fails on v0.2.47's. This head's own parity job prepares with its preparer and checks the digest. Merging.
2026-10-26 00:47:05 +01:00
Ludovic Marie 911ca721de Merge pull request #1983 from ludovic111/perf/cover-test-debug
🤖 Agent: merged on verify. CI speed (Ludovic's focus tonight); measurements in the body and in horizon-parity/ci-speed/LOG.md.
2026-10-26 00:46:46 +01:00
Ludovic MarieandClaude Opus 5.5 5034e4557d parity: clear rust-<name>-parity outputs after a pass, reuse oracles on CI
Disk: clear_large_outputs only looked at .local directories named after the
harness, so the 174 harnesses writing to rust-<name>-parity kept every dump
(86 GB in one runner checkout, 13 GB for state_provider alone). It now
clears those too, keeps vanilla outputs only up to 256 MB (larger ones cost
more disk than rerunning their oracle), and handles shard names.
collision, world and geometry dropped one mkdtemp run directory per run
(11 GB): each run now removes the earlier ones.

Time: harnesses inherited the runner's stdin, a pipe on GitHub's runner, and
parity_shim never reuses an oracle run whose stdin is a pipe, so oracle reuse
never worked on CI. Harnesses now get /dev/null.

CI: parity.yml falls back to the head's own selection when the names main's
`--list` printed don't match its summary count.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 00:44:22 +01:00
Ludovic MarieandClaude Opus 5.5 40f79354d6 release: build the preparer and compile the exporters fresh in the exporters job
The clean checkout removes target/, and vanilla-data.sh builds horizon-prepare only when it
prepares afresh, so compile-exporters had no binary on a cached preparation. The bundle is
also compiled in the job instead of reused from .local/exporters, which vanilla-data.sh links
to preparations that pull requests' parity jobs write, and the job fails if anything but our
classes and the manifest would leave the runner.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 00:40:43 +01:00
rdbtCVSandCodex gpt-6.1-sol 2d088739e5 Fix dropped items following fluid currents
Sample item immersion and apply shared entity currents before and after movement, matching vanilla tick order. Cover item-sized fluid scans and flowing-water movement.

Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-10-25 23:40:17 +00:00
Ludovic MarieandClaude Opus 5.5 0a8af38ac4 Rings: search them on a thread behind the server, as the world opens
The previous commit decides chunks far from the rings without them. The
128 biome searches now start on a helper thread as an overworld with
structures opens, behind the server and render threads, so an eye of ender
or a chunk near a ring finds them done or shares what is left, instead of
running them all on the thread that asked.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 00:39:18 +01:00
Ludovic MarieandClaude Opus 5.5 40c08c2d12 streaming: check every fourth cell of the cover test in debug builds
the_grid_answers_as_the_scan compared the grid with the scan on every cell of
60 cases and took 20 of the ~30 s of `cargo test -p horizon-client` on the
ThinkPad. A debug build now checks every fourth cell of each row, shifted by
row and case, so the cases together still meet every cell position relative
to the areas; a release build checks every cell.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 00:38:47 +01:00
Ludovic MarieandClaude Opus 5.5 73da2f04dd CI: parity on main, a batch of merges at a time; releases wait for it
parity-main runs the vanilla harnesses on every push to main, one run at a
time: merges that land during a run wait in one pending run, which checks
every merge since main's last green parity-main (tools/parity_main.py base,
then parity.py --changed). A failure is bisected over the batch's
first-parent merges, running only the failing harnesses, and the breaking
merge's pull request gets a comment.

Game releases ship only a commit both checks and parity-main passed on
(release_train.py; release.yml also starts when parity-main finishes; a
forced release doesn't wait). The per-PR parity job now runs only for a PR
that changes a harness, an oracle, a probe or the runner, or carries
run-parity.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 00:35:28 +01:00
rdbtCVSandCodex 0b970c0c9a Cover instant-break sounds alongside replanting
Move the instant_digging regression from redundant PR #1966 into the existing stale-dig fix for #1880 and #1908.

Co-Authored-By: Codex (gpt-6.1-sol) <[email protected]>
2026-10-25 23:35:22 +00:00
Ludovic Marie 39efe6ef39 Merge pull request #1968 from ludovic111/ci/parity-select-from-main
🤖 Agent: merged on verify. It changes only which harnesses CI picks: main's item-level selection for every queued branch. I checked on main that the --list format gives the names line by line, then the summary line.
2026-10-26 00:33:51 +01:00
Ludovic Marie da66056bd5 Merge remote-tracking branch 'origin/main' into perf/behaviour-cache-hash-once 2026-10-26 00:33:29 +01:00
Ludovic Marie d5699d5f90 Merge remote-tracking branch 'origin/main' into perf/sculk-ticking-chunks 2026-10-26 00:33:28 +01:00
Ludovic Marie 7092d5f4d7 Merge remote-tracking branch 'origin/main' into perf/rings-out-of-reach 2026-10-26 00:33:28 +01:00
Ludovic MarieandClaude Opus 5.5 e5403c0224 Merge origin/main into fix/item-cooldowns
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 00:33:20 +01:00
Ludovic Marie 29c09b4bf2 Merge pull request #1794 from ludovic111/perf/paletted-locate-no-div
🤖 Agent: verify and parity pass on 40c77c02f; −12 to −14% per read of 5–7-bit sections (body). Queued by perf-next-2.
2026-10-26 00:32:09 +01:00
Ludovic Marie 1fd42f311f Merge remote-tracking branch 'origin/main' into perf/collision-scan-edges 2026-10-26 00:31:46 +01:00
Ludovic Marie 2189baea6d Merge pull request #1778 from ludovic111/perf/worker-count
🤖 Agent: verify and parity pass on 1437537cb; spawn load −15% and teleports −12% on a 4-thread laptop (measurements in the body). Queued by perf-next-2.
2026-10-26 00:29:11 +01:00
Ludovic MarieandClaude Opus 5.5 836c1ee3b9 CI parity: pick harnesses with main's selection code, run them at the head
The parity job checks out the PR head, so every branch made before #1964
still selected with the old per-crate rule. The step now runs main's
tools/parity.py and tools/parity_reach.py with --changed --list, restores the
head's files, and runs the named harnesses with the head's parity.py. A
branch that edits either file, or a failure of main's selection, falls back
to the head's own `--changed` run.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 00:27:32 +01:00
Ludovic MarieandClaude Opus 5.5 0baf6cbd62 CI: the desktop runner works while its screen is locked or untouched for 15 min
Ludovic's rule (2026-10-26): runner_pause.py pauses at once on keyboard, mouse or gamepad input
(read from /dev/input, only whether bytes came), on an unlocked session touched in the last
15 min, at 90 °C, on a game or the GPU. Fixes the service start: the link loop's last test
failed the script under set -e.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 00:27:22 +01:00
Ludovic MarieandClaude Opus 5.5 ef5361e4c2 Creatures: drop the doc comments the removed cooldown fields left behind
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 00:26:17 +01:00
Ludovic Marie cc7534341c Merge pull request #1964 from ludovic111/perf/ci-speed-selection
🤖 Agent: merged on verify. It changes only which harnesses run, not the game; evidence in the body: all 61 failed parity runs of 10-18..10-25 are still caught, and LLVM coverage on 6 harnesses confirms the selection. Its own parity selects no harness. CI speed is Ludovic's priority tonight.
2026-10-26 00:23:09 +01:00
some-du6eandClaude Opus 5.5 97e78e0f11 horizon-prepare: find 26.3 in ATLauncher's and the CurseForge app's folders
ATLauncher saves Mojang's version JSON as is in configs/json/minecraft and
the client jar as the library net.minecraft:client:<version>, with the
libraries at their version JSON paths and the assets and Java runtimes in
Mojang's layouts. horizon-prepare now reads that layout, in the chosen
folder, the launcher above a chosen instances/<name> folder, or, with no
folder chosen, ATLauncher's default folder on each system after the
official launcher's, then the CurseForge app's Install folder. The lock's
pins gate it as before, and nothing is downloaded. The error names every
folder looked in and the launchers Horizon reads.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 17:22:32 -06:00
Kacpherek a6548089c4 Count the death roll in ticks so the dead camera eases instead of shaking
The camera's death roll and zoom (GameRenderer.bobHurt, Camera.modifyFovBasedOnDeathOrFluid) read deathTime + partialTick. Horizon derived deathTime from the wall clock since the death message, which the server thread path never set: there deathTime stayed 0, so every frame used 0 + partialTick and the view rolled from 0 to 0.2 degrees and snapped back twenty times a second (with a matching zoom pulse) instead of settling at vanilla's 3.6 degrees. In frame, the wall-clock ticks were not in step with the frame's partial tick either.

deathTime is now the tick counter LivingEntity.tickDeath keeps: the in-frame tick already counted it, and the server thread path now resets it when the death screen opens and counts it once per client tick. horizon-ctl state reports it as player.death_time.
2026-10-26 00:20:39 +01:00
Kacpherek bbc57aa838 Save furnace, brewing stand and crafter edits made through a server menu
A server player's clicks in a furnace, brewing stand or crafter menu wrote
the block entity's slots but never marked it changed, so the chunk was
not saved: an idle furnace (nothing burning, so its own tick changes
nothing) given or emptied through its menu rolled back on reopen or
chunk unload, losing or duplicating the items. Comparators kept the old
reading too.

When the click changed the container's slots, call container_changed
(BlockEntity.setChanged, reached through Slot.setChanged in vanilla's
AbstractContainerMenu.doClick and quickMoveStack), as chest-like menus
already do.
2026-10-26 00:20:26 +01:00
Kacpherek d143666e46 Mark a server's player dead while its arrival in a level is loading
A server's player on a trip into another level was skipped by
tick_guests before its zero-health body was marked dead, so a death
during the arrival (a /kill, damage) only counted once the destination
had loaded. The client already showed the death screen, and a Respawn
press in between was refused (guest_respawn wants a dead player) and
then lost, leaving the player on the death screen until a second press.

LivingEntity.hurt calls die at once, loaded or not: the death half of
the guest tick is now its own guest_die, which also runs for a player
on a trip.
2026-10-26 00:19:55 +01:00
socksproxandDevin ea1c0a4490 Render mannequin entities (server NPCs)
Servers on 26.3 spawn NPCs as minecraft:mannequin — the avatar entity
vanilla added for exactly this — instead of fake players (gommehd.net's
"Gommunity" lobby NPCs are invisible today). The client never registered
the type: MobTypes.load only builds species::ALL, so AddEntity produced a
tracked entity but nothing to draw.

- EntityDataSerializers.RESOLVABLE_PROFILE (41) decodes into
  Value::Profile. Before, the entry list ended at it and the mannequin's
  DATA_IMMOVABLE and DATA_DESCRIPTION after it never decoded.
- species::ALL gains mannequin (player#main model, Humanoid anim), and
  the player-only render paths — skin texture, sleep pose,
  AvatarRenderer's 0.9375 scale — take an avatar() check instead of the
  id string.
- SetEntityData's apply stores DATA_PROFILE on the mob, and
  update_remote_skins resolves its textures property through
  PlayerSkinRenderCache's rules (unsigned allowed, SkinManager's
  insecure path) the way a tab player's skin resolves.
- Mannequins get the shared-flags crouch and sleeping-position handling
  other players already had.

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-26 00:19:44 +01:00
Kacpherek c7432c21e1 Check a server player's moves with the box of its pose
A player sprint-swimming up under a ceiling was sent back every tick on the server thread (and for LAN guests): the server never updated the player's pose, and handlePlayerPositionChange's collision check always used a standing 1.8-block box, so the 0.6-block swimming box the client moved with looked like it entered the ceiling. Player.tick ends with updatePlayerPose, and the check uses getBoundingBox, so run updatePlayerPose in the server player's tick and check moves with the pose's dimensions. Fixes #1899.
2026-10-26 00:17:03 +01:00
Kacpherek a7ff22eea0 Give a server player's new body its effects at once after a level change
A server's player gets a new body when it joins, respawns or changes
level, but what the old body had been given by its effects (the applied
attribute modifiers and the armor protection table) was kept in its
per-player record. apply_effect_status then saw nothing to change, so
after a trip to another dimension Speed kept its icon while the body's
movement speed fell back from 0.14 to 0.10, until the effect was
reapplied.

Vanilla keeps the same ServerPlayer through ServerPlayer.teleport into
another level, with its attributes and MobEffect modifiers. A new body
now starts with nothing applied, and its effects are applied as soon as
it is created, before the movement that brought it is handled.
2026-10-26 00:16:57 +01:00
Kacpherek 2c0d9cc30e Tick End gateways' server copies on a server without a window
A used End gateway never worked again on the server thread or on a
dedicated server. TheEndGatewayBlockEntity's server ticker (portalTick)
ran only inside the client block-entity pass, which returns at once when
there is no renderer, so the 40 tick cooldown that
EndGatewayBlock.entityInside starts (triggerCooldown) never ran out.

A Game without a renderer now runs portalTick on the gateways it keeps a
server copy of, in block-ticking chunks as Level.tickBlockEntities does,
and sends triggerCooldown's block event every 2400 ticks of age.
2026-10-26 00:16:50 +01:00
Kacpherek f988ed417a Drop a pending server trip when its player is teleported within the level
A server's player sent into another level waits on a trip until the
chunks around its target load, and the server leaves its body alone and
skips its portal checks until then. A /tp within the level while the
arrival was still loading left that trip behind: its target lost its
loading tickets and never finished, so Nether, End and gateway portals
stopped taking the player until they went back to the abandoned spot.

ServerPlayer.teleport within a level places the player at once, so the
pending arrival is now abandoned. server-dimension-players gains the
case: sent far into the Nether, teleported back near the origin before
the far chunks load, then an End portal there.
2026-10-26 00:15:54 +01:00
rdbtCVSandCodex 3adff150cf Record preparation validation timings and measurement limits
Co-Authored-By: Codex <[email protected]>
2026-10-25 23:15:40 +00:00
Ludovic MarieandClaude Opus 5.5 1e6f05ed5a Chat option: filter the chat's lines both ways, as setVisibleMessageFilter
The chat keeps where each message came from and the Chat option's
abilities. A message the option hides is still dropped on arrival, but a
change of the option now also hides the lines already in the chat that
it no longer shows, and brings them back when it shows them again
(ChatComponent.setVisibleMessageFilter -> refreshTrimmedMessages), on
apply_settings. Every Chat::add names its source. The Restrictions
screen reads its two flags from ChatAbilities instead of its own copy.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 00:15:37 +01:00
Ludovic MarieandClaude Opus 5.5 21d8e53034 parity: pick harnesses by the Rust items their probes reach, not whole crates
`--changed` / `--changed-files` now narrow each harness's recorded Rust
sources to those its probes can reach (tools/parity_reach.py): an item-level
reachability walk from the probe modules a harness ran (or the binary it
built) over free items, inherent methods, trait impls and macros, matched by
name so it only over-approximates. Parent modules count through their `mod`
declarations, compared with the merge base. A harness whose probe can't be
placed keeps every source; `--by-crate` turns the narrowing off.

On the last 20 PRs merged on 2026-10-25: 2255 -> 1061 harnesses picked,
2384 -> 1592 harness-minutes. Replaying the week's 61 failed CI parity runs,
every failed harness the old selection picked is still picked; LLVM coverage of
six harnesses' probes executes only kept files.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 00:14:51 +01:00
Kacpherek 21019afc2b Vehicle storage menus edit the vehicle's own items
A chest minecart, hopper minecart or chest boat's menu edits a stand-in
container, which is meant to be copied back to the vehicle when the menu
closes. close_container took the open container before calling
close_cart_container, which then found nothing open and copied nothing:
items taken out came back on the next open, and items put in vanished.

Copy the stand-in back from the key close_container took. A server
player's clicks in such a menu copy it back at once, since the menu is
the vehicle's own inventory (ContainerEntity, AbstractMinecartContainer
and ChestBoat implement the container the menu reads and writes).
2026-10-26 00:14:27 +01:00
Kacpherek 785e83feeb Firework explosions hurt nearby players, not only the boosted one
FireworkRocketEntity.dealExplosionDamage hurts every LivingEntity within 5
blocks that the rocket can see (its feet or its middle, ClipContext
COLLIDER with no fluids), besides the entity it is attached to. Horizon only
hurt the attached player and the nearby mobs, so a rocket bursting next to a
player on foot did nothing to them, in both single player modes.

The player (unless it is the attached one) and every guest in the level now
take damageAmount * sqrt((5 - distanceTo) / 5) as fireworks damage, with the
armor wear of hurtServer. distanceTo is a float, as vanilla computes it, for
the mobs too.
2026-10-26 00:14:19 +01:00
rdbtCVSandCodex 03190ec724 Update the runtime download fixture for runtime-only discovery
Co-Authored-By: Codex <[email protected]>
2026-10-25 23:13:02 +00:00
Ludovic MarieandClaude Opus 5.5 5e9d45bc9a /pregen: off unless the server turns it on
Vanilla has no /pregen, so with it always registered /help and the command
tree differed from vanilla's (the command_exec harness failed on /help).
Horizon's own commands are now added to the game's tree only when the owner
asks: `horizon-pregen=true` in server.properties (`horizon-pregen = true`
under [properties] in server.toml) for a dedicated server, `--pregen` for
the game (single player, LAN). Off, the trees are vanilla's own, unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 00:11:39 +01:00
rdbtCVSandCodex f45e3f1237 Optimize launcher preparation with precompiled, batched exporters
Compile and validate original exporter bundles during release builds, cache development compilation, and reuse initialized JVMs and collision data during preparation. Keep legacy preparers compatible while current releases need only a Java runtime.

Co-Authored-By: Codex <[email protected]>
2026-10-25 23:11:23 +00:00
Kacpherek 9aa6d7723f Shulker bullets home on the guest the shulker targets
A mob that sees a guest (a LAN guest, or the player of the server thread)
holds Who::Player as its target, and what it does to that player runs with
the guest swapped in. A shulker's bullet, though, keeps the target it was
fired with until it lands, and the projectiles tick outside that swap:
there Who::Player is the host's own player, so on a server thread the
bullet found no target, stopped homing and fell onto the blocks.

The bullet now carries the guest's entity id, as ShulkerBullet.finalTarget
holds the targeted entity in vanilla (ShulkerAttackGoal.tick passes
Shulker.getTarget()), so it homes on the guest among the projectiles'
targets and its hit runs as that guest: 4 damage and Levitation.
2026-10-26 00:10:54 +01:00
Ludovic MarieandClaude Opus 5.5 fbd6bee3e3 Official mods: FPS Counter and Keystrokes, the first extras as mods
The FPS counter and keystrokes extras rewritten as mods on the public API
(render, input, ui, storage), in mods/official/, with the extras' options
on their settings pages and the extras' look: the same boxes, colours and
text positions, pixel for pixel. They are workspace members, so clippy and
pr_ready cover them; a maintainer publishes them in horizon-mods. The
built-in extras stay until the removal PR, which installs these for the
players who had them on.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 00:08:55 +01:00
Kacpherek e50a4c6443 Play no place sound when a block item placement is refused
Clicking with a torch or flower into a space that is already taken (or where the block cannot stand) made the server-thread client play the block's place sound when the server resent the refused target. BlockItem.place returns FAIL before playSound when canPlace() is false, and the client only hears its own predicted placement, so clear the pending place sound when no placement was predicted.
2026-10-26 00:06:53 +01:00
Ludovic MarieandClaude Opus 5.5 6824224a33 Player heads: read the profile's skin once, and no skull on a piston head
- Skull keeps what its profile draws (the skin URL, its skins/<sha1>
  sprite and the default skin), computed when the block entity loads,
  instead of decoding base64 JSON and hashing it for every head every
  frame; skull_skin_at no longer scans the chunk's saved block entities.
- BlockEntity::for_block gave minecraft:piston_head a skull block entity
  (it ends in _head), which would have been saved as minecraft:skull.
- AGENTS.md is left as it is on main: the workflow note is not part of
  this fix.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 00:06:22 +01:00
Ludovic MarieandClaude Opus 5.5 c28bea8e96 Mod API: input's permission line says it sees keys and clicks
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 00:06:16 +01:00
Ludovic Marie 6df37324f5 Merge pull request #1853 from ludovic111/fix/mob-ambient-particles
🤖 Agent: merged after verify and the parity harnesses this change reaches passed on its head.
2026-10-26 00:01:39 +01:00
Ludovic MarieandClaude Opus 5.5 2a576b5886 Merge origin/main into fix/water-cauldron-extinguish
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 00:01:15 +01:00
Ludovic MarieandClaude Opus 5.5 eb443cdacc Merge origin/main into fix/server-player-sculk-events
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 23:59:04 +01:00
rdbtCVSandCodex 4be53ee28a Reduce resource preparation filesystem work
Share the asset index and verification state, stage checked imports while hashing, preserve indexed replacements, and clone independent resource copies where supported.

Co-Authored-By: Codex (GPT-6.1) <[email protected]>
2026-10-25 22:58:00 +00:00
Ludovic MarieandClaude Opus 5.5 9d5301627b Chickens: the flap test uses the shared test mob type
chicken_flap reads only the ground flag, so the test builds its mob from
mobs::sensor_scan_tests::kind() instead of a 50-line copy of a MobType.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 23:55:43 +01:00
Ludovic MarieandClaude Opus 5.5 d5b88ff130 Merge origin/main into fix/chicken-flap-remote
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 23:55:43 +01:00
Ludovic MarieandClaude Opus 5.5 b7629cf656 Enchantments: say which callers have no language
full_name reads the process's language, which the server thread shares
with its window; only a process with no language loaded (a dedicated
server, the probes) keeps the English names.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 23:46:36 +01:00
Ludovic MarieandClaude Opus 5.5 a413812363 Scroll bars: the screen's GUI size, and a test of the drag
stats_scroll and stats_drag take the GUI height from gui_size() instead
of a copy of it. The Game Rules list's press, drag and release get the
test from #1913 (vlesmentie's take on the same fix).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 23:46:15 +01:00
Ludovic MarieandClaude Opus 5.5 dd73faccbc Merge origin/main into fix/draggable-scrollbars
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 23:43:27 +01:00
rdbtCVSandCodex 072f5ce2ee Fix custom item names above targeted item frames
Show the framed item's custom name through the existing world name-label renderer, matching vanilla's HUD and crosshair conditions. Extend the LAN frame check for normal and glow frames.

Co-Authored-By: Codex <[email protected]>
2026-10-25 22:43:07 +00:00
Ludovic MarieandClaude Opus 5.5 401ac1322f Banners: one loader for saved and generated banners
The generated banner reuses save.rs's BannerBlockEntity.loadAdditional
(now decode_banner) instead of a copy of it, and the generated block
entity clones its NBT only for the kinds that read it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 23:43:03 +01:00
rdbtCVSandOpenAI Codex 28206976d8 Move parity fixtures onto server execution
Co-Authored-By: OpenAI Codex (GPT-6.1) <[email protected]>
2026-10-25 22:36:21 +00:00
Ludovic MarieandClaude Opus 5.5 d81aa97e84 CI: the desktop runner works only while nobody is at the screen; desktop as a parity host
runner_pause.py pauses on any seat0 session (locked too), games, GPU use or a pause file, and is
the only thing that starts the runner; a job under way is throttled, then the runner stops. The
desktop host in parity_hosts.json refuses work while paused and its workers stop on a pause. The
nightly audit stays on the Mac mini. Docs and a unit test of the watcher.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 23:12:09 +01:00
Ludovic MarieandClaude Opus 5.5 8463325648 CI: Linux runners for the parity job (desktop, ThinkPad), paused while a game runs
runner-env.sh and vanilla-data.sh find Python 3.13, the pinned JDK and sccache on NixOS;
linux-runner.sh installs a runner as a systemd user service with its own screen; runner-pause.py
stops it from taking jobs while a game runs.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 23:12:09 +01:00
Ludovic Marie d58d26c793 Merge pull request #1798 from socksprox/fix/mob-combat-poses
🤖 Agent: thank you! Merged after the contribution gate (checked against vanilla 26.3; one integration branch of main + its batch: clippy -D warnings, tests, the affected parity harnesses (111/112, all pass), in game on the server thread and in-frame, lan_e2e in both host modes) and this head's verify.
2026-10-25 23:10:07 +01:00
Ludovic Marie 93e3096e06 Merge pull request #1875 from kaicsm/fix/client-mods-server-thread
🤖 Agent: merged after contribution gate 14 (integration run of main + this PR) and verify on the head with main merged in. Thank you!
2026-10-25 23:08:25 +01:00
Ludovic MarieandClaude Opus 5.5 e3bd626688 Mod API: what the extras need as mods: corner panels, frame stats, mappings, buttons
The built-in extras become official mods; these are the calls the FPS
counter and keystrokes overlay need to look and behave the same:

- render.set-corner(corner, w, h, shapes): a box the game places with
  its own corner stacking (under the status effects, with the overlays
  and hud panels), clear-corner; text-width(text) in the HUD font
  (advances for U+0000..U+024F, 6 otherwise); frame-stats(): the game's
  FPS counter (fps, min, 1% low).
- input.mapping(name): a movement or click mapping's key label and
  whether it is held; clicks-per-second() for attack and use.
- ui: a button widget (200 x 20, select(id, 0)) for settings pages.
- The game keeps its FPS and click counters running while a mod runs,
  and refreshes the snapshot every frame for mods with a frame hook.
- The ui test guest gains /corner and /readings, with a sandbox test.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 23:08:15 +01:00
rdbtCVSandCodex 0b49da2466 Fix lead drops when leashed mobs die
Follow Leashable.tickLeash for dying mobs and cover trader/player-held llama leads through the death animation.

Co-Authored-By: Codex <[email protected]>
2026-10-25 22:07:58 +00:00
Ludovic Marie e05933169a Merge pull request #1857 from Joansitoh/fix/item-lore-tooltips
🤖 Agent: merged after contribution gate 14 (integration run of main + this PR, harnesses for the touched areas) and verify on the resolved head. Thank you!
2026-10-25 23:01:30 +01:00
Ludovic Marie bb3497bea0 Merge pull request #1671 from socksprox/fix/elytra-fireworks
🤖 Agent: merged after contribution gate 14 (integration run of main + this PR, harnesses for the touched areas) and verify on the resolved head. Thank you!
2026-10-25 23:00:42 +01:00
Ludovic MarieandClaude Opus 5.5 6ef8deedca Merge origin/main into fix/mob-combat-poses
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 22:56:32 +01:00
Ludovic MarieandClaude Opus 5.5 d63d0c1744 Merge origin/main into fix/client-mods-server-thread
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 22:56:31 +01:00
Ludovic Marie f5e97deac2 Merge pull request #1834 from vlesmentie/fix/lava-cauldron-player
🤖 Agent: merged after contribution gate 14 (integration run of main + this PR, harnesses for the touched areas) and verify on the resolved head. Thank you!
2026-10-25 22:55:16 +01:00
Ludovic Marie 1a857b285d Merge branch 'feat/prepare-output-check' into fix/prepare-rewritten-version-file 2026-10-25 22:50:36 +01:00
Ludovic MarieandClaude Opus 5.5 116a7f7dca horizon-prepare: OUTPUT_VERSION, the number of what it prepares, checked by CI
Launchers copy a player's prepared resources to a new release while its preparer declares the
same OUTPUT_VERSION (the launcher side is its own pull request). The number is bumped by hand;
CI keeps it honest: the parity job already prepares with every change to the preparer, and
tools/prepare_output.py check now fails when that preparation's digest is not the one
crates/horizon-prepare/output-versions.txt gives the current number, or when the file changed
other than by appending a version. The digest canonicalises what differs between two runs of
the same preparer (JSON key order, creative tab tag order, lambda class addresses, logs, the
sounds): 10 preparations by 5 preparer versions on the three Mac runners give one digest, and a Linux preparation by an older preparer gives the Mac runners' digest for it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 22:48:51 +01:00
Ludovic MarieandClaude Opus 5.5 acb68ed163 launcher: reuse prepared resources while the preparer's OUTPUT_VERSION is the same
A version's resources were reused only when the preparer's sources and the exporters were
byte for byte the same, so any change to crates/horizon-prepare (even one that prepares the
same files) made every player prepare again. The launcher now compares the number the preparer
declares for what it makes (`pub const OUTPUT_VERSION: u32` in crates/horizon-prepare/src/lib.rs,
bumped by hand when the output changes and checked by CI in the follow-up). Preparers from
before the number are known by a fingerprint of their sources and exporters: v0.2.49 to
v0.2.64 prepare version 1's output (measured on the CI runners' preparations). Older versions
are compared as before.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 22:44:56 +01:00
rdbtCVSandOpenAI Codex b6efebc879 Play anvil result sounds once for every player
Deliver anvil level events to the acting player and keep their local sound out of positional sound capture. Extend the anvil LAN check to count sounds for both players using either player’s anvil result.

Co-Authored-By: OpenAI Codex <[email protected]>
2026-10-25 21:39:14 +00:00
socksproxandDevin 2cdb6362ca Particles keep their sprites over a resource reload
The particle engine copied the GUI atlas's sprite rects once at start; a pack or server-pack
change rebuilt the atlas but not the engine, so every named and set sprite then drew a stale
region — break crumbs showed other blocks and campfire smoke showed random items (#1923).
The engine now re-reads the rects and the packs' sprite sets on reload, and a particle whose
sprite-set index outlived the rebuild keeps its sprite instead of indexing past the new list.
The reload also layers content mods over the root again, which startup does but reload forgot,
so the first reload silently dropped their assets and data.

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-25 22:38:40 +01:00
Ludovic MarieandClaude Sonnet 5.5 31e9e2da97 Merge origin/main into fix/item-lore-tooltips
In stack_tooltip keep #1806's translate through `display_get` and this
PR's `push` helper for the styled tooltip lines.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-25 22:26:10 +01:00
Ludovic Marie 74768d2eeb Merge pull request #1939 from ludovic111/roadmap/player-ideas-1025
🤖 Agent: merged after verify (roadmap data only).
2026-10-25 22:24:22 +01:00
vlesmentieandClaude Opus 5.5 15877931db Merge origin/main into fix/water-cauldron-extinguish
Keeps both sides of #1823 (wither rose) and this branch (lava and water
cauldrons) in motion.rs's inside-block match, the MotionEvent variants,
game.rs and lan_actions.rs, and all three tests.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 18:22:46 -03:00
Ludovic MarieandClaude Opus 5.5 a4a709c2d6 Roadmap: OpenGL shipped in v0.2.63; three players' ideas kept by Ludovic
One-command server install, launcher-style game menus (off by default), AppImage and a Nix flake.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 22:16:18 +01:00
Ludovic Marie b4345b1c2b Merge pull request #1813 from luiS1q1/fix/ios-dualsense-build
🤖 Agent: thank you @luiS1q1! Merged on Ludovic's go-ahead as iOS groundwork (desktop targets unchanged, clippy clean, this head's verify).
2026-10-25 22:16:05 +01:00
Ludovic Marie ed11f614f8 Merge pull request #1870 from ludovic111/fix/complete-sounds-panorama
🤖 Agent: merged after verify and the parity harnesses this change reaches passed on its head.
2026-10-25 22:13:20 +01:00
levzzz 61c5f29ab7 Game Rules and Statistics: the scroll bars can be dragged
Both lists drew a scroll bar but only listened to the mouse wheel: a press
inside the bar column, the drag that follows and the release were dropped.
`ui::Scroll` already models AbstractSelectionList's press/drag/release (the
server list uses it); the two screens now keep their scrolling in it and the
window passes the press, the pointer's moves while it is held and the release
along.

Fixes #1782
2026-10-26 02:13:10 +05:00
Ludovic MarieandClaude Opus 5.5 fada660310 Mod API: capability input (experimental): claimed items' use, mod keys
- WIT `input`: claim-items(ids) makes the use button on those items the
  mod's: use-start, then use-stop with the ticks held (button up, item
  out of the hand, or a screen opening); the game does nothing else
  with the click. register-key(name, label, key) for up to 16 keys
  (key.keyboard.r), heard as key-down / key-up while playing; the
  game's own mapping on the key still runs. held(), poll() (256 kept).
- game/mod_input.rs: play_mouse asks mod_use first (local and remote
  windows), the tick ends a use whose item left the hand, the keyboard
  and horizon-ctl's key send the physical key to mod_key.
- No claim and no key, no cost: one check each.
- Experimental: the manifest needs experimental = ["input"].
- The ui test guest gains /input_claim, /input_poll and /input_held,
  with a sandbox test. Docs: mods.md, stability.md, the changelog, the
  regenerated reference.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 22:10:44 +01:00
Ludovic Marie da0056c248 Merge pull request #1936 from ludovic111/fix/opengl-gl33
🤖 Agent: merged after verify and real GL runs on the ThinkPad (Intel HD 620, GL 3.3 with extensions hidden: title, server thread and in-frame render, 0 errors; screenshots match Vulkan). Released at once: any GL driver without compute shaders crashed at start without it.
2026-10-25 22:10:39 +01:00
rdbtCVSandCodex gpt-6.1-sol 40b3c1e3d6 Preserve requested GUI scale across temporary small surfaces
Keep framebuffer limits in rendering and explicit option cycling rather than overwriting the saved choice during startup or settings application.

Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-10-25 21:08:55 +00:00
Ludovic MarieandClaude Sonnet 5.5 2364bd3504 Merge origin/main into fix/elytra-fireworks
Keep main's item_sync.rs and lan::guest::stack_to_network for stacks on
the wire (#1874) and move the firework component decoding there:
Component::Fireworks and Component::FireworkExplosion in
item_sync::components_tag, with fireworks_tag.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-25 22:04:54 +01:00
Ludovic MarieandClaude Opus 5.5 2fb0ac186a Behaviour cache: hash the source files once per process
The snapshot's key hashes the 70 MB of source tables it was resolved from.
The client's tables and the world generator's are loaded from the same
folder, so a world opening hashed them a second time: a third of a second
of the server thread's start on a 4-thread laptop, before any chunk. The
hash is now kept for the process, by each file's length and modification
time; a changed file is read again.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 22:04:13 +01:00
Ludovic MarieandClaude Opus 5.5 4a2b262343 Structure starts: decide chunks no ring search can reach without the rings
The 128 stronghold biome searches (two seconds of CPU on a 4-thread laptop)
ran when the first chunk of a new world looked up its structure starts, so
nothing near spawn was generated until they were done. Each search ends
within 8 chunks of its ring chunk, which is known before searching, and
strongholds lie 1,400 blocks out or more: a chunk with no ring search in
reach (exclusion zones included) is now decided without them, which is what
they would say. They run when a chunk near a ring, /locate or an eye of
ender needs them (ring_positions now searches on demand instead of returning
nothing before a lookup did).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 22:00:07 +01:00
socksproxandDevin 820cc211d3 Player heads render their owner's skin
Placed player_head blocks kept no profile: skulls had no block entity, so the
renderer fell back to a hardcoded steve for every head. Give skulls a real
BlockEntity carrying vanilla's SkullBlockEntity fields (profile,
note_block_sound, custom_name) so the placement pipeline lands
minecraft:profile in it the way block_entity_item already maps, encode and
decode it through save so regions keep it, send its update tag to remote
clients, and resolve the profile through the skin downloader the head items
and the tab list share: the downloaded skin slot once it lands, the
profile's DefaultPlayerSkin uuid pick while it fetches, SKIN_BY_TYPE steve
without a profile.

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-25 21:55:02 +01:00
socksproxandDevin 8dcd00fc62 AGENTS.md: the feature-request loop
When an agent is asked for a feature it runs the whole loop itself: check open
PRs for the area first, work in a new worktree on the latest upstream main,
build and run pr_ready, then push to the fork and open the pull request
against upstream, never the fork.

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-25 21:54:47 +01:00
Ludovic Marie 82b0051a3c Merge pull request #1856 from Domanffe/fix/furnace-experience
🤖 Agent: thank you! Merged after the contribution gate (checked against vanilla 26.3; one integration branch of main + its batch: clippy -D warnings, tests, the affected parity harnesses (111/112, all pass), in game on the server thread and in-frame, lan_e2e in both host modes) and this head's verify.
2026-10-25 21:53:58 +01:00
Ludovic Marie e0abe90c5c Merge pull request #1858 from Excurs1ons/fix/server-death-messages
🤖 Agent: thank you! Merged after the contribution gate (checked against vanilla 26.3; one integration branch of main + its batch: clippy -D warnings, tests, the affected parity harnesses (111/112, all pass), in game on the server thread and in-frame, lan_e2e in both host modes) and this head's verify.
2026-10-25 21:53:06 +01:00
Ludovic Marie 982034f5f8 Merge pull request #1806 from Excurs1ons/fix/localized-ui-text
🤖 Agent: thank you! Merged after the contribution gate (checked against vanilla 26.3; one integration branch of main + its batch: clippy -D warnings, tests, the affected parity harnesses (111/112, all pass), in game on the server thread and in-frame, lan_e2e in both host modes) and this head's verify.
2026-10-25 21:51:23 +01:00
rdbtCVSandCodex 95c336a387 fix(threads): lower helper thread priority on Android
Co-Authored-By: Codex (gpt-6.1-sol) <[email protected]>
2026-10-25 20:50:57 +00:00
Ludovic MarieandClaude Opus 5.5 2761349d98 Renderer: no indirect buffer usage without indirect draws (OpenGL before 4.3)
wgpu refuses to create a buffer with INDIRECT usage on a device without
DownlevelFlags::INDIRECT_EXECUTION (OpenGL before 4.3 without compute shaders), so the game
panicked creating 'Section draws'. Multi-draw now also needs indirect execution, and without it
the buffer has no INDIRECT usage: sections are drawn one direct draw each, as they already were
without INDIRECT_FIRST_INSTANCE.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 21:50:49 +01:00
levzzzand[CC] <deepseek-v4.1-flash> b71623dfd7 Item cooldowns: the sweep on a slot, and no use while one runs
Nothing tracked `ItemCooldowns`: `draw_item_decorations` passed a cooldown of
0.0 with a note that there was none to shade, and ender pearls were held back
by an ad-hoc `pearl_cooldown` counter.

`horizon_client::cooldowns` keeps the `use_cooldown` component's entries by
cooldown group the way `Player.getCooldowns` does (start and end ticks, the
percent `getCooldownPercent` returns), `ItemInfo` now carries the item's
`use_cooldown`, and the game uses it where vanilla does:
`ServerPlayerGameMode.useItem`/useItemOn refuse a stack on cooldown,
`ItemStack.applyAfterUseComponentSideEffects` starts the entry when a use
finishes (a thrown pearl, a wind charge, an eaten chorus fruit),
`GuiGraphicsExtractor.itemCooldown` shades the slot, and a server's guests
keep their own and are sent `ClientboundCooldownPacket` (started, and 0 when
it ends), which a client applies.

Fixes #1763

Co-Authored-By: [CC] <deepseek-v4.1-flash> <[email protected]>
2026-10-26 01:50:22 +05:00
Ludovic MarieandClaude Sonnet 5.5 21d8b0702c Merge origin/main into fix/lava-cauldron-player
Keep both the lava cauldron and wither rose match arms in
MotionWorld's entity-inside handling, and both tests.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-25 21:50:03 +01:00
Ludovic Marie d0f61d07cf Merge pull request #1879 from socksprox/fix/legacy-colour-codes
🤖 Agent: thank you! Merged after the contribution gate (checked against vanilla 26.3; one integration branch of main + its batch: clippy -D warnings, tests, the affected parity harnesses (111/112, all pass), in game on the server thread and in-frame, lan_e2e in both host modes) and this head's verify.
2026-10-25 21:48:30 +01:00
Ludovic Marie 4e8269276f Merge pull request #1884 from cryingtoilet/fix/1764-creative-search-word-delete
🤖 Agent: thank you! Merged after the contribution gate (checked against vanilla 26.3; one integration branch of main + its batch: clippy -D warnings, tests, the affected parity harnesses (111/112, all pass), in game on the server thread and in-frame, lan_e2e in both host modes) and this head's verify.
2026-10-25 21:47:46 +01:00
Alexander KrylovandHermes Agent 3762ab7cda Enchantments: the name and the level follow the selected language
The registry resolved every `description` from the pinned `en_us` map at load
and `full_name` took `enchantment.level.<n>` from the same table, so a tooltip
showed an English enchantment line whatever language the player picked.

Keep the description's translation key and resolve it, and the level, through
the current language at display time, with the English text as the fallback for
the server thread, the probes and an old resource root.

Refs #1774.

Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
2026-10-25 23:47:14 +03:00
Ludovic Marie fd56c1f358 Merge pull request #1850 from rdbtCVS/t3/fix-end-crystal-explosion
🤖 Agent: thank you! Merged after the contribution gate (checked against vanilla 26.3; one integration branch of main + its batch: clippy -D warnings, tests, the affected parity harnesses (111/112, all pass), in game on the server thread and in-frame, lan_e2e in both host modes) and this head's verify.
2026-10-25 21:47:00 +01:00
levzzzand[CC] <deepseek-v4.1-flash> b3807d6e50 Chat: the Chat option hides what it says it hides
The Chat Settings option was saved and shown but never applied: Hidden and
Commands Only still sent and displayed ordinary chat, and Hidden still
displayed the server's messages.

Minecraft.computeChatAbilities turns the option into ChatRestriction's
permissions, and they are now honoured where vanilla honours them:
ChatScreen refuses to submit input they take (hasAllowedInput) and says why
in a red usage line, ClientPacketListener.sendChat/sendCommand stop it before
the packet, ChatListener drops the player, server and client sources the
option hides, and ClientInformation carries the visibility to a server (at
login, and again when it changes, as Options.broadcastOptions does).

Fixes #1376

Co-Authored-By: [CC] <deepseek-v4.1-flash> <[email protected]>
2026-10-26 01:45:17 +05:00
Ludovic Marie fa2ae64d83 Merge pull request #1874 from Domanffe/fix/creative-item-components
🤖 Agent: thank you! Merged after the contribution gate (checked against vanilla 26.3; one integration branch of main + its batch: clippy -D warnings, tests, the affected parity harnesses (111/112, all pass), in game on the server thread and in-frame, lan_e2e in both host modes) and this head's verify.
2026-10-25 21:45:05 +01:00
Ludovic Marie c5d816cc12 Merge pull request #1920 from ludovic111/fix/opengl-downlevel
🤖 Agent: merged after verify and a real OpenGL run on the desktop (Mesa, GL 4.6 and 3.3: no crash, animated sprites on the CPU below 4.3). Released at once on Ludovic's request.
2026-10-25 21:44:24 +01:00
Ludovic Marie 99a954d665 Merge pull request #1823 from vlesmentie/fix/wither-rose-player
🤖 Agent: thank you! Merged after the contribution gate (checked against vanilla 26.3; one integration branch of main + its batch: clippy -D warnings, tests, the affected parity harnesses (111/112, all pass), in game on the server thread and in-frame, lan_e2e in both host modes) and this head's verify.
2026-10-25 21:43:03 +01:00
Ludovic Marie f3efe2802c Merge pull request #1811 from vlesmentie/fix/zombie-horseman-peaceful
🤖 Agent: thank you! Merged after the contribution gate (checked against vanilla 26.3; one integration branch of main + its batch: clippy -D warnings, tests, the affected parity harnesses (111/112, all pass), in game on the server thread and in-frame, lan_e2e in both host modes) and this head's verify.
2026-10-25 21:42:22 +01:00
rdbtCVSandCodex 09ff66bfee Fix stale server digging after instant survival breaks
Clear completed guest digging so replanted flowers and crops are not broken again after attack is released. Add a LAN replanting regression for both single-player paths.

Co-Authored-By: Codex (gpt-6.1-sol) <[email protected]>
2026-10-25 20:39:44 +00:00
Ludovic MarieandClaude Opus 5.5 f65750baed Merge origin/main into fix/opengl-downlevel
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 21:35:35 +01:00
Ludovic MarieandClaude Opus 5.5 61c44c22ef Mod API: capability render (experimental): HUD shapes and camera shake
Mods draw through the game's own renderer, never their own shaders:

- WIT `render`: set-hud(shapes) with fill, quad (any four corners),
  sprite (the game's GUI sprites), item (scaled) and text shapes, up
  to 1024, kept until replaced; gui-size(); shake(strength, seconds).
- Drawn over the HUD after the extras, under chat and screens, hidden
  with F1 (game/mod_render.rs). Colours are 0xAARRGGBB.
- shake turns the view, not the player: jolts fade out squared, add up
  to 1, at most 3 degrees of yaw and 2 of pitch, scaled by Distortion
  Effects (screenEffectScale; 0 turns it off).
- Experimental: the manifest needs experimental = ["render"].
- No mod drawing, no cost: an empty iterator and an is_empty check.
- cargo horizon-mod check reports shakes and HUD shapes. The ui test
  guest gains /render_hud, /render_clear and /render_shake, with a
  sandbox test; HudBatch gets fill_quad.
- Docs: mods.md, stability.md, the changelog, the regenerated reference.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 21:35:12 +01:00
Ludovic MarieandClaude Opus 5.5 21203de7f0 Animated sprites: the CPU path also when the driver cannot build the compute pipeline
An OpenGL driver can list compute shaders among its extensions while wgpu writes GLSL of its core
version, which has none (seen under MESA_GL_VERSION_OVERRIDE=3.3: the pipeline failed and the game
panicked). The pipeline is now created inside error scopes, and a failure falls back to the CPU path
with one warning.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 21:33:05 +01:00
sl4ppleware 386912a76d Hearts blink again when the server sets health: LocalPlayer.hurtTo's damageCooldownTime 2026-10-25 23:32:27 +03:00
Ludovic Marie 731d88a1d8 Merge pull request #1917 from ludovic111/fix/opengl-fallback
🤖 Agent: merged after verify, a real OpenGL run (Mesa GL 4.6: title and a world rendered, no errors) and the backend/shader unit tests. Released at once on Ludovic's request (release-now): a player's Radeon HD 7400M could not start Horizon.
2026-10-25 21:30:01 +01:00
vlesmentieandClaude Opus 5.5 72de6e0b70 Water cauldrons put out a burning player and lose a level
On top of the lava cauldron's inside test: water and powder snow
cauldrons are no longer skipped. Their entity-inside shape is the
cauldron's or their content, Block.column(12, 4, 6 + 3 per level), and
LayeredCauldronBlock.entityInside applies: on a server, a burning
player first asks for handleEntityOnFireInside (runBefore EXTINGUISH),
which lowers the fill level (lowerFillLevel, a powder snow cauldron
turning to water of its level first, block_change with no source); then
EXTINGUISH (Entity.clearFire) puts the fire out.

Fixes #1330.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 17:25:48 -03:00
Ludovic MarieandClaude Opus 5.5 1a16e35aaa Animated sprites without compute shaders (OpenGL before 4.3)
SpriteAnimator wrote every changed sprite with a compute pass, which OpenGL 3.3 to 4.2 has no
equivalent for in wgpu: creating the pipeline failed. Without compute shaders and storage, the same
jobs now run on the CPU with animate.wgsl's integer mix, and each level is copied into its layer
through the upload belt. The GPU test runs both paths against the same CPU reference.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 21:25:37 +01:00
Ludovic MarieandClaude Opus 5.5 b3e400a526 Renderer: sprite arrays OpenGL can view as arrays
wgpu's GL backend picks a texture's target from its layer count when it creates it: one layer makes
a 2D texture and a multiple of six on a square texture a cube map, and neither can be bound as
terrain.wgsl's texture_2d_array. On GL, the 1-layer stand-in for unused sprite arrays gets two
layers and a sprite array of 1 or 6n layers one empty layer more. Other backends are unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 21:25:37 +01:00
Ludovic MarieandClaude Opus 5.5 ac47b988c9 Shader packs, clean room 42: culled entities, the first-person player, shadowPlayer and shadowLightBlockEntities in the shadow pass
Entities Horizon's entity culling hides from the camera keep their vertices in a hidden span that
only the pack's shadow pass draws; the local player's model is built in first person, hidden from
the camera, while the pack casts entity shadows. shadowPlayer draws the player alone when
shadowEntities is off, shadowLightBlockEntities the light-emitting block entities when
shadowBlockEntities is off. Without a shader pack nothing changes.

Clean-room: implemented from cleanroom/shader-packs-spec.md revision 1, §2.4, §3.2; no other
loader's code read.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 21:21:05 +01:00
Ludovic MarieandClaude Opus 5.5 903da4f1ab Cargo.lock: wgpu's GL backend dependencies (glow, khronos-egl, glutin_wgl_sys)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 21:20:02 +01:00
Ludovic MarieandClaude Opus 5.5 54bb3e4fbe Screen effects: one sampler per entry point, so the shader compiles for OpenGL
GLSL combines a texture with its sampler, and naga refuses an entry point that reads one texture
through two (ImageMultipleSamplers): the BLUR override left both branches in fs_screen and
fs_vignette, so creating the screen effect pipelines failed on wgpu's GL backend at startup. Blurred
sprites now have their own entry points (fs_screen_blur, fs_vignette_blur); each pipeline still
samples once per pixel.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 21:19:47 +01:00
Ludovic MarieandClaude Opus 5.5 a013e35a61 Collision scans: pass the rest of an outer row in a contained section at once
On the rows along a box's bottom, top and sides every cell is outer, so in a
section no block reaches out of, the rest of the row up to the chunk's edge
is passed in one step instead of cell by cell. The candidate budget still
ends the scan where it did.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 21:17:59 +01:00
Ludovic MarieandClaude Opus 5.5 03a5cbdcd8 Renderer: OpenGL as the last fallback, for GPUs with no Vulkan or DirectX 12 driver
wgpu's GL backend (WGL on Windows, EGL on Linux). Auto tries Vulkan, then DirectX 12 on Windows,
then OpenGL; graphicsApi:opengl (or HORIZON_GRAPHICS_API=opengl) picks it. A Radeon HD 7400M
(TeraScale 2: DirectX 11, OpenGL 4.x) could not start Horizon at all.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 21:11:47 +01:00
socksproxandDevin 74e3b0adf6 ui: recapture the mouse when a mod's screen vanishes, borrow its widgets
Review on #1864: when a mod stops while its screen or settings page is
up, draw_mod_screen dropped the player back to Playing without
capture(true), leaving the mouse free until the next click into the
world. The dead surface now leaves through close_mod_screen, which puts
back the screen it covered and holds the mouse on a return to Playing;
the same goes for a screen dropped while it was up.

mod_ui_view also cloned the title and every widget (up to 64, lists to
512 rows) on each frame, click and scroll. It now borrows the mod's
stored widgets — the screen's scrolls live in a disjoint field — and the
click finds the picked row first so the &mut ui_select runs after.

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-25 21:10:52 +01:00
vlesmentieandClaude Opus 5.5 de0f76476f Big dripleaves tilt under a player standing on them
The leaf's tilt ticks, its reset and its redstone reset were already in
horizon-world (big_dripleaf_tick, big_dripleaf_reset_tilt), but nothing
started them: BigDripleafBlock.entityInside was missing. An entity on
the ground whose feet are above the leaf's y + 0.6875 (canEntityTilt)
tilts a flat, unpowered leaf UNSTABLE and schedules its tick in 10.

The block tick's inside scan now also passes the host's and the LAN
guests' feet and on-ground flag to Behaviours::entity_on_big_dripleaf.

Fixes #1683.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 17:10:26 -03:00
Ludovic Marie b7b4a26164 Merge pull request #1891 from rdbtCVS/fix/renderer-surface-fallback
🤖 Agent: thank you @rdbtCVS! Merged after review (a surface failure now falls through to the next backend, as adapter and device failures do) and this head's verify. Follow-up: an OpenGL fallback for GPUs with neither Vulkan nor DirectX 12 builds on this.
2026-10-25 21:09:37 +01:00
Ludovic Marie 8b4336013a Merge pull request #1864 from socksprox/feat/mod-ui
🤖 Agent: thank you @socksprox! Merged after review (two follow-up commits on your branch: `ui` is opt-in through `experimental = ["ui"]`, and the Settings button now appears when a mod sets its page after loading), tests in game in-frame, on a server-thread window and as a LAN guest, and this head's verify.
2026-10-25 21:07:26 +01:00
Ludovic Marie 1c3be171ce Merge pull request #1867 from ludovic111/fix/prepare-regression
🤖 Agent: merged after verify and the parity harnesses this change reaches passed on its head.
2026-10-25 20:58:37 +01:00
Ludovic MarieandClaude Opus 5.5 befec1995f ui: a Settings button appears when the mod sets its page later
has_settings was read once, when the mods loaded, so a mod that called
set-settings from a command or a tick never got its Settings button
until a reload. The Mods page's rows now follow each mod call's output.
Also clippy with --features mods: needless borrows of the atlas in
draw_mod_screen.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 20:57:10 +01:00
Ludovic MarieandClaude Opus 5.5 05396b4edf ui: experimental, opted into with experimental = ["ui"] in the manifest
docs/api/stability.md: a new API starts experimental, usable only by
opting in with a manifest field naming it. Capability ui now needs
experimental = ["ui"] beside capabilities; without it the manifest is
refused with that line to add. A capability promoted later stays
accepted in the list, so such manifests keep loading.

The ui test guest opts in; stability.md gets ui's row; docs/mods.md
shows the key.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 20:57:10 +01:00
Ludovic Marie 3d8a8f7d02 Merge remote-tracking branch 'origin/main' into cg14/pr1671
# Conflicts:
#	crates/horizon-client/src/lan/guest.rs
2026-10-25 20:54:03 +01:00
Ludovic MarieandClaude Opus 5.5 200b756b4a Sculk: pass over idle sensors and catalysts in the block entity tick
A sculk sensor, shrieker or catalyst with no vibration chosen, travelling or
waiting, and no charge cursors, ticks without changing anything (vanilla's
VibrationSystem.Ticker.tick and SculkSpreader.updateCursors return at once).
The tick now leaves those out before asking whether the chunks around them
tick (nine lookups each) and before looking them up a second time.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 20:53:31 +01:00
levzzzand[CC] <deepseek-v4.1-flash> bb9c474c64 End city banners keep their template patterns
A structure template saves a banner's layers in its block entity NBT, and
StructureTemplate.placeInWorld hands that NBT to the level, where
BannerBlockEntity.loadAdditional reads `patterns`. Horizon built the client
block entity from the block alone (BlockEntity::for_block), so the End
cities' magenta banners came out bare.

Fixes #1438.

Co-Authored-By: [CC] <deepseek-v4.1-flash> <[email protected]>
2026-10-26 00:53:17 +05:00
Ludovic MarieandClaude Opus 5.5 4459f132fe Collision scans: skip the outer cells of sections no block reaches out of
BlockCollisions keeps a block on the cells around the box (Cursor3D types 1
and 2) only if it has a large collision shape or is a moving piston. Most
sections hold neither in their palette, so the scan now asks the provider
once per section and passes those cells without reading them: a mob's box
reads about a fifth of the cells it did. Same hits, same order.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 20:44:09 +01:00
Alexander KrylovandHermes Agent bc09558dfa Merge origin/main into fix/creative-item-components
Main gained PotionContents on the wire (#1796) in the same two functions
this branch hands to the shared codec. Resolved by keeping the codec and
teaching it the potion: components_tag writes it by its built-in registry
name, and the remote's id lookup asks the built-in registries first, as a
hub's SyncData::id already does. The round-trip test now covers a splash
potion.

Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
2026-10-25 22:42:28 +03:00
rdbtCVSandOpenAI Codex GPT-6.1 bd09f8b03e Migrate player integration coverage to server-thread hosting
Co-Authored-By: OpenAI Codex GPT-6.1 <[email protected]>
2026-10-25 19:36:11 +00:00
vlesmentieandClaude Opus 5.5 89d6e1c2a5 Structure chests keep the enchantments their loot rolled
The loot rolls carry enchantments and potions (horizon_world::loot runs
enchant_with_levels, enchant_randomly and set_potion), but chests filled
when a structure is placed, and mineshaft chest minecarts, turned each
LootStack into an item with loot_item, which keeps only the item, count,
damage and exploration map. End city gear came out bare. Both now use
loot_item_with_components, as vaults, trial spawners and decorated pots
already do.

Fixes #1437.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 16:34:39 -03:00
Ludovic Marie 7fd1f3118f Merge pull request #1848 from kacpherek/fix/1501-shulker-nesting
🤖 Agent: thank you! Merged after the contribution gate (checked against vanilla 26.3; one integration branch of main + all 14 of your PRs: fmt, clippy -D warnings, horizon-client/world tests, horizon-ctl scenarios on the server thread and in-frame, lan_e2e in both host modes, and the 30 harnesses covering these changes: 29 pass, the 30th is /help listing /pregen from #1805) and this head's verify.
2026-10-25 20:32:18 +01:00
rdbtCVSandOpenAI Codex GPT-6.1 84a3f1ee18 Emit sculk movement events for each server player
Co-Authored-By: OpenAI Codex GPT-6.1 <[email protected]>
2026-10-25 19:30:34 +00:00
Ludovic Marie 4309d96924 Merge pull request #1845 from kacpherek/fix/1297-server-freeze-damage
🤖 Agent: thank you! Merged after the contribution gate (checked against vanilla 26.3; one integration branch of main + all 14 of your PRs: fmt, clippy -D warnings, horizon-client/world tests, horizon-ctl scenarios on the server thread and in-frame, lan_e2e in both host modes, and the 30 harnesses covering these changes: 29 pass, the 30th is /help listing /pregen from #1805) and this head's verify.
2026-10-25 20:30:27 +01:00
Ludovic Marie 478b8c19c4 Merge pull request #1844 from kacpherek/fix/1520-ride-mobs
🤖 Agent: thank you! Merged after the contribution gate (checked against vanilla 26.3; one integration branch of main + all 14 of your PRs: fmt, clippy -D warnings, horizon-client/world tests, horizon-ctl scenarios on the server thread and in-frame, lan_e2e in both host modes, and the 30 harnesses covering these changes: 29 pass, the 30th is /help listing /pregen from #1805) and this head's verify.
2026-10-25 20:29:24 +01:00
Ludovic Marie 2af75ba9f6 Merge pull request #1849 from kacpherek/fix/1329-cursor-death
🤖 Agent: thank you! Merged after the contribution gate (checked against vanilla 26.3; one integration branch of main + all 14 of your PRs: fmt, clippy -D warnings, horizon-client/world tests, horizon-ctl scenarios on the server thread and in-frame, lan_e2e in both host modes, and the 30 harnesses covering these changes: 29 pass, the 30th is /help listing /pregen from #1805) and this head's verify.
2026-10-25 20:27:19 +01:00
Ludovic Marie 89888e9b07 Merge pull request #1847 from kacpherek/fix/1351-pearl-vanish-on-death
🤖 Agent: thank you! Merged after the contribution gate (checked against vanilla 26.3; one integration branch of main + all 14 of your PRs: fmt, clippy -D warnings, horizon-client/world tests, horizon-ctl scenarios on the server thread and in-frame, lan_e2e in both host modes, and the 30 harnesses covering these changes: 29 pass, the 30th is /help listing /pregen from #1805) and this head's verify.
2026-10-25 20:25:09 +01:00
rdbtCVSandOpenAI Codex GPT-6.1 9c7909f770 Advance remote fireballs between server tracker updates
Co-Authored-By: OpenAI Codex GPT-6.1 <[email protected]>
2026-10-25 19:23:55 +00:00
Ludovic Marie 1110a28521 Merge pull request #1902 from ludovic111/fix/supervisor-lock-test-race
🤖 Agent: merged after verify passed (test-only fix).
2026-10-25 20:23:30 +01:00
Ludovic Marie f6e451162c Merge pull request #1843 from kacpherek/fix/1504-hardcore-spectator
🤖 Agent: thank you! Merged after the contribution gate (checked against vanilla 26.3; one integration branch of main + all 14 of your PRs: fmt, clippy -D warnings, horizon-client/world tests, horizon-ctl scenarios on the server thread and in-frame, lan_e2e in both host modes, and the 30 harnesses covering these changes: 29 pass, the 30th is /help listing /pregen from #1805) and this head's verify.
2026-10-25 20:21:24 +01:00
Ludovic Marie 6ce6577886 Merge pull request #1841 from kacpherek/fix/1378-team-chat-names
🤖 Agent: thank you! Merged after the contribution gate (checked against vanilla 26.3; one integration branch of main + all 14 of your PRs: fmt, clippy -D warnings, horizon-client/world tests, horizon-ctl scenarios on the server thread and in-frame, lan_e2e in both host modes, and the 30 harnesses covering these changes: 29 pass, the 30th is /help listing /pregen from #1805) and this head's verify.
2026-10-25 20:19:46 +01:00
Ludovic Marie 8f2aa7f7e9 Merge pull request #1836 from kacpherek/fix/1483-mob-nbt-attributes
🤖 Agent: thank you! Merged after the contribution gate (checked against vanilla 26.3; one integration branch of main + all 14 of your PRs: fmt, clippy -D warnings, horizon-client/world tests, horizon-ctl scenarios on the server thread and in-frame, lan_e2e in both host modes, and the 30 harnesses covering these changes: 29 pass, the 30th is /help listing /pregen from #1805) and this head's verify.
2026-10-25 20:17:30 +01:00
rdbtCVSandOpenAI Codex GPT-6.1 481e75bc33 Clear off-hand items in vanilla inventory slot order
Co-Authored-By: OpenAI Codex GPT-6.1 <[email protected]>
2026-10-25 19:16:36 +00:00
Ludovic MarieandClaude Opus 5.5 4513af8995 horizon-server: the lock test waits for a fork-held descriptor to close
Tests in the same binary spawn processes (tar, the engine); between fork and exec the child holds a copy
of every open descriptor, the lock's too, so relocking right after drop could see WouldBlock. Seen twice
on CI (#1833, #1895).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 20:13:36 +01:00
Ludovic MarieandClaude Opus 5.5 574d65a03c stability.md: server mods' row names world and typed commands
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 20:08:47 +01:00
Ludovic Marie 8177a6f450 Merge pull request #1807 from ludovic111/fix/no-nether-portal-in-the-end
🤖 Agent: merged after verify and the parity harnesses this change reaches passed on its head.
2026-10-25 20:03:49 +01:00
Ludovic Marie f6b882c9da Merge pull request #1802 from ludovic111/fix/mob-flags-and-swings
🤖 Agent: merged after verify and the parity harnesses this change reaches passed on its head.
2026-10-25 20:01:58 +01:00
Ludovic Marie 9e248a3214 Merge pull request #1797 from ludovic111/fix/tool-use-on-block-ends-click
🤖 Agent: merged after verify and the parity harnesses this change reaches passed on its head.
2026-10-25 20:00:04 +01:00
Ludovic Marie 781b7b877b Merge pull request #1796 from ludovic111/fix/potion-contents-over-network
🤖 Agent: merged after verify and the parity harnesses this change reaches passed on its head.
2026-10-25 19:58:08 +01:00
Ludovic Marie 50f4c913a5 Merge pull request #1868 from ludovic111/fix/modrinth-loader-profiles
🤖 Agent: merged after verify and the parity harnesses this change reaches passed on its head.
2026-10-25 19:55:40 +01:00
Ludovic MarieandClaude Opus 5.5 dab13669f5 Creative screen drops reach the server: SetCreativeModeSlot(-1)
A stack picked from a creative tab exists only on the client. Thrown out of
the creative screen as a server's client, it went out as a ContainerClick to
slot -999, which the server had no carried stack for, so nothing dropped. The
client now sends handleCreativeModeItemDrop's SetCreativeModeSlot(-1, stack),
and the server drops it from the player's hand, with vanilla's drop spam
throttle.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 19:53:09 +01:00
Ludovic MarieandClaude Opus 5.5 9ec5f1ddbc Rockets: a guest's boost rocket hurts them; fireworks sent to a server's clients
FireworkRocketEntity.dealExplosionDamage hurts attachedToEntity with the
full 5 + 2 per explosion: a guest's boost rocket now hurts that guest
instead of nobody. The host's encoder (network_stack) now sends
minecraft:fireworks and minecraft:firework_explosion too, so a rocket's
flight duration and a star's explosion reach the server thread's window,
LAN guests and a dedicated server's players; the creative slot encoder
shares it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 19:51:09 +01:00
vlesmentieandClaude Opus 5.5 e4a151c2e9 Menu buttons show the pointing hand under the mouse
26.3's AbstractWidget.handleCursor asks for CursorTypes.POINTING_HAND
when a widget is hovered, or NOT_ALLOWED when it is inactive, and the
frame applies the last request to the window (GuiGraphicsExtractor
requestCursor and applyCursor, Window.selectCursor). Horizon never
changed the cursor, so it stayed an arrow over buttons.

widgets.rs keeps the frame's request, ui::button and the resource pack
screen's buttons make it from the mouse hover (not keyboard focus), and
the game sets winit's Pointer, NotAllowed or Default cursor after each
frame when it changes.

Fixes #1347.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 15:49:15 -03:00
Ludovic Marie e555c4ee48 Merge pull request #1831 from kacpherek/fix/1503-guest-chest-comparator
🤖 Agent: thank you! Merged after the contribution gate (checked against vanilla 26.3; one integration branch of main + all 14 of your PRs: fmt, clippy -D warnings, horizon-client/world tests, horizon-ctl scenarios on the server thread and in-frame, lan_e2e in both host modes, and the 30 harnesses covering these changes: 29 pass, the 30th is /help listing /pregen from #1805) and this head's verify.
2026-10-25 19:43:16 +01:00
Ludovic Marie f5adaec209 Merge pull request #1828 from kacpherek/fix/1335-crafting-table-valid
🤖 Agent: thank you! Merged after the contribution gate (checked against vanilla 26.3; one integration branch of main + all 14 of your PRs: fmt, clippy -D warnings, horizon-client/world tests, horizon-ctl scenarios on the server thread and in-frame, lan_e2e in both host modes, and the 30 harnesses covering these changes: 29 pass, the 30th is /help listing /pregen from #1805) and this head's verify.
2026-10-25 19:41:29 +01:00
Ludovic Marie 406fbd7793 Merge pull request #1827 from kacpherek/fix/1334-menu-range
🤖 Agent: thank you! Merged after the contribution gate (checked against vanilla 26.3; one integration branch of main + all 14 of your PRs: fmt, clippy -D warnings, horizon-client/world tests, horizon-ctl scenarios on the server thread and in-frame, lan_e2e in both host modes, and the 30 harnesses covering these changes: 29 pass, the 30th is /help listing /pregen from #1805) and this head's verify.
2026-10-25 19:39:17 +01:00
Ludovic Marie c0567a323c Merge pull request #1804 from kacpherek/pr/surface-rule-bands
🤖 Agent: thank you! Merged after the contribution gate (checked against vanilla 26.3; one integration branch of main + all 14 of your PRs: fmt, clippy -D warnings, horizon-client/world tests, horizon-ctl scenarios on the server thread and in-frame, lan_e2e in both host modes, and the 30 harnesses covering these changes: 29 pass, the 30th is /help listing /pregen from #1805) and this head's verify.
2026-10-25 19:38:20 +01:00
Ludovic MarieandClaude Opus 5.5 f969415c7a Creative stacks: reuse the wire codec both sides already had
Horizon already encoded stacks for the wire (lan::guest::stack_to_network,
what a Horizon server sends its clients: damage, enchantments, stored
enchantments, repair cost, custom name, dyed colour, trim, maps, charged
projectiles, books) and decoded them on the client (remote.rs's
protocol_components + item_data::decode_components, every component it has
a codec for). The new encode/apply pair covered six components and
duplicated both.

- The client's SetCreativeModeSlot stack goes through stack_to_network.
- The decoder moves into item_sync::from_network, used by the client for
  what its server sends and by the server (Hub::stack_from_network) for a
  creative client's slot, so the server now keeps every component the
  client's decoder reads, not six.
- One round-trip test covers encode then decode with registries.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 19:36:52 +01:00
Ludovic Marie 0713ee5ec0 Merge pull request #1803 from kacpherek/pr/keep-scheduled-ticks
🤖 Agent: thank you! Merged after the contribution gate (checked against vanilla 26.3; one integration branch of main + all 14 of your PRs: fmt, clippy -D warnings, horizon-client/world tests, horizon-ctl scenarios on the server thread and in-frame, lan_e2e in both host modes, and the 30 harnesses covering these changes: 29 pass, the 30th is /help listing /pregen from #1805) and this head's verify.
2026-10-25 19:36:08 +01:00
Ludovic MarieandClaude Opus 5.5 c5bb51bb05 Mod API 0.3: typed server commands, in players' command trees
A server mod's registration now lists each command's arguments: a name, a
kind (word, text, integer with bounds, number, boolean, player, block-pos)
and whether it is optional. The server adds them to the command tree every
player receives, after vanilla's commands, so a player's chat box completes
and colours them as vanilla's. When a player sends one, the server parses
the line as vanilla's parsers do (horizon-mods' commands.rs: ~ coordinates
from the player, @s/@p, integer bounds) and calls the mod's command hook
with the values by name, or answers with vanilla's error without calling
the mod.

The hub keeps the server mods' commands as a root in the commands report's
format and resends players' trees when they change (CommandTree::
packet_with); the game sets it when its mods start, stop or reload, or a
hub opens. In-frame, the window's own chat box lists them by name.

SDK: Registration::command_with(name, [Argument::...]), Argument builders,
Args (text(), integer(), player(), block_pos() ...); command(name) is free
text, an optional text argument. Examples: welcome's /mined [player],
beacons' /beacon [height] and /ground [pos], with scripts for values and
errors.

Docs: docs/mods.md (typed commands, kinds table), docs/api/mods.md and
the regenerated reference.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 19:35:09 +01:00
Ludovic Marie f9d85d2e38 Merge remote-tracking branch 'origin/main' into cg14/pr1874 2026-10-25 19:28:34 +01:00
Ludovic Marie b4f12543ca Merge remote-tracking branch 'origin/main' into cg14/pr1850
# Conflicts:
#	tools/lan_e2e.py
2026-10-25 19:26:13 +01:00
Ludovic Marie 4604491fcb Merge pull request #1691 from socksprox/fix/translucent-particles 2026-10-25 19:23:59 +01:00
Ludovic MarieandClaude Opus 5.5 96199c23e7 Shader packs, clean room 41: entities and block entities in the shadow pass
shadow_entities and shadow_block (each falling back to shadow) load with
the entities' vertex inputs unless shadowEntities or shadowBlockEntities is
false (spec 2.4, 3.2; both on by default, OQ-19). They draw the same entity
and block-entity runs the gbuffers programs would, with the same per-draw
uniforms, into the shadow map after the solid terrain and before the
shadowtex1 copy. A run is uploaded when a gbuffers program or a shadow
program draws it; Horizon still draws the runs no gbuffers program covers.

Without a pack nothing changes: only the pack's entity code is touched.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 19:20:17 +01:00
vlesmentieandClaude Opus 5.5 2c8930c48d Chickens flap while they fall on a client of a server
Chicken.aiStep updates flap and flapSpeed on the client too, for the
chickens it only displays. tick_remote_entities never did, so with
single player on its server thread (and on servers) a falling chicken
kept its wings folded. The wing part of chicken_ai_step is now
chicken_flap, and the remote tick runs it from the server's on-ground
flag; the falling slowdown and egg laying stay the server's.

Fixes #1706.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 15:16:32 -03:00
rdbtCVSandOpenAI Codex b3840adccd Fix renderer backend fallback after surface creation failure
Keep surface creation errors in the backend failure list so Windows Auto can try DirectX 12 after Vulkan fails.

Co-Authored-By: OpenAI Codex <[email protected]>
2026-10-25 18:08:03 +00:00
Joansitoh 90eda915d5 Add a lore tooltip harness against ItemLore.addToTooltip.
The styled lines have to keep matching vanilla, including an explicit non-italic span and a lore component hidden by tooltip_display.
2026-10-25 18:58:52 +01:00
Ludovic MarieandClaude Opus 5.5 35a89eaebe Mod API 0.3: capability world for server mods (blocks, biomes)
Server mods get interface world: get-block(dimension, x, y, z) reads a
loaded chunk's block state as "minecraft:oak_log[axis=y]", get-biome its
biome ("minecraft:plains"), and
set-blocks(dimension, changes) sets up to 4096 blocks per call, checked at
once, made as /setblock makes them (place_state: neighbours, light, block
entities, every client told through the block log) after the call, before
that dimension's next tick.

Reads cost no copy: for a mod call the game lends Mods the levels' chunk
maps (moved out with mem::take and put back after), only while a running
mod has world. Writes for a level that isn't ticking (another dimension,
in-frame) wait for it, up to 65,536.

Test scripts get blocks (setup) and expect.blocks; cargo horizon-mod check
reports a trial run's block writes. Example beacons: /beacon builds a
glowstone pillar beside the player, /ground reads the block underfoot and the biome.
parse_state is now a free function so the mod path shares it.

Docs: docs/mods.md (the capability, script keys), docs/api/mods.md and the
regenerated reference.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 18:55:29 +01:00
Ludovic MarieandClaude Opus 5.5 bdbc816486 Creative search: match the localized hover name, not the item's base name
The base name (Tipped Arrow, Potion, White Banner) matched where vanilla's
first tooltip line (Arrow of Poison, Water Bottle, Ominous Banner) doesn't:
the creative harness found extra results for 'e', 'potion', 'tipped' and
'white'. name_line builds the full hover name through display_get.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 18:53:20 +01:00
Ludovic MarieandClaude Opus 5.5 3a35e147e5 Server mods run in the game: server thread, in-frame, LAN hosts, dedicated
The game that simulates a world now starts its server mods (API 0.3,
experimental): single player's server thread, single player in-frame
(beside client mods), a LAN host either way, and the dedicated server.
Windows that are clients of a server never run them.

game/server_mods.rs: tick_server_mods at the end of the tick diffs the
players (player_joined, player_left), delivers the tick's chat and block
events in order, then calls tick. Block events come from players' breaking
and placing (the acting player is the guest during as_guest); chat from the
local chat path and the hub's new HostEvent::PlayerChat; a guest's /command
reaches its mod before vanilla's dispatcher. tell and command replies go to
their player (the window's chat or a SystemChat); broadcast goes to every
player and the log.

--dev-mod now works on the server thread, and servers check a dev mod's
file once a second from their tick. Every hook returns at once when no
server mod runs; without the mods feature they are empty stubs.

Docs: docs/mods.md (where server mods run, when hooks run, not yet),
docs/api/mods.md.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 18:48:13 +01:00
Ludovic Marie 6f8cad0020 Merge pull request #1692 from socksprox/fix/streamed-server-pack-zip 2026-10-25 18:45:56 +01:00
Ludovic MarieandClaude Opus 5.5 aa2f60b7bd Copper golem statues turn to their next pose when right-clicked
CopperGolemStatueBlock.useItemOn: anything but an axe, an empty hand too,
cycles copper_golem_pose with its sound and block_change game event; honeycomb
on a weathering statue passes on to waxing. Axe scraping, unwaxing and
stripping and honeycomb waxing now count the item as used, and waxing sends
its block_change game event, as vanilla's useOn does. The use_item_on harness
gains statue cases.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 18:43:03 +01:00
Jan PresandOpenCode 13508cbd9c Creative: Ctrl+Backspace deletes a word in the search
The creative inventory's search box is an append-only EditBox of 50
characters, but its editing was a plain String pop, so Ctrl+Backspace
removed one character instead of the word before the caret, unlike every
other text field and vanilla's EditBox.keyPressed.

Move EditBox.getWordPosition and deleteWords into one shared helper in
ui.rs (the UI and chat text fields had two copies of it), and route a
Ctrl+Backspace in the creative search through it. A Select All selection
is still what backspace deletes first, as in vanilla.

Fixes #1764.

Co-Authored-By: OpenCode (DeepSeek V4.1 Flash Free) <[email protected]>
2026-10-25 17:41:07 +00:00
Ludovic MarieandClaude Opus 5.5 c965aad8d3 Shader packs, clean room 40: terrain in the shadow map, culled from the light
The shadow program now draws the world's terrain from the light's view.
The shadow pass culls its own section list against the shadow camera's
frustum (render distance, capped by shadowDistanceRenderMul), nearest
the light first, and draws only the faces toward the light. Its draws go
by arena page within 64-block bands from the light, as the camera's solid
draws do, so the flat path draws them in about ten calls per list instead
of one per page change (over a thousand with BSL). Its draws
follow the camera's translucent draws in the same instance and indirect
buffers, so the flat path gives them calls of their own. Sections never
sorted for the camera are sorted once so their translucent indices are
valid.

The frame's first shadow pass clears the shadow map and shadowcolor
buffers as it begins, instead of a separate pass that loads and stores
them all; the translucent shadow pass is timed under its own label.

Without a pack nothing changes: the shadow lists are empty and the
terrain stays one draw call.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 18:36:20 +01:00
socksproxandDevin 72b9daabf0 Resolve legacy § codes where the font draws text
Name tags and styled HUD text drew a span's § codes as glyphs instead of
restyling what follows, as StringDecomposer.iterateFormatted does. A team
prefix like "§c" showed literally over players' heads, and a resource
pack's styled menu.disconnect kept its codes on the leave-server button.

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-25 18:24:46 +01:00
Ludovic Marie 1087739079 Merge pull request #1877 from ludovic111/roadmap/decided-2026-10-25
🤖 Agent: merged after verify; Ludovic's yes/no on the 54 candidates from the project's history (roadmap check, 20 roadmap tests, 31 website tests, site builds with 90 items).
2026-10-25 18:17:00 +01:00
Ludovic Marie d8ed068db8 Merge pull request #1781 from levzzz5154/fix/vehicle-passenger-pose
🤖 Agent: thank you! Merged after the contribution gate (checked against vanilla 26.3, tested in the modes it touches; pr_ready on main + the six gate PRs: fmt, clippy, 1327 tests, 112/112 affected harnesses) and this head's verify.
2026-10-25 18:16:18 +01:00
Ludovic MarieandClaude Sonnet 5.5 2ec2964962 Roadmap: the items Ludovic decided on (55 added, 4 updated, 3 areas)
Adds the ideas he said yes to, as roadmap items with plain and dev text,
and extends ai-companion, new-versions, benchmarks and intel-mac. New
areas: platforms, ai, community.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-25 18:09:13 +01:00
Ludovic MarieandClaude Opus 5.5 bb414b263c Merge main into fix/vehicle-passenger-pose: keep both lan_e2e checks
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 18:08:13 +01:00
Ludovic MarieandClaude Opus 5.5 a276f81aba Shader packs, clean room 39: the run doc comment back on run
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 17:55:23 +01:00
Ludovic Marie f324251563 Merge remote-tracking branch 'origin/main' into cg14/pr1813 2026-10-25 17:53:14 +01:00
socksproxandDevin f2832f83ef check: cover the ui open/close-screen outputs
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-25 17:53:13 +01:00
Ludovic MarieandClaude Opus 5.5 5ee1130719 horizon-dualsense: iOS comments name only what exists
The repo has no horizon-gamecontroller crate, and on iOS the Link never
opens, so no rumble covers vibration there.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 17:53:13 +01:00
Alexander KrylovandHermes Agent 9fe77c7a56 Creative: a stack keeps its components when it crosses the wire
`Game::to_network` wrote damage alone into SetCreativeModeSlot, and
`item_from_network` read back damage and enchantments, so anything else a
creative stack carried was gone once the server echoed the inventory: a
named sword lost its name, trimmed armour its trim, an enchanted book its
stored enchantments and its repair cost.

`item_sync` is the pair for the components Horizon models: `encode` writes
them with the synced registries' ids (an unknown name is left out rather
than sent as id 0, which is a different entry) and `apply` reads them back
into the same `StackData` fields `item_data::decode_components` fills.

Potions are not carried yet: `minecraft:potion_contents` has no id or codec
in horizon-protocol, so a creative potion still arrives as an uncraftable
one (#1832). Nor are containers, charged projectiles, written books and a
map id, which the packet models but this patch does not write.

Refs #1832

Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
2026-10-25 19:49:08 +03:00
Ludovic MarieandClaude Opus 5.5 6443847633 Launcher: add the panorama, Unifont, languages and sounds prepared resources lack
Versions prepared while the asset store could not be read (Prism Launcher
installs since 0.2.48) are silent with a grey title screen. Like the built-in
packs (#1539), the launcher now copies each of these from the player's own
asset store into prepared resources that lack it, at start, after an install
and before Play, without preparing again.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 17:42:00 +01:00
Ludovic MarieandClaude Opus 5.5 b873f30c29 wears_freeze_immune above sync_player_body's doc comment, which it had taken over
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 17:41:04 +01:00
Ludovic MarieandClaude Opus 5.5 8c54e18587 /ride dismount: the boats' BOARDING_COOLDOWN, not a second 60
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 17:40:41 +01:00
Ludovic MarieandClaude Opus 5.5 0dfc6d8700 command_exec harness: summoned mobs' saved attribute bases and health (#1483)
A cow summoned with max_health and movement_speed bases, and a zombie
with an attack_damage base and a saved Health, against vanilla.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 17:38:28 +01:00
Ludovic Marie b983282247 Merge remote-tracking branch 'origin/main' into cg14/pr1850
# Conflicts:
#	tools/lan_e2e.py
2026-10-25 17:38:04 +01:00
Ludovic MarieandClaude Opus 5.5 a495fcf7a7 End crystal: one blast call for both paths
The respawn check is read before the blast, so the two copies of the
explosion collapse into one.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 17:37:51 +01:00
Ludovic Marie 83e8cbc4b6 Merge remote-tracking branch 'origin/main' into cg14/pr1856 2026-10-25 17:37:23 +01:00
Ludovic MarieandClaude Opus 5.5 b729cfc833 Furnace experience: createExperience's chance, orbs, every removal, guests
- createExperience floors count * experience and adds one with the chance of
  the fraction (a level random nextFloat); flooring alone paid nothing for a
  single iron ore (0.7).
- The payout pops ExperienceOrb.award's orbs at the player, recipe by recipe,
  instead of adding points directly.
- A server's player (the default single-player path, LAN and dedicated)
  gets it too: lan_machine_click pays when the guest takes from the output.
- AbstractFurnaceBlockEntity.preRemoveSideEffects pays at the block's centre
  on any removal (explosions, /setblock, creative breaking), after the
  contents spill, so it moves from break_block to the removal side effects.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 17:37:22 +01:00
Ludovic MarieandClaude Opus 5.5 1a39af6b62 /pregen: plan any radius at once, and skip a strip's rows outside the circle
Pregen::new counted the area chunk by chunk and split the strips with
an i32 product: /pregen start 30000000 overflowed, then hung the server
thread for hours. Rows are now counted from the circle's half-width,
the strip edges in i64, and each strip starts and ends at the rows the
circle reaches, so a band never walks empty corner rows. Unit tests
check that every chunk of squares and circles is swept once and that
the largest radius plans at once.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 17:36:22 +01:00
Ludovic MarieandClaude Opus 5.5 71f0ffde4b Prepare: a launcher's rewritten version file falls back to Mojang's (#1772)
Launchers like HMCL rewrite versions/<v>/<v>.json in the official layout
(same jars and libraries, their own fields), and horizon-prepare refused it
as a version Mojang may have republished. Mojang's copy saved as the imported
manifest.json is now used instead, and without one the error says the
launcher may have rewritten it and how to get Mojang's.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 17:31:44 +01:00
Ludovic MarieandClaude Opus 5.5 0298d10988 Shulker boxes: one canFitInsideContainerItems for both menus, and a unit test
The client's menu and the server's guest menus now share
menu::fits_inside_container_items, and a resource-backed test checks
that a guest's shulker box menu refuses shulker boxes by pickup,
shift-click and number key while other containers take them.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 17:31:37 +01:00
Ludovic Marie 8942d00ace Merge remote-tracking branch 'origin/main' into cg14/pr1857 2026-10-25 17:30:38 +01:00
Ludovic MarieandClaude Opus 5.5 0609fb2beb Tooltip: lore after the enchantments, as ItemStack.addDetailsToTooltip
Vanilla adds LORE after CONTAINER, BANNER_PATTERNS, FIREWORKS,
POTION_CONTENTS, TRIM and the enchantments, and before the attribute
modifiers, not right under the name.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 17:30:37 +01:00
Ludovic Marie e37770bbfe Merge remote-tracking branch 'origin/main' into cg14/pr1858 2026-10-25 17:30:01 +01:00
Ludovic MarieandClaude Opus 5.5 70aff6e90b Death screen: empty message with showDeathMessages off; forget a guest's record when they leave
- ServerPlayer.die sends CommonComponents.EMPTY to the death screen when
  showDeathMessages is off, as the local player's death_message already
  does; the combat record is only read when the rule is on.
- A guest's CombatTracker leaves guest_combat with them, so a long-running
  server doesn't keep one per connection.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 17:30:01 +01:00
Ludovic Marie b9d2d93ccb Merge remote-tracking branch 'origin/main' into cg14/pr1806 2026-10-25 17:27:41 +01:00
Ludovic MarieandClaude Opus 5.5 df23e1bfed Keep vanilla's death screen message; resolve with the loaded language
- Leave ServerPlayer.die's message as it was: with showDeathMessages off
  vanilla sends CommonComponents.EMPTY to the death screen, and #1858
  reworks the death message from the combat record.
- The effects panel and recipe book tooltips use Names::display_get, so a
  key missing from the language still falls back instead of showing the key.
- net::load_language reuses the language lang already loaded (packs
  included) instead of reading it from disk again.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 17:27:41 +01:00
Ludovic MarieandClaude Opus 5.5 75951a769a Merge #1847 into #1849: LocatorBarClient's respawn and use commands, the harness scaffold
The three harnesses add commands to the same LocatorBarClient lines;
this branch now builds on #1847 (itself on #1843), keeps all three
commands, and test_cursor_death_parity.py runs through
locator.one_player.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 17:27:31 +01:00
Ludovic MarieandClaude Opus 5.5 94bc569a7f Merge #1843 into #1847: share LocatorBarClient's respawn command and the harness scaffold
Both add the same respawn command to LocatorBarClient; this branch now
builds on #1843's, and test_pearl_death_parity.py runs through
locator.one_player.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 17:27:00 +01:00
Ludovic Marie 5115bc276b Merge pull request #1650 from socksprox/fix/proxy-transfers
🤖 Agent: thank you! Merged after the contribution gate (checked against vanilla 26.3, tested in the modes it touches; pr_ready on main + the six gate PRs: fmt, clippy, 1327 tests, 112/112 affected harnesses) and this head's verify.
2026-10-25 17:26:57 +01:00
Ludovic MarieandClaude Opus 5.5 50727eb5c3 Harnesses: one scaffold for one-player dedicated server scripts
test_hardcore_respawn_parity.py copied test_locator_bar_parity.py's
build and run code; both now share locator.sides, and one-player
scripts run through locator.one_player.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 17:26:43 +01:00
Ludovic MarieandClaude Opus 5.5 65d3edfc3a Launcher: find the client jar of a Modrinth App mod loader profile (#1783)
The Modrinth App keeps a Fabric or Quilt profile's vanilla client jar as
`versions/<v>-<loader>/<v>-<loader>.jar`; a player who only played such a
profile has no `versions/<v>/`, and Prepare said Minecraft was not found.
Those jars are now candidates too, checked against the pinned sha1 like the rest.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 17:26:03 +01:00
Ludovic MarieandClaude Opus 5.5 ee74553314 Merge #1827 (container reach) into #1828: share its reach helper
Both PRs add a stillValid check to the same tick functions; this branch
now builds on #1827's lan::guest::within_block_range for the guest's and
the in-frame table checks, and the in-frame check sits beside the
container one in storage.rs.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 17:25:53 +01:00
Ludovic Marie 7bac73094f Merge pull request #1860 from ludovic111/fix/launcher-pack-test-windows
🤖 Agent: merged after verify and a launcher dry run on all three platforms (Windows tests pass); the launcher release follows.
2026-10-25 17:25:11 +01:00
Ludovic MarieandClaude Opus 5.5 6d3cd330d1 One isWithinBlockInteractionRange for guests, the in-frame player, stations, lecterns and signs
The reach test was written out four times (stations, lecterns, the
in-frame container check and the guest's); they now share
lan::guest::within_block_range.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 17:25:10 +01:00
Ludovic Marie ef82e97d67 Merge pull request #1652 from socksprox/fix/remote-player-skins
🤖 Agent: thank you! Merged after the contribution gate (checked against vanilla 26.3, tested in the modes it touches; pr_ready on main + the six gate PRs: fmt, clippy, 1327 tests, 112/112 affected harnesses) and this head's verify.
2026-10-25 17:24:13 +01:00
Ludovic Marie 1920106445 Merge pull request #1793 from socksprox/launcher-dark-titlebar
🤖 Agent: thank you! Merged after the contribution gate (checked against vanilla 26.3, tested in the modes it touches; pr_ready on main + the six gate PRs: fmt, clippy, 1327 tests, 112/112 affected harnesses) and this head's verify.
2026-10-25 17:23:29 +01:00
Ludovic Marie aca93fa902 Merge pull request #1779 from levzzz5154/fix/sprint-fov-stuck
🤖 Agent: thank you! Merged after the contribution gate (checked against vanilla 26.3, tested in the modes it touches; pr_ready on main + the six gate PRs: fmt, clippy, 1327 tests, 112/112 affected harnesses) and this head's verify.
2026-10-25 17:23:02 +01:00
Ludovic MarieandClaude Opus 5.5 204859a2cb Launcher: a preparation reusing imported game files still finds the assets
When an update's preparation reused the game files an earlier version imported,
`preparer` handed the chosen folder as it was to the sound copy and the panorama,
fonts, packs and languages step. A Prism Launcher instance or Modrinth App profile
holds no asset store, so the game was silent with a grey title screen, and
Prepare again did the same. It now resolves the folder (linking another
launcher's files into the official layout) either way; with reused imports a
missing one is a log line, not an error.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 17:20:41 +01:00
Ludovic Marie e99414e9d3 Merge pull request #1795 from socksprox/t3/fix-remote-close-ghost-grid
🤖 Agent: thank you! Merged after the contribution gate (checked against vanilla 26.3, tested in the modes it touches; pr_ready on main + the six gate PRs: fmt, clippy, 1327 tests, 112/112 affected harnesses) and this head's verify.
2026-10-25 17:16:15 +01:00
Ludovic Marie 66a35ab2ea Merge pull request #1865 from ludovic111/fix/website-docker-data
🤖 Agent: merged after verify; image build simulated locally (hub, data, docs/api in dist).
2026-10-25 17:15:31 +01:00
Ludovic MarieandClaude Opus 5.5 d41a3075aa Launcher: a local registry's file:/// URLs open on Windows
file:///C:/x left /C:/x after the scheme, which Windows can't open; the pack tests now
write real file:/// URLs (the index only accepts those), with forward slashes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 17:13:26 +01:00
Ludovic MarieandClaude Opus 5.5 fd154dcbba Website image: copy data/ so the build finds the roadmap
build.mjs reads website/data/roadmap.json since the hub (#1819), but the Dockerfile never
copied data/, so every site deploy since then failed and playhorizon.dev kept the old build.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 17:13:06 +01:00
socksproxandDevin af7a685b5c Mod API: capability ui — mod-owned screens and settings pages
New WIT interface `ui` (text, item and list widgets; open-screen,
close-screen, set-settings) plus a `ui-events` export (select, closed,
settings-opened), in a second world `horizon-ui-mod` so mods built before
it keep loading. The game lays out and draws the widgets with its own
menus' code: a screen the mod owns (Esc/Done to leave) and a Settings
button on the mod's Options > Mods row that opens its page.

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-25 17:09:25 +01:00
Ludovic MarieandClaude Opus 5.5 df8b23637e horizon-mods: rustfmt
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 17:09:21 +01:00
kaicsm fdfa3d568d Merge branch 'ludovic111:main' into fix/client-mods-server-thread 2026-10-25 13:01:39 -03:00
Ludovic Marie 7ab91a4d2e Merge pull request #1859 from ludovic111/feat/mod-tool-content
🤖 Agent: merged after verify passed; site built with every docs page and no broken links (platform-2), website tests pass.
2026-10-25 17:00:44 +01:00
Ludovic Marie 9f761a3e3b Merge pull request #1810 from ludovic111/docs/api-portal-website
🤖 Agent: merged after verify passed; site built with every docs page and no broken links (platform-2), website tests pass.
2026-10-25 16:58:41 +01:00
Ludovic MarieandClaude Opus 5.5 c640561000 Launcher: the registry pack tests build file URLs with forward slashes, so they pass on Windows
A Windows temp path's backslashes went into the test index's JSON as escapes, so the
index failed to parse and the Windows launcher release stopped at its tests.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 16:56:26 +01:00
Ludovic Marie d18b7701da Merge pull request #1852 from ludovic111/feat/hub-docs
🤖 Agent: merged after verify passed (hub: website tests 30/30, roadmap tests + sync check, telemetry server 35 tests; local end-to-end run by roadmap-1).
2026-10-25 16:56:07 +01:00
Ludovic Marie 1bf957989a Merge pull request #1851 from ludovic111/feat/hub-forms
🤖 Agent: merged after verify passed (hub: website tests 30/30, roadmap tests + sync check, telemetry server 35 tests; local end-to-end run by roadmap-1).
2026-10-25 16:54:01 +01:00
Ludovic Marie a230411fa3 Merge pull request #1839 from ludovic111/feat/hub-intake
🤖 Agent: merged after verify passed (hub: website tests 30/30, roadmap tests + sync check, telemetry server 35 tests; local end-to-end run by roadmap-1).
2026-10-25 16:51:54 +01:00
Ludovic Marie 26b94efcdf Merge pull request #1838 from ludovic111/feat/hub-votes
🤖 Agent: merged after verify passed (hub: website tests 30/30, roadmap tests + sync check, telemetry server 35 tests; local end-to-end run by roadmap-1).
2026-10-25 16:49:41 +01:00
Ludovic Marie e5214f7f25 Merge pull request #1821 from ludovic111/feat/hub-board
🤖 Agent: merged after verify passed (hub: website tests 30/30, roadmap tests + sync check, telemetry server 35 tests; local end-to-end run by roadmap-1).
2026-10-25 16:48:00 +01:00
Ludovic Marie 2576e80ff2 Merge pull request #1819 from ludovic111/feat/roadmap-hub
🤖 Agent: merged after verify passed (hub: website tests 30/30, roadmap tests + sync check, telemetry server 35 tests; local end-to-end run by roadmap-1).
2026-10-25 16:45:51 +01:00
Ludovic MarieandClaude Opus 5.5 effea2cfd4 Merge feat/hub-board (main's registry pages) into feat/hub-forms
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 16:41:10 +01:00
Ludovic MarieandClaude Opus 5.5 bcee0d02ec Merge feat/roadmap-hub (main's registry pages) into feat/hub-board
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 16:40:01 +01:00
Ludovic MarieandClaude Opus 5.5 78ab03e554 Merge main into feat/roadmap-hub: the hub beside the registry's pages
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 16:39:17 +01:00
Ludovic MarieandClaude Opus 5.5 390da26cf4 horizon-mod-registry: the form's test sends a shader pack through the game's check
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 16:38:54 +01:00
Ludovic MarieandClaude Opus 5.5 ba4cf45aac cargo horizon-mod: content mods (new --template content, build packs assets and data, check takes no code)
- `new --template content`: a content mod with one block (Glowing Amethyst,
  copying vanilla's amethyst block) and its block state, model, item model,
  name, loot, recipe and pickaxe tag; `--template code` is the default.
- `build` packs the mod's `assets/` and `data/` (read and checked as the
  game reads them) beside the component; a folder without `Cargo.toml` is a
  content mod and needs no compiler.
- `check` takes a `.hmod` with no `mod.wasm`: "no code: content only,
  nothing to run" instead of the trial run, which refused it, so the
  registry can publish content mods.
- The guide (content mods, make a mod) and docs/mods.md, docs/api/mods.md
  say so.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 16:35:58 +01:00
Joansitoh d9360338f6 Show item lore in tooltips with vanilla purple italic styles.
Stack tooltips skipped minecraft:lore entirely; draw the component lines as Text so colours and explicit non-italic spans survive.
2026-10-25 16:33:35 +01:00
Ludovic MarieandClaude Opus 5.5 1285247230 Registry: shader packs are checked by the game's own shader check, and open
The `shader` kind's check (index::inspect) reads the .hshader and runs
horizon_shader_api::check, the same check the game runs before it loads a
pack, so the registry's CI (`cargo horizon-mod check` / `registry build`)
refuses a pack the game would refuse, with each problem at its file and line.
Its facts are the manifest's id, version and shader API line. The kind's
files are .hshader, `cargo horizon-mod check` recognises them, and
registry::open takes shader submissions. Installing one from the launcher
stays refused (check_file) until the launcher installs shader packs.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 16:31:05 +01:00
JasonGuandOpenCode DeepSeek V4.1 Flash feec1b67e8 Server deaths: build the message from the player's own combat record
A guest's death only had `death_cause`, which `/kill` alone sets, so a
server-thread, LAN or dedicated death fell back to "X died" or a generic
cause. Give each guest its own CombatTracker, fed by the motion events
the server already records, and take the message from
CombatTracker.getDeathMessage with the dying player's own name. The
wither's skull names the wither that launched it (projectile owner).

The death screen now resolves the server's component in the client's
current language instead of the session's en_us table, so it matches the
chat line.

Co-Authored-By: OpenCode DeepSeek V4.1 Flash <[email protected]>
2026-10-25 23:29:23 +08:00
Alexander KrylovandHermes Agent bf81ae6358 Furnace: pay the experience a smelt earned
RecipesUsed was written as an empty compound and never read, so a furnace
paid nothing when its output was taken and nothing when it was broken.

- Furnace keeps the per-recipe counter, saves and loads it, and floors each
  recipe on its own as AbstractFurnaceBlockEntity.createExperience does.
- FurnaceResultSlot.checkTakeAchievements pays on a take: the counter is
  read and cleared when the output slot loses an item.
- AbstractFurnaceBlockEntity.playerDestroy pays the same counter as orbs at
  the block when it is broken, whatever the tool.

Fixes #1732

Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
2026-10-25 18:28:57 +03:00
Ludovic MarieandClaude Opus 5.5 6320718491 Shader packs, clean room 39: shadow buffers, their samplers and shadowcomp
When the shadow pass runs, the pack gets real shadow buffers (spec 6.3):
shadowtex0 (cleared to 1.0 every frame), shadowtex1 (a copy, made only when
a program reads it) and shadowcolor0/1 (each with two sides, made only when
used, cleared to their ShadowClear colour, white by default), square at
shadowMapResolution within the device's limit.

Samplers: shadowtex0/watershadow, shadowtex1, `shadow` (shadowtex1 beside
watershadow) and the HW forms read the shadow map, through a linear depth
comparison when declared sampler2DShadow; shadowcolor/shadowcolor0/1 read
the colour buffers. In the shadow programs, buffer n is shadowcolor n and
colortex reads a stand-in. Without the shadow pass nothing changes: the
names read the stand-ins as before.

shadowcomp..shadowcomp99 run after the shadow pass and before prepare, each
drawing into the alt side of the shadowcolor buffers it writes and flipping
them after, as composite programs do with colortex (6.1.6). The shadow pass
itself draws nothing yet, so the map stays clear.

Clean-room: implemented from cleanroom/shader-packs-spec.md revision 1,
sections 2.1, 5.2 (samplers), 6.1.6 and 6.3; no other loader's code read.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 16:25:54 +01:00
Ludovic MarieandClaude Opus 5.5 86d8d9e3a1 Shader packs, clean room 38: load the shadow programs
The shadow pass runs when some program reads a shadow buffer (shadow,
watershadow, shadowtex0/1, their HW forms, shadowcolor, shadowcolor0/1) or
shadow.enabled=true; shadow.enabled=false keeps it off (spec 2.5). Then
`shadow` (solid and cutout terrain), `shadow_water` falling back to
`shadow` (translucent terrain) and shadowcomp..shadowcomp99 load, in the
new Shadow and ShadowComp groups (2.1, 2.4). shadow_solid and shadow_cutout
are not used: the game's solid and cutout terrain is one draw.

Their output n writes shadowcolor n (6.3): draw buffer lists stop at the
first index past 1. translate_shadow gives a shadow program the shadow
camera's matrices as its model-view and projection (5.2). The game does not
draw any of them yet.

Clean-room: implemented from cleanroom/shader-packs-spec.md revision 1,
sections 2.1, 2.4, 2.5, 5.2 and 6.3; no other loader's code read.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 16:25:34 +01:00
Ludovic MarieandClaude Opus 5.5 5dcbe222c9 Shader packs, clean room 37: shadow settings and the shadow camera
The stage files' shadow constants (shadowMapResolution, shadowDistance,
shadowDistanceRenderMul, shadowIntervalSize, sunPathRotation, shadowMapFov,
shadowNearPlane, shadowFarPlane, shadowHardwareFiltering[0|1] and the
shadowcolor0/1 Format, Clear and ClearColor) are read into Config::shadow
(spec 6.3, 7.1).

The shadow matrices are no longer a placeholder (OQ-6): the camera looks
from the shadow light, sits 128 blocks toward it from a point that moves in
steps of shadowIntervalSize along the shadow view's own axes, and projects
orthographically over shadowDistance each side (or in perspective with
shadowMapFov), between shadowNearPlane and shadowFarPlane.

sunPathRotation turns the sun and moon's path about the world's X axis:
sunPosition, moonPosition, shadowLightPosition and the shadow camera follow
it, and so do the sun, moon and stars a pack's gbuffers_skytextured and
gbuffers_skybasic draw.

Clean-room: implemented from cleanroom/shader-packs-spec.md revision 1,
sections 6.3 and 7.1; no other loader's code read.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 16:25:34 +01:00
Ludovic MarieandClaude Opus 5.5 a6f951e126 Website: Make a mod, the full guide from the first line to the registry
Six pages under a "Make a mod" group of the docs: set up and see a mod in
the game (the launcher's Game arguments or cargo horizon-mod run), the
manifest and every capability, the hooks and calls with a worked example
(Playtime, built and checked), content mods, testing and check, and
publishing (minisign signing, the submit form, review, updates). /mods,
the submit page, the docs index, Mods and data packs and the Mod API link
to it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 16:22:19 +01:00
Ludovic Marie 1a91727eee Merge pull request #1815 from ludovic111/feat/roadmap
🤖 Agent: merged after verify passed (hub: website tests 30/30, roadmap tests + sync check, telemetry server 35 tests; local end-to-end run by roadmap-1).
2026-10-25 16:17:14 +01:00
Ludovic MarieandClaude Opus 5.5 818b9638ed CONTRIBUTING.md and docs/README.md point to the hub
Where to start is playhorizon.dev/hub: the roadmap, the work board, good first
tasks and the forms; roadmap PRs carry their roadmap:<id> label, ideas go to the
suggestion queue and issues are for bugs.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 16:15:16 +01:00
Ludovic Marie c462eb0b48 Merge pull request #1846 from ludovic111/feat/registry-owner-links
🤖 Agent: merged after verify passed (website + launcher owner links, tests added).
2026-10-25 16:14:48 +01:00
Ludovic MarieandClaude Opus 5.5 c1ffa17c87 Hub: vote for roadmap items, report a bug and suggest something from the page
Each item still to come gets a "Want it" button with its count; the Contribute
tab gets a bug report form (the #horizon-bugs template, which the telemetry
server opens as a GitHub issue and links back) and a suggestion form (to the
private queue Kira relays), and a Contributors list that shows the Discord name
of those who agreed on Kira (/contributors.json). The forms and buttons appear
once the hub's server answers; until then the page points to GitHub and Discord.
roadmap/README.md documents the intake API that Kira shares.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 16:14:12 +01:00
Ludovic MarieandClaude Opus 5.5 4a9ca29887 Website: the API portal at /docs/api, with Markdown tables
The repository's docs/api/ (the portal, its surface pages and the generated mod and
server references) is built into the docs section: /docs/api is its README and
/docs/api/<page> each page, in two nav groups, "Build on Horizon" and "API reference".
The pages are read from the checkout or from main on GitHub, like the README, so the
site never holds a copy. Links between portal pages become the site's (and the build's
link check covers them); other relative links go to the file on GitHub.

The renderer gains tables: the "Ways in" table, the references and the other tables
on the site (apis, performance) showed as raw pipes. A link whose label is a code span
no longer throws.

Caddy and the dev server route /docs/api/<page> and redirect /docs/api/ to /docs/api;
Railway rebuilds when docs/api/ changes. /docs/mods links the Mod API, its reference
and its ways in; the summary page is now "APIs at a glance".

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 16:14:03 +01:00
Ludovic MarieandClaude Opus 5.5 bac51e3fba Telemetry server: the hub's bug reports and suggestions
POST /hub/report opens a GitHub issue at once (labels bug and from-website, the
every template field required, people's text made inert (no mentions, issue
references, HTML or closing keywords) and the same report twice answered with the
first issue. If GitHub fails it answers 202 and the upkeep tries again for a week.
Off (503) without HORIZON_TELEMETRY_GITHUB_TOKEN.

POST /hub/suggest queues a suggestion; GET /hub/suggestions?after=<id> hands the
queue to Kira with the HORIZON_TELEMETRY_HUB_TOKEN bearer (404 when unset, 401 for
a wrong token). Nothing is published.

Per-address limits (20 an hour, bursts of 2) on keyed hashes of the address, daily
caps from everyone (30 reports, 200 suggestions), a honeypot field, entries kept
90 days.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 16:10:18 +01:00
Ludovic MarieandClaude Opus 5.5 91148cdf74 Telemetry server: hub votes allow 600 an hour per address (bursts of 60)
A visitor voting down the roadmap, or a school behind one address, ran out of
the 12-request burst. Votes are cheap; the item cap and the voter id still bound them.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 16:09:48 +01:00
Kacpherek 5bfc649edd Drop the cursor stack of a dead player
A stack held on the inventory cursor at death stayed there through the
respawn, even with keep_inventory off. In vanilla the dead player is
removed 20 ticks after dying (or when it respawns sooner), and
Player.remove's inventoryMenu.removed drops what the cursor held where
the player died. Do the same for server players and the in-frame
player, and add a harness that dies with a cursor stack on a dedicated
server and compares with vanilla.
2026-10-25 16:09:09 +01:00
Ludovic MarieandClaude Opus 5.5 0eb475152e Registry: show who publishes a project, linked to their GitHub profile
The index already carries each project's owners (the GitHub accounts the
registry checked at submission), but neither the website nor the launcher
showed them. The project page now says "Published by @login" with the
avatar and a link to the profile; the launcher adds an @login link per
owner. Only strings that are valid GitHub logins become links.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 16:07:46 +01:00
Kacpherek 0c017678eb Keep shulker boxes out of shulker boxes on the server
ShulkerBoxSlot.mayPlace refuses items that cannot fit inside container items, which BlockItem says of every shulker box. The server's menus for guests treated a shulker box's slots like a chest's, so with the server thread a shulker box could be clicked, shift-clicked or swapped into another. Apply the slot rule when the open menu is a shulker box.
2026-10-25 16:05:57 +01:00
Kacpherek 10efca9e6c Discard a dead player's ender pearl when the rule says so
With ender_pearls_vanish_on_death on, a pearl thrown just before death
kept flying and pulled the respawned player back to it. Vanilla's
ThrownEnderpearl.tick discards the pearl instead of ticking it while
its owner is dead. Add a harness that throws a pearl, dies and respawns
on a dedicated server and compares the teleports with vanilla's.
2026-10-25 16:05:52 +01:00
Ludovic MarieandClaude Opus 5.5 e458e37249 Hub: journal.json in the flat format Kira reads, at /data/journal.json
One entry per merged pull request (with its roadmap item), game or launcher
release, shipped roadmap item (post and announce) and weekly summary, newest
first, ids stable forever: the feed kira's src/journal.js posts to #updates and
X. The journal's item links are hub#<id>; the board's and the journal's ids are
reserved too, and the page-wide unique-id test covers every tab.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 16:02:36 +01:00
Kacpherek 436c2fefb4 Freeze server-thread players in powder snow
With the server thread, the host's copy of a player never set isInPowderSnow from the blocks along its moves and never ran LivingEntity.aiStep's freezing, so a frozen player took no freezing damage and the client's own damage was overwritten. Record powder snow's FREEZE on the server path and run the freezing countdown and its damage every 40 ticks there. The guest's freeze-immune armor now comes from its worn armor, as for the local player.
2026-10-25 16:01:39 +01:00
Kacpherek f48cfb7966 Let /ride mount and dismount mob riders
/ride now looks up a mob's vehicle as well as the player's, so mobs
can mount mobs, get the already-riding and loop errors, and dismount
to the vehicle's dismount spot. execute on vehicle uses the same
lookup. The command_exec corpus gets a mob riding group.
2026-10-25 16:01:39 +01:00
Kacpherek 0481dbdb81 Make hardcore players spectators when they respawn
A hardcore death offered another survival life. Vanilla's death screen
says Spectate in hardcore, and the server makes the respawned player a
spectator (ServerGamePacketListenerImpl.handleClientCommand). Do the
same on the server and in the in-frame world, and add a harness that
compares a hardcore respawn on a dedicated server with vanilla's.
2026-10-25 16:01:39 +01:00
Ludovic MarieandClaude Opus 5.5 c04b7afdb9 Hub: the Kanban and the Journal tabs, and /hub/journal.json
Kanban: every open issue and pull request and what closed in the last seven days, placed in To do,
In progress, In review and Done by the rules of Kira's work board, each card with its roadmap item
(its roadmap:<id> label or the item linking it) and a filter by item. Journal: every release with its
changes grouped by roadmap item, the merges no release has yet, and a weekly summary from
roadmap/journal/<year>-<week>.md. The same journal is /hub/journal.json, for Kira's #updates and posts
elsewhere: each entry names the roadmap items it moved and shipped, with their 240-character text.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 16:00:03 +01:00
Ludovic MarieandClaude Opus 5.5 c5a640dd56 Endermen, endermites, blazes, eyes of ender, dried ghasts and end portals make their ambient effects
The client-side branches vanilla runs on its own level: Enderman and
Endermite aiStep (two portal particles a tick), Blaze aiStep (two large
smoke particles, the burn sound one tick in 24), EyeOfEnder.tick (a
portal particle behind it, four bubbles in water), and the
DriedGhastBlock and EndPortalBlock animateTick. They run on the window's
game in-frame and as a server's client, never on a server.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 15:59:56 +01:00
Ludovic MarieandClaude Opus 5.5 68234ac407 Hub: the roadmap at /data/roadmap.json and items at hub#<id>, as Kira reads them
Kira (kira #10-#14) fetches https://playhorizon.dev/data/roadmap.json and links
items as <hub>#<id>: the JSON moves there (readable from any origin) with the
announce flag, and each item's anchor is its id. Ids the page uses itself (tabs,
sections, headings) are refused by the check; a test keeps every id of the page
unique, which caught the overview's headings sharing ids with the roadmap's.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 15:59:31 +01:00
rdbtCVSandOpenAI Codex 8ffd595575 fix: suppress secondary explosions from destroyed End crystals
Pass the crystal damage cause through destruction so explosion damage removes crystals without another blast, including during dragon respawn. Keep fight callbacks and direct-hit blasts.

Co-Authored-By: OpenAI Codex <[email protected]>
2026-10-25 14:58:42 +00:00
Ludovic MarieandClaude Opus 5.5 e936e412ba Website: the hub (/hub) with the roadmap, a Simple / Technical switch and live states
playhorizon.dev/hub is the public entry point to what is happening on Horizon. Overview: where
Horizon is, the 3 to 5 things in focus, what is next, what shipped last, how to help. Roadmap: Now /
Next / Later / Help wanted / Shipped, filtered by area, every item with its issues and pull requests
in their GitHub state (baked at build, refreshed in the browser). Contribute: good first tasks,
claiming, connecting an agent to Kira, the rules. A Simple / Technical switch swaps every item's
plain and dev text and is remembered. /roadmap redirects to the Roadmap tab.

tools/roadmap.py build now also writes website/data/roadmap.json, which the site reads and serves at
/hub/roadmap.json.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 15:57:46 +01:00
Ludovic MarieandClaude Opus 5.5 a95be8a7f3 Roadmap: announce = true marks items Kira also posts on X when they ship
Kira's X poster (kira #12) posts shipped items flagged announce in journal.json.
Set on the five whose release is news: modpack world generation, the mod
registry, Discord Rich Presence, Microsoft sign-in and CurseForge. Flags must be
booleans.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 15:57:34 +01:00
Ludovic MarieandClaude Opus 5.5 bcd42f50e9 cargo horizon-mod test: server mods' scripts
A test script drives a server mod as a server would: join = "Alex" brings a
player in (ids stable per name, so a rejoin is the same player), as = "Steve"
picks who acts (the first player on otherwise), and player, chat, command,
break and place are that player's doing; leave = "Alex" lets them go. Server
expectations: told (every players.tell, "Name: text") and broadcast; feedback
and error read the reply to the player who typed the command. Ticks call both
kinds of tick hooks, so one script format serves client and server mods.

The welcome example gets two scripts (greeting, a newcomer announced once
even after a reload, /online across dimensions, /mined per player), run by
the sandbox tests with coords-hud's; docs/mods.md lists the server keys.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 15:55:55 +01:00
Ludovic Marie bd591f9857 Merge pull request #1814 from ludovic111/fix/mod-registry-pr-marker
🤖 Agent: merged after verify passed; mod-registry-land-1's combined check (pr_ready --quick on all seven, launcher tests 125, website 25/25, no harness affected) and live end-to-end on horizon-mods.
2026-10-25 15:54:52 +01:00
Ludovic MarieandClaude Opus 5.5 5d40d6a33e Telemetry server: the hub's votes (POST /hub/vote, GET /hub/votes)
A thumbs-up on a roadmap item at playhorizon.dev/hub: one per browser (a random
id the page keeps), taken back with "up": false, counts cached a minute. Item ids
are checked for shape and capped at 1000 distinct items. Rate limits key their
buckets by a keyed hash of the address (key drawn at start, in memory); no address
is stored. CORS preflight for the website's JSON posts.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 15:54:39 +01:00
Ludovic Marie 930a18c912 Merge pull request #1640 from ludovic111/feat/launcher-horizon-modpacks
🤖 Agent: merged after verify passed; mod-registry-land-1's combined check (pr_ready --quick on all seven, launcher tests 125, website 25/25, no harness affected) and live end-to-end on horizon-mods.
2026-10-25 15:53:18 +01:00
Kacpherek 13e83e4e63 Read saved attribute bases for every mob
LivingEntity.readAdditionalSaveData applies the attributes list for any
living entity, then sets the health (the max health when none is saved).
Only horses read max_health and movement_speed, so a summoned cow kept its
type defaults. Read max_health, movement_speed and attack_damage for all
mobs before the health; horses still read jump_strength.
2026-10-25 15:51:29 +01:00
Ludovic Marie 1c67541379 Merge pull request #1638 from ludovic111/feat/launcher-horizon-tab
🤖 Agent: merged after verify passed; mod-registry-land-1's combined check (pr_ready --quick on all seven, launcher tests 125, website 25/25, no harness affected) and live end-to-end on horizon-mods.
2026-10-25 15:51:16 +01:00
Ludovic MarieandClaude Opus 5.5 c9f554d1c4 Mod API 0.3 (experimental): server mods' contract, runtime and SDK
A server mod runs where the world is simulated. Its manifest says side =
"server" and api = "0.3" (only a server mod may), and its component targets
the new WIT world horizon-server-mod:

- imports log and storage as client mods do, and the new capability players:
  online() (id, name, position, dimension, game mode, health), tell(id, text),
  broadcast(text), 8 lines per call shared with chat;
- exports server-events: init, tick, player-joined, player-left, chat, command
  (with the player who typed it; its feedback goes back to that player),
  block-broken and block-placed (with the block state as text).

horizon-mods: a second bindgen world sharing log and storage with the first;
a running mod holds either kind of hooks; Mods::load_for(Runs) starts only the
mods a game runs (the window client mods, the world's server server mods,
in-frame single player both) and marks the others Elsewhere. Client and server
commands are owned separately. The game passes Runs::Client for now, so
nothing changes in game until its server code calls these hooks.

The SDK gains horizon_mod_sdk::server (ServerMod, Registration,
export_server_mod!), the example server mod welcome (greets, announces
newcomers kept in storage, /online, /mined), and cargo horizon-mod check's
trial run plays a server mod with a player joining, chatting, typing its
commands and leaving. The manifest refuses client capabilities on a server
mod and players on a client mod. The generated reference, the guide, the
portal and the stability table describe it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 15:51:15 +01:00
Kacpherek 3843a9a4aa Use the team-formatted player name in chat
Chat, /say, /me and /tell now name the sender the way
Player.getDisplayName does: the team prefix, the name and the suffix
in the team colour. On a server the hub gets the scoreboard's teams
each tick; in-frame the existing player_display_name is used.
2026-10-25 15:51:11 +01:00
Ludovic Marie e28b453762 Merge pull request #1637 from ludovic111/feat/launcher-horizon-mods
🤖 Agent: merged after verify passed; mod-registry-land-1's combined check (pr_ready --quick on all seven, launcher tests 125, website 25/25, no harness affected) and live end-to-end on horizon-mods.
2026-10-25 15:49:13 +01:00
Kacpherek 467277ef6d Update comparators when a guest changes a chest through its menu
A container click from a server-thread or LAN player wrote the slots but
never called BlockEntity.setChanged, so comparators kept the old output.
Notify the block entity when the click changed the container's slots.
2026-10-25 15:48:51 +01:00
vlesmentieandClaude Opus 5.5 4bfa496173 Lava cauldrons burn the player inside them
The player's inside-block sweep skipped lava cauldrons, so standing in
one did nothing. It now tests the player against LavaCauldronBlock's
entity-inside shape (the cauldron's shape or its lava, Block.column(12,
4, 15)) the way Entity.checkInsideBlocks does, and applies
LavaCauldronBlock.entityInside: CLEAR_FREEZE, LAVA_IGNITE (15 seconds)
and Entity.lavaHurt (lava, 4), for the game's own player and a server's.

Fixes #1320.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 11:47:21 -03:00
Ludovic Marie 6a2512bfe7 Merge pull request #1636 from ludovic111/feat/launcher-horizon-source
🤖 Agent: merged after verify passed; mod-registry-land-1's combined check (pr_ready --quick on all seven, launcher tests 125, website 25/25, no harness affected) and live end-to-end on horizon-mods.
2026-10-25 15:47:09 +01:00
Ludovic Marie bd7bb1dfc6 Merge pull request #1822 from ludovic111/feat/native-shaders-sdk
🤖 Agent: merged after verify passed (horizon-shader-api only).
2026-10-25 15:46:54 +01:00
Ludovic Marie e785362aa7 Merge pull request #1635 from ludovic111/feat/website-mods-submit
🤖 Agent: merged after verify passed; mod-registry-land-1's combined check (pr_ready --quick on all seven, launcher tests 125, website 25/25, no harness affected) and live end-to-end on horizon-mods.
2026-10-25 15:45:17 +01:00
Kacpherek 18ec4145c4 Close crafting tables that are removed or out of reach
CraftingMenu.stillValid needs the block to still be a crafting table and the
player within reach of it. The table's position is now kept with the open
grid, so the menu closes in the tick (giving its grid back) and a stale
click no longer crafts, in single player and for LAN guests.
2026-10-25 15:44:49 +01:00
Ludovic Marie 6c4d1d6cbb Merge pull request #1596 from ludovic111/feat/website-mods
🤖 Agent: merged after verify passed; mod-registry-land-1's combined check (pr_ready --quick on all seven, launcher tests 125, website 25/25, no harness affected) and live end-to-end on horizon-mods.
2026-10-25 15:43:14 +01:00
Ludovic Marie 2dbdcda89c Merge pull request #1820 from ludovic111/feat/native-shaders-check
🤖 Agent: merged after verify passed (horizon-shader-api only).
2026-10-25 15:41:39 +01:00
Kacpherek ed7d88b213 Close container menus when the player moves out of reach
Container.stillValidBlockEntity also checks the player's block interaction
range plus a 4-block buffer. Guest menus now close in the tick once out of
reach and ignore clicks while invalid, and the in-frame single player menu
closes the same way.
2026-10-25 15:40:19 +01:00
Ludovic MarieandClaude Opus 5.5 49f62d02cb Horizon shader packs: horizon-shader new/check/pack, templates, an example pack and the guide
The SDK's tool, in horizon-shader-api: `horizon-shader new <dir> --template
post|sky|material` makes a pack that passes the check, `check <pack>...
[--json]` runs the check the game, the launcher and the registry run and
prints each stage's cost and the pack's SHA-256 or every problem at its
file:line:column, and `pack <dir>` checks then writes the reproducible
.hshader. Golden Hour, the example pack, warms the sky around a low sun and
adds a soft glow and a colour grade, with five options. docs/shaders.md is the
authors' guide.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 15:38:23 +01:00
Ludovic Marie 220f057647 Merge pull request #1818 from ludovic111/feat/native-shaders-stages
🤖 Agent: merged after verify passed (new crate horizon-shader-api + generated reference with drift test).
2026-10-25 15:37:32 +01:00
Ludovic Marie f94b05032b Merge pull request #1817 from ludovic111/feat/native-shaders-format
🤖 Agent: merged after verify passed (new crate horizon-shader-api only).
2026-10-25 15:34:25 +01:00
Ludovic MarieandClaude Opus 5.5 923fdee42e Horizon shader packs: the check every pack passes before it runs
horizon_shader_api::check builds each stage as the renderer does and judges
it the same way wherever it runs (the game, the launcher, the registry's CI,
the coming horizon-shader tool): naga parses and validates it; the stage
function must have its signature; the pack's files declare no bindings, no
entry points and no horizon_ names; each override is an option of its type or
has a default, and each option is used; a stage stays within 20 000
expressions and 64 texture reads; and every entry point translates to SPIR-V,
Metal and HLSL with the options at their defaults, minimums and maximums.
Problems carry the pack's own file, line and column. Pack::sha256 gives the
reproducible package's hash.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 15:34:07 +01:00
vlesmentieandClaude Opus 5.5 5fb0cad6e7 Wither roses give the player Wither
WitherRoseBlock.entityInside adds getBeeInteractionEffect (Wither, 40
ticks) to a living entity inside it on a server level that isn't
peaceful, unless it is invulnerable to wither damage. The player's
inside-block dispatch had no wither rose case, so neither singleplayer
mode applied it. The sweep now asks for it, the game adds it to its own
player, and the server-thread path adds it to a server's player.

Fixes #1319.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 11:31:02 -03:00
Ludovic MarieandClaude Opus 5.5 749a2dc9bd Global level events reach a server's players; explosions play their packet's sound
ServerLevel.globalLevelEvent (an end portal opening, 1038; the dragon's
death roar, 1028) went to the Game's own audio without a position, which
is never forwarded: on the server thread, LAN and dedicated servers
nobody heard them. They now go to every guest as a global LevelEvent,
which a client plays two blocks from its camera towards the event
(LevelEventHandler.globalLevelEvent).

ClientPacketListener.handleExplosion plays the packet's explosionSound;
the client always played entity.generic.explode (a vanilla server's wind
charge sounded like TNT).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 15:30:43 +01:00
260dd423c9 Keep sprint as a state so a sprint-jump keeps its forward impulse
PlayerInput was rewriting the server sprint flag from the Ctrl and forward
keys. That flag is synced back onto the client, and jumpFromGround adds its
0.2 look-direction impulse only while the flag is set, so releasing Ctrl
before the jump (or double-tapping forward) produced a plain hop. Sprint
still starts and stops through PlayerCommand.

Co-Authored-By: Grok <[email protected]>

Co-authored-by: 一水合氨NH3-H2O <[email protected]>
2026-10-25 14:30:42 +00:00
Ludovic MarieandClaude Opus 5.5 6b97107dc7 Transfers: handshake with the TRANSFER intent, answer login cookie requests
Vanilla 26.3 reconnects after ClientboundTransferPacket with a TransferState,
so Connection.initiateServerboundPlayConnection sends ClientIntent.TRANSFER (3):
a target with accepts-transfers=false refuses the player, and the target can
ask for the carried cookies during login
(ClientHandshakePacketListenerImpl.handleRequestCookie). Both now match.

A transfer from single player closes the world instead of connecting
elsewhere (handleTransfer: "Cannot transfer to server from singleplayer").

Adds a unit test for the play-state store/request round trip and the
Transfer event.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 15:29:59 +01:00
Fo 4b5fa2ea6b build: make horizon-dualsense compile on iOS
hidapi 2.6.7 has no iOS backend, so the crate failed to build for
aarch64-apple-ios (hidapi's build script looked for hid.h). Gate the
hidapi and cpal dependencies plus all HID device/haptics code behind
cfg(not(target_os = "ios")) and provide an inert stub on iOS
(Link::start() is a no-op), mirroring how horizon-gamecontroller
already compiles empty on iOS.

Verified with cargo check -p horizon-dualsense --target
aarch64-apple-ios: clean, no warnings. Host clippy/tests are left to
CI (this sandbox has no C toolchain for the host target).

Co-Authored-By: Fo <[email protected]>
2026-10-25 14:28:55 +00:00
Ludovic MarieandClaude Opus 5.5 608d1a70a7 Horizon shader packs: the stages of API 0.1 and their generated reference
Three stages, each a WGSL function with a fixed signature that Horizon calls
inside its own pass: sky (fn sky(input: SkyInput) -> vec4<f32>), material
(fn material(input: MaterialInput) -> Surface) and post (fn post(input:
PostInput) -> vec4<f32>). A stage's WGSL is a generated prelude (the frame
uniform, the stage's structs, its resources in bind group 3, the post
helpers), the pack's shared files, its own file, then the entry points; each
part is located so errors point at the pack's file and line.

The frame is the Rust struct the renderer will upload; its WGSL, its offsets
and docs/api/shaders-reference.md are generated from it, and tests check that
the preludes compile, that naga's layout of the WGSL is the Rust layout, that
the sample pack's stages compile, and that the reference is up to date.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 15:27:43 +01:00
Ludovic MarieandClaude Opus 5.5 7bffc4c670 cargo horizon-mod test: run a mod's TOML test scripts headless
A mod's tests/<name>.toml scripts run in the sandbox the game uses, with no game
and no window. The runner plays the game's part (the player and clock the mod
reads, ticks, frames, chat lines, commands, a reload) and each step checks what
the mod did: its commands, chat lines, command feedback and errors, HUD panel
and corner, or that it was turned off. A failing step names itself and prints
expected against got. Each script runs on a scratch copy, so storage starts
empty and is removed afterwards.

horizon_mods::testing (feature runtime) holds TestGame and Script; the cargo
subcommand builds the mod (or takes --hmod), filters scripts by name, and fails
when one does. The template gains tests/hello.toml, coords-hud gains panel and
where scripts, and the sandbox tests run the example's scripts and check the
runner's failures, reload and expected stops. Scripts don't care what language
the mod is written in, so script mods (Lua) get the same runner.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 15:25:52 +01:00
Ludovic MarieandClaude Opus 5.5 98c8db50a3 Horizon shader packs: horizon-shaders.toml (schema 1) and the .hshader package
A new crate, horizon-shader-api, reads and checks Horizon's own WGSL shader
packs (docs/api/shaders.md), so the game, the launcher and the registry judge
a pack the same way. This first part is the format: the manifest (id, name,
version, the api line, shared files, the sky, material and post stages,
options typed for WGSL override constants, a GPU time budget) and the package
(a folder or a .hshader zip, built reproducibly so its SHA-256 depends only on
its files). The stages' WGSL is checked in the next parts.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 15:18:03 +01:00
Ludovic MarieandClaude Opus 5.5 6d418fdb3e Docs: publish a mod through the registry, and how horizon-mods runs
docs/mods.md gets "Publish your mod" (the form, the review, updates) and
its Distribution section points at the registry instead of the plan for
it. docs/api/mod-index.md says how horizon-mods runs: its checks, merging,
Pages, the weekly cleanup, and that removing an entry needs an admin merge
(takedowns go in revoked.toml). The service's README says where it runs on
Railway.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 15:17:30 +01:00
Ludovic MarieandClaude Opus 5.5 98bebd26bd Launcher: one place reads the pinned appearance (theme::pinned_dark)
The title bar and the theme both resolved HORIZON_LAUNCHER_APPEARANCE and the
Settings choice; titlebar::apply_appearance now uses the theme's helper.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 15:14:48 +01:00
Ludovic MarieandClaude Opus 5.5 22c3f2bd0f Roadmap: roadmap/roadmap.toml, ROADMAP.md generated from it, and its checks
One source of truth for what is being done, what comes next and where to help: roadmap/roadmap.toml,
each item said twice (plain words for players, the technical picture for contributors), Now / Next /
Later / Help wanted / Shipped, no dates. tools/roadmap.py builds ROADMAP.md from it, checks its rules
offline (CI, through tests/test_roadmap.py) and against GitHub (links resolve, no stale Now item),
ships items whose pull requests all went out in a release (sync), and keeps a roadmap:<id> label per
item (labels). README links it; roadmap/README.md is the maintenance guide.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 15:13:51 +01:00
Ludovic Marie 44ba133710 Merge branch 'feat/shader-packs-held-items' into feat/shader-packs-hand 2026-10-25 15:09:46 +01:00
Ludovic Marie aa04a30ad2 Merge branch 'feat/shader-packs-weather' into feat/shader-packs-held-items 2026-10-25 15:09:46 +01:00
Ludovic Marie 612024eff4 Merge branch 'feat/shader-packs-clouds' into feat/shader-packs-weather 2026-10-25 15:09:45 +01:00
Ludovic Marie c39579772c Merge branch 'feat/shader-packs-translucent-entities' into feat/shader-packs-clouds 2026-10-25 15:09:45 +01:00
Ludovic Marie 257654ef30 Merge branch 'feat/shader-packs-entities' into feat/shader-packs-translucent-entities 2026-10-25 15:09:44 +01:00
Ludovic Marie c4856e6feb Merge branch 'feat/shader-packs-alpha-test' into feat/shader-packs-entities 2026-10-25 15:09:44 +01:00
Ludovic Marie 90fe138e83 Merge branch 'feat/shader-packs-entity-spans' into feat/shader-packs-alpha-test 2026-10-25 15:09:44 +01:00
Ludovic Marie 15e0fcb3d8 Merge branch 'feat/shader-packs-sildurs' into feat/shader-packs-entity-spans 2026-10-25 15:09:43 +01:00
Ludovic Marie 07668d2d1e Merge branch 'feat/shader-packs-mip-current' into feat/shader-packs-sildurs 2026-10-25 15:09:43 +01:00
Ludovic Marie 202c421f6e Merge branch 'feat/shader-packs-biomes' into feat/shader-packs-mip-current 2026-10-25 15:09:43 +01:00
Ludovic Marie 90b5da76c4 Merge branch 'feat/shader-packs-normals' into feat/shader-packs-biomes 2026-10-25 15:09:42 +01:00
Ludovic Marie 57dca25dd6 Merge branch 'feat/shader-packs-block-ids' into feat/shader-packs-normals 2026-10-25 15:09:42 +01:00
Ludovic Marie dbc59979fb Merge branch 'feat/shader-packs-arena-storage' into feat/shader-packs-block-ids 2026-10-25 15:09:42 +01:00
Ludovic Marie 53db90264c Merge branch 'feat/shader-packs-sun-moon' into feat/shader-packs-arena-storage 2026-10-25 15:09:41 +01:00
Ludovic Marie 27f68454c1 Merge branch 'feat/shader-packs-sky' into feat/shader-packs-sun-moon 2026-10-25 15:09:41 +01:00
Ludovic Marie 1aad193be0 Merge branch 'feat/shader-packs-draw-uniforms' into feat/shader-packs-sky 2026-10-25 15:09:40 +01:00
Ludovic Marie c55f89ef98 Merge branch 'feat/shader-packs-mipmaps' into feat/shader-packs-draw-uniforms 2026-10-25 15:09:40 +01:00
Ludovic Marie 2617b67c79 Merge branch 'feat/shader-packs-timing' into feat/shader-packs-mipmaps 2026-10-25 15:09:39 +01:00
Ludovic Marie 15cf80921e Merge branch 'feat/shader-packs-water' into feat/shader-packs-timing 2026-10-25 15:09:39 +01:00
Ludovic Marie 60e8ec326a Merge branch 'feat/shader-packs-real-buffers' into feat/shader-packs-water 2026-10-25 15:09:39 +01:00
Ludovic Marie bf5c7e4ff2 Merge branch 'feat/shader-packs-custom-uniforms' into feat/shader-packs-real-buffers 2026-10-25 15:09:38 +01:00
Ludovic Marie aebc95fd00 Merge branch 'feat/shader-packs-run-globals' into feat/shader-packs-custom-uniforms 2026-10-25 15:09:37 +01:00
Ludovic Marie 754437fac6 Merge branch 'feat/shader-packs-terrain-draw' into feat/shader-packs-run-globals 2026-10-25 15:09:37 +01:00
Ludovic Marie 2f527aaaf2 Merge remote-tracking branch 'origin/main' into feat/shader-packs-terrain-draw
# Conflicts:
#	crates/horizon-client/src/gfx.rs
2026-10-25 15:09:36 +01:00
Ludovic MarieandClaude Opus 5.5 0f2a497f02 Trapped chests give their signal, barrels show they're open
TrappedChestBlock.getSignal is the chest's opener count (at most 15);
TrappedChestBlockEntity.signalOpenCount updates the blocks around it and
below it when the count changes. GameLevel answered 0 for every trapped
chest. BarrelBlockEntity's openers set open=true for the first one and
open=false when the last one leaves; nothing set it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 15:09:16 +01:00
Ludovic MarieandClaude Opus 5.5 a9e6818d2a Merge main (workspace: main's registry exclude + this branch's predator-missile example)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 15:09:03 +01:00
Ludovic MarieandClaude Opus 5.5 ceff5a2110 Launcher: installing a taken-down registry mod says it was taken down, and why
It said the mod had no version for 26.3.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 15:06:34 +01:00
vlesmentieandClaude Opus 5.5 e9d2c10417 Peaceful zombie horses spawn without their zombie rider
ZombieHorse.finalizeSpawn gets its rider from EntityType.create, which
returns null for a notInPeaceful type (the zombie) in peaceful, so vanilla
spawns the zombie horse alone there. spawn_jockeys created the rider
regardless. mobs/finalize.rs already skips it through level.create.

Fixes #1761.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 11:06:00 -03:00
Ludovic MarieandClaude Opus 5.5 93d05591c2 horizon-mod-registry: a submission's pull request says it came through the form
The form opens pull requests with the registry's token, which is Ludovic's
account, so the body now starts "🤖 Submitted through playhorizon.dev by
@login" and says neither Ludovic nor a reviewer wrote it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 15:04:04 +01:00
Ludovic MarieandClaude Opus 5.5 319c7fcaee A server's client shows the nether portal effect
NetherPortalBlock.entityInside runs on the client's own level too: its
setAsInsidePortal drives LocalPlayer.handlePortalTransitionEffect (the
purple overlay, block.portal.trigger). The remote motion loop dropped
MotionEvent::InsidePortal, so single player on the server thread, LAN
guests and dedicated servers' players went through portals without it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 14:58:51 +01:00
Ludovic MarieandClaude Opus 5.5 5680da9834 Merge feat/launcher-horizon-tab (main merged) into feat/launcher-horizon-modpacks
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 14:57:32 +01:00
Ludovic MarieandClaude Opus 5.5 41e68aeb76 Merge feat/launcher-horizon-mods (main merged) into feat/launcher-horizon-tab
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 14:57:31 +01:00
Ludovic MarieandClaude Opus 5.5 28ad41eb22 Merge feat/launcher-horizon-source (main merged) into feat/launcher-horizon-mods
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 14:57:31 +01:00
Ludovic MarieandClaude Opus 5.5 de195755f7 Merge origin/main into feat/launcher-horizon-source
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 14:57:20 +01:00
Ludovic Marie 587af1e345 Merge feat/website-mods (main merged) into feat/website-mods-submit
Co-Authored-By: Claude Opus 5.5 <[email protected]>

# Conflicts:
#	website/build.mjs
2026-10-25 14:57:08 +01:00
Ludovic MarieandClaude Opus 5.5 65fa108cba Merge origin/main into feat/website-mods
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 14:56:52 +01:00
Ludovic MarieandClaude Opus 5.5 a2fccf0205 Flint and steel lights nether portals only in the Overworld and the Nether
BaseFireBlock.inPortalDimension: a frame lit in the End stays a fire.
The world code's fire placement already checked it; the flint-and-steel
path (Game::try_light_portal) didn't, so a portal could be lit and used
in the End in every mode.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 14:52:17 +01:00
socksproxandDevin 56560b467a Multiplayer: answer cookie requests, store cookies, follow transfer packets
ClientboundCookieRequestPacket in play state decoded as Unknown and was
dropped; StoreCookie and Transfer likewise, and the configuration-phase
CookieRequest was always answered empty. Proxies (BungeeCord, Velocity)
store and read cookies around a server switch and send Transfer to move
a player to another address — both ended in a kick.

Play-state cookie_request/store_cookie/transfer now have packet types,
cookie requests answer from the stored cookies in both configuration and
play (ClientCommonPacketListenerImpl.handleRequestCookie), stored cookies
land in the session's map (handleStoreCookie), and a transfer reconnects
the game to the target address carrying them (handleTransfer →
ConnectScreen.startConnecting with TransferState).

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-25 14:51:32 +01:00
JasonGuandOpenCode DeepSeek V4.1 Flash b457f6382a Localization: names, container titles, death text and search follow the language
- item and block names in tooltips, held labels, command feedback and the
  creative tabs read the selected language; the pinned English map stays
  for logic and search
- Game::translate resolves through lang::get_or, so station titles follow
  the language screen
- container titles and Inventory labels for chests, hoppers, furnaces,
  smokers, blast furnaces, crafters, shulkers and mounts are translated
- server-resolved components (menu titles, death messages, command
  suggestion tooltips) load the selected language over en_us, and follow
  a language change while a world is open
- death messages always reach the death screen translated; the chat
  broadcast still obeys showDeathMessages
- creative search matches the localized display name
- sign editors use sign.edit and hanging_sign.edit

Co-Authored-By: OpenCode DeepSeek V4.1 Flash <[email protected]>
2026-10-25 21:42:01 +08:00
Kacpherek dbafda06ed Surface: specialise the material rule per gradient band and surface side
Each chunk's compiled rule is specialised once for every band of y that
its vertical gradients bound, and for blocks below and at or above the
preliminary surface when the rule asks for it. Conditions those facts
decide are folded away, so a deep stone block evaluates only the rules
that can apply there (ore veins, the underground rules) instead of
walking the whole surface tree. Conditions only read the position and
the world, so the blocks are the same.

Surface stage, one thread, seed 1234: 2.45 -> 2.08 ms per chunk.
2026-10-25 14:40:37 +01:00
Kacpherek e181b17e17 Register new chunks' ticks before post-processing them
postprocess_chunks ran before tick_blocks registered the chunks that
arrived since the last tick, so the fluid and block ticks their marks
schedule went to chunks the scheduler did not know and were dropped.
Vanilla registers a chunk's tick containers before postProcessGeneration.
2026-10-25 14:40:37 +01:00
Kacpherek 4cd1504513 Pregen: report what a stalled band waits for, and requeue lost light
When the band hasn't moved for 30 seconds the task logs how many chunks
of its first row are not loaded, being lit, unlit with no light job or
not post-processed, with the streaming stage counts, and queues light
again for unlit chunks whose neighbours are all loaded.
2026-10-25 14:40:37 +01:00
Kacpherek f56785c2f5 Keep a chunk's scheduled ticks when it unloads before the next tick
attach_save_data took a chunk's ticks only from the tick scheduler, which
registers new chunks once a tick. A chunk loaded and unloaded again in
between was saved with its block and fluid ticks cleared, though its own
saved list still held them.
2026-10-25 14:40:37 +01:00
Kacpherek 35254c4f02 Pregen: keep an empty dedicated server from pausing while it runs
pause-when-empty-seconds paused a server with nobody online after a
minute, and a paused server skips its tick: post-processing stopped, so
the band waited forever on a row of lit chunks. A running /pregen now
counts as activity; the countdown starts once it is done.
2026-10-25 14:40:37 +01:00
Kacpherek 1506c1808a Add /pregen: generate, light and save an area ahead of the players
A Chunky-like command for single player and servers: /pregen start
<radius> [square|circle] [<x> <z>], status, pause, continue and cancel,
with progress (chunks, chunks/s, time left) in the log and to the player
who started it.

The area is swept strip by strip by a band of rows that streaming treats
like a player's area (Streaming::set_band). A row is done once its chunks
are lit and post-processed; the rows behind the band then unload and are
saved, so memory holds only the rows in flight. The band's chunks are
ranked for decoration row by row as they come within reach, so a task
decorates in the same order however its jobs are timed. Between ticks
the server waits on the workers' results and schedules the next jobs at
once instead of once a tick.
2026-10-25 14:40:37 +01:00
Ludovic MarieandClaude Opus 5.5 68deac8533 Server clients see mobs' aggression and arm swings
Mob.DATA_MOB_FLAGS_ID now carries the aggressive bit (with its default
0, so it goes back off) and every client mob reads no AI, left-handed
and aggressive from it: a vindicator draws its axe, skeletons their bow,
piglins and illagers their attack poses on the server thread, LAN guests
and dedicated servers. Mobs' swings (LivingEntity.swing) go out as
ClientboundSwingAnimationPacket.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 14:38:44 +01:00
Ludovic MarieandClaude Opus 5.5 2f1f0150eb mobs diagnostics: say why passenger is set outside the json! macro
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 14:37:26 +01:00
cristibctrandOpenAI Codex c756a372a3 Fix stale Windows resource pack overlays
Invalidate version 3 overlays that can omit untouched vanilla resources. Cover folder and ZIP pack cache recovery, and use a writable timestamp handle in the Windows cache test.

Co-Authored-By: OpenAI Codex <[email protected]>
2026-10-25 15:32:57 +02:00
Ludovic MarieandClaude Opus 5.5 fb1142cd71 Skins: keep vanilla's whole-payload rejection, drop the dead name fallback
authlib 10.0.77 MinecraftServicesSessionService.unpackTextures returns
MinecraftProfileTextures.EMPTY when any texture has no URL or a URL outside
the allowed domains, so skins::unpack goes back to main's behaviour.

remote_player_names: r.players and r.tab_players are filled from the same
ADD_PLAYER entries and emptied by the same PlayerInfoRemove, so the
tab-list fallback could never apply.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 14:31:41 +01:00
Ludovic MarieandClaude Opus 5.5 806f7d87f3 Merge origin/main into fix/vehicle-passenger-pose
Keeps main's crystals check and this branch's boat-dismount-pose check in tools/lan_e2e.py.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 14:29:29 +01:00
Ludovic MarieandClaude Opus 5.5 e069750ca1 Merge origin/main into fix/sprint-fov-stuck
Keeps main's crystals check and this branch's sprint-fov check in tools/lan_e2e.py.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 14:29:28 +01:00
socksproxandDevin 41b799042a Sync mobs' aggressive and item-use flags so clients pose them right
Mob.DATA_MOB_FLAGS_ID's aggressive bit and LivingEntity's
DATA_LIVING_ENTITY_FLAGS never reached a client for mobs: on the
server thread and for LAN guests a skeleton kept its arms down while
its arrows flew, and a drawn bow never pulled. Send both entries (with
their defaults so a flag going back to 0 is sent too), decode them for
every mob, and let a remote mob's item-use ticks count up as
LivingEntity.updatingUsingItem does. The same entries drive the zombie
arms, the vindicator's axe, the drowned's trident and the spellcasters'
poses, so the pillager's charging and evoker's/illusioner's casting
entries go with them. A left-handed mob now mirrors the pose and the
held item (Mob.getMainArm), and a bow in the off hand counts for
LivingEntity.isHolding.

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-25 14:26:55 +01:00
Ludovic MarieandClaude Sonnet 5.5 c5cfab5e07 Merge feat/server-dialog-screen into feat/styled-menu-titles
Clean merge.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-25 14:25:14 +01:00
Ludovic MarieandClaude Sonnet 5.5 ec2f1b2a0c Merge feat/server-dialog-screen into feat/server-dialog-inputs
Clean merge.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-25 14:25:14 +01:00
Ludovic Marie 6a065fa489 Merge pull request #1788 from ludovic111/feat/discord-presence
🤖 Agent: merged after verify passed; Rich Presence e2e 6/6 on macOS and Linux.
2026-10-25 14:24:58 +01:00
Ludovic MarieandClaude Sonnet 5.5 330a5205ae Merge feat/server-dialogs into feat/server-dialog-screen
Conflict: lib.rs module list (dialog_screen + discord_presence), both kept.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-25 14:24:52 +01:00
Ludovic MarieandClaude Sonnet 5.5 49dd31d36f Merge origin/main into feat/server-dialogs
Conflicts: lib.rs (dialog + discord_presence modules), packets/play.rs tests
(boss_event_round_trips + dialogs_round_trip), both kept.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-25 14:24:45 +01:00
Ludovic Marie e6dbc3b39f Merge pull request #1787 from ludovic111/feat/discord-presence-ipc
🤖 Agent: merged after verify passed; Rich Presence e2e 6/6 on macOS and Linux (see #1788).
2026-10-25 14:22:55 +01:00
Ludovic Marie e6b0f92427 Merge main 2026-10-25 14:21:57 +01:00
Ludovic Marie dbc66a5a41 Merge pull request #1697 from ludovic111/feat/shader-packs-terrain
Shader packs, clean room 13: loading the terrain program
2026-10-25 14:21:33 +01:00
Ludovic Marie d14b100a97 Merge pull request #1670 from ludovic111/feat/shader-packs-atlas
Shader packs, clean room 12: the block atlas packs sample
2026-10-25 14:21:21 +01:00
Ludovic Marie 9ed21bf436 Merge pull request #1708 from rdbtCVS/t3/7c6098f0
Website: unify navigation across Home, Releases, Benchmarks and Docs
2026-10-25 14:21:12 +01:00
Ludovic Marie ab2006a230 Merge pull request #1669 from ludovic111/feat/shader-packs-gbuffers-translate
Shader packs, clean room 11: translating gbuffers programs
2026-10-25 14:21:07 +01:00
Ludovic Marie b61ef8f9be Merge pull request #1668 from ludovic111/feat/shader-packs-post-perf
Shader packs, clean room: fewer passes and loads in post-processing
2026-10-25 14:20:56 +01:00
Ludovic Marie bd4236f3dd Merge pull request #1656 from ludovic111/feat/shader-packs-post
Shader packs, clean room 10: post-processing over Horizon's world
2026-10-25 14:20:44 +01:00
Ludovic Marie 8ec23a2606 Merge pull request #1655 from ludovic111/feat/shader-packs-uniforms
Shader packs, clean room 9: frame uniforms, and gl_FragCoord in buffer passes
2026-10-25 14:20:32 +01:00
Ludovic Marie 1b6a55e47d Merge pull request #1654 from ludovic111/feat/shader-packs-post-programs
Shader packs, clean room 8: a dimension's post-processing programs
2026-10-25 14:20:19 +01:00
Ludovic Marie c143be7fd0 Merge pull request #1653 from ludovic111/feat/shader-packs-post-load
Shader packs, clean room 7: buffer settings and output lists from stage files
2026-10-25 14:20:05 +01:00
Ludovic Marie da8ae88eee Merge pull request #1601 from ludovic111/feat/shader-packs-translate
Shader packs, clean room 6: composite-style programs to naga
2026-10-25 14:19:52 +01:00
Ludovic MarieandClaude Opus 5.5 df626cf2f3 A tool that changes a block ends the click on a server's client
On a server's client (single player on the server thread, LAN guests), a
shovel making a path, an axe stripping, a hoe tilling or honeycomb waxing
did not end the right click: the client went on to the other hand, so an
off-hand torch was predicted as placed and its hand swung instead of the
tool's (#1289).

Vanilla's client runs `Item.useOn` itself: its SUCCESS swings that hand
and `Minecraft.startUseItem` returns before the off hand. The client now
does the same when its prediction of the tool's use succeeds.

Fixes #1289

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 14:15:28 +01:00
Ludovic Marie 3a419877b0 Merge pull request #1752 from rdbtCVS/t3/fix-end-crystal-attack
Fix End crystal attacks from connected clients
2026-10-25 14:11:52 +01:00
Ludovic Marie 5fefa6ce42 Merge pull request #1689 from rdbtCVS/t3/d001fd2c
Slow Falling clears the fall distance so landing does not hurt
2026-10-25 14:11:43 +01:00
Ludovic Marie 6e4cb6210e Merge pull request #1687 from rdbtCVS/fix/creative-hotbar-hover
Creative inventory: highlight hovered hotbar slots
2026-10-25 14:11:33 +01:00
Ludovic Marie 985ec67b83 Merge pull request #1685 from sdraeger/fix/nbt-fireballs
Fireballs from NBT fly: /summon, chunk loads, selectors and /data get
2026-10-25 14:11:23 +01:00
Ludovic Marie ca05c89c6e Merge pull request #1676 from sdraeger/fix/ctl-screenshot-spaces
horizon-ctl screenshot: paths with spaces
2026-10-25 14:11:15 +01:00
Ludovic Marie 18c74b438c Merge pull request #1675 from sdraeger/fix/tp-mob-pitch
/tp: keep the pitch for mobs
2026-10-25 14:11:05 +01:00
Ludovic Marie ff7cfe0f10 Merge pull request #1695 from Domanffe/chore/deslop
Cleanup: one predicate property helper, a Windows-only allow, tolerant locks in the bots
2026-10-25 14:10:57 +01:00
Ludovic Marie 250c148e78 Merge pull request #1673 from Domanffe/fix/title-menu-labels
Title: the main menu labels follow the game language
2026-10-25 14:10:49 +01:00
Ludovic Marie 40fcc91eff Merge pull request #1682 from Domanffe/feat/horizon-lang-namespace
Options: Horizon's own captions read a bundled language namespace
2026-10-25 14:10:41 +01:00
Ludovic Marie 8b3d4a069c Merge pull request #1686 from Domanffe/fix/launcher-remove-retry
Launcher: a version removal retries a locked file and leaves the UI thread
2026-10-25 14:10:32 +01:00
Ludovic Marie a839062259 Merge pull request #1694 from Domanffe/fix/swallowed-save-errors
Save: the write paths report a failure instead of dropping it
2026-10-25 14:10:24 +01:00
Ludovic Marie 6194463454 Merge pull request #1693 from Domanffe/fix/pack-overlay-windows
Packs: an untouched vanilla directory is mirrored on Windows too
2026-10-25 14:10:15 +01:00
Ludovic Marie a5f574b77e Merge pull request #1707 from Domanffe/chore/clippy-green
Clippy: clear the two warnings the whole tree fails on
2026-10-25 14:10:08 +01:00
Ludovic Marie c4573fb561 Merge pull request #1674 from sdraeger/fix/client-mods-server-thread
Client mods: tick, commands and chat on the server thread
2026-10-25 14:09:59 +01:00
Ludovic Marie 57fc1570b2 Merge pull request #1681 from socksprox/fix/join-loading-status
Multiplayer: drop the player name from the join loading line
2026-10-25 14:09:51 +01:00
Ludovic Marie 628e230217 Merge main (the save_root lint fix, #1789) 2026-10-25 14:08:29 +01:00
Ludovic Marie 6926c76492 Merge pull request #1590 from socksprox/feat/boss-overlay 2026-10-25 14:02:40 +01:00
Ludovic MarieandClaude Opus 5.5 47d1b060b2 Discord presence: test where the game looks for Discord on macOS, Linux, Flatpak and Snap
sockets() reads the environment through a function, so a unit test can
check the paths a real Discord listens on for each platform ($TMPDIR on
macOS, $XDG_RUNTIME_DIR and its Flatpak and Snap subfolders on Linux,
named pipes on Windows). Offscreen e2e runs can't cover this: an automated
run only talks to a fake Discord named by HORIZON_DISCORD_IPC_DIR.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 13:57:19 +01:00
socksproxandDevin 87b3c9aa38 Drop the stale craft grid when a remote menu closes
The server's AbstractContainerMenu.removed gives the grid and the
carried stack back to the inventory and syncs those slots, but the
client kept its copies: a crafting table closed with items inside left
ghost stacks in the inventory's 2x2 grid that could not be picked up.

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-25 13:51:12 +01:00
Ludovic MarieandClaude Opus 5.5 40c77c02f0 PalettedContainer: look up a word's filled bits, not divide for them
`locate` found an entry's word by multiplication (the MAGIC table) but
still divided `64 / bits` at run time for the shift, on every block read
and write of a section stored in 5, 6 or 7 bits. A division by a value the
compiler does not know costs tens of cycles on x86: `perf annotate` of a
10-bot server on the ThinkPad put 12% of `PalettedContainer::get`, the
server thread's top function, on that `div`. The table now also holds the
bits a word's entries fill, so the shift is `i * bits - word * filled`.
A test checks `locate` against the division for every width and entry.

ThinkPad (i5-7200U), 4M random reads, 5-7 bit palettes: 28.5-36.9 ->
24.6-32.4 ns over 2,000 sections, 6.7-7.1 -> 5.4-5.8 ns within one;
4 and 8 bits (shifts, unchanged) the same.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 13:50:35 +01:00
Ludovic MarieandClaude Opus 5.5 224b83c6a6 Shader packs, clean room 36: the hand
A pack with gbuffers_hand (or textured_lit, textured, basic; spec 2.3)
draws the first-person hand and held items after the opaque world, into
its own buffers and the world's depth, without blending and with the
entities' 0.1 alpha cut. The vertices are Horizon's hand geometry, read
as entities are; `gl_ProjectionMatrix` is the hand's own projection (the
hand's field of view and bob) with depth scaled by MC_HAND_DEPTH, while
`gbufferProjection` stays the world's (5.2). Horizon's own hand draw is
skipped only when the pack draws it.

Clean-room: implemented from cleanroom/shader-packs-spec.md revision 1;
no other loader's code read.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 13:48:03 +01:00
Ludovic MarieandClaude Opus 5.5 9e21868dfa Potions keep their potion across the connection
A server's client (single player on the server thread, LAN guests, a
dedicated server's players) saw every potion as an "Uncraftable Potion"
in the water colour: the server never wrote `minecraft:potion_contents`
and the client read past it. A potion taken from the creative menu also
reached the server without its potion, so it splashed as water.

PotionContents now goes as vanilla's stream codec writes it: the potion
by its `minecraft:potion` id and the custom colour, with the custom
effects and name kept as their bytes. The server writes it for every
stack it sends, the client reads it into the item's components, and a
creative pick and the server's read of it keep the potion too.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 13:46:45 +01:00
socksproxandDevin 3740de5116 Launcher: pin the macOS title bar to the chosen appearance
The launcher's Appearance setting darkened the page but not the window's native chrome: NSWindow keeps the system appearance, so a launcher set to Dark still drew a light title bar on a light desktop. apply_appearance (titlebar.rs) pins the NSWindow's appearance to the choice at startup and when the setting changes; System clears the pin so the chrome follows the computer again.

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-25 13:40:07 +01:00
Ludovic MarieandClaude Opus 5.5 18cac82d44 Shader packs, clean room 35: held item ids and light
`item.properties` (spec 4.1) gives `heldItemId` and `heldItemId2` the
id of the main and off hand's item: -1 for an item the file lacks or an
empty hand, 0 without the file (5.3). `heldBlockLightValue(2)` are the
light levels of the held items' blocks, as Horizon's dynamic lights
already compute them. With `oldHandLight` (on unless the pack turns it
off, OQ-21) the first id and light are those of the brighter hand.

Clean-room: implemented from cleanroom/shader-packs-spec.md revision 1;
no other loader's code read.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 13:35:44 +01:00
Ludovic Marie d2c96af142 Merge pull request #1789 from ludovic111/fix/main-unused-save-root
Fix main's build: unused save_root in open_world
2026-10-25 13:32:01 +01:00
socksproxandDevin 0bc85843ce Client: remote players keep their skins instead of falling back
skins::unpack discarded a profile's whole textures payload when any one
entry lacked "url" or pointed outside the texture services' domains, so
a player with e.g. an unusual cape lost their skin too; it now drops only
the offending texture. The 32 skin atlas slots evicted past players back
to defaults on busy servers — now 128. A dimension reset (join game or
respawn onto another world) now also clears the entity-id→profile map so
a re-used entity id can't pin the wrong skin or name to a player, and the
nametag join falls back to the player-list entry's name.

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-25 13:27:54 +01:00
Ludovic MarieandClaude Opus 5.5 be378bb773 Particles: vault connections are translucent, shadows keep their range
- FlyTowardsPositionParticle puts vault_connection on the translucent
  layer: its LifetimeAlpha(0, 0.6, 0.25, 1) is not opaque.
- The shadow draw started at world_quads - shadow_quads; with the new
  bucket after it, that drew translucent particles with the shadow
  pipeline and dropped as many shadows.
- One atlas lookup per named-sprite particle and frame: the engine keeps
  each sprite's layer beside its rect instead of hashing the name twice.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 13:27:49 +01:00
socksproxandDevin 4032e162ce Draw translucent particles after translucent terrain
Quad particles all went into the entity batch, drawn before translucent
terrain with depth writes on: a stained-glass crumb wrote its depth and
the sorted translucent pass then culled the glass behind it, so breaking
glass in front of glass cut holes through the wall.

Vanilla 26.3 submits quad particles to two phases: opaque layers draw
with the solids and translucent layers draw in an "afterTerrain" phase
right after the translucent section pass (the translucent particle
pipeline still writes depth, so ordering is what matters). Mirror that:
HudAtlas records which sprites have a 0 < alpha < 255 texel
(`SpriteContents.transparency().hasTranslucent`), particles classify
their layer the way `SingleQuadParticle.getLayer` does — the type's
hardcoded layer, or `Layer.bySprite` for terrain, item and block marker
sprites — and the entity batch routes those quads to a sixth bucket the
renderer draws immediately after translucent terrain.

Fixes #1679.

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-25 13:27:49 +01:00
Ludovic MarieandClaude Opus 5.5 d33f84ef73 Fix main's build: drop the unused save_root left in open_world
#1665 moved the save path into finish_open_world and #1666's merge kept the
old line, so clippy -D warnings fails on main (unused variable) since 7d0ae5658.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 13:24:35 +01:00
Ludovic Marie ad63f2e960 Merge pull request #1659 from ludovic111/feat/mod-registry-server 2026-10-25 12:46:00 +01:00
levzzzandCodex 1ec6d6d734 Detach entity passengers when their vehicle is removed
Clear remote passenger mount and pose flags when a vehicle disappears, matching Entity.setRemoved. Add a two-passenger boat destruction check for both single-player paths and LAN observers.

Co-Authored-By: Codex (GPT-6) <[email protected]>
2026-10-25 16:15:36 +05:00
Ludovic Marie 7d0ae56589 Merge pull request #1666 from rdbtCVS/t3/explicit-execution-roles 2026-10-25 12:04:39 +01:00
Ludovic Marie e3de4a4604 Merge pull request #1665 from rdbtCVS/t3/separate-world-entry-loading 2026-10-25 12:02:41 +01:00
Ludovic Marie 9ae86086dc Merge pull request #1664 from Domanffe/fix/data-get-live-passengers 2026-10-25 12:00:55 +01:00
levzzzandCodex 57ee081038 Keep sprint state independent of held sprint keys
Let PlayerCommand control the server sprint flag, matching vanilla, so releasing Ctrl cannot leave the client's speed modifier and FOV stuck. Cover the packet sequence and FOV recovery in regression tests.

Co-Authored-By: Codex (GPT-6) <[email protected]>
2026-10-25 15:56:53 +05:00
Ludovic MarieandClaude Opus 5.5 1437537cbf Worker threads: leave one core, as vanilla does, not two
Graphics::worker_threads sizes the worker pools (the server thread's, which
generates and lights, and the window's, which meshes). Its automatic count
left two cores to the render and server threads; vanilla's executor leaves
one (Util.maxAllowedExecutorThreads). The workers run behind those threads
(niced on Linux; above four cores, raised server and render threads on
macOS and Windows), so the second core mostly idled: on a 4-thread laptop
only 2 cores generated while a world loaded.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 11:49:57 +01:00
Ludovic MarieandClaude Opus 5.5 3063d87199 docs: Discord Rich Presence is live with the Horizon application
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 11:49:53 +01:00
Ludovic MarieandClaude Opus 5.5 15e46114a6 Discord Rich Presence: show what the player does, with a switch in Horizon Settings
Each frame the game hands the presence thread its status (menus; single
player, multiplayer or hosting a world with its party size; the
dimension; the game mode), and only a change wakes it. The switch
"Discord Rich Presence" (on by default, discordPresence in
horizon-extras.txt) sits beside Controller Settings. Dedicated servers
never start it. tools/discord_presence_e2e.py runs real games against a
fake Discord: title, world, Nether and quit on the server thread and
in-frame, a LAN host (both paths) and guest, the switch, and a Discord
started after the game.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 11:49:44 +01:00
Ludovic Marie fbfec6435f Merge pull request #1588 from socksprox/feat/server-resource-packs 2026-10-25 11:49:29 +01:00
Ludovic MarieandClaude Opus 5.5 e49de342ed Discord Rich Presence: the Horizon application's id
Discord's "Horizon" application (1563857609738690560, public) has the
horizon logo as its large image; no game-mode images yet. The feature is
live: only HORIZON_NO_DISCORD=1, the switch and automated runs keep it
quiet.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 11:47:13 +01:00
Ludovic MarieandClaude Opus 5.5 c879c22a6a Merge the branch's own main merge
item.rs: the same components either way; keep the branch's order.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 11:45:44 +01:00
Ludovic MarieandClaude Opus 5.5 39904c8051 docs/mods.md: the contract lists the command interface
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 11:33:08 +01:00
Ludovic MarieandClaude Opus 5.5 e7ca664e01 Server: a guest's boost rocket is theirs
The server attached every boosting rocket to its own player: it followed
the host, and DATA_ATTACHED_TO_TARGET named the host (or nothing on the
server thread, which has none), so the guest's client never saw a rocket
attached to them and did not boost. The rocket now keeps the guest it was
fired by, follows them and names their entity.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 11:32:23 +01:00
Ludovic Marie 7a3578b7d6 Merge pull request #1587 from levzzz5154/fix/predict-block-placement
Predict block placement on the client, so fast bridging stops losing clicks
2026-10-25 11:32:09 +01:00
Ludovic MarieandClaude Opus 5.5 11035336a0 Server: a gliding player's rocket boosts them
FireworkRocketItem.use attaches a rocket only to a player who is fall
flying. The server tracks a guest's glide (START_FALL_FLYING) but the
body it swaps in for their item use left fall_flying unset, so on the
server thread, LAN and dedicated servers the rocket did nothing.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 11:24:29 +01:00
socksproxandDevin c99de82044 Merge main into fix/elytra-fireworks: keep firework components beside trim/raw passthrough
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-25 11:13:42 +01:00
Ludovic MarieandClaude Opus 5.5 604ec7e2e4 Discord Rich Presence: the IPC client and its thread (inert until wired)
discord_presence.rs speaks Discord's local IPC itself (discord-ipc-N
socket or named pipe; opcode + length + JSON frames; HANDSHAKE, then
SET_ACTIVITY with the pid) from a thread of its own. The game hands it a
status; the thread connects lazily, retries every 15 s, keeps within 5
updates per 20 s, refreshes every minute and clears the activity on
quit. Inert while DISCORD_APP_ID is the placeholder, with
HORIZON_NO_DISCORD=1, and for automated runs unless
HORIZON_DISCORD_IPC_DIR points at a test's fake Discord; lab.game_env
sets HORIZON_NO_DISCORD=1.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 11:04:38 +01:00
Ludovic Marie 21ff80eb8c Merge pull request #1775 from ludovic111/docs/benchmark-your-computer
🤖 Agent: merged after verify passed and the integration run on the desktop.
2026-10-25 11:03:40 +01:00
Ludovic MarieandClaude Opus 5.5 f3213aa158 Docs: how players benchmark their computer and share the report
The Performance page now gives the launcher command (horizon-launcher play --arg=--benchmark
--arg=standard), where the report lands on each system, what it holds, and the two ways to
share it: horizon-launcher data benchmark, or the Discord.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 11:02:06 +01:00
Ludovic MarieandClaude Opus 5.5 173463acdf Shader packs, clean room 34: rain and snow
A pack with gbuffers_weather (or its fallbacks textured_lit, textured,
basic; spec 2.3) draws the game's rain and snow after translucent
terrain, blended, with vanilla's 0.1 alpha cut. The weather quads become
entity vertices with their UV animation applied on the CPU (the same
phase Horizon's weather shader computes; a test checks it against the
weather columns bit for bit), and rain and snow each read a copy of
their sprite, so the pack's `texture` repeats down the column as a
standalone texture would. The shared atlas gains COPY_SRC for that copy.

Without a pack nothing changes: Horizon's weather draw is skipped only
when the pack draws it.

Clean-room: implemented from cleanroom/shader-packs-spec.md revision 1;
no other loader's code read.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 10:51:53 +01:00
Ludovic MarieandClaude Opus 5.5 5262effa7e cargo horizon-mod check: list the commands a mod runs
Main's check reports every output; the command capability adds one.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 10:48:35 +01:00
Ludovic MarieandClaude Opus 5.5 c2c8df4542 Mods: run a mod's commands on the next tick
Run while the mod's output was shown, a command's chat line reached the
mods again inside that call: a mod that answers a line with a command
that prints one recursed without end, and when a control-socket command
set the hook off, the mod's commands ran with the console's level. Queue
them for the next tick instead, where the player's own level applies (the
server already checks it on a server).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 10:47:54 +01:00
Ludovic MarieandClaude Opus 5.5 f08546fd74 Merge main into the mod command capability
Cargo.toml: keep main's horizon-shaders member beside predator-missile.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 10:46:18 +01:00
Ludovic MarieandClaude Opus 5.5 e7f85e5bfd Zip: test the streamed shapes Java 25's ZipFile opens
ZipFile refuses a 0xFFFFFFFF size without its Zip64 extra field ("invalid
zip64 extra len size"), so the test builds both shapes it accepts: real
sizes, and the sentinel with the extra (the zip crate reads the real size
from it). With every accepted shape carrying a real size, the inflate
limit needs no special case.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 10:44:31 +01:00
Ludovic MarieandClaude Opus 5.5 abe7047d19 Shader packs, clean room 33: clouds
A pack with gbuffers_clouds draws Horizon's cloud geometry itself (spec
2.3, OQ-3: no fallback), blended, after the opaque world. Horizon's own
cloud draw is skipped then, also when the pack's program writes nothing
(BSL and Complementary hide vanilla clouds that way). Without a pack the
cloud draw is unchanged.

The translator now rewrites a bare `discard;` or `return;` to
`if (true) discard;` / `if (true) return;`, so code a pack leaves after
it stays reachable for naga ("Expression not visited"). This also fixes
Complementary Unbound's gbuffers_skytextured.

Clean-room: implemented from cleanroom/shader-packs-spec.md revision 1;
no other loader's code read.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 10:32:10 +01:00
Ludovic MarieandClaude Opus 5.5 272e230371 Merge main into the elytra fireworks branch
Main now reads past components it doesn't model (Component::Raw) and
added ArmorTrim; this branch's Fireworks and FireworkExplosion keep
their structured decode beside them, ahead of the Raw fallback.
remote_projectiles takes main's FallStep import too.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 10:24:06 +01:00
Ludovic MarieandClaude Opus 5.5 90ae953747 Rockets: collapse the boost's nested if (clippy::collapsible_if)
Verify failed on it with warnings denied.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 10:21:48 +01:00
Ludovic MarieandClaude Opus 5.5 3bc38cebb4 YUNG jigsaws: the start is not counted, priority, depth and bounds rules run
From spec-yungsapi rev 3 §3, §3.1 and §3.2 (impl-questions IQ-2, IQ-3):
- the start piece does not count toward max_count;
- is_priority entries are tried first, in the pool's shuffled order, without
  a weighted draw; min_required_depth skips a pick (no draw) while the parent
  piece is shallower; ignore_bounds places without the free-space test (its
  box is still taken, IQ-12);
- the fallback was already shuffled only when reached.

The port no longer counts these three fields as lost. The layout harness gets
yung-probe and yung-probe2: the spec team's probe pack (built from the oracle
profile) carried as a mod, compared start by start with the oracle's census:
384 of 384 and 32 of 32 starts match.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 10:17:21 +01:00
Ludovic MarieandClaude Opus 5.5 3a7d00a74c Shader packs, clean room 32: translucent entity parts and particles
A shader pack now draws particles with `gbuffers_particles` (falling
back to `gbuffers_textured_lit`, `gbuffers_textured`, `gbuffers_basic`)
and the entity parts that blend, a sneaking player's faded name tag and
entity shadows, with `gbuffers_entities_translucent` (falling back to
`gbuffers_entities`). Both blend with the translucent default unless
`blend.*` says otherwise; particles keep vanilla's alpha cut at 0.1.
Name tag text draws with `gbuffers_entities`, as cutout (2.3).

Particles draw after deferred, with translucent terrain, unless
`particles.ordering=before` or `particles.before.deferred=true`;
translucent entities draw before deferred unless
`separateEntityDraws=true` (3.2).

Without a shader pack nothing changes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 10:11:09 +01:00
Ludovic MarieandClaude Opus 5.5 ab289a2cf3 Shader packs, clean room 31: entities and block entities
A shader pack now draws entities with `gbuffers_entities` and block
entities with `gbuffers_block` (or their fallbacks, spec 2.3), after
terrain. Each run of the entity spans is one draw with its own
`entityId` or `blockEntityId` and `entityColor` (the hurt red, the
white flashes) in a per-draw slot. Horizon still draws everything else
(particles, cracks, beams, name tags, shadows, End portals, clouds) in
its own single entity draw, without the runs the pack drew.

The entity vertices are read again at set 1, so the pack gets the face
normal where a vertex has none (block entities), `at_tangent` along the
texture's u and `mc_midTexCoord`, as for terrain.

Entities and block entities draw without blending, with the loader's
alpha test at GREATER 0.1, as cutout (OQ-23); name tags and shadows,
which blend, stay Horizon's for now.

`HORIZON_SHADER_PACK_TIMING` no longer panics when a pass skips a whole
report window (no entities in view).

Without a shader pack nothing changes: the split only runs while a pack
draws entities.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 09:51:55 +01:00
Ludovic MarieandClaude Opus 5.5 b5a791170d Shader packs, clean room 30: the loader's alpha test
Gbuffers programs get the loader's alpha test on output 0 when they
write `gl_FragData`/`gl_FragColor` (spec 3.2 `alphaTest.<program>`;
with `out` variables the pack tests alpha itself). `GbuffersInputs`
carries the geometry's default, none for terrain, water and the sky so
far; `alphaTest.<program>` replaces it for that program (`off`, or a GL
comparison and a reference).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 09:51:55 +01:00
Ludovic MarieandClaude Opus 5.5 71f3c23dd9 Merge main into the launcher removal branch
Main moved the removal to a thread in #1715 (Msg::Removed, the removing
row, the selection), which is this branch's UI half: main's version is
kept. What stays is backend::remove's retry on a file Windows still
holds.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 09:38:45 +01:00
Ludovic Marie 574d54e6e0 Merge pull request #1747 from ludovic111/perf/mesh-model-lookup 2026-10-25 09:38:10 +01:00
Ludovic MarieandClaude Opus 5.5 c74490a003 Multiplayer: the join line reads through lang with en_us's own fallback
lang::get_or, as the title screen's labels do; en_us 26.3 has
multiplayer.downloadingTerrain = "Loading terrain...".

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 09:37:56 +01:00
Ludovic MarieandClaude Opus 5.5 d6cc631735 Merge main into the End crystal attack branch
Conflict in tools/lan_e2e.py's check list: main added shulker_bullets
beside crystals; both kept, crystals' entry aligned with the others.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 09:37:40 +01:00
Ludovic MarieandClaude Opus 5.5 0d18ff7ca7 Fall distance: Levitation resets it too, as LivingEntity.aiStep does
aiStep calls resetFallDistance under either Slow Falling or Levitation.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 09:36:54 +01:00
Ludovic MarieandClaude Opus 5.5 b7fcd446b6 Fireballs: LeftOwner and HasBeenShot are saved as Projectile writes them
Projectile.addAdditionalSaveData writes LeftOwner only when it is set and
HasBeenShot always (true from the first tick on); fireball_to_nbt wrote
LeftOwner:0b and no HasBeenShot. Same shape as arrow_to_nbt.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 09:36:32 +01:00
Ludovic MarieandClaude Opus 5.5 f23db61ecf /tp: a mob's head turns and its old rotation follows, as teleportSetPosition does
Entity.teleportSetPosition calls setYRot, setYHeadRot and setXRot, then
setOldPosAndRot: the head faces the new yaw and yRotO/xRotO match, so a
teleported mob doesn't sweep from its old pitch on the next frame.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 09:35:46 +01:00
Ludovic MarieandClaude Opus 5.5 b470bec351 Cleanup: one lock helper in horizon-bots, the predicate helper beside its trait
The twenty unwrap_or_else(|e| e.into_inner()) calls become one lock()
helper, so the bots read as they did with the same poison tolerance.
bound_property moves from worldgen.rs (between DecodeError's impls) to
block_predicate.rs, next to PredicateBlockResolver, whose
predicate_property it implements.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 09:35:25 +01:00
Ludovic MarieandClaude Opus 5.5 53e1191480 Title: the Friends icon reads vanilla's gui.friends.open
FriendsButton's message is gui.friends.open, so the title screen needs
no Horizon key for it. The fallbacks are en_us's own text for the
three icons ("Language...", "Accessibility Settings..."), as the
other labels' are.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 09:34:33 +01:00
Ludovic MarieandClaude Opus 5.5 f28722fb15 Lang: drop horizon.menu.friends, vanilla has gui.friends.open
TitleScreen's Friends button is vanilla's FriendsButton, whose message
is gui.friends.open ("Friends" in en_us, translated in every language
the prepared resources carry), so Horizon needs no key of its own for
it. Bundled now sits after the trait it implements.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 09:34:24 +01:00
Ludovic MarieandClaude Opus 5.5 a61c7faa23 Launcher: a version being removed shows Removing… instead of its buttons
With the removal off the UI thread, the row kept its Use and Remove
buttons, so a second Remove or Use could race the deletion. The retry
loop also reads as one match now.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 09:34:02 +01:00
Ludovic MarieandClaude Opus 5.5 c0bf5cf954 Dedicated: the list saves log vanilla's warnings
DedicatedPlayerList logs "Failed to save user banlist: " (and ip
banlist, operators list, white-list) when a startup save fails; the
shared closure printed "Failed to load user banlist write". The closure
now takes the verb, so loads and saves read as vanilla's lines.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 09:33:13 +01:00
Ludovic MarieandClaude Opus 5.5 fb1f421117 Clippy: take the decorated region by value, and gate the unix-only test helper
receive_decorated moved the nine protos out of the box anyway (the
array iterator holds them inline), so taking [Proto; 9] and unboxing at
the three call sites costs the same and leaves boxed_local nothing to
flag on any target. The #[expect] failed on Linux, where the lint does
not fire, which is why verify was red.

scratch is only called from the #[cfg(unix)] tests, so it carries the
same cfg as installed() next to it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 09:32:55 +01:00
Ludovic MarieandClaude Opus 5.5 eb6c81945a Lithostitched processors: unit tests rewritten from the spec
The previous tests of litho.rs are deleted unread (clean-room ruling of
2026-10-25). The new ones build their own block registry from a facts
table and run each processor through process_list against a one-block
world: block_swap's property copy (CTOV's beetroots), references in
order and discard_input, the probe pack's nested condition with else,
apply_random per piece and per block, set_block's providers and its
block entity modifier.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 09:29:53 +01:00
Ludovic Marie 0c5d229ad0 Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	Cargo.toml
2026-10-25 09:13:06 +01:00
Ludovic Marie 8d9e1e95ac Merge remote-tracking branch 'origin/main' into fix/remote-tnt
# Conflicts:
#	tools/lan_e2e.py
2026-10-25 09:12:04 +01:00
Ludovic Marie 327a6cafc7 Merge pull request #1736 from ludovic111/feat/yung-jigsaw-draws 2026-10-25 09:08:04 +01:00
Ludovic Marie 6a32ee68d4 Merge pull request #1725 from RVZO6/feature/text-display-rendering
Client: render server text displays and lobby labels
2026-10-25 09:07:59 +01:00
Ludovic Marie 0e73edceff Merge pull request #1718 from RVZO6/feature/text-display-state
Client: model text-display metadata against the Java wire codec
2026-10-25 09:07:02 +01:00
Ludovic MarieandClaude Opus 5.5 d89fb16f60 Structure blocks harness: CTOV's known differences
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 09:06:14 +01:00
Ludovic Marie f7bd3e381b Merge pull request #1729 from RVZO6/fix/stop-sound-packet
Client: honor server stop-sound event and source filters
2026-10-25 09:06:02 +01:00
Ludovic MarieandClaude Sonnet 5.5 ea0905e8a2 lan_e2e: no blank lines inside the docstring after the merge
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-25 08:58:25 +01:00
Ludovic MarieandClaude Opus 5.5 58e184f5d5 Structure blocks harness: place the oracle's own pieces, split piece boxes from their surroundings
The mod world now takes each saved start whose pieces are all jigsaw pieces
as the oracle saved it (chunk_dump's HORIZON_SAVED_STARTS, through a new
GeneratorWorker::use_saved_start and PoolPiece::from_saved), so the blocks
are checked whatever the layout: Lithostitched lays jigsaws out its own way
(spec-lithostitched OPEN-L5) and the layout harness checks layouts apart.
Lithostitched's wrapper elements place their delegate; 1.21.1 block states
are read in 26.3's shape.

Counts are split between positions inside a piece's box and around them
(terrain adaptation, decoration), and every mismatch class is written to
.local/structure-layout/<profile>/blocks-report.json.

A CTOV profile carries Lithostitched and ChoiceTheorem's Overhauled Village
from a pack's jar folder (.local/structure-oracle/pack-jars).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 08:51:17 +01:00
Ludovic MarieandClaude Opus 5.5 c43f4bdb2b Lithostitched's processors as the game's own types; replace the old Lithostitched conversion
Clean-room implementation from spec-lithostitched.md §3-§5 and spec-ctov.md §4.

The game reads Lithostitched's processors as its data writes them
(processor/litho.rs): block_swap (only the properties the new block can
have: ripe wheat becomes ripe carrots, beetroot keeps its default age),
reference, apply_random, condition (with else, one processor or a list),
set_block (simple and weighted providers, preserve_state, block entity
modifier) and discard_input; the conditions all_of, any_of, not,
random_chance, true and matching_blocks (input or location, properties).
Their random follows random_mode per block or per piece; Lithostitched's own
seeding is open (OPEN-L6), so a positional random of the world seed and the
processor stands in (impl-questions IQ-7).

horizon-port-pack's new lithostitched module carries the processors as they
are, with the processors nested in them carried, drops those the game cannot
read (lists named by tag, other providers) as Java processors, turns CTOV's
modular_compat into a reference when its mod is ported along (else nothing),
maps lithostitched:jigsaw to vanilla's jigsaw (vanilla layouts, the decision
on OPEN-L5), unwraps limited/guaranteed/delegating elements to their
delegate (limits counted as lost until the next PR) and writes
add_template_pool_elements modifiers as horizon:add_to pools when their
NeoForge load conditions pass.

Deleted unread: legacy_structure_mods/lithostitched.rs and horizon-world's
structure/processor/lithostitched.rs. Blind edits to the legacy folder,
located by exact text or the compiler's error position: its module line
removed, dead_code allowed on ctov (its modular compat is unused now) and on a
test helper, and ctov's modular compat test ignored.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 08:51:17 +01:00
Ludovic Marie b149a0ec3f Merge main into fix/stop-sound-packet: keep both sides' appended checks
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>

# Conflicts:
#	tools/lan_e2e.py
2026-10-25 08:51:09 +01:00
Ludovic Marie 81b1be055c Merge pull request #1739 from rdbtCVS/t3/fix-hero-trading-discount
Trading: keep Hero of the Village discounts for server players
2026-10-25 08:50:37 +01:00
Ludovic Marie ebf76f4c03 Merge main into t3/fix-hero-trading-discount: keep both sides' appended checks
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>

# Conflicts:
#	tools/lan_e2e.py
2026-10-25 08:41:23 +01:00
Ludovic Marie fa093994ae Merge pull request #1733 from rdbtCVS/t3/d8cfe14b
/tp: dismount players before teleporting
2026-10-25 08:40:30 +01:00
Ludovic Marie 273c456adc Merge pull request #1717 from ludovic111/fix/jigsaw-oracle-layouts 2026-10-25 08:34:01 +01:00
Ludovic Marie d30be3d994 Merge main into t3/d8cfe14b: keep both sides' appended checks
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>

# Conflicts:
#	tools/lan_e2e.py
2026-10-25 08:32:49 +01:00
Ludovic Marie c1e90297e2 Merge pull request #1728 from rdbtCVS/t3/7a98935a
Server thread: remove forgotten recipes from the client's book
2026-10-25 08:32:26 +01:00
Ludovic Marie 9dfdff9d62 Merge main into t3/7a98935a: keep both sides' appended checks
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>

# Conflicts:
#	tools/lan_e2e.py
2026-10-25 08:25:09 +01:00
Ludovic Marie e56acaa7a9 Merge pull request #1727 from rdbtCVS/t3/9148b77e
Fix invisible shulker bullets on server clients
2026-10-25 08:24:27 +01:00
Ludovic Marie 65425da01d Merge pull request #1633 from some-du6e/fix/remote-trims 2026-10-25 08:23:48 +01:00
Ludovic Marie 6b88291009 Merge pull request #1631 from some-du6e/fix/item-components 2026-10-25 08:21:58 +01:00
Ludovic Marie e29a71e5e0 Merge pull request #1632 from some-du6e/fix/advancement-text 2026-10-25 08:20:14 +01:00
Ludovic Marie 80eafe84e9 Merge main into t3/9148b77e: keep both sides' appended checks
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>

# Conflicts:
#	tools/lan_e2e.py
2026-10-25 08:16:44 +01:00
Ludovic Marie 6017830623 Merge pull request #1719 from rdbtCVS/t3/13ed5f77
Fix crops remaining after server-side farmland trampling
2026-10-25 08:16:06 +01:00
Ludovic MarieandClaude Sonnet 5.5 fb34815ce1 Merge main into server-farmland-trampling: keep both appended lan_e2e checks
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-25 08:07:28 +01:00
Ludovic Marie f49e3a2d24 Merge pull request #1726 from rdbtCVS/t3/9a51354a
Fix offhand eating and drinking cancellation
2026-10-25 08:07:06 +01:00
Ludovic Marie cb0865e242 Merge pull request #1724 from rdbtCVS/t3/19c12e83
Fix spectator collision after dimension travel
2026-10-25 08:07:03 +01:00
Ludovic Marie a27322f250 Merge pull request #1716 from rdbtCVS/t3/395312eb
Fix double-chest inventory and hotbar clicks
2026-10-25 08:05:50 +01:00
Ludovic Marie ed3243ca62 Merge pull request #1730 from rdbtCVS/fix/launcher-java-pairing
Launcher: use one compatible JDK for resource preparation
2026-10-25 08:05:45 +01:00
Ludovic Marie f59bb8b8d5 Merge pull request #1738 from rdbtCVS/fix/pre-push-ref-history
Scope pre-push history checks to pushed refs
2026-10-25 07:54:48 +01:00
Ludovic MarieandClaude Opus 5.5 aac2f3a903 Shader packs, clean room 29: entity spans
While a shader pack is loaded, building the entity geometry records where
each entity's vertices begin, with its render stage (entities, block
entities, particles, breaking cracks) and its `entityId` or
`blockEntityId` from the pack's `entity.properties` and
`block.properties` (65535 for a name the file lacks, 0 without the
file). The spans reach the renderer in `Frame::entity_spans`, for the
pack's per-entity draws that follow; Horizon's own entity draw stays one
call and without a pack nothing is recorded.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 07:44:21 +01:00
Ludovic Marie 00705d8431 Merge pull request #1630 from some-du6e/fix/ipv4-first 2026-10-25 07:10:05 +01:00
Kaic Salomão da150409ea Complete and color mod commands in the chat on the server thread 2026-10-25 02:49:22 -03:00
Ludovic MarieandClaude Opus 5.5 f80f4e01e7 Structure blocks harness: the blocks Horizon's structure mods place against the structure oracle's
The clean-room structure oracle saved, for each start of a mod world, the
blocks of its box and every position around it that differs from the no-mod
world of the same seed. tools/test_structure_blocks_parity.py generates the
same chunks in full with Horizon twice (26.3 alone, and with the mods carried
by horizon-port-pack as players' packs are) and compares: a position counts
only where Horizon's no-mod world equals the oracle's (1.21.1 and 26.3 differ
elsewhere), and a mismatch is reported by its no-mod, expected and Horizon
blocks, so a missing processor rule reads as one line.

Better Dungeons (3 seeds, 96 complete starts): every structure is listed as
known, with the reason: the placeholder processors, the biome modifier and the
mod's own structure types are not implemented yet.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 06:39:52 +01:00
Ludovic Marie 0564ab0bcd Merge pull request #1748 from ludovic111/fix/server-player-targets-barrier
server_player_targets harness: end each step by the server's clock, not 2.5 s
2026-10-25 06:39:50 +01:00
rdbtCVSandOpenCode GPT-6.1 9409d2c19b Multiplayer: allow punching End crystals
Include End crystals in attack-only object hit-testing using their network entity IDs. Add a real-mouse LAN regression for survival and creative, reach, block occlusion, passing item use, and explosion damage.

Co-Authored-By: OpenCode GPT-6.1 <[email protected]>
2026-10-25 05:38:00 +00:00
Ludovic MarieandClaude Opus 5.5 7b35ca2799 Shader packs, clean room 28: Sildurs Vibrant renders
Three things kept Sildurs Vibrant black or wrong:

- Matrix and array varyings (`varying mat3 tbnMatrix;`): wgpu's stage
  interfaces take only scalars and vectors, so the translator now carries
  each column or element as its own varying and copies them out of and
  into a private global around the pack's `main`.
- Switch options defined in an included file whose name is not a shader
  extension (`#include "shaders.settings"`) were not read, so every
  `program.<name>.enabled` that tested them was false and the composites
  were skipped (OQ-13).
- `fogStart`/`fogEnd` mixed the start of one of vanilla's two fogs with
  the end of the other, so on clear days fog began at the camera and
  Sildurs' water turned white. They now come as the pair of the fog that
  is complete nearer (OQ-36).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 06:35:43 +01:00
Ludovic Marie eb1fdf5fb4 Merge pull request #1745 from ludovic111/fix/command-exec-random-count
command_exec harness: start the script without random sequences rolled before the setup
2026-10-25 06:35:31 +01:00
Ludovic Marie 2ee008f8fa Merge pull request #1646 from ludovic111/feat/mod-registry-tools 2026-10-25 06:34:00 +01:00
Ludovic Marie 8a0303ed62 Merge pull request #1642 from ludovic111/feat/mod-check 2026-10-25 06:32:11 +01:00
Ludovic MarieandClaude Opus 5.5 b15e46c3af lan_e2e stopsound: each target's client gets the stop (status counts stops)
Sounds can't be observed playing without an audio device, so the control
status counts each stopped selection, as it counts the sounds heard.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 06:26:45 +01:00
Ludovic Marie e988b9b833 Merge pull request #1641 from ludovic111/perf/face-cull-cr1 2026-10-25 06:22:47 +01:00
Ludovic MarieandClaude Opus 5.5 379692f355 Port YUNG's worldgen as written; delete the old YUNG conversion and assembler
Clean-room (spec-yungsapi.md §2, §3, §5; no mod source read; the legacy
files were deleted unread). horizon-port-pack's new `yung` module carries
`yungsapi:yung_jigsaw` structures and `yungsapi:yung_single_element` pool
elements as the mods write them, so players' packs reach the YUNG draws of
the jigsaw assembler. Fields whose behaviour the specification leaves open
(adapt_noise, max_y/min_y, enhanced terrain, element conditions, modifiers,
priorities, depths, ignore_bounds) are counted as lost by name. YUNG's
other structure, placement and processor types go to the general code
(unsupported or dropped) until the specification covers them.

Deleted: legacy_structure_mods/yung.rs, horizon-world's structure/yung.rs,
the `horizon:yungsapi` keys and `horizon:disables`, which nothing writes
now. Two test initializers of the removed field (enhanced_terrain.rs,
piece_limited.rs) were removed by line from the compiler's error positions.

The layout harness now carries the mods through `horizon-port-pack carry`:
82 of 84 Better Dungeons starts match the oracle piece for piece, as before.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 06:19:43 +01:00
Ludovic MarieandClaude Opus 5.5 504cb4f74c Text displays: lines split once per render state, as vanilla caches them
TextDisplayRenderer.splitLines uses Font.split (no indent on wrapped
lines, unlike chat's wrapComponents), and TextDisplay keeps the result
until its render state changes; the frame no longer wraps and measures
every display again. A render state is rebuilt only for the ids vanilla
lists (RENDER_STATE_IDS, TEXT_RENDER_STATE_IDS) and starts from the
progress the last frame drew (Display.lastProgress).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 06:13:11 +01:00
Ludovic MarieandClaude Opus 5.5 659bfad86c Merge the text-display model fixups (#1718) into the renderer
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 06:13:00 +01:00
Ludovic MarieandClaude Opus 5.5 9a8453c612 Shader packs, clean room 27: sample mip chains only while current
A program sampled every level of a buffer that has mipmaps, even after a
later program had rewritten the buffer: the levels below the first were
stale. Complementary's FXAA, which samples its colour buffer with implicit
derivatives inside branches, picked those levels at edges and drew black
dots along every edge. A buffer now binds its whole chain only while the
chain matches its content, and its first level otherwise (6.1.7).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 06:12:32 +01:00
Ludovic MarieandClaude Opus 5.5 bf59c0c4f7 Shader packs, clean room 26: biome uniforms and constants
Custom uniform expressions and programs now see the biome at the camera:
`biome`, `biome_category`, `biome_precipitation`, `temperature` and
`rainfall`, and the `BIOME_*`, `CAT_*` and `PPT_*` constants (spec 3.4).
Before, `biome` and every `BIOME_*` read 0, so a test such as
`biome == BIOME_PALE_GARDEN` held everywhere: Complementary drew the pale
garden's fog over every world, and BSL treated every biome as cold, dry,
swampy and so on at once.

OQ-11: `biome` is the index in the game's biome registry (vanilla's
biomes sorted by identifier); names from before the 1.18 renames map to
their successors; a `BIOME_` name that matches no biome reads -1.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 06:12:32 +01:00
Ludovic MarieandClaude Opus 5.5 76408b734e Text displays: an Align enum, and the text shared rather than copied
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 06:12:31 +01:00
Ludovic MarieandClaude Opus 5.5 4e3e038c80 Meshing: look up a block's baked models by index, not by model name
Every block the mesher draws asked the model catalog for its models: a
BTreeMap lookup of the state, a Vec of the chosen variants, then for each
variant a clone of its model name and a string-keyed BTreeMap lookup of the
baked model, collected into a second Vec. On generated terrain that was a
fifth of the mesher's time.

The catalog now resolves each variant to its baked model's index once, at
load (CompiledChoices maps its variants), keeps states in a Vec by id, and
the mesher reuses one list of models for every block of a section. The
random draws are the same calls in the same order (CompiledChoices::for_each
is the old collect), so the same models are chosen.

examples/chunk_load_bench.rs generates and lights real terrain with the
vanilla generator and meshes it on one thread, with a checksum of the meshes:
identical meshes, 45.8 -> 35.4 us per section on the Mac mini.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 06:11:52 +01:00
Ludovic Marie 30c4b5e80d Merge remote-tracking branch 'origin/main' into sh-top 2026-10-25 06:08:23 +01:00
Ludovic MarieandClaude Opus 5.5 b0918f3be1 /stopsound: send each targeted server player the stop-sound packet
StopSoundCommand sends ClientboundStopSoundPacket to every target. The
command stopped the server's own sounds instead, so on the server thread
(single player's default) and for LAN guests nothing stopped.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 06:07:43 +01:00
Ludovic MarieandClaude Opus 5.5 08491bcda3 Java selection: keep Windows JDK homes free of the \\?\ prefix
fs::canonicalize returns verbatim paths on Windows, and the chosen home
goes into JAVA_HOME and the PATH of external preparers.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 05:59:21 +01:00
Ludovic MarieandClaude Opus 5.5 b165506d6e Recipe book: removeRecipes returns the display ids it removed, as vanilla
The /recipe take path no longer computes them a second time.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 05:49:46 +01:00
Ludovic MarieandClaude Opus 5.5 09e95ae5a1 Merge main: keep both appended lan_e2e checks
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 05:48:59 +01:00
Ludovic MarieandClaude Opus 5.5 5a0e650e63 Offhand use: one in-hand check for held and consumed items
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 05:48:40 +01:00
Ludovic MarieandClaude Opus 5.5 3b586ff903 Server players: one helper finishes a landing's block updates, turtle eggs too
The server's player tick dropped FallEvent::Destroy, so an egg a guest
broke by landing left its neighbours unupdated. Both paths now share the
same helper.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 05:48:29 +01:00
Ludovic MarieandClaude Opus 5.5 6acf7fb6ab server_player_targets harness: end each step by the server's clock
Each step waited a fixed 2.5 s. When the machine is loaded the vanilla server stalls for longer than that (CI on
#1571 and #1585: after Bob's trip to the Nether, vanilla's `tp Bob 4.5 -60 0.5` from the console ran a step late),
so its packets were filed under the next step and the transcripts differed although both servers sent the same.

A step now ends once the server has done its work:
- the console's own `time query gametime` comes back, so the step's console commands have run (the console runs
  its queue in order);
- LocatorBarClient's new `sync NAME` runs `time query gametime` as that player and waits for the reply (not
  recorded), so that player's commands have run and every packet sent before the reply is recorded;
- the server's game time has advanced STEP_TICKS (40) more ticks, then each player sends its position reminder
  and syncs once more.

game_time() moves out of settle() in test_dedicated_server_parity.py so both use it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 05:46:22 +01:00
Ludovic MarieandClaude Opus 5.5 afb3ae79a2 command_exec harness: start the script without the random sequences of mobs that died before it
/random reset * in the ticking group counted 12 sequences in vanilla against Horizon's 11 on one CI run (#1585).
Locally 1 oracle run in 24 did the same, and its extra sequence was minecraft:entities/bat. Before the setup's
gamerules (spawn_mobs and mob_drops off), the oracle ticks until the entities around the player have loaded, a
number of ticks that depends on the machine; mobs spawn meanwhile, and a bat that died then rolled its loot table.
Once mob_drops is off no mob death rolls one (LivingEntity.shouldDropLoot), so the setup now ends with an
unrecorded /random reset *: every run starts the script with no sequence, and the later count holds only what the
corpus itself rolled.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 05:44:02 +01:00
Ludovic Marie 4a675206b1 Merge pull request #1740 from ludovic111/refactor/port-pack-legacy-structure-mods
🤖 Agent: merged after verify passed and the integration run on the desktop.
2026-10-25 05:41:33 +01:00
Ludovic Marie 066f07968b Merge remote-tracking branch 'origin/refactor/port-pack-legacy-structure-mods' into feat/yung-port-as-written 2026-10-25 05:38:20 +01:00
Ludovic MarieandClaude Sonnet 5.5 486df91b2d refactor(port-pack): move YUNG, Lithostitched and CTOV conversion into legacy_structure_mods
A pure move, no behaviour change. The conversion of YUNG's API and its mods,
Lithostitched and ChoiceTheorem's Overhauled Village was mixed with the general
conversion code in worldgen.rs and processors.rs. It now lives in
crates/horizon-port-pack/src/legacy_structure_mods/{yung,lithostitched,ctov}.rs,
and the general code reaches it through a few entry points, so a clean-room
rewrite can replace the module without touching the rest.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-25 05:35:36 +01:00
Ludovic MarieandClaude Opus 5.5 732efe69bb Shader packs, clean room 25: terrain normals and tangents
Flat terrain's vertex stage reads its quad's first three corners from the
arena page (bound per page at set 3, beside mc_Entity's table) and gives
gl_Normal / vaNormal the face normal and at_tangent the tangent along
the texture's u, with w the handedness of bitangent = cross(tangent,
normal) (spec 5.1). The pack's terrain and water pipelines cull back
faces as the game's own terrain does (clockwise front faces, rows being
flipped to GL's): water's surface no longer shows its downward copy.

Flat draws call a hook per arena page (Flat::draw_with); the arena lists
its page buffers.

Clean-room: implemented from cleanroom/shader-packs-spec.md revision 1,
5.1; no other loader's code read.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 05:16:38 +01:00
rdbtCVSandOpenCode gpt-6.1-sol 2c80009c70 Scope pre-push history checks to pushed refs
Read Git's pre-push ref updates and inspect the full reachable history of each non-deleted local object ID. Ignore unrelated refs while rejecting private artifacts in deleted files, merge-only paths, and stored history masked by local replacements.

Add regression coverage for branches, tags, raw revisions, multiple updates, deletion-only pushes, and artifact detection.

Co-Authored-By: OpenCode gpt-6.1-sol <[email protected]>
2026-10-25 04:15:14 +00:00
Ludovic MarieandClaude Opus 5.5 41f8cfa3bc YUNG's API jigsaw draws: yung_jigsaw structures and yung_single_element read as written
Clean-room, from spec-yungsapi.md §2.1 and §3.1 (no mod source read).
A `yungsapi:yung_jigsaw` structure lays out with the jigsaw assembler and
YUNG's draws: the start pool's file entries shuffled, then the rotation,
then a weighted pick in the shuffled order; each jigsaw's target pool is
drawn by weight from its shuffled entries without replacement, an element
whose `name` already has `max_count` pieces skipped at no other draw.
`yungsapi:yung_single_element` places what a single element places; its
pool entry carries the name and limit.

tools/test_structure_layout_parity.py --profile betterdungeons reads the
mod's own data from its jar into an overlay of the 26.3 resources and
compares the mod world's pieces: 82 of 84 Better Dungeons starts match
piece for piece (60 small, 11 skeleton, 11 zombie dungeons, up to 790
pieces); the other 2 sit in 26.3's sulfur_caves, which 1.21.1 lacks.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 05:08:43 +01:00
Ludovic Marie 1ef5b82b7d Merge pull request #1571 from ludovic111/fix/home-leak-tests
Tools and harnesses start the game in scratch folders, not a player's
2026-10-25 05:07:12 +01:00
Ludovic MarieandClaude Opus 5.5 441f749c25 Shader packs, clean room 24: mc_Entity from block.properties
block.properties (spec 4.1) is read: `block.<id>=` names with optional
namespaces and state filters; the most specific matching entry wins.
Terrain vertices carry their sprite layer, not their block, so the game
builds a table of the block sprite layers: each takes the id of the block
named like its sprite, else the id most blocks drawing it share; y is 1
on the water and lava sprites. Flat terrain reads it from a storage
buffer at set 3; other draws and packs without the file read (-1, -1).
Building the table takes 14 ms for BSL, 39 ms for Complementary.

Clean-room: implemented from cleanroom/shader-packs-spec.md revision 1,
4.1, 5.1, 5.3; no other loader's code read.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 04:59:39 +01:00
rdbtCVSandOpenCode gpt-6.1-sol 5d19fcaa06 Dismount players before command teleports
Reuse stop_riding before applying a player teleport destination so the riding tick cannot return the player to the vehicle. Add vanilla command parity and both-mode LAN regressions for mounted teleports.

Co-Authored-By: OpenCode gpt-6.1-sol <[email protected]>
2026-10-25 03:38:17 +00:00
rdbtCVSandOpenCode 489b5a4ffa Trading: keep Hero discounts for server players
Preserve the Hero effect captured by the guest's merchant session and cover discounted prices and payments for both single-player modes.

Fixes #1365

Co-Authored-By: OpenCode (gpt-6.1-sol) <[email protected]>
2026-10-25 03:36:35 +00:00
rdbtCVSandOpenCode gpt-6.1-sol 342cb6c500 Fix Java compiler/runtime selection during preparation
Pair PATH tools from a compatible JDK and always forward its home to external preparers so Java 25 exporters run with a compatible runtime.

Co-Authored-By: OpenCode gpt-6.1-sol <[email protected]>
2026-10-25 03:32:23 +00:00
rdbtCVSandOpenCode eed98be9cf Merge main and retain both LAN regression checks
Combine the shulker bullet check with main's new falling-entity coverage.

Co-Authored-By: OpenCode (agentrouter/gpt-6.1-sol) <[email protected]>
2026-10-25 03:31:45 +00:00
rdbtCVSandOpenCode agentrouter/gpt-6.1-sol 859236bd9d Fix recipe removal notifications for server players
Notify connected players of the display IDs forgotten by /recipe take.
Discard queued additions for recipes the player no longer knows.
Add LAN regression coverage for both single-player paths.

Co-Authored-By: OpenCode agentrouter/gpt-6.1-sol <[email protected]>
2026-10-25 03:25:56 +00:00
RVZO6andCodex 07519df062 Handle server stop-sound packets through the existing sound engine
Decode source/event selection flags, including 26.3 UI source 10, and route them to Audio.stop_sounds. Compare all 36 native Java codec selections and round-trip their bytes; reject invalid sources and truncated packets.

Co-Authored-By: Codex (gpt-6.1-sol) <[email protected]>
2026-10-24 23:23:59 -04:00
Ludovic MarieandClaude Opus 5.5 b940754555 Terrain vertex arena readable as storage where flat draws run
The terrain vertex arena's pages also get STORAGE usage on devices that
run flat draws (vertex storage and compute), so a shader pack's terrain
program can read the other corners of a vertex's quad to give packs a
face normal and tangent. Horizon's own pipelines are unchanged; the A/B
in the PR shows no frame-time change without a pack.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 04:23:38 +01:00
rdbtCVSandOpenCode agentrouter/gpt-6.1-sol b1eae8a5b8 Tests: keep spectator regression separate from new LAN checks
Move the spectator-travel check before the existing villagers check so it merges cleanly with the newly added falling check on main.

Co-Authored-By: OpenCode agentrouter/gpt-6.1-sol <[email protected]>
2026-10-25 03:13:12 +00:00
rdbtCVSandOpenCode cc65eeec82 Make shulker bullets visible on server clients
Assign mob-fired projectiles IDs when they are added and recognize shulker bullet spawns on remote clients. Extend the live LAN checks for both hosting modes.

Co-Authored-By: OpenCode (agentrouter/gpt-6.1-sol) <[email protected]>
2026-10-25 03:11:09 +00:00
RVZO6andCodex 792ac58f46 Render server text displays with billboard transforms and styled glyphs
Apply metadata, lifetime and position/transform interpolation; render wrapped/aligned text, background, opacity, shadows, brightness and see-through flags. Upload dynamic font glyphs and expose actual emitted vertices for end-to-end checks. Verify 108 matrices with the native client renderer and exercise a real Java-server/Rust-client fixture.

Co-Authored-By: Codex (gpt-6.1-sol) <[email protected]>
2026-10-24 23:10:47 -04:00
rdbtCVSandOpenCode GPT-6.1-sol 578f077347 Merge main and retain both LAN regression checks
Resolve the concurrent farmland and falling check additions in tools/lan_e2e.py.

Co-Authored-By: OpenCode GPT-6.1-sol <[email protected]>
2026-10-25 03:06:23 +00:00
Ludovic MarieandClaude Opus 5.5 36f68ab838 Server dialogs: text and boolean inputs
A dialog's text inputs are edit boxes (labelled above, CommonLayouts.
labeledElement; width, max length and initial value from the dialog) and
its boolean inputs are checkboxes with their label. A click focuses a box
or toggles a box, Tab and Shift+Tab move between the text inputs, the
keys, shortcuts and typed text go to the focused one. What they hold
fills the command templates and custom actions' payloads.

With this, AuthMe's register and login dialogs work: players type their
password and join instead of being kicked after 30 s.

horizon-ctl's `type` types into the focused input of an open dialog (the
first one when none is), in configuration too. server_e2e's dialog check
now types a number and ticks a box, and checks the command the template
makes of them and the payload of a custom action.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 04:05:25 +01:00
Ludovic MarieandClaude Opus 5.5 230730d1f5 Shader packs, clean room 23: the sun and moon (gbuffers_skytextured)
gbuffers_skytextured (or gbuffers_textured, gbuffers_basic) draws the
frame's sun and moon quads, with their render stages, `texture` reading
the shared atlas their sprites live in. The sky's parts draw in vanilla's
order (disc, sunset, sun, moon, stars, void), in one render pass while
consecutive programs write the same buffers. The sun and moon add to the
sky (SRC_ALPHA, ONE) unless blend.<program> says otherwise. Horizon's
world pass skips its own sun and moon when the pack draws them; a part
without a program stays Horizon's.

Clean-room: implemented from cleanroom/shader-packs-spec.md revision 1,
2.3, 5.4; no other loader's code read.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 04:02:13 +01:00
rdbtCVSandOpenCode b6ec56ac9b Merge origin/main into double-chest click fix
Co-Authored-By: OpenCode (GPT-6.1-Sol) <[email protected]>
2026-10-25 03:00:14 +00:00
rdbtCVSandOpenCode gpt-6.1-sol e334dd3d55 Fix offhand eating and drinking cancellation
Co-Authored-By: OpenCode gpt-6.1-sol <[email protected]>
2026-10-25 02:59:33 +00:00
Ludovic MarieandClaude Opus 5.5 8948bf90c4 Structure layout harness: Horizon's starts piece by piece against the structure oracle
The clean-room structure oracle saved, for 31 starts of vanilla structures
(villages, outposts, trial chambers, shipwrecks, ruined portals, desert
pyramids over seeds 1234, 2 and 42), the no-mod world's pieces. A new
structure_pieces probe lists the starts a chunk owns, piece by piece, and
tools/test_structure_layout_parity.py compares them with those saves.

22 of the 31 match piece for piece (every village and outpost that 26.3
still places there, and the horizontal boxes of the code-built pieces).
The other 9 differ between 1.21.1 and 26.3 themselves: 26.3 vanilla's own
pieces there (VanillaChunkGenerationOracle scan) equal Horizon's for all 18
starts that differ from 1.21.1. No vanilla jigsaw fix is needed.

Templates now keep the id they were loaded under, so a piece names its
element.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 03:57:01 +01:00
rdbtCVSandOpenCode GPT-6.1-sol 753a99b933 Fix crop updates after server-side farmland trampling
Forward landing block-write events to notify_written so unsupported crops break, drop their items, and synchronize to clients. Add LAN regression coverage for the host and guest in both hosting paths.

Co-Authored-By: OpenCode GPT-6.1-sol <[email protected]>
2026-10-25 02:55:30 +00:00
RVZO6andCodex dc1e16f472 Model every text-display metadata field against the Java wire codec
Add a native Java oracle for the actual Display/TextDisplay accessors and encoded metadata, with 513 combinations of flags, opacity, billboard mode and transforms. Preserve styled text and partial updates.

Co-Authored-By: Codex (gpt-6.1-sol) <[email protected]>
2026-10-24 22:52:51 -04:00
Ludovic Marie f2f58aca94 Merge pull request #1690 from ludovic111/fix/remote-items-fall 2026-10-25 03:45:53 +01:00
Ludovic MarieandClaude Opus 5.5 7d6583dc4d Shader packs, clean room 22: the sky (gbuffers_skybasic)
The pack's gbuffers_skybasic (or gbuffers_basic) draws vanilla's sky
geometry, as Horizon's own sky builds it: the disc at y 16, the sunset
fan while the sun is low, the stars and the void disc, each with its
render stage. One static vertex buffer; the frame's colours and rotations
sit in a small uniform the vertex inputs read. The sky draws after the
world copy and before terrain, at the far plane: only where nothing else
drew, without writing depth. It blends with the translucent default unless
blend.<program> says otherwise. Horizon's world pass then skips its own
sky disc, stars and void. The sun and moon stay Horizon's for now
(gbuffers_skytextured is next): the pack's sky covers them.

load_post takes the game's inputs per geometry (GameInputs);
GbuffersInputs gains statements run after the pack's main.

Clean-room: implemented from cleanroom/shader-packs-spec.md revision 1,
2.1, 2.3, 5.4; no other loader's code read.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 03:45:24 +01:00
Ludovic Marie f9e3244281 Merge pull request #1715 from ludovic111/fix/launcher-remove-async
🤖 Agent: merged after verify passed and the integration run on the desktop.
2026-10-25 03:30:13 +01:00
Ludovic MarieandClaude Opus 5.5 1fdb119587 Server menus keep their styled titles
A server's open_screen title is a text component, and container screens
draw it as AbstractContainerScreen does: in the label grey, under the
component's own colours, bold, italics and fonts, with the § codes a
literal holds. Horizon flattened it to a plain string, so a plugin's
gold and bold "Server Selector" menu showed in plain grey (#1645).
Chests, hoppers, dispensers, droppers, brewing stands and the other
workstations now draw the server's title. Before, the workstations
ignored it and showed their default name.

horizon-ctl's `screen` reports the title span by span (text, colour,
bold, italic). server_e2e's new `titles` check names a chest on the
vanilla server with a styled CustomName, opens it, and checks the title.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 03:30:04 +01:00
Ludovic MarieandClaude Opus 5.5 a7f8650c6b Server dialogs: the dialog screen, its buttons and their actions
A server's show_dialog (configuration and play) opens vanilla's
DialogScreen instead of being dropped: the header with the title and the
warning button, the message bodies in a scrolling column, the action
buttons packed into columns (MultiButtonDialogScreen) or side by side in
the footer (notice, confirmation). A button runs its click event: a
custom action goes back as custom_click_action, a command runs (in play;
in configuration vanilla only warns), show_dialog opens another dialog,
copy_to_clipboard copies. after_action closes the screen, leaves it, or
opens the WaitingForResponseScreen. The warning button opens the
"custom screen" warning, whose Disconnect leaves the server. clear_dialog
closes it, and so do the level loading screen and a disconnect. The world
and HUD blur behind it as behind any in-game screen.

Inputs come next; here they are not drawn and keep their initial values.

tools/server_e2e.py is a new harness: a vanilla 26.3 dedicated server and
one Horizon client. Its dialog check shows a dialog with /dialog show,
clicks a command button (the server's trigger), a custom action (the
server's debug log shows its payload) and a wait_for_response one, and
clears a dialog with /dialog clear.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 03:29:45 +01:00
Ludovic MarieandClaude Opus 5.5 b9b47f12da Shader packs, clean room 21: per-draw uniforms (renderStage)
A gbuffers program's renderStage, entityId, blockEntityId and entityColor
(spec 5.3, updated per draw) move to a block of their own at set 2 that
the game picks per draw with a dynamic offset: one slot per render stage.
Terrain draws as TERRAIN_SOLID, translucent terrain as
TERRAIN_TRANSLUCENT. The terrain test pack paints any other stage red.

Clean-room: implemented from cleanroom/shader-packs-spec.md revision 1,
5.3, 5.4; no other loader's code read.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 03:29:21 +01:00
rdbtCVSandOpenCode c36e81916b Fix double-chest inventory and hotbar clicks
Co-Authored-By: OpenCode (GPT-6.1-Sol) <[email protected]>
2026-10-25 02:24:16 +00:00
Ludovic MarieandClaude Sonnet 5.5 87f377b29c launcher: remove a version on a background thread
Clicking Remove deleted the version's thousands of files on the UI thread, so
the whole launcher froze for seconds (more on Windows). The removal now runs
on a thread and ends with Msg::Removed. The version leaves the installed list
at once (nothing can launch, select or prepare it) and its row shows
"Removing…" with a moving bar; installing it again is refused until it is gone.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-25 03:24:05 +01:00
rdbtCVSandOpenCode agentrouter/gpt-6.1-sol 6e86e07781 Multiplayer: keep spectator collision state after dimension travel
Restore the current game mode's spectator flag when remote_teleport creates a replacement player body. Add an end-to-end wall traversal regression for Nether travel and return, covering the integrated-server host, LAN guest and in-frame host.

Fixes #1299

Co-Authored-By: OpenCode agentrouter/gpt-6.1-sol <[email protected]>
2026-10-25 02:22:41 +00:00
Ludovic MarieandClaude Opus 5.5 5e348aa479 Shader packs, clean room 20: buffer mipmaps
`const bool <buffer>MipmapEnabled = true;` in a deferred, composite or
final program's fragment stage now regenerates that buffer's mip chain
right before the program (6.1.7), on the side programs read, and only
when the buffer changed since its last chain this frame. Each level is
the average of the 2x2 texels above it; levels attach one at a time and
programs sample all of them. Unfilterable buffers (integer, 32-bit
float) get none, with a note.

BSL's bloom read every tap from the full-size buffer before; with its
chain composite4 drops from 0.38 to 0.29 ms at 720p on the Mac mini
(HORIZON_SHADER_PACK_TIMING, the chains included), and blurs as intended.

Clean-room: implemented from cleanroom/shader-packs-spec.md revision 1,
section 6.1.7; no other loader's code read.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 03:03:54 +01:00
Ludovic MarieandClaude Opus 5.5 1d90d72a64 Server dialogs: the packets and the dialog model
show_dialog and clear_dialog in configuration and play, and
custom_click_action, the answer, in both. Dialog.DIRECT_CODEC as
horizon_client::dialog: the five dialog types, their bodies, inputs and
actions with vanilla's defaults, the values inputs hold, and
Action.createAction (a command template's $(key) substitution with
escapeWithoutQuotes, a custom action's payload of every input's tag).

Nothing uses them yet: the screen comes next.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 02:59:28 +01:00
Ludovic MarieandClaude Opus 5.5 b6345fc61d Shader packs, clean room 19: GPU time of each pass (developers' aid)
HORIZON_SHADER_PACK_TIMING=1 puts timestamps around every pass of a
shader pack, one frame in 30, and logs each pass's median every 30
timed frames. The frame after a timed one waits for the GPU before
reading them (Metal resolves timestamps without waiting for the passes
that write them). Passes overlap on tiled GPUs, so a pass is charged
from the later of its beginning and the previous pass's end.

This is what attributes a pack's cost to its own programs or to ours
when the machine is shared and FPS comparisons are noisy.

Clean-room: Horizon's own tooling, no spec section; no other loader's
code read.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 02:57:15 +01:00
Ludovic Marie 4bb7f96f81 Merge pull request #1634 from ludovic111/feat/mod-index 2026-10-25 02:52:31 +01:00
Ludovic MarieandClaude Opus 5.5 3e2c30a852 Shader packs, clean room 18: translucent terrain drawn by the pack
With a pack whose gbuffers_water (or a fallback down gbuffers_terrain's
chain) loads, translucent terrain (water, stained glass) is drawn by it
after the deferred programs and before the composites, in place, sorted
back to front as the game sorts it, and Horizon's own translucent
terrain draw is skipped.

- blend.<program> (off, or four GL factors); translucent terrain
  defaults to SRC_ALPHA ONE_MINUS_SRC_ALPHA ONE ONE_MINUS_SRC_ALPHA, and
  only blendable buffer formats blend.
- depthtex1/depthtex2: depth without translucent terrain, a copy taken
  just before it when a later program reads them (before it, depthtex0
  itself). The hand is drawn after the pack, so depthtex2 = depthtex1.

Clean-room: implemented from cleanroom/shader-packs-spec.md revision 1,
sections 2.1, 2.3, 3.2 (blend), 6.2 (depthtex1/2); no other loader's
code read.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 02:47:14 +01:00
rdbtCVSandOpenCode gpt-6.1-sol e820e9e1af Website: use one navigation bar across pages
Co-Authored-By: OpenCode gpt-6.1-sol <[email protected]>
2026-10-25 01:40:39 +00:00
Ludovic Marie 89f426c48f Merge pull request #1648 from ludovic111/fix/remote-fall-hurt 2026-10-25 02:38:44 +01:00
Alexander Krylov 961af30997 Clippy: clear the two warnings that the whole tree fails on
The update tests' scratch helper only feeds unix-gated tests, and
receive_decorated takes the box the queue already put the protos in.
2026-10-25 04:34:05 +03:00
Ludovic MarieandClaude Opus 5.5 a2ec3fc791 Shader packs, clean room 17: real packs' buffers and samplers
- RGBA16 and R11F_G11F_B10F buffers get their own formats where the device has them
  (16-bit normalized render targets need the adapter's format features): the float stand-in
  kept NaN and values past 1, and BSL's TAA history went black for good.
- Textures share one sampler variable per kind (linear, nearest, repeat, nearest-repeat,
  comparison): devices have as few as 16 sampler slots, Complementary's terrain program
  declares 23 textures.
- A gbuffers pass reading the depth buffer or a buffer it draws into reads a blank texture
  instead (a feedback loop: undefined in GL, refused by wgpu, and a crash here).
- A bind group layout that fails validation skips the program with the error's details
  instead of ending the game.
- shadowModelView / shadowProjection (and inverses) for programs that read them before the
  shadow pass exists: an orthographic camera 160 blocks each side, toward the shadow light.
- HORIZON_SHADER_PACK_SKIP=a,b leaves programs out (a developers' aid to bisect a pack).

Clean-room: implemented from cleanroom/shader-packs-spec.md revision 1, sections 6.1, 6.7,
5.2; no other loader's code read.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 02:03:17 +01:00
Ludovic MarieandClaude Opus 5.5 505e8e960f Shader packs, clean room 16: custom uniforms
`uniform.<type>.<name>` and `variable.<type>.<name>` of shaders.properties (3.4): an
expression parser with C precedence, the function table (math, vectors, `if`, `between`,
`equals`, `smooth`, `random`...), evaluated on the CPU once per frame in file order after the
frame's own uniforms. `smooth` keeps its state per call site and halves the distance to its
target every fade time (OQ-26 working answer). A name neither the frame nor the pack defines
reads 0 and is listed once in the log.

Clean-room: implemented from cleanroom/shader-packs-spec.md revision 1, section 3.4; no other
loader's code read.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 02:01:48 +01:00
Ludovic MarieandClaude Opus 5.5 6c8880fbd8 Shader packs, clean room 15: globals initialized at run time, switch options
naga keeps only constant initializers of globals, so every initialized global that is not
`const` (and every conditional one) gets its value from a function of ours that main calls
first, one per declarator, converting as GLSL's initialization does (`vec2 v = atlasSize`).
`vec3 a = x, b = y;` no longer loses `b`.

Program-enable expressions (3.3) now see the pack's switch options that are on by default
(8.1): `#define NAME` with nothing after it, tested with `#ifdef`/`#ifndef` anywhere in the
pack (OQ-13 working answer). BSL's `deferred` (ambient occlusion) and its TAA ran with options
off before.

Clean-room: implemented from cleanroom/shader-packs-spec.md revision 1, sections 3.3, 8.1;
no other loader's code read.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 02:00:40 +01:00
Alexander Krylov c10261f008 Cleanup: one predicate property helper, a Windows-only allow, tolerant locks in the bots
predicate_property was the same fifteen lines in block_feature and
state_provider; it is worldgen::bound_property now. The mojang link helpers are
unix-only in use but worth testing everywhere, so they are allowed to look dead
elsewhere, which clears the launcher's -D warnings red on Windows. The bots
harness recovers from a poisoned lock instead of panicking.
2026-10-25 03:58:02 +03:00
Alexander Krylov a96dccf691 Save: the write paths report a failure instead of dropping it
The moderation lists on a dedicated server, command storage, boss bars, the
world clocks, the difficulty and the emergency save before a crash all used
let _ = and lost the error. write_dat also swallowed the rename that moves the
current file aside, which made the following rename fail on the existing path
with nothing reporting it.
2026-10-25 03:57:31 +03:00
Ludovic MarieandClaude Opus 5.5 1fa46979c8 Server clients: primed TNT falls and burns its fuse by itself between updates
PrimedTnt keeps Entity's NO_OP InterpolationHandler and ticks on a client
too: a position packet puts it there at once, its own tick moves it on
(gravity, collisions, bounces, drag) and burns the fuse down, and a client
discards it when the fuse runs out. Horizon interpolated it towards each
update instead, so it trailed about two ticks behind the server.

lan_e2e's falling check now drops primed TNT too and checks that the
guest has it exactly where its fall from rest puts it for the fuse the
server sent.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 02:57:26 +02:00
Alexander Krylov 6d1f6965d2 Packs: an untouched vanilla directory is mirrored on Windows too (#1688)
link cannot symlink a directory on Windows, so mirror has to recurse into one
itself, but it only did so when a pack had filters. An untouched directory fell
through to the link branch and vanished from the overlay, which took
textures/font and the panorama with it and made a pack that overrides a font
crash the client before its window.
2026-10-25 03:57:00 +03:00
Devin 348675c538 Packs: open streamed archives the way Java's ZipFile does
A resource pack written while it streams (Nexo's server behind
resourcepack.gommehd.io, gommehd.net's required pack) leaves every local
header a stub, every uncompressed size at 0xFFFFFFFF and the END record's
entry counts and disk number at 0 and 0xFFFF. Java tolerates all three;
the zip crate's ZipArchive refuses the END record as multi-disk and empty,
so a required server pack failed its reload and the join never got through
(each retry also rebuilt the renderer, once a second).

Count the real central-directory entries and normalize the END record's
disk and count fields when they disagree with it, and bound the inflate
output when an entry's size is the 0xFFFFFFFF sentinel.

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-25 02:52:19 +02:00
some-du6eandClaude Opus 5.5 f66c257ae5 Advancement chat hover: normalise the title with canonical_component
scoreboard::canonical_component already re-encodes a component as vanilla
does, list form included, so decorate_name uses it instead of its own copy.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 18:46:24 -06:00
some-du6eandClaude Opus 5.5 9f1a880951 Advancement chat hover: a title in list form keeps its first element's style
A component list is its first element with the rest as siblings, so
decorateName merges the frame colour under that element's style. The
parsed list moved a styled first element down a level, which gave the
description the frame colour instead of the title's.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 18:43:58 -06:00
some-du6eandClaude Opus 5.5 7a7bfdde79 Advancement chat hover: the description takes the title's own style
Advancement.decorateName merges the frame colour under the title's own
style and appends the description after its siblings. Built from the
parsed Text, a styled root with siblings had its style moved down a level,
so the description showed in the frame colour instead of the title's
(a gold, bold title gave a green description). The hover is now built on
the title's JSON, and the local spans are decoded from that JSON, so an
in-frame player sees the description coloured as a client does.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 18:39:57 -06:00
Ludovic Marie 947d65e64d Merge pull request #1678 from ludovic111/fix/no-play-packets-in-reconfig 2026-10-25 02:38:40 +02:00
Ludovic MarieandClaude Opus 5.5 fe253fe534 Server clients: dropped items and falling blocks fall by themselves between updates
A server sends an item's and a falling block's position only every 20 ticks.
As a client (multiplayer, a LAN guest, single player on its server thread)
Horizon moved them only when a position arrived, three steps towards it, so a
dropped item or falling sand hung in the air and then jumped down once a
second (#1568).

Vanilla runs ItemEntity.tick and FallingBlockEntity.tick on the client too
(gravity, collisions, drag), takes their motion from AddEntity
(recreateFromPacket) and SetEntityMotion (Entity.lerpMotion), and snaps them to
each position packet: neither overrides Entity's NO_OP InterpolationHandler.
The client now does the same, with the same ticks single player in-frame uses;
only the server removes them or places the block.

lan_e2e --check falling: the guest samples both every 2 ticks over 16 ticks.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 02:36:48 +02:00
some-du6eandClaude Opus 5.5 96b6ed4916 Test that a connection reaches IPv4 first and report the first address's error
A loopback test listens on both [::1] and 127.0.0.1 and checks the
connection lands on IPv4 although IPv6 is listed first; it fails without
the sort. Another checks the fallback to IPv6 when IPv4 refuses. When every
address fails, the error is now the first address's (the one vanilla tries)
rather than an IPv6 fallback's, which is often only "no route to host".

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 18:33:22 -06:00
rdbtCVSandopencode 6240124557 Slow Falling clears the fall distance so landing does not hurt
The effect capped downward gravity but left the fall distance to accumulate, so the landing tick still dealt fall damage. Clear it at the start of check_fall_damage, where both local travel and the server thread's replay of a guest's move pass.

Co-Authored-By: opencode (deepseek-v4.1-flash-free) <[email protected]>
2026-10-25 01:21:59 +01:00
Ludovic MarieandClaude Opus 5.5 e55b5b7c00 Boss events: custom bars first, as a joining player gets them
CustomBossEvents.onPlayerConnect adds a joining player to its bars before any
entity, dragon fight or raid tells it about theirs.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 02:21:03 +02:00
rdbtCVSandopencode b0a5334cb5 Creative inventory: highlight hovered hotbar slots
The item-tab hotbar loop only set the tooltip, so the nine slots never
showed the white hover fill the 5x9 grid uses. Apply the same fill,
including over empty slots, before picking the tooltip item.

Fixes #1341

Co-Authored-By: opencode (deepseek-v4.1-flash-free) <[email protected]>
2026-10-25 01:17:28 +01:00
Alexander Krylov a93c1507f9 Launcher: a version removal retries a locked file and leaves the UI thread
remove_dir_all stops at the first file another process still holds and leaves
the rest deleted, so a removal that worked reported os error 5. Retry five times
250ms apart and treat a missing folder as done. The window froze because the
deletion ran from the update loop; it now runs on a thread and reports back
with Msg::Removed, as install already did.
2026-10-25 03:16:56 +03:00
Ludovic MarieandClaude Opus 5.5 a26297058a Trims: a Horizon host sends them too
The host wrote no trim component, so single player on its server thread,
LAN guests and dedicated-server players saw armour without its trim.
network_stack now takes the connection's synced-registry lookup for any
registry and writes Component::Trim when both names are in it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 02:15:25 +02:00
Ludovic Marie 149864c3b4 Merge remote-tracking branch 'origin/main' into perf/face-cull-cr1
# Conflicts:
#	crates/horizon-client/src/mesher/leaf_culling_tests.rs
2026-10-25 02:15:14 +02:00
Ludovic MarieandClaude Opus 5.5 7c27b85fbd server_pack_e2e: start the game and the servers through lab.game_env
The new home-leak test rejects scripts that set HORIZON_NO_AUDIO by hand; this one, added on main
since, did.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 02:08:31 +02:00
Ludovic MarieandClaude Opus 5.5 42afd721e7 Server packs: the download toast shows over the connection screen
A server sends its packs in the configuration phase, while the joining
screen covers the frame; vanilla's ToastManager draws over every screen.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 02:04:39 +02:00
Simon Dräger 4d3f3b6391 Load fireballs from NBT as live projectiles 2026-10-24 17:02:57 -07:00
Ludovic Marie 75d70a1a74 Merge pull request #1584 from ludovic111/feat/shader-packs-read 2026-10-25 01:55:19 +02:00
Ludovic Marie 856f0c2e40 Merge pull request #1583 from ludovic111/docs/platform-apis 2026-10-25 01:53:57 +02:00
Ludovic MarieandClaude Opus 5.5 75c009e3fc Boss events: one path from the host's diff to every overlay
The host diffed each viewer's boss events into a Change enum, then turned the
changes into packets for guests and, through a second match, into setter calls
on the window's own overlay; the client turned packets into the same setters a
third way. The diff now yields ClientboundBossEventPacket values, guests get
them as packets and the local overlay applies them with BossOverlay::apply,
the same function a server's packets reach. Gone: Change, Snap, Sent,
apply_local, boss_packet and the overlay's nine-argument add and setters.
UUIDUtil.uuidFromIntArray joins uuid_ints in lan::playerdata.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 01:53:03 +02:00
Ludovic Marie 213f1bbb2e Merge pull request #1582 from ludovic111/docs/server-api-reference 2026-10-25 01:52:31 +02:00
Alexander Krylov e30166f23b Options: Horizon's own captions read a bundled language namespace (#1672)
The settings screen's own options (controller, vibration, dead zones and the
rest) are not vanilla options and have no vanilla keys, so their captions
were English literals. They now read horizon.options.* from assets/horizon/lang,
which the client bundles as one more language source placed first, so a pack
or the prepared root can still override it. English and Russian ship here.
2026-10-25 02:51:11 +03:00
socksproxandDevin ba09b086a6 Multiplayer: drop the player name from the join loading line
The pre-world overlay said "Joined as {name}; loading terrain..." after
Event::Joined. Vanilla's LevelLoadingScreen only ever draws the
multiplayer.downloadingTerrain text there — use that translation, no
name.

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-25 01:49:31 +02:00
Ludovic MarieandClaude Opus 5.5 f07d66c7cf Trims: their own arm in Component::read
The trim decode sat inside the read-past fallback and matched the kind
there; it now has a TRIM arm, and the fallback only reads past. Long
comment lines wrapped.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 01:44:21 +02:00
Ludovic MarieandClaude Opus 5.5 6d735c26d3 Server packs: the download toast follows DownloadedPackSource's notifier
What the toast showed differed from vanilla 26.3 in several ways:
- the title and the failure line had no arguments: download.pack.title takes
  the request number and the batch size, download.pack.failed the failures
  and the batch size;
- sizes read "1.3 KiB" style; com.mojang.realmsclient.Unit.humanReadable gives
  "1.3 KB" ("512 B" below 1 KiB, half-up rounding), checked against the jar;
- each update didn't restart SystemToast's 5 s display time, so a download
  longer than 5 s lost its toast, and the Notification Time option was ignored;
- a finished batch removed the toast at once instead of forceHide's slide out;
- the failure title sat at y 7 in white; SystemToast draws titles yellow, and a
  title without a message at y 12;
- a second batch replaced the first one's channel and lost its events.

The download thread now reports HttpUtil's listener calls (requestStart,
downloadStart with the Content-Length, downloadedBytes, requestFinished), and
each batch gets a PackNotifier (createDownloadNotifier) with its own toast. The
toast is a shared toasts::SystemToast; the system background moved to
toasts.rs with a y, so the pack toasts stack under an advancement toast.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 01:40:53 +02:00
Ludovic MarieandClaude Opus 5.5 0f9de6dbb0 Shader packs, clean room 14: terrain drawn by the pack's program
With a pack whose gbuffers_terrain (or a fallback) loads, solid and
cutout terrain is drawn by it into the pack's buffers, in place, between
prepare and deferred, and Horizon's own solid terrain draw is skipped.

- depthtex0 is now a Depth32Float attachment: the blit writes the
  world's depth there (GL window depth, GL rows), terrain tests and
  writes against it, so entities and terrain occlude each other; later
  programs sample it as before.
- The vertex glue reads Horizon's TerrainVertex on both draw paths
  (instanced, and flat with the page tables in bind group 1) and gives
  atlas coordinates, lightmap coordinates in 0-240, the vertex colour,
  mc_midTexCoord at the sprite's centre and mc_Entity = -1. Normals,
  tangents and block ids come later.
- texture/gtexture/tex sample the block atlas, lightmap the lightmap;
  atlasSize is set.
- Occlusion culling is off while a pack draws terrain (it reads the
  depth of Horizon's terrain draw), and flat draws stay on.
- Uniforms a program reads that are not provided yet are reported.
- Program building is shared by quads and terrain (build_pass, bind).
- terrain-test: Horizon's own pack for this slice.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 01:39:28 +02:00
Ludovic MarieandClaude Opus 5.5 3dc14b6eda Advancement chat names: the description goes after the title's siblings
Advancement.decorateName appends a new line and the description to the title's copy;
adv_name replaced the title's extra list, so a title built from siblings (a
plugin's {"text":"","extra":[...]}) reached clients as just the description.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 01:29:12 +02:00
Alexander Krylov f500be1b5e Title: the main menu labels follow the game language (#1672)
The screen built them from literals, so the buttons stayed English in every
language while the world select screen next to them was translated. Each has
a vanilla key the prepared resources already carry; Friends is Horizon's own
and reads horizon.menu.friends with the English text as fallback.
2026-10-25 02:28:27 +03:00
Ludovic MarieandClaude Opus 5.5 3e3ab7a413 Merge main into fix/ipv4-first: IPv4 first for SRV targets too
Main's SRV resolver (#1578) added Connection::connect_server; both connect paths now
go through connect_addresses, which puts IPv4 first, as ServerAddressResolver.SYSTEM's
InetAddress.getByName does for the original host and the SRV target alike.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 01:24:52 +02:00
Ludovic MarieandClaude Opus 5.5 0253d9bc47 Multiplayer: no play packets after the client acknowledges a reconfiguration
A proxy switching servers (Velocity, BungeeCord) sends the client back to
configuration. The network thread acknowledged it, but the game thread kept
ticking the old level and its client_tick_end went out right after the
acknowledgement, where the server reads it as configuration packet 13.
Velocity drops the client: "A packet did not decode successfully", shown as
"An internal error occurred in your connection".

Vanilla's handleConfigurationStart clears the level before it acknowledges,
so nothing of play follows. Session::send now drops play packets while the
connection is in configuration, checked under the writer lock that the
acknowledgement is sent with.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 01:23:59 +02:00
Ludovic MarieandClaude Opus 5.5 fab71df185 Mesher: sort solid quads in the meshing workers, not in Mesher::mesh
Mesher::mesh keeps model order, which the leaf-culling test pairs with
the baked faces; the section and tile workers sort what they upload.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 01:21:23 +02:00
Ludovic MarieandClaude Opus 5.5 8698b4ad0c Shader packs, clean room 13: loading the terrain program
load_post takes the game's terrain vertex inputs and, given them, loads
the first program of gbuffers_terrain's fallback chain that translates
(gbuffers_terrain, textured_lit, textured, basic). Program loading is
shared by the post-processing and gbuffers programs.

The translator no longer rewrites a variable, parameter or member that
has the name of a sampler (`float specular` beside `uniform sampler2D
specular`), which kept Complementary's gbuffers programs from compiling.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 01:20:16 +02:00
socksproxandDevin 372f918e44 Multiplayer: keep firework flight duration, boost gliding players
Two halves of FireworkRocketEntity on a server were missing:

- SetCreativeModeSlot encoded only damage, so a creative-picked rocket
  lost its minecraft:fireworks component (flight_duration and explosions
  gone); the wire codec also had no decoder for it or firework_explosion,
  so a stack carrying them failed to decode at all. The components now
  follow Fireworks.STREAM_CODEC/FireworkExplosion.STREAM_CODEC both ways
  (component ids 71/70).
- A remote rocket never read DATA_ATTACHED_TO_TARGET,
  DATA_ID_FIREWORKS_ITEM or DATA_SHOT_AT_ANGLE, so the server-attached
  boost rocket was an inert marker: attached_to now comes from
  EntityDataSerializers.OPTIONAL_UNSIGNED_INT (the id plus one), the item
  feeds the explosion list, and FireworkRocketEntity.tick's
  isFallFlying boost applies to the local player.

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-25 01:18:53 +02:00
Ludovic Marie 76638aaf80 Merge pull request #1658 from rdbtCVS/t3/remove-unused-load-block-section
🤖 Agent: merged after verify passed and the integration run on the desktop.
2026-10-25 01:15:24 +02:00
Ludovic Marie eaa9f3c203 Merge pull request #1651 from rdbtCVS/t3/remove-orphaned-baseline-loader-profile
🤖 Agent: merged after verify passed and the integration run on the desktop.
2026-10-25 01:14:55 +02:00
Ludovic Marie 395c256d11 Merge pull request #1657 from rdbtCVS/t3/remove-unused-launcher-release-wrapper
🤖 Agent: merged after verify passed and the integration run on the desktop.
2026-10-25 01:14:39 +02:00
Ludovic MarieandClaude Opus 5.5 f31c18aa6c Shader packs, clean room 12: the block atlas packs sample
Packs sample blocks from one 2D atlas; Horizon keeps them in sprite
arrays. While a pack is loaded, every layer and its mip levels are
copied into a grid, and the animator reports the layers it rewrote so
only those are copied again. The sprite arrays gain COPY_SRC.

The gbuffers pass binds it in the next slice.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 01:14:25 +02:00
Ludovic MarieandClaude Opus 5.5 a2a91f98f9 Shader packs, clean room 11: translating gbuffers programs
translate_gbuffers places a vertex-input glue the game writes (it knows
its own vertex formats) in front of a pack's gbuffers program, sets the
compatibility matrices from gbufferModelView/gbufferProjection and the
lightmap texture matrix, and gives declared attributes the game's values
(zero when it has none).

Nothing calls it yet.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 01:14:15 +02:00
Ludovic Marie 75983739ec Merge branch 'main' into fix/home-leak-tests 2026-10-25 01:09:27 +02:00
Ludovic Marie a4fa1a770d Merge pull request #1667 from ludovic111/fix/release-page-launcher-first
🤖 Agent: merged after verify passed and the integration run on the desktop.
2026-10-25 01:07:03 +02:00
Ludovic Marie 8a7831e58b Merge pull request #1662 from ludovic111/docs/how-horizon-is-built
🤖 Agent: merged after verify passed and the integration run on the desktop.
2026-10-25 01:01:52 +02:00
Ludovic MarieandClaude Opus 5.5 3612f51812 Game releases send people to the launcher: notes open with it, archives labelled
GitHub's "Latest release" is the newest game release, so new players download the game's archive, which
does not start on its own (#1647). Making a launcher release the latest instead would break every
installed horizon-server, install.sh and install.ps1: they ask for releases/latest and expect the game's
archives, and a server that can't update can't fetch a fix either. So the game stays the latest and says
where to go:

- The notes open with "Want to play? Download the Horizon launcher, not the files below", linking
  playhorizon.dev and the launcher releases. It is raw HTML on one line, which GitHub renders and the
  launcher's notes view and one-line summary skip (pulldown-cmark HTML events), so launchers already
  installed don't tell players to download themselves. The game footer that said the same at the bottom
  goes. The site's Releases page leaves the block out.
- release.yml gives each archive a label ("Game files the launcher installs: Linux (...)", "Dedicated
  server for Linux (...)"), which GitHub shows instead of the file name. Everything that downloads
  matches the asset's name, which is unchanged.

Fixes #1647

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 01:00:21 +02:00
Ludovic Marie d6e5fb5331 Merge pull request #1606 from rdbtCVS/t3/vectorize-perlin-noise
🤖 Agent: merged after verify passed and the integration run on the desktop.
2026-10-25 00:59:02 +02:00
Ludovic Marie 3b3590ea03 Merge pull request #1580 from levzzz5154/feat/boat-passengers
🤖 Agent: merged after verify passed and the integration run on the desktop.
2026-10-25 00:54:21 +02:00
rdbtCVSandOpenAI Codex GPT-6.1 c41a053c15 refactor: separate client world entry from authoritative loading
Give integrated and dedicated startup a synchronous loading entry point without client deferral or integrated-server selection. Keep client orchestration and background loading unchanged.

Co-Authored-By: OpenAI Codex GPT-6.1 <[email protected]>
2026-10-24 23:53:29 +01:00
rdbtCVSandOpenAI Codex 0d1736da8f Make startup and world-loading execution roles explicit
Separate client, integrated-server and dedicated-server roles from connection state. Use roles for startup, presentation work and server dimension loading while preserving in-frame client simulation.

Co-Authored-By: OpenAI Codex (GPT-6.1) <[email protected]>
2026-10-24 23:53:00 +01:00
Alexander KrylovandHermes Agent 977ef7181f Data: /data get entity writes a mob's live riders
A mounted mob answered "Found no elements matching Passengers": the command
built the entity with the flat serializer, so `Passengers` was never in the
tag, even though the game had the relationship and saving a world kept it.

Persistence already writes the tree (`Game::mob_tree_nbt`, which also carries
the leash), and vanilla's command path uses the same one, so the command reads
it now.

Fixes #1519.

Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
2026-10-25 01:51:47 +03:00
Ludovic Marie 75b7812f68 Merge pull request #1595 from Domanffe/fix/paused-player-tick
🤖 Agent: merged after verify passed and the integration run on the desktop.
2026-10-25 00:49:56 +02:00
Ludovic Marie e0344f1f79 Merge pull request #1597 from Domanffe/fix/remote-mob-fluid-contact
🤖 Agent: merged after verify passed and the integration run on the desktop.
2026-10-25 00:48:10 +02:00
Ludovic MarieandClaude Opus 5.5 6774b2b3e7 Docs: "How Horizon is built", a page on who writes it and why it's safe to run
New players ask on Discord whether Horizon is legit and whether it is vibecoded. One plain page answers
both, every claim checked against the repository today: agents write most of the code, what checks it
instead of a human reading every line (harnesses, verify, parity, the contribution gate, the release
smoke test), signed checksums, the macOS and Windows signing status as it is (not notarized, not
code-signed), the mod sandbox, opt-in telemetry, no Mojang files and no other project's code.

On the website (docs nav under Start here, a card in the home page's community section, the footers) and
as docs/how-horizon-is-built.md. CONTRIBUTING.md gets the public "Only original code" rule the page links.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 00:47:49 +02:00
Ludovic MarieandClaude Opus 5.5 466b1de4c4 Shader packs: fewer passes and no needless loads in post-processing
- A buffer is cleared only when a program reads it before one writes all
  of it; the clears left ride in the blit pass as attachments the blit
  shader leaves alone, instead of passes of their own.
- The blit writes depthtex0 only when some program reads it.
- Attachments a pass overwrites whole (the blit's colortex0 and depth,
  every composite output, final's window) start cleared rather than
  loaded, which spares a tiled GPU reading them back first.
- The blit's bindings are kept until the world target changes size.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 00:46:07 +02:00
socksproxandDevin 282ecf16a9 Tests: fix boss-event verify failures
boss_event_round_trips missed its #[test] attribute, failing clippy's
dead-code lint; unknown_packets_keep_their_payload decoded boss_event,
which this PR gives a real decoder, so the garbage payload no longer
comes back Unknown — it uses player_look_at, which still has none.

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-25 00:45:16 +02:00
rdbtCVSandOpenAI Codex 7072a40701 Remove orphaned baseline loader benchmark
Co-Authored-By: OpenAI Codex <[email protected]>
2026-10-24 23:25:02 +01:00
rdbtCVSandOpenAI Codex 2a6347dc55 Remove unused saved-chunk section loader
Co-Authored-By: OpenAI Codex <[email protected]>
2026-10-24 23:25:01 +01:00
rdbtCVSandOpenAI Codex f1cfb3069f Remove unused launcher release wrapper
Co-Authored-By: OpenAI Codex <[email protected]>
2026-10-24 23:25:01 +01:00
Ludovic Marie de840da0e5 Merge pull request #1594 from Domanffe/feat/bundled-languages
🤖 Agent: merged after verify passed and the integration run on the desktop.
2026-10-25 00:15:26 +02:00
Ludovic MarieandClaude Opus 5.5 8800d1878a Perlin volume: one loop with a BATCH switch instead of a test copy
The batched loop and the one-at-a-time reference were two copies of the
same 100 lines; a const generic picks the path, so the test compares the
code that ships with itself minus the batching. Drops the ignored
microkernel timer.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 00:03:46 +02:00
Ludovic MarieandClaude Opus 5.5 1776d7ca5c Shader packs, clean room 10: post-processing over Horizon's world
The first slice that draws: with HORIZON_SHADER_PACK=<name>, the pack in
shaderpacks/<name> (folder or zip) runs its prepare, deferred, composite
and final programs over the world, under the hand and the HUD. Gbuffers
programs come later, so the composites read Horizon's own picture.

- The world renders into its own target (as with a resolution scale); its
  colour becomes colortex0 and its reverse-Z depth becomes depthtex0 in
  GL's window depth (1 - d for the same planes), rows flipped to GL's
  order. depthtex1 and depthtex2 read the same depth for now.
- colortex buffers in the formats the pack asked for (the nearest one
  wgpu renders to), created only when a program uses them, each with two
  textures: programs read main and write alt, the buffers a program wrote
  flip after it, flip.* keys and deferred_pre/composite_pre apply, and
  the mapping carries over to the next frame. Clears at the start of the
  frame with the pack's colours or the defaults.
- noisetex (256x256 white noise, repeat), a lit stand-in behind shadow
  samplers until shadow maps exist, a 1x1 stand-in for other names; the
  frame uniforms of the previous commit and the translator's own block.
- final draws onto the window; without it colortex0 is shown.
- Pipelines are built through validation scopes: a program wgpu refuses
  is reported with the full error and skipped. The dimension's folder is
  reloaded when the player changes dimension.

Nothing changes without the environment variable: players see no change.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 23:46:24 +02:00
Ludovic MarieandClaude Opus 5.5 7b56b59e7c Mod registry: the submission form's service (crates/horizon-mod-registry)
Its own workspace, as the telemetry server's, for its own Railway service.
GitHub sign-in with no scope (the user's token reads the login once and is
dropped), signed-cookie sessions, an origin check and a CSRF token, six
submissions per account and sixty in all per hour. A submission runs the
file's own checks (horizon_mods::index::inspect, the signature, the entry's
fields through registry::project), uploads it to the registry's `staging`
release as <sha256>.<extension>, writes mods/<id>.toml on a new branch with
the submitter as co-author, and opens the pull request. Only an entry's
owners publish updates; a key change is a pull request by hand. Shader packs
are refused until their format exists.

Tested end to end against a mock GitHub: sign-in, the signed sample mod,
what lands on GitHub, and what is refused.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 23:43:20 +02:00
Ludovic MarieandClaude Opus 5.5 7d76923a04 Mod registry tools: the registry's entries, build and review
- `horizon_mods::index::registry`: the registry repository's
  `mods/<id>.toml` and `revoked.toml`, `build` (every entry and file
  checked, every problem listed, the index written) and `review` (a
  published file never changes or disappears, a kind never changes;
  non-owner, owner and key changes pointed out).
- `cargo horizon-mod registry build|review|check|list` for the registry's CI.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 23:42:57 +02:00
Ludovic MarieandClaude Opus 5.5 59281c5c73 Mod registry: cargo horizon-mod check, each kind's own checks
- `horizon_mods::index::inspect`: each kind's own check (a mod's manifest
  and content, a modpack's manifest and overrides, a resource pack's
  pack.mcmeta), returning what the file says; `check_file` now compares
  those facts with the index. Shader packs and unknown kinds are refused,
  and `index::open` says which kinds the registry takes: shader
  submissions stay closed until Horizon's own format exists.
- `cargo horizon-mod check <file>`: those checks plus a trial run of a mod
  in the sandbox (ticks, frames, a chat line, each command), text or JSON.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 23:42:17 +02:00
Ludovic MarieandClaude Opus 5.5 303d497a67 Shader packs, clean room 9: frame uniforms, and gl_FragCoord in buffer passes
The values a pack's post-processing programs read once per frame, in
GL's conventions: camera position and the previous one, eye altitude,
up/sun/moon/shadow-light positions in view space (the sky's own frame for
the sun and moon), sun and shadow angles, world time, day and moon phase,
frame counter, frame time and run time, view size and aspect, near and
far, rain, smoothed wetness, thunder, fog colour, start and end, sky
colour, eye in fluid, blindness, night vision, darkness, brightness, GUI
hidden, eye brightness and its smoothed value, and the model-view and
projection matrices with their inverses and last frame's. The projection
is GL's perspective with the world's near and far planes times vanilla's
view bobbing. Each value is written as the type the pack declared.

The game passes what only it knows (day time, weather levels, the fluid
at the camera, light at the camera, blindness, F1, brightness) in the
renderer's Frame when HORIZON_SHADER_PACK is set.

Fix (slice 4): programs that draw to buffers read gl_FragCoord through a
variable only final set; every target sets it now, with a test. Found by
the first frame drawn with a pack.

Nothing reads the uniforms yet: players see no change.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 23:26:42 +02:00
Ludovic Marie 7334bebc1b Merge pull request #1578 from RVZO6/fix/minecraft-srv-resolution
🤖 Agent: merged after verify passed and the integration run on the desktop.
2026-10-24 23:23:21 +02:00
Ludovic MarieandClaude Opus 5.5 d604409be2 A client's own fall no longer hurts it: the server's hurt starts the tilt
As a client (multiplayer, a LAN guest, single player on its server
thread) Horizon ran its own fall damage, fire, freezing, berry bushes and
starvation through the local hurt, so the camera's hurt tilt started even
when the server cancelled the damage (lobbies that turn fall damage off).
Vanilla's client never hurts itself: Entity.hurt does nothing outside a
ServerLevel and FoodData.tick is ServerPlayer's. Its hurt animation comes
from the server: LocalPlayer.hurtTo on a health drop, and
ClientboundDamageEventPacket / ClientboundHurtAnimationPacket for itself.

- PlayerMotion.client_side: hurt_blocking and food_tick do nothing;
  landing sounds and fall-distance tracking still run.
- remote: SetHealth flashes on a drop after the first health, and the
  player's own damage events and hurt animations restart its hurt time.
- horizon-ctl state reports the player's hurt_time.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 23:17:14 +02:00
Ludovic MarieandClaude Opus 5.5 2716ad41be Renderer: Metal's flat draws and occlusion culling switch on by sections in view
Both switched on past 4,096 section draws, measured when a section had
2.2 to 2.5 draws. A section's draw count now depends on the sides it shows
the eye, so the bars count sections in view: 1,600, where 4,096 draws fell.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 23:16:05 +02:00
Ludovic Marie 791eaca45f Merge pull request #1629 from ludovic111/fix/parity-trim-own-dirs
🤖 Agent: merged after verify passed and the integration run on the desktop.
2026-10-24 23:15:30 +02:00
Ludovic MarieandClaude Opus 5.5 cb1c9e6935 Parity: a pass trims only its own .local directory
decorated_region compiles its oracle into chunk-generation-parity; when it
passed while chunk_generation was still running, the trim deleted
chunk_generation's dumps mid-run (FileNotFoundError on
*-987654321987-full.txt in CI on #1569, #1571 and #1584).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 23:09:48 +02:00
Ludovic MarieandClaude Opus 5.5 0849283a9d Pause: drop the state-packet gates, Game::tick doesn't run while paused
The frame loop skips Game::tick while the game is paused, so these never
fired; the server thread's paused pass is what moved the player on.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 23:04:59 +02:00
Ludovic Marie 40eb3e202a Merge branch 'cg8-1595-new' into cg8-1595 2026-10-24 23:04:58 +02:00
some-du6eandClaude Opus 5.5 be34f4cd5f Connect to a server's IPv4 address first, as vanilla does
Vanilla resolves a server with InetAddress.getByName, and Java's default
(java.net.preferIPv6Addresses=false) puts IPv4 addresses first. Horizon
tried addresses in the system resolver's order, which on macOS puts IPv6
first: a server whose IPv6 address resets the connection showed
"Connection reset by peer (os error 54)" where vanilla joined.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 15:04:45 -06:00
Ludovic MarieandClaude Opus 5.5 dbdc0389fe Launcher: Horizon modpacks from the registry, each its own profile
A registry modpack (.hpack) now shows under Modpacks with the Horizon
source, with its mods and what each may do, and installs into
horizon-packs/<id>/: the checked pack, pack.json and home/, where each of
its mods is downloaded and checked like any registry mod (pinned to the
pack's version and SHA-256) and turned on or off as the pack says. Its
overrides are copied on the first install only. A pack whose mod was taken
down installs nothing and leaves nothing behind. The Modpacks tab lists
installed registry packs with Play and Remove (confirmed); they play through
modpacks::apply_profile with their own HORIZON_HOME, taken-down mods turned
off first. CLI: `mods install --source horizon <pack>`, and `modpack
list|play|remove` see registry packs.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 22:57:54 +02:00
Ludovic MarieandClaude Opus 5.5 f602ad6fe3 Launcher Mods page: Horizon's registry beside Modrinth
The source switch now picks Modrinth or Horizon (CurseForge stays coming
soon). On Horizon each result says whether its files are signed and what
the mod may do, in the game's words, before Install; no Java check, no
download count, no Modrinth filters. Installed Horizon mods get an
on/off switch (horizon-mods.txt); one the registry took down is turned
off when the page reads the index and can't be turned back on.
Registry icons load from its GitHub Pages. HORIZON_LAUNCHER_SOURCE=horizon
opens the page on it, for screenshots.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 22:57:54 +02:00
Ludovic MarieandClaude Opus 5.5 b49fd602df Launcher: install from Horizon's registry, enable and disable, takedowns
Mods install into horizon-mods/<id>.hmod (turned on) and resource packs into
resourcepacks/, each downloaded to mod-browser/downloads/ and checked by
horizon_mods::index::check_file (size, SHA-256, signature, manifest) before it
is moved into place. Files the index takes down are turned off when the index
is read or the game starts, and can't be installed or turned back on. CLI (and
so MCP): --source horizon, mods enable|disable, and search shows whether a
project is signed and what a mod may do.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 22:57:54 +02:00
Ludovic MarieandClaude Opus 5.5 7e513d2d5c Launcher: Horizon's registry as a mod browser source
A browser source for Horizon's own registry (docs/api/mod-index.md): it reads index.json
(HORIZON_MOD_INDEX for a test registry, cached in the game folder for when the registry can't
be reached), searches it by kind and words, lists versions with the SHA-256 the index gives,
and blocks the versions it says were taken down. Downloads accept the registry's release host
and check SHA-256; file:// files only from a local index.

Installing, the takedown check before play and the CLI come next.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 22:57:54 +02:00
Ludovic MarieandClaude Opus 5.5 bffb68b0c5 Mod registry index: schema 1, .hpack and the checks before install
horizon_mods::index (feature index): index.json's types and rules (unique
ids, https URLs, plain file names, hex SHA-256, minisign keys and
signatures, a modpack's mods present in the index), open-ended kinds
(mod, modpack, resourcepack, shader; unknown kinds kept, not offered),
revocations, and check_file: size, hash, revocation, signature and the
file's own manifest against its entry. horizon_mods::modpack reads
.hpack files: horizon-modpack.toml and an allow-list of overrides.
docs/api/mod-index.md documents both, with a sample index the tests read.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 22:57:52 +02:00
Ludovic Marie 65d7c4b027 Merge pull request #1563 from ludovic111/pa1/remove-cull-leaves 2026-10-24 22:50:34 +02:00
Ludovic Marie 7becd7fdca Merge pull request #1559 from ludovic111/pa1/remove-mod-surveys 2026-10-24 22:48:55 +02:00
some-du6eandClaude Opus 5.5 4ad05aa94a Show armour trims on items from a server
Decode the trim component as its material and pattern registry ids when both
are references (a direct holder stays read past) and name them through the
server's synced trim_material and trim_pattern registries, as enchantments
are, so worn and held trimmed armour draws its trim in multiplayer.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 14:44:29 -06:00
some-du6eandClaude Opus 5.5 8c611e677b Advancement titles and descriptions show a whole text component
A server's custom advancements often name themselves with an empty text and
the words in its siblings ({"text":"","extra":[...]}); only the top-level
text or translate key was read, so their tooltips were blank. Read them as
the component they are (ComponentSerialization), with siblings, styles,
translation arguments and fallbacks.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 14:44:28 -06:00
Ludovic MarieandClaude Opus 5.5 c6980fb2b7 Drop the lang.rs test: layers_selected_language_over_english covers it
It reads the same files the same way (assets/pack.mcmeta's language
section, a language offered only with its file, Language::load over
en_us). Leaving the client out also keeps this PR to horizon-prepare, so
its check doesn't rerun the client's harnesses.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 22:43:26 +02:00
Ludovic MarieandClaude Opus 5.5 3f1a7b28af Placement prediction: one helper for predicted blocks, the sound where the block went
- predict_use and predict_place set their blocks through one
  predict_blocks (retainKnownServerState), instead of two copies of the
  loop.
- remote_can_place returns where BlockItem.place put the block, and the
  placer hears the place sound there: before, set_block only played it at
  the clicked face's neighbour, so a block that replaced grass or snow
  where the player clicked made no sound for its placer now that the
  server leaves the placer out.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 22:39:13 +02:00
Ludovic MarieandClaude Opus 5.5 6c6c08f4d9 Fluid contact on a client: only for the entities that tick
A frozen level (TickRateManager.isEntityFrozen) ticks only the players, so
their fluid contact is the only one that moves on, as in the loop below.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 22:39:13 +02:00
Alexander KrylovandHermes Agent 7d06aca17a Player: also stop sending the state packet while the game is paused
Measured on the built branch: it does not stop the drowning from #1592 on
its own. Kept because it is what forcing the level to stand still looks
like, and the negative result is in the PR.

Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
2026-10-24 23:29:55 +03:00
Ludovic MarieandClaude Opus 5.5 0e607404d1 Pause: the server thread stops ticking its players, not the window
The window's own tick doesn't run while paused (the frame loop skips
Game::tick), so the two gates added there never fired. What kept going was
the server thread: its paused pass still ran tick_guests, the players'
doTick, so air, hunger and drowning damage moved on behind the pause menu.
Vanilla's IntegratedServer.tickPaused only ticks the connections, and
ServerGamePacketListenerImpl.tick skips tickPlayer while the server is
paused. tick_lan now does the same: the paused pass keeps the connections
and skips tick_guests.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 22:28:34 +02:00
rdbtCVSandCodex gpt-6.1-sol 66070b918e Batch cached Perlin Y samples for portable SIMD
Evaluate four same-cell Y samples together while preserving coordinate
setup, gradient caching and each sample's floating-point operation order.
Safe arrays let LLVM vectorize the interpolation on the portable target;
cell boundaries and tails keep scalar evaluation.

Add bitwise differential coverage against the original volume loop.
Two paired chunk-generation batches reproduce a 4.6–4.7% Nether wall-time
reduction, with smaller or unresolved effects in the other dimensions.

Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-10-24 21:25:27 +01:00
Ludovic MarieandClaude Opus 5.5 6b2ec5f830 Shader packs, clean room 8: a dimension's post-processing programs
What the renderer needs to run a pack's post-processing, still without
wgpu: the programs of one dimension (prepare, deferred, composite and
final with their numbered variants, in the order they run), each
preprocessed and translated, with the buffers it writes and the flips
that follow it, and the buffer settings its stage files carry.

- A dimension's folder (world0, world-1, world1) replaces the base folder
  when it exists. program.<name>.enabled expressions over switch options,
  flip.<program>.<buffer> keys and the virtual deferred_pre and
  composite_pre. Programs with a geometry stage, or that fail to
  preprocess or translate, are reported and skipped.
- tests/packs/post-test: Horizon's own small test pack (tint, depth in
  colortex2, depth bands on half the screen, vignette).

Black box over the public test packs: every dimension of every pack
loads its post-processing programs (2 to 9) with no note.

Nothing uses it yet: players see no change.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 22:23:43 +02:00
Ludovic MarieandClaude Opus 5.5 d064dba945 Shader packs, clean room 7: buffer settings and output lists from stage files
The configuration packs write into their stage files, read from live code
only (the preprocessor has dropped false branches, so an option's choice
is the one in effect):

- the buffers a fragment shader writes: the last live DRAWBUFFERS or
  RENDERTARGETS comment, at most 8, buffers 0-7 without one;
- per colortex buffer (names and aliases): <buffer>Format in block
  comments, with every format name of the spec in any case, as channels,
  bits and kind; <buffer>Clear, ClearColor and MipmapEnabled.
  A later file changing a setting, an unknown format and a value of the
  wrong type are reported with their file and line. Buffer 1 becomes
  RGBA32F when a program declares gdepth and the pack set no format.

Nothing uses it yet: players see no change.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 22:23:43 +02:00
Simon Dräger 72f88befb8 Allow spaces in horizon-ctl screenshot paths 2026-10-24 13:21:23 -07:00
Simon Dräger 891e1c1283 Run client mod hooks on the server thread 2026-10-24 13:21:23 -07:00
Simon Dräger d4b2e24535 Add mod command capability and Predator Missile example 2026-10-24 13:21:23 -07:00
Simon Dräger d83b3e18bc Keep mob pitch on /tp 2026-10-24 13:21:23 -07:00
Ludovic MarieandClaude Opus 5.5 fb4897ec09 Website: /mods/submit, the registry's submission form
Sign in with GitHub through the registry's service (no scope), then send
the file and its page; the service checks the file, stages it and opens the
pull request, and the page links to it or says what to fix. A file named
<id>-<version>.<ext> fills in its kind, id and version. Shader packs are
listed and disabled until Horizon's own format exists. Caddy sends
/api/mods/* to REGISTRY_UPSTREAM (unset: the form says it isn't open), and
"Submit yours" now leads here.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 22:10:05 +02:00
Ludovic MarieandClaude Opus 5.5 37c36b548a Merge main into docs/server-api-reference
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 22:09:34 +02:00
Ludovic MarieandClaude Opus 5.5 6e8a96a734 Boats push the player, and startRiding's quiet failures
- AbstractBoat.push(player) moves the player too: the boat's own tick and
  the player's pushEntities run on the player's client in vanilla
  (pushableBy lets a client push only its local player), so the window's
  player is pushed in-frame and a server's client pushes itself in
  remote_pushes, as it does for mobs.
- A mob the boat would take that can't get in (boarding cooldown, boat
  under water) is left alone, not pushed: the tick calls startRiding,
  which fails quietly, instead of push.
- Mob.startRiding drops the mob's lead.
- look_at_entity reuses the guest's eye position.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 22:03:53 +02:00
Alexander KrylovandHermes Agent 508cb2df9f Fish: the window tracks the fluid contact of the mobs it only displays
`m.fluids` is filled by `Mob::update_fluids`, which only runs in the mob tick
on the server. A window that is a client of a server never runs that tick, so
every mob it draws kept the default `Fluids { in_water: false }` for its whole
life. Cod, salmon and tropical fish rotate onto their side while `!in_water`
(`mobs/aquatic.rs`), so they were drawn lying on their flank in water, while
their position and heading, which come from the server, looked correct (#1591).

`tick_remote_entities` now runs vanilla's `EntityFluidInteraction.update` for
the displayed mobs before it ticks their animations, so the renderers read the
water contact the same way they do for a mob the window simulates itself.

Fixes #1591.

Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
2026-10-24 23:01:31 +03:00
Alexander KrylovandHermes Agent 4c3abd86de Player: the window stops ticking its player while the game is paused
Air kept draining behind the Escape menu in singleplayer and the drowning
damage landed on closing it. The server pauses (`IntegratedServer::set_paused`
reads the flag), but the window's own copy of the player kept running
`controlled_simulation_tick` every frame, and that is where
`tick_environment_damage` decrements the air and pushes `MotionEvent::Drown`.

`Minecraft.tick` skips the level while the game is paused, so both local paths
now skip the player's simulation: the client of a server (`tick_remote_level`)
and in-frame single player. Keys are already neutral when a screen is open, so
this only stops the environment, hunger and fall prediction from running ahead
of the paused server.

Fixes #1592.

Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
2026-10-24 22:53:12 +03:00
socksproxandDevin a5f5d8df34 Server packs: the download progress toast
#1570 landed the client pack manager without vanilla's
PackDownloadNotificationComponent: the SystemToast that follows a
download with download.pack.progress.percent or .bytes and stays as
download.pack.failed when a batch fails. The download queue reports
requestStart/downloadedBytes/downloadFinished on a channel (vanilla's
DownloadProgressListener), the game ticks them into a toast it slides
in for the vanilla 5 seconds with the toast in/out sounds.

Co-Authored-By: Devin <[email protected]>
2026-10-24 21:49:01 +02:00
Alexander KrylovandHermes Agent adcfe810f2 rustfmt
Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
2026-10-24 22:46:35 +03:00
Alexander KrylovandHermes Agent 3802a97dc9 Language: the prepared resources carry every bundled language and the list
The language screen offered English alone. The client jar holds only
`en_us.json` and `deprecated.json` under `assets/minecraft/lang`, and the list
the screen shows comes from `pack.mcmeta`'s `language` section, which the jar
does not carry at all. Both live in the asset index, so `horizon-prepare
languages` copies `pack.mcmeta` and every `minecraft/lang/*.json` from the
asset store the way the sounds, the panorama and Unifont already are, and full
preparation runs it with the rest.

Every bundle the client declares is then offered, `Русский` among them, and
`lang` in options.txt selects one (#1272).

Fixes #1272.

Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
2026-10-24 22:42:39 +03:00
levzzzandClaude Opus 5.5 90b5e162ae Play a placed block's sound once for its placer
BlockItem.place plays it with playSound(player, ...): the server sends it to
the others, and the placer's client plays it as its block appears. The
server sent it to the placer too, so each block sounded twice.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 00:38:39 +05:00
Ludovic MarieandClaude Opus 5.5 b61b25f80f Shader packs, clean room 6: composite-style programs to naga
A program's preprocessed GLSL, any version from 110 to 460, compatibility
or core, rewritten into the Vulkan-flavoured GLSL 450 that naga's frontend
reads, then parsed and validated into naga modules with the program's
interface: its uniform block (offsets from naga's layout), its samplers
and their bindings, its output locations. The rewrite works on
preprocessing tokens and keeps the pack's lines, so every error points at
the pack's own file and line through the preprocessor's line map.

- Loose uniforms go into one block shared by both stages; bool is stored
  as int. Samplers are split into texture and sampler objects, combined
  again where used, and passed as two arguments to the pack's functions.
- The compatibility built-ins become declarations of ours (prefix hz_):
  gl_Vertex, gl_MultiTexCoordN, the matrices and ftransform(), gl_FragData,
  gl_FragColor, gl_TexCoord, gl_FrontColor, gl_Fog (from vanilla's fog),
  texture2D and the other old texture functions, shadow2D; varyings get
  locations by name across the stages; words that became keywords after
  GLSL 1.x are renamed in old code.
- naga gaps covered: modf, conditional initializers of globals (run by our
  main), and unsized arrays sized from their constructor.
- The pack's main is called by one of ours that draws the quad from (0,0)
  to (1,1) and adapts GL's conventions: clip y negated for every target but
  the window, so memory rows are GL's rows and gl_FragCoord needs no change;
  clip z from GL's [-w, w] to [0, w], so stored depth is GL's window depth.
  In final, gl_FragCoord.y is counted from the bottom of the window.

Black box over the public test packs: all 105 composite-style programs
(composite, deferred, final, prepare, begin, shadowcomp of the base and
dimension folders) translate and validate.

Nothing uses it yet: players see no change.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 21:35:37 +02:00
Ludovic MarieandClaude Opus 5.5 ff91907057 SRV redirects: ask the name servers directly instead of hickory + tokio
Vanilla's ServerRedirectHandler asks through JNDI's DNS client: the
system's name servers in turn, one 1 s UDP try each (retries = 1), TCP when
the reply is truncated, the first SRV record of the answer. resolver/dns.rs
does that by hand in ~200 lines with wire-format tests and a local stub
server, so the game, the dedicated server and the bots no longer pull in
tokio, hickory and ~25 other crates (none of them were in the game before).

Name servers come from resolv.conf on Unix, as JNDI reads them, and from
the network adapters on Windows (ipconfig, Windows only; the crate keeps
forbid(unsafe_code)). Each server waits 1 s, as vanilla does, instead of
the whole connect timeout.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 21:30:32 +02:00
socksproxandDevin d742d725f4 Client: the boss-overlay — BossEvent bars, wither darkening and world fog
The clientbound boss_event packet (add/update/remove/style/progress), a
BossOverlay that renders each bar, darkens the world under a wither, and
suppresses the create-the-world fog — vanilla's BossHealthOverlay and
GameRenderer behaviour on a real server.

Co-Authored-By: Devin <[email protected]>
2026-10-24 21:29:08 +02:00
Ludovic MarieandClaude Opus 5.5 787c3a2f19 Website: /mods, Horizon's registry of mods, modpacks and resource packs
The list (search, a filter per kind) and a page per project: what a mod
may do in the game's own words, Signed or Unsigned, how to install it
from the launcher, the CLI or by hand, its file, SHA-256 and versions,
and links to its source, its registry entry and the report form. Baked
from the published index at build time (HORIZON_MOD_INDEX for a test
index) and refreshed by the page; Caddy serves /mods/<id> from its baked
page, else the list page, which shows a project published since.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 21:20:34 +02:00
Ludovic Marie fe128948ba Merge pull request #1575 from RobertTGreat/fix/launcher-startup-windows
🤖 Agent: merged after verify passed and the integration run on the desktop.
2026-10-24 21:16:44 +02:00
Ludovic Marie 33c2512cbd Merge pull request #1576 from RobertTGreat/fix/launcher-mod-lists
🤖 Agent: merged after verify passed and the integration run on the desktop.
2026-10-24 21:14:20 +02:00
Ludovic Marie 2e35028930 Merge pull request #1574 from RobertTGreat/fix/launcher-window-layout
🤖 Agent: merged after verify passed and the integration run on the desktop.
2026-10-24 21:13:52 +02:00
Ludovic Marie d0b7c20371 Merge pull request #1577 from RVZO6/fix/server-text-debug-shortcuts
🤖 Agent: merged after verify passed and the integration run on the desktop.
2026-10-24 21:13:24 +02:00
Ludovic MarieandClaude Opus 5.5 7c1dd49810 Renderer: leave out solid quads that face away from the eye
Meshing sorts each section's and distant tile's solid quads into seven
buckets: one per axis direction (all four corners in one axis plane, both
triangles wound the same way) and one for everything else, and keeps each
axis bucket's extreme plane. A quad facing +X at x = p is a back face from
an eye at x <= p, which the solid pipeline culls anyway, so the frame draws
only the buckets that can face the eye, merged into runs of adjacent
buckets: one to three indirect draws per section on the existing page-run,
occlusion-culling and flat paths. The eye comes from the frame's
view-projection, so view bobbing is exact. The picture does not change;
`horizon-ctl section-sides on|off` switches it for A/B checks.

Written from scratch from the general back-face technique; no other
renderer's code or Horizon's removed code read.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 21:13:00 +02:00
Ludovic MarieandClaude Opus 5.5 f239955997 Shader packs, clean room 5: the GLSL preprocessor and #include
A stage file goes through comment removal, #include, conditionals and
macro expansion, and every output line remembers the pack file and line it
came from, so later errors point at the pack's own code.

- #include "x" from the including file's folder, "/x" from the shaders
  folder, expanded in live code only (an include inside a false #if is not
  read), ten levels at most, paths that leave the pack refused; a file is
  read once per run however often it is included.
- #define, #undef, line continuations, __LINE__ and __VERSION__; #version
  and #extension kept apart for the translation's own header; the block
  comments of live code kept for the directives packs write in them
  (DRAWBUFFERS, buffer formats); #error and unknown directives diagnosed.

Black box over the public test packs: every stage file of the base and
dimension folders (126 to 219 per pack) preprocesses with no diagnostic.

Nothing uses it yet: players see no change.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 21:09:29 +02:00
Ludovic MarieandClaude Opus 5.5 4c65ed2f5c Shader packs, clean room 4: function-like macros and the standard macros
The macro engine the GLSL preprocessor needs, shared with the .properties
reader: function-like macros take their arguments (expanded first, raw
beside ##, which pastes) when a ( follows their name; a macro is still not
expanded inside its own expansion. Text is cut into preprocessing tokens,
so numbers such as 1e5 or 0x1F are never mistaken for names.

The #if/#elif/#else/#endif state moves out of the .properties reader so
the preprocessor uses the same code.

Standard macros: MC_GL_VERSION and MC_GLSL_VERSION 460, the 24
MC_RENDER_STAGE_* as ascending integers in draw order, render and shadow
quality 1.0, MC_HAND_DEPTH 0.125, MAX_COLOR_BUFFERS 16.

Nothing uses it yet: players see no change.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 21:09:07 +02:00
Ludovic MarieandClaude Opus 5.5 afcb4062cb Merge main into pa1/remove-mod-surveys (keep the surveys deleted)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 21:07:46 +02:00
levzzzandClaude Opus 5.5 352645b417 Predict block placement on the client, so fast bridging stops losing clicks
MultiPlayerGameMode.useItemOn places the block on the client's level at
once, kept under the use's sequence until BlockChangedAck. Without it the
next fast click aimed past the block still on its way from the server, the
server refused it, and the arm swung for nothing.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 00:07:31 +05:00
Ludovic MarieandClaude Opus 5.5 aee0b8b589 Format the endpoint table
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 21:07:26 +02:00
Ludovic Marie 7f995af677 Merge pull request #1570 from ludovic111/feat/server-packs-client 2026-10-24 21:02:56 +02:00
Ludovic Marie 61e044e67c Merge pull request #1569 from ludovic111/feat/server-packs-manager 2026-10-24 21:01:05 +02:00
Ludovic Marie 18b63d7acb Merge pull request #1581 from ludovic111/docs/mod-api-reference 2026-10-24 20:58:01 +02:00
Ludovic Marie e683cdf322 Merge pull request #1579 from ludovic111/fix/builtin-pack-icons 2026-10-24 20:56:28 +02:00
Ludovic MarieandClaude Opus 5.5 e5bfce3b37 Horizon APIs: ways in for mods, server mods as api 0.3, who promotes to stable
docs/api/mods.md gets "Ways in": one API (the WIT world) and one sandbox
(WebAssembly) for every language. Rust is first-class; Lua scripts are
planned, run by an interpreter that is itself a WebAssembly mod linked with
the script's capabilities only, with hot reload of the script; C, C++, Zig,
TinyGo, AssemblyScript and JavaScript/TypeScript are to be evaluated, each
listed with its cost measured against Rust.

Server mods are their own line (api 0.3); block entities are no longer
tied to a draft. Promotion to stable: the orchestrator proposes with the
evidence, Ludovic approves.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 20:53:43 +02:00
Ludovic MarieandClaude Opus 5.5 0a1d3a7534 Horizon APIs: the docs portal and the stability and versioning policy
docs/api/ gets one page per surface other programs build on (mods,
shaders, agent control, servers, the launcher, data formats), each with an
overview measured from the source, the guide, the generated reference, examples,
its stability, what is not there yet and a changelog, plus the policy every
API follows (docs/api/stability.md): a version per API, semver lines,
experimental / preview / stable, deprecation windows, compatibility tests and
the level of every part today.

The website gets a "Build on Horizon" page (website/docs/apis.md) that
summarises them and links the repository's pages.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 20:50:20 +02:00
levzzzandClaude Opus 5.5 66235cc981 Let mobs ride boats, and make boats collide with and push entities
A moving boat now stops at other boats, minecarts, mobs and players
(AbstractBoat.canVehicleCollide), and on the server it pushes what it
touches and is pushed back (AbstractBoat.push, LivingEntity.pushEntities).
A boat no player steers picks up the mobs that touch it, two seats in a
boat and one in a chest boat, with the player always in front
(getPassengerAttachmentPoint, clampRotation). Mobs get out when the boat
breaks or stays under water, are saved under the boat's Passengers, and
are seated by the client, which is sent no passenger's position.

A boat can't be placed where it would overlap an entity (BoatItem.use's
noCollision), and the player sits at the raft's and chest boat's vanilla
seat. A server player breaking a boat with a mob in it hits the boat.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 23:49:37 +05:00
Ludovic MarieandClaude Opus 5.5 f80d00802e Server reference generated from horizon-server's source
docs/api/server-reference.md lists horizon-server's commands (its help
text), every server.toml key (config::TEMPLATE: default, the
server.properties key it writes, what it does) and the control API's
endpoints. Tests fail when the page is out of date (HORIZON_WRITE_DOCS=1
rewrites it), when the template and Config disagree on the keys, when a key
has no comment, or when api::ROUTES and the endpoints route() answers
differ.

api::ROUTES replaces the endpoint list in api.rs's module comment and the
hand-kept path list of the 405 arm. The template's uncommented keys get a
comment (what init writes for new servers; no setting changes).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 20:42:51 +02:00
Ludovic MarieandClaude Opus 5.5 397d529db7 Shader packs, clean room 3: .properties files with conditional directives
shaders.properties and the id files (block, item, entity, dimension):
key=value lines in file order with their line numbers, # comments, a value
continued on the next line by a trailing backslash, and the conditional
directives evaluated with the standard and option macros. A # line is a
directive only when its first word is one, so a commented-out key such as
#ifdefault=1 stays a comment. #define and #undef apply to the lines after
them in every one of these files: a public pack defines a macro inside
#ifdef MC_OS_MAC in its shaders.properties and tests it further down. A
later duplicate key replaces the earlier one; broken conditionals, lines
that are not key=value and duplicates are kept as notes for the pack's log.

Nothing uses it yet: players see no change.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 20:42:33 +02:00
Ludovic MarieandClaude Opus 5.5 eb0a6f74e6 Shader packs, clean room 2: macros and #if conditions
The integer arithmetic of #if/#elif that a pack's .properties files and,
later, its GLSL preprocessor share: defined X and defined(X), decimal,
hexadecimal and octal literals, the C operators with C precedence, macros
expanded to their values (a macro is not expanded inside its own
expansion), and an identifier that is not a macro read as 0. A bad
condition says why. The standard macros that need no renderer:
MC_VERSION, from the pinned version by the documented rule (26.3 is
260300), and the system's MC_OS_*.

Nothing uses it yet: players see no change.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 20:42:05 +02:00
Ludovic MarieandClaude Opus 5.5 07e46daa59 Shader packs, clean room 1: read packs and their lang files
A new crate, horizon-shaders, the first piece of shader pack support written
again from a specification of the format. It opens a pack (a folder or a zip
whose root holds a shaders folder; decoy folders and files beside it are
ignored), lists the packs in a shaderpacks folder with the reason a broken
one can't be used, and reads a pack's lang files: English under the player's
locale, file names matched ignoring case (packs ship en_US.lang).

Nothing uses it yet: players see no change.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 20:41:16 +02:00
Ludovic MarieandClaude Opus 5.5 f1ff81e874 launcher: spacers carry only their height; the version verdict leaves the check thread plainly
- `Span::Space` kept the range of rows it hid, written and never read: it is just a height.
- The check thread sent the project page's verdict from inside a `map` closure; it is now an
  explicit step before the result goes to the results list.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 20:38:34 +02:00
Ludovic MarieandClaude Opus 5.5 1b23a14ee7 Mod API reference generated from the WIT contract
docs/api/mods-reference.md lists every capability, world, interface, type,
function and hook of horizon:mods, generated from
crates/horizon-mod-sdk/wit/horizon.wit and the manifest's capabilities by a
test in horizon-mods (feature pack, as CI runs it). The test fails when the
page is out of date (HORIZON_WRITE_DOCS=1 rewrites it), when a function has
no doc comment, or when a capability grants an interface the contract lacks.

The WIT's undocumented functions and fields get doc comments (no change to
the contract itself).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 20:38:18 +02:00
Ludovic Marie 16d49fe660 Merge pull request #1558 from rdbtCVS/t3/retire-java-optimization-mod
🤖 Agent: merged after verify passed and the integration run on the desktop.
2026-10-24 20:33:21 +02:00
Ludovic Marie fccac1e744 Merge pull request #1560 from rdbtCVS/t3/remove-voxel-kernels
🤖 Agent: merged after verify passed and the integration run on the desktop.
2026-10-24 20:31:50 +02:00
Ludovic MarieandClaude Opus 5.5 42714deca6 Merge origin/main into fix/server-text-debug-shortcuts
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 20:27:43 +02:00
Ludovic MarieandClaude Opus 5.5 1a4a2ec120 Debug keys: check the title's focused field inline
The helper's release check repeated the `pressed &&` guard below it, and
its test only covered the helper. The comment now says why text screens
take presses first: vanilla types through charTyped, which debug keys
cannot swallow, while here the character comes with the key press.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 20:27:37 +02:00
Ludovic MarieandClaude Opus 5.5 ac43cdea70 launcher, server: one hidden-command helper per crate, server children keep its terminal
- horizon-bench reads the machine with horizon-telemetry's `command_output` (now public) instead
  of a second copy of it; the console flag lives in one place.
- horizon-server's `process::command` hides the console only when the server has none (started
  by the launcher). Run from a terminal, `schtasks`, `tar` and `mklink` share it as before, so
  their output still reaches it: a CREATE_NO_WINDOW child can't write to its parent's console.
- horizon-prepare's java/javac keep the stdin they had; only the console flag is new.
- launcher `process::hide_console` returns the command (no `let _ =` on other systems); the
  machine-details thread failing just leaves the report without them, as a slow one does.
- README: the Windows subsystem's effect on the command line (interactive cmd/PowerShell don't
  wait or set the exit code) under "Command line", the deferred probe under "Performance log".

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 20:26:50 +02:00
Ludovic MarieandClaude Opus 5.5 6d81b98f18 launcher: keep one landscape per width step, free replaced ones, keep the top bar's wordmark off Windows
- A resize re-baked Home's landscape every 6 px and never removed the old images from the
  window's sprite atlas (GPUI keeps an image there until `drop_image`): dragging a window wider
  piled up textures of up to ~14 MB each. The landscape now grows in steps of 64 texels, is kept
  when the window narrows, and the one it replaces leaves the atlas on the next frame.
- The landscape was laid out from the width (sun at 0.68 w, one random stream for every part), so
  it reshuffled hills, trees and the sun at each step of a resize. Each part now has its own
  stream drawn left to right: a wider landscape only adds columns.
- macOS and Linux keep their system title bar, so the top bar keeps the mark and "HORIZON"
  there (docs/design-system.md, Navigation); only the Windows title bar carries them.
- The before/after screenshots were for the pull request, not the launcher's docs.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 20:22:10 +02:00
some-du6eandClaude Opus 5.5 9223b401e7 Decode every item data component a server sends
Horizon decoded 25 of 26.3's 122 data component types. Item data has no
length prefix, so a stack with any other component (an armour trim,
banner patterns, pot sherds, a stew's effects, a plugin's item) made the
whole packet undecodable and it was dropped: on a Paper server the
inventory arrived empty until each slot was clicked, the advancements
screen said "There doesn't seem to be anything here..." and the recipe
book was empty.

Every other component is now read to its end with its vanilla
StreamCodec (item/skip.rs) and kept as its bytes, written back as they
came; components without a network codec go as their NBT. The client
leaves them out of the item's components, so the item keeps its
prototype's values for them.

tools/test_item_wire_parity.py checks it against vanilla: the oracle
encodes 374 stacks with ItemStack.OPTIONAL_STREAM_CODEC (every default
component value of every item, SNBT fixtures for the rest, direct
holders and removals built in Java), covering all 122 component types,
and the Rust probe must consume every byte, find the same component ids
and encode the read-past ones back to the same bytes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 12:20:45 -06:00
Ludovic MarieandClaude Opus 5.5 1dffadeea3 Pack screen: draw each pack's own pack.png instead of unknown_pack
PackSelectionScreen.loadPackIcon registers every pack's pack.png as a
texture; Horizon never loaded them, so every pack (Default, Programmer Art,
High Contrast and the player's own) fell back to unknown_pack, the
cobblestone. The icons now load from each pack when the screen opens and are
written into 64 new 128x128 HUD atlas slots, one per icon on screen. The
vanilla pack's icon is the client JAR's pack.png, which horizon-prepare now
copies into the prepared resources.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 20:19:05 +02:00
Ludovic Marie 29c5e65d17 Merge pull request #1573 from Domanffe/fix/placed-block-entity-keeps-state
🤖 Agent: merged after verify passed and the integration run on the desktop.
2026-10-24 20:12:22 +02:00
Ryan BandOpenAI Codex gpt-6.1-sol 83657f3bbf fix: resolve Minecraft DNS SRV redirects for ping and login
Co-Authored-By: OpenAI Codex gpt-6.1-sol <[email protected]>
2026-10-24 14:04:46 -04:00
Ryan BandOpenAI Codex gpt-6.1-sol 094d5c8e16 fix: let server text fields receive debug-bound characters
Co-Authored-By: OpenAI Codex gpt-6.1-sol <[email protected]>
2026-10-24 14:03:32 -04:00
RobertTGreatandOpenAI Codex 98639d1471 fix(launcher): virtualize long mod lists from the first frame
Render nearby rows in the existing page scrollbar, defer hidden icons and cached verdict reads, and preserve bottom anchoring as row heights are measured.

Co-Authored-By: OpenAI Codex (GPT-6.1 Sol) <[email protected]>
2026-10-24 18:49:37 +01:00
Ludovic MarieandClaude Opus 5.5 0c6816811c Commands: fold the block check into place_state's condition; cover /fill in the corpus
The comment names vanilla's rule (LevelChunk.setBlockState keeps or creates
the block entity when the block, not the state, is still there after
onPlace). The corpus checks the `enabled=false` flip, drops the cleanup lines
the group's closing /fill already does, and adds a powered hopper placed by
/fill, which goes through the same place_state.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 19:49:37 +02:00
RobertTGreatandOpenAI Codex 1be8a4caea fix(launcher): defer hardware probes and hide Windows consoles
Collect performance-report hardware after the first frame and preserve logs, input and CLI pipes while suppressing background terminal windows.

Co-Authored-By: OpenAI Codex (GPT-6.1 Sol) <[email protected]>
2026-10-24 18:34:08 +01:00
RobertTGreatandOpenAI Codex 4ebba25280 fix(launcher): fill window width and add Windows title bar
Keep page content and pixel art responsive, align navigation left, and use native Windows control hit regions in themed chrome.

Co-Authored-By: OpenAI Codex (GPT-6.1 Sol) <[email protected]>
2026-10-24 18:30:36 +01:00
Ludovic Marie 7fca3c2f97 Merge pull request #1572 from ludovic111/fix/mac-dock-icon 2026-10-24 19:11:53 +02:00
Ludovic Marie db758a622f Merge remote-tracking branch 'origin/main' into pa1/remove-face-groups
# Conflicts:
#	crates/horizon-client/src/gfx.rs
2026-10-24 18:54:45 +02:00
Ludovic Marie 2ad81884f9 Merge remote-tracking branch 'origin/main' into pa1/remove-cull-leaves
# Conflicts:
#	crates/horizon-client/src/graphics.rs
2026-10-24 18:54:18 +02:00
Ludovic Marie 84bdd1d0b0 Merge remote-tracking branch 'origin/main' into pa1/remove-mod-surveys
# Conflicts:
#	docs/research/java-mod-compat.md
2026-10-24 18:53:51 +02:00
Alexander KrylovandHermes Agent 493e5f8d3a Commands: a placed block keeps its block entity when a neighbour update changes its state
/setblock created the block entity only when the state that landed matched the
requested state exactly. HopperBlock.onPlace sets enabled=false while the block is
being placed, so a hopper put into power had no inventory: /data get block answered
"not a block entity", items dropped into it stayed item entities, and
/setblock hopper{Items:[...]} dropped the NBT as well.

Compare the block at the position with the block that was placed, the way vanilla
does when it creates the entity from the placed state, before the neighbour updates
run.

Fixes #1506.

Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
2026-10-24 19:50:45 +03:00
Ludovic Marie ccb827282b Merge remote-tracking branch 'origin/main' into pa1/gate-1558
# Conflicts:
#	docs/vulkan.md
#	mods/horizon/src/main/java/dev/horizonlab/client/HorizonClient.java
#	mods/horizon/src/test/java/dev/horizonlab/client/DistantTerrainSettingsTest.java
#	mods/horizon/src/test/java/dev/horizonlab/metrics/ClientGraphicsIntegration.java
#	packs/START-HERE.txt
#	packs/mods.lock.json
#	tests/test_fixtures.py
#	tests/test_packs.py
#	tools/Compare-Benchmarks.html
#	tools/build_pack.py
#	tools/compare_benchmarks.py
#	tools/test_client_graphics.py
2026-10-24 18:47:57 +02:00
Ludovic Marie 56aa5f9549 Merge remote-tracking branch 'origin/main' into pa1/gate-1560
# Conflicts:
#	Cargo.toml
2026-10-24 18:47:16 +02:00
Ludovic MarieandClaude Sonnet 5.5 d271176f23 Tools and harnesses start the game in scratch folders, not a player's
The game logs into HORIZON_HOME and falls back to the player's own game folder when it is unset. Harnesses
that set only HORIZON_NO_AUDIO (and test_native_client, which set nothing) wrote logs, and on a world run
advancements, into ~/Library/Application Support/horizon.

lab.game_env gives every game a fresh HORIZON_HOME, HORIZON_LAUNCHER_HOME, XDG_DATA_HOME and XDG_CONFIG_HOME,
no audio and no launcher self-update; a folder the caller passes is kept. Every launcher of the game uses it,
parity.py gives each harness its own HORIZON_HOME, and tests/test_game_home.py fails for a script in tools/
that starts the game without it.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-24 18:34:32 +02:00
Ludovic Marie d65282fb0f Merge pull request #1566 from ludovic111/fix/baby-villager-type-texture 2026-10-24 18:34:08 +02:00
Ludovic MarieandClaude Opus 5.5 6a698c40f0 Launcher: start the game from a Horizon.app on macOS, so the Dock and Cmd+Tab show Horizon
Started as a bare executable, the game showed in the Dock and Cmd+Tab as
"horizon-client" with the generic executable icon: winit's window icon does
nothing on macOS. The launcher now keeps a small Horizon.app in each version
folder (Info.plist, the launcher's icon, hard links to the version's
horizon-client and horizon-ctl) and starts the game from it. Server setup
leaves the bundle out of its copy.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 18:34:04 +02:00
Ludovic Marie 71292d2ed7 Merge pull request #1562 from ludovic111/feat/server-pack-status 2026-10-24 18:12:16 +02:00
Ludovic Marie 46f4237b67 Merge pull request #1565 from ludovic111/fix/remote-villager-data 2026-10-24 18:03:07 +02:00
Ludovic Marie 643f331f8c Merge pull request #1561 from ludovic111/fix/remove-shader-packs 2026-10-24 17:42:17 +02:00
Ludovic MarieandClaude Opus 5.5 186ab70305 Server resource packs: prompt, Server Resource Packs setting, apply and remove
Servers that push a resource pack now work (reported by scanfee on Discord:
"Servers requiring custom resource packs do not work yet"; net.rs declined
every push).

- net.rs forwards resource_pack_push and _pop in configuration and play, and
  answers in the phase the connection is in.
- The prompt is vanilla's PackConfirmScreen on a shared ConfirmScreen
  (confirm_screen.rs, now also used by the game rules screen): the required
  or optional wording, the server's message, Proceed/Disconnect or Yes/No,
  Escape for no. Declining a required pack disconnects with vanilla's reason.
- The server list's Edit screen has the Server Resource Packs cycle button
  (Enabled, Disabled, Prompt), saved as acceptTextures; the prompt's answer
  is saved there too.
- Downloaded packs go on top of the player's packs, in push order, through
  the same reload as the Resource Packs screen; they are removed on pop and
  when leaving the server.

tools/server_pack_e2e.py drives the real client against vanilla's dedicated
server or Horizon's, with a test pack (blue dirt, lang strings) from a local
HTTP server, and checks the statuses the client sends through net_capture:
decline, proceed, the cache, a wrong SHA-1, an optional pack declined and
accepted, a dead URL.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 17:20:35 +02:00
Ludovic MarieandClaude Opus 5.5 cdfcc3d4d4 Server resource packs: the client's pack manager and downloads
Vanilla's ServerPackManager, DownloadedPackSource and DownloadQueue, without
the game around them yet: the prompt state, each pack's download and
activation, the statuses in vanilla's order (onSuccess repeats
SUCCESSFULLY_LOADED, a failed reload recovers with the packs that loaded),
pop, and leaving.

Downloads follow HttpUtil.downloadFile: downloads/<pack id>/<sha1>, a cached
file whose hash matches is reused, a wrong hash fails, no hash downloads to a
temporary file renamed to its SHA-1, 250 MiB at most, vanilla's X-Minecraft-*
headers, a log.json entry per download, and 20 files kept.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 17:15:35 +02:00
Ludovic MarieandClaude Opus 5.5 7a1638a190 Renderer: remove the section face groups modelled on another renderer
#503 grouped each section's solid quads by the axis side they face and
skipped the groups facing away from the eye. Its code and PR named an
internal class of another project as the model. Horizon doesn't build on
another project's code, so the grouping goes and every section draws its
solid quads in one draw, as vanilla does; the GPU's back-face culling
discards the back faces, so the picture is unchanged.

This costs frame time, measured on the Mac mini in the PR. A clean-room
version can come back later, designed from vanilla and from first
principles only.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 16:24:46 +02:00
Ludovic MarieandClaude Opus 5.5 ab8b379097 Dedicated server: offer server.properties' resource pack as vanilla does
ServerResourcePackConfigurationTask: after the tags, the configuration phase
sends the pack (id from the URL when none is given, hash, required, prompt)
and waits for a final answer, with keepalives every 15 s. A declined required
pack disconnects with multiplayer.requiredTexturePrompt.disconnect, in
configuration or in play, with vanilla's log lines.

tools/test_server_resource_pack_parity.py drives a vanilla 26.3 server and
Horizon's with the same five players (declines, accepts then declines in
play, fails the download, answers after 20 s, declines an optional pack) and
compares what each one receives.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 16:21:53 +02:00
Ludovic MarieandClaude Opus 5.5 a3b4ed57b2 runtime.py: two blank lines between functions
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 16:21:21 +02:00
Ludovic MarieandClaude Opus 5.5 4e7c25bb55 Remove shader packs
A developer of Iris showed that Horizon's shader pack code was written from
Iris's source. This removes it: the horizon-shaderpack crate, Java pack
loading and its render stages, Horizon's own WGSL packs and the example
packs, the Shader Packs screen and its option, the launcher's shader pack
browsing and installing, and the docs about them. Shader packs may come
back later as a clean-room rewrite.

The vanilla renderer and its post-processing (resolution scale, FXAA) are
unchanged. Options files that name a pack load and drop the key; the
launcher reads manifests that list a shader pack, and files in
`shaderpacks/` are never deleted.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 16:15:39 +02:00
Ludovic MarieandClaude Opus 5.5 ba9b876e3e website: the mods page no longer links the removed Java mod notes
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 16:12:39 +02:00
Ludovic MarieandClaude Opus 5.5 e1052e1bd3 Baby villagers wear their biome's clothes again
VillagerProfessionLayer takes a baby's type texture from
`entity/villager/baby/<type>` (and `entity/zombie_villager/baby/<type>`),
not `type/baby/<type>`: that path names no texture, so baby villagers and
baby zombie villagers of every biome drew without their type layer.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 16:06:50 +02:00
Ludovic MarieandClaude Opus 5.5 afb638b81c lan_e2e: a guest sees villagers' types, professions, levels and new jobs
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 16:06:50 +02:00
Ludovic MarieandClaude Opus 5.5 f1369b4efe Clients show villagers' professions, types and levels
A client of a server (single player on its server thread, a LAN guest, a
dedicated server's player) never read Villager.DATA_VILLAGER_DATA or
ZombieVillager's from SetEntityData, so every villager drew as a plains
villager with no profession and no level badge, and a villager that took a
job never changed. The client now reads the type and the profession by
their built-in registry ids, and the level, which VillagerRenderer reads
from the synched data in vanilla.

horizon-ctl entities reports a villager's type, profession and level as the
game knows them, so a client's view can be checked.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 16:06:50 +02:00
rdbtCVSandOpenAI Codex gpt-6.1-sol d49b0eb98b Name the profiling documentation for its current use
Co-Authored-By: OpenAI Codex gpt-6.1-sol <[email protected]>
2026-10-24 14:55:53 +01:00
rdbtCVSandOpenAI Codex gpt-6.1-sol 68bc9f82b6 Remove documentation of the retired Java product
Co-Authored-By: OpenAI Codex gpt-6.1-sol <[email protected]>
2026-10-24 14:55:32 +01:00
Ludovic MarieandClaude Opus 5.5 717acb07af Graphics: remove Cull Leaves, which was adapted from the CullLeaves mod
The opt-in Cull Leaves setting (#420, from #387) was adapted from the
CullLeaves mod's code (MIT, credited in THIRD_PARTY_NOTICES.md). Horizon
doesn't build on another mod's code, so it goes, with its notice.

Leaves now follow vanilla only: with opaque leaves (cutoutLeaves off) the
faces between two leaf blocks are skipped (LeavesBlock.skipRendering),
as before #420 and as the setting's Off did. Mangrove roots keep every
face. An old options file's cullLeaves line is ignored.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 15:55:06 +02:00
Ludovic Marie e423e570c9 Merge pull request #1557 from ludovic111/fix/launcher-cmd-q
🤖 Agent: merged after verify passed and the integration run on the desktop.
2026-10-24 15:52:15 +02:00
rdbtCVSandOpenAI Codex gpt-6.1-sol 06ec15f3a0 Retire the legacy Java optimization mod and performance packs
Preserve Java parity oracles and comparison benchmark instrumentation. Move their remaining dependency locks under tools/benchmark-deps and remove legacy product packaging, fixtures, and CI checks.

Co-Authored-By: OpenAI Codex gpt-6.1-sol <[email protected]>
2026-10-24 14:50:55 +01:00
rdbtCVSandOpenAI Codex gpt-6.1-sol 2a9e0ab28d Remove unused voxel-kernels research experiment
Co-Authored-By: OpenAI Codex gpt-6.1-sol <[email protected]>
2026-10-24 14:49:46 +01:00
Ludovic MarieandClaude Sonnet 5.5 87aa88de54 Launcher: Cmd+Q quits and Cmd+W closes the window on macOS
The launcher had no app menu and no key bindings, so Cmd+Q did nothing.
Add Quit and CloseWindow actions, bind them to cmd-q and cmd-w on macOS,
and set an app menu with "Quit Horizon Launcher" and a Window menu with
"Close Window". Quit shares one shut_down path with closing the window
(stop the servers this launcher started, send telemetry, exit), so a
running game is treated exactly as it is today.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-24 15:45:29 +02:00
Ludovic MarieandClaude Opus 5.5 7efdbdb39a docs: remove the surveys of other mods; mod code is never read
Removes docs/performance/java-perf-mods.md (a survey of eight Java
performance mods' techniques) and docs/research/java-mod-compat.md (notes
on how Fabric, NeoForge and Mixin work, with a list of mods to port).
Links to them now point at mods.md, which says in two lines why Java jars
don't run in Horizon.

docs/modpacks.md no longer lists a mod's decompiled code as an input of a
port: a mod's code is never decompiled, read, quoted or paraphrased, and
ports are written from the mod's observed behaviour (the oracle and the
harness), its own data files and its public documentation only.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 15:42:44 +02:00
Ludovic Marie 535f2d2a04 Merge pull request #1556 from ludovic111/docs/agents-server-thread-default
🤖 Agent: merged after verify passed and the integration run on the desktop.
2026-10-24 15:09:23 +02:00
Ludovic MarieandClaude Opus 5.5 7eb7791240 AGENTS.md: the server thread is single player's default (#1514), releases ship in batches (#1554)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 15:08:04 +02:00
Ludovic Marie ca90a20992 Merge pull request #1551 from levzzz5154/fix/vehicles-server-thread
🤖 Agent: merged after verify passed and the integration run on the desktop.
2026-10-24 14:32:57 +02:00
Ludovic Marie 923368f4df Merge pull request #1553 from ludovic111/cg3/pr1534-fixup 2026-10-24 14:31:42 +02:00
Ludovic Marie 25bdaf5321 Merge pull request #1552 from ludovic111/cg3/pr1502-fixup 2026-10-24 14:30:12 +02:00
Ludovic MarieandClaude Opus 5.5 8a04e18b91 Measure the minecart push with the player standing half in it
Walking through the cart pushes it back once past its middle, so the
check stands the player half into the cart from the north and waits.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 14:23:40 +02:00
Ludovic Marie 5d54f28c42 Merge pull request #1547 from ludovic111/fix/kb1-cooldown-knockback 2026-10-24 14:12:58 +02:00
Ludovic Marie 818d1515c6 Merge pull request #1554 from ludovic111/fix/release-batches
🤖 Agent: merged after verify passed and the integration run on the desktop.
2026-10-24 13:53:46 +02:00
Ludovic MarieandClaude Opus 5.5 e61e6345bc Releases: in batches of 5 pull requests, or when the oldest has waited 6 hours
One release per merge made about 40 releases in 31 hours, hard for players to follow (asked on Discord).
release-now still releases at once.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 13:47:19 +02:00
Ludovic Marie 02039fed5b Merge pull request #1539 from ludovic111/fix/launcher-builtin-packs 2026-10-24 13:43:38 +02:00
Ludovic Marie 976956fc3e Merge pull request #1546 from ludovic111/fix/kb1-enchantment-ids 2026-10-24 13:41:22 +02:00
Ludovic Marie dfae12cfe2 Merge pull request #1550 from ludovic111/fix/kb1-knockback-yaw 2026-10-24 13:34:45 +02:00
Ludovic MarieandClaude Opus 5.5 8a8348c93f Merge origin/main into fix/vehicles-server-thread
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 13:29:05 +02:00
Ludovic MarieandClaude Opus 5.5 0a3965a7b1 Check vehicles on both sides of a LAN game
lan_e2e's vehicles check: a creative break whose block is put back stays,
a player stands on a boat, lava breaks a ridden boat and lets the rider
out, a minecart in lava breaks and a TNT one goes off, and walking into a
minecart pushes it, for the host and the guest.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 13:28:50 +02:00
Ludovic MarieandClaude Opus 5.5 704931ed14 Keep horizon-ctl getblock on a client's copy, run server ones in the owner's level
On a LAN guest or any remote client, getblock and container read what
that client has again (test_friends waits on a guest's getblock). On the
server thread the block commands run in the owner's level, not whichever
level the server ticked last.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 13:27:50 +02:00
Ludovic MarieandClaude Opus 5.5 a713dbd46d Break vehicles in lava on their first tick, as vanilla does
AbstractBoat.tick runs applyEffectsFromBlocks twice, and a minecart takes
lavaHurt from its behaviour's applyEffectsFromBlocks and again from its
own isInLava check: two 40-point hurts a tick, so lava breaks either on
its first tick inside (a TNT minecart primes instead). A minecart has no
baseTick and a boat never outlives lava, so the fire countdown is gone.

Minecart pushing takes the player's own box and returns at once when
there are no carts.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 13:27:50 +02:00
Ludovic Marie 1868fc39ae Merge pull request #1549 from ludovic111/fix/kb1-sprint-reset 2026-10-24 13:18:50 +02:00
Ludovic Marie 50f2894ca8 Merge pull request #1548 from ludovic111/fix/kb1-server-velocity 2026-10-24 13:13:08 +02:00
Ludovic Marie 4b44399242 Merge pull request #1545 from ludovic111/fix/kb1-knockback-harness 2026-10-24 13:11:32 +02:00
Ludovic Marie 43e6bfcf98 Merge pull request #1542 from Excurs1ons/fix/java-pack-bob-in-view
🤖 Agent: merged after verify passed and the integration run on the desktop.
2026-10-24 13:07:33 +02:00
levzzzandClaude Opus 5.5 f688726b34 Let server players push minecarts, without pushing the player back
Guests on the server thread (and LAN) now push carts with vanilla's push
math. The push back on the player is dropped in both modes: vanilla only
syncs it to other players, so the pusher's own client never feels it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 15:56:12 +05:00
levzzzandClaude Opus 5.5 826cf8ffe6 Burn boats and minecarts in lava, and place boats on lava
Vehicles take lava damage and keep burning for 15 seconds like vanilla
(lavaHurt, remainingFireTicks), breaking and dropping their items; a TNT
minecart primes from fire. Boat placement stops at lava as well as water.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 15:56:12 +05:00
levzzzandClaude Opus 5.5 e2dd6b67df Make boats solid to players
Boats join happy ghasts in the player's entity collision list, so a
player can't walk through a boat and steps up onto it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 15:56:12 +05:00
levzzzandClaude Opus 5.5 ebd25abd0e Run horizon-ctl block commands in the server's world
With the integrated server thread on, setblock, fill, getblock and
container ran on the client's copy only. They now run on the server, and
on a remote server setblock and fill go out as chat commands.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 15:56:11 +05:00
levzzzandClaude Opus 5.5 9624e99801 Clear a creative player's dig after the block breaks on the server
A creative START destroys the block at once and its client never sends
STOP, so the kept dig broke whatever was placed in that cell next, every
destroy delay, until the player broke something else.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 15:56:11 +05:00
Ludovic MarieandClaude Opus 5.5 6a5c749f7c Aim extra knockback with Mth's sine table
Player.causeExtraKnockback pushes along Mth.sin/Mth.cos of the attacker's
yaw, which come from vanilla's 65536-entry table: facing east, the push
has no z at all. Horizon took f32 sin/cos, which left -4.4e-8 of z per
block of push (a sprinting or Knockback hit, on mobs and players).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 12:13:31 +02:00
Ludovic MarieandClaude Opus 5.5 c33acfdd05 Stop a guest's sprint when its hit knocks back
Player.causeExtraKnockback stops the attacker's sprint on the server, and
the shared flag reaches the attacker's own client (ServerEntity sends a
player its own dirty entity data). A guest's sprint was only stopped on
the server copy for the length of the hit and never sent, so a guest (and,
on the server thread, the window) kept sprinting: every hit while
sprinting had the sprint knockback, where vanilla's needs the sprint to
start again.

- leave_guest keeps the sprint stop a guest's hit made; a guest hitting a
  player stops its sprint too.
- The hub sends each guest its own shared flags (crouching, sprinting,
  gliding) when they change.
- A remote client takes the sprinting bit of its own shared flags (the
  flag only, as vanilla's assignValues: the speed modifier and the
  wasSprinting it compares stay, so it sends START_SPRINTING again only
  after it has stopped).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 12:13:31 +02:00
Ludovic MarieandClaude Opus 5.5 7c83fbb6df Travel a guest's server-side velocity, and send hurts after it
A guest's (and, on the server thread, the window's) knockback started from
the velocity the last hit left on its server copy, which nothing ever
slowed down, and each hit went out at once. So every hit after the first
pushed harder (0.72, then 0.76 blocks a tick where vanilla sends 0.4 each
time), netherite resisted from the wrong base, and a zombie's hit reached
the client before the friction of the tick it landed in.

Vanilla's server simulates a player too (MoveSimulationType
AUTHORITATIVE_SIDE_AND_SERVER): ServerPlayer travels every tick with no
input, and ServerEntity.sendChanges sends a velocity a hurt marked
(Entity.markHurt) as it is after that travel. A player's melee hit is the
exception (Player.causeExtraKnockback): it sends at once, then puts the
server copy back to its velocity from before the hit.

- MotionWorld::server_travel: Player.travel with no input, keeping only
  the velocity (the client's moves place the player). The server's copy
  checks no fall damage on its own moves (isLocalInstanceAuthoritative).
- guest_server_tick runs it last, then sends the velocity if a hurt
  marked it since the last tick.
- guest_hit_by leaves a marked velocity to that; guest_struck_by (melee
  from a player) sends at once and restores.
- lan_e2e `knockback`: three hits each way push the victim as far each
  time.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 12:13:30 +02:00
Ludovic MarieandClaude Opus 5.5 c26ced533e Merge origin/main into fix/worldgen-clippy-bool-20261023
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 11:31:38 +02:00
Ludovic MarieandClaude Opus 5.5 e0d29dc233 Drop the aquifer helper and correct the fire burnout comment
Stable Clippy (1.99, what CI runs) does not flag q < -2. || q > 2., so the
helper and its test only served Rust 1.90's Clippy. Fire burnout is not
'intentionally disabled': vanilla's FireBlock.tick reads the
INCREASED_FIRE_BURNOUT environment attribute, set by eight overworld biomes,
which Horizon does not read yet; the comment now says so.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 11:31:38 +02:00
Ludovic MarieandClaude Opus 5.5 6eb34eb868 Merge origin/main into fix/client-clippy-baseline-20261023
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 11:31:10 +02:00
Ludovic MarieandClaude Opus 5.5 b21423ae01 Keep only the Clippy rewrites that read as well on stable
CI runs stable Clippy (1.99), which is clean on main. The #[expect(clippy::boxed_local)]
in streaming.rs is unfulfilled there and fails CI under -D warnings, so it goes.
Also back out the rewrites that only satisfied Rust 1.90's Clippy and read
worse: the dragon's range helper and its test, the axolotl test's restated
formula, and two De Morgan expansions (first_person_probe, explosion).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 11:31:10 +02:00
Ludovic MarieandClaude Opus 5.5 a6e9374f88 Java shader packs: say where vanilla and Iris put the view bob
Vanilla 26.3 multiplies bobHurt/bobView into the projection
(GameRenderer.renderLevel); Iris's MixinModelViewBobbing moves them into
the model-view when a pack is in use. The comment said vanilla keeps the
bob in the PoseStack, which is Iris's behaviour, not vanilla's.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 11:30:12 +02:00
Ludovic MarieandClaude Opus 5.5 50aee22996 Add a knockback harness: a dedicated server against vanilla's
tools/test_server_knockback_parity.py runs vanilla's dedicated server and
Horizon's (`--dedicated --nogui`) on one flat world and joins both with two
vanilla-codec players (KnockbackClient):

- Alice hits a cow and a zombie (plain, sprinting, Knockback II, critical,
  in the air); the game is frozen around the hit and stepped a tick at a
  time, and the mob's Motion and Pos over 20 ticks are compared exactly;
- Alice hits Bob (the same, a shield, netherite, two hits, two sprinting
  hits, a mace smash, a stronger hit inside the hurt cooldown): Bob's own
  motion packets, Alice's and her sprint flag are compared;
- a zombie hits Bob twice: his motion packets.

7 of 21 cases match on main. KNOWN lists the other 14 with their causes:
the guest's server-side velocity never travels (knockback adds up from hit
to hit), the server never sends a guest its own sprint flag, the extra
knockback's direction uses f32 sin/cos instead of Mth's table, a hit in
the hurt cooldown still knocks back, and a mace smash on a player runs
none of MaceItem.hurtEnemy.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 11:23:04 +02:00
Ludovic MarieandClaude Opus 5.5 01133fac82 No default knockback from a hit inside the hurt cooldown
A hit that lands while the player is still in its hurt cooldown, harder
than the last one, only hurts by the difference (LivingEntity.hurtServer):
it marks nothing and deals no default knockback. Horizon knocked the
player back 0.4 for it anyway, so a quick second hit (a weak swing, then a
charged one) pushed twice.

The hurt now reports whether its knockback hook ran (a fresh hit, not
fully blocked), and mob_hits_player knocks back only then.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 11:13:27 +02:00
Ludovic MarieandClaude Opus 5.5 4507ded0e4 Read enchantment ids without a namespace as minecraft ones
`/give @s wooden_sword[enchantments={knockback:2}]` (or an item saved
that way) kept the key `knockback`, which no enchantment lookup matched:
the sword showed no enchantment and knocked back like a plain one.
Vanilla reads the keys as Identifiers, so `knockback` is
`minecraft:knockback`.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 11:13:27 +02:00
Ludovic Marie 46304cc1a7 Merge pull request #1489 from rdbtCVS/docs/generic-agent-attribution
🤖 Agent: merged after verify passed and the integration run on the desktop.
2026-10-24 11:10:16 +02:00
Ludovic MarieandClaude Opus 5.5 377d81a329 AGENTS.md: keep Ludovic's markers, let a contributor's agent name its own person and tool
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 11:07:44 +02:00
Ludovic Marie 905acfe7cd Merge remote-tracking branch 'origin/main' into pr-1489 2026-10-24 11:07:43 +02:00
Ludovic Marie c18f3dbfde Merge pull request #1536 from ludovic111/fix/dg1-villager-doors 2026-10-24 09:50:38 +02:00
JasonGuandDeepSeek V4.1 Flash 9c843819b7 Java shader packs: keep the view bob in the view, not the projection
A pack rebuilds view positions from the depth buffer with
`inverse_projection` and then multiplies by `inverse_view`
(`gbufferModelViewInverse`), and Horizon's own packs do the same through
`world_offset` and `frame.inverse_view`. Passing
`world_projection(fov, aspect, far) * bob` as the projection while `view`
stayed unbobbed left the two halves disagreeing by the walk bob, so
everything a pack reconstructed — reflections, ambient occlusion,
godrays — turned with the bob. That reads as reflections wobbling in
step with the first-person hand, and changing the FOV made it worse.

Vanilla and Iris carry the bob in the world's pose stack (the model-view
matrix), not in the projection. Pass `bob * view` as the view and the
plain world projection instead: `projection * view` is unchanged, so
terrain, entities and the pack's own geometry programs draw as before,
while depth reconstruction is now consistent. The hand's uniforms
already carried the bob in its pose and are untouched.

Checked with Complementary Reimagined r5.9.3 over the depth-feedback
passes at 70 and 110 FOV.

Co-Authored-By: DeepSeek V4.1 Flash (OpenCode agent) <[email protected]>
2026-10-24 15:49:14 +08:00
Ludovic Marie c466c5ddad Merge remote-tracking branch 'origin/main' into fix/dg1-villager-doors 2026-10-24 09:07:55 +02:00
Ludovic Marie d5bd35025e Merge pull request #1541 from ludovic111/fix/probe-target-names 2026-10-24 08:57:26 +02:00
Ludovic Marie 2cd8b88a5b Merge pull request #1540 from ludovic111/fix/villager-golem-summon 2026-10-24 08:18:46 +02:00
Ludovic MarieandClaude Opus 5.5 0b581f9ffe Give each crate's probe example its own target name
horizon-client, horizon-world and horizon-protocol each had an example
named `probe`. Cargo copies every crate's examples into one
<profile>/examples/ directory, so parallel parity harnesses sharing a
target dir could run another crate's probe (CI parity run 40055656303:
command_parse ran the horizon-world dispatcher). The examples are now
client_probe, world_probe and protocol_probe ([[example]] in each
Cargo.toml; the files stay where they are), and every harness builds
and runs its own crate's name.

tools/parity.py recognises the new names when it counts the probes a
harness ran, and renames the old `probe` in recorded artifacts to the
crate's target (from the record's -p, else its probe sources), so the
prebuild still builds them instead of dropping them as unknown.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 08:08:18 +02:00
Ludovic Marie f67b2b3204 Merge pull request #1537 from ludovic111/fix/mob-path-max-visited 2026-10-24 07:52:25 +02:00
Ludovic Marie d139f9bee6 Merge pull request #1514 from ludovic111/feat/st-default
🤖 Agent: merged after verify and CI parity passed, built merged with current main. Single player runs on its server thread by default, as vanilla does; `--in-frame` brings back the old mode. Evidence: the smoke session passes 48/48 in both modes. ThinkPad (HD 620, Mesa) RD 16: 651 vs 185 FPS. M4 Pro, v0.2.25 release build, forest, offscreen, alternating runs: 1% lows 224–231 → 399–476 at RD 16 and 120–124 → 210–213 at RD 32; average FPS within 4% at RD 32.
2026-10-24 07:27:50 +02:00
Ludovic Marie 03d93d79b2 Merge pull request #1513 from ludovic111/fix/st-cursor-close 2026-10-24 07:26:18 +02:00
Ludovic MarieandClaude Opus 5.5 1b14e0d171 Villagers summon iron golems the vanilla way and remember them
Villager.spawnGolemIfNeeded set GOLEM_DETECTED_RECENTLY on the villagers
near a new golem, but Horizon wrote only villager::Ext, and the next brain
tick copied the empty memory back over it: the same villagers could summon
another golem before their sensor saw the first. golem_detected now sets the
brain memory (599 ticks).

IronGolem.checkSpawnObstruction checks the floor's top face (no golems on
dirt paths, farmland or slabs) and three blocks, the feet whatever fluid is
there; Horizon checked four blocks and no floor, so a roof three blocks up
kept golems out. The search's open block is air or a liquid block, not any
waterlogged one, and the villagers asked are those whose boxes meet the
caller's box grown by 10. The golem is created (yaw, follow range bonus,
left-handedness from the level random) before the obstruction check, as
trySpawnMob does.

The search and checks move to mobs::villager so the mob_ai probe runs the
game's code: four new scenarios (gossip, glass roof, dirt paths, panic) match
vanilla; with the old checks all four diverge.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 07:25:46 +02:00
Ludovic Marie f8e4e31b12 Merge pull request #1512 from ludovic111/fix/st-hits 2026-10-24 07:24:47 +02:00
Ludovic Marie 67e07f3faa Merge pull request #1511 from ludovic111/fix/st-player-body 2026-10-24 07:23:17 +02:00
Ludovic Marie 2bbbc70620 Merge pull request #1510 from ludovic111/fix/st-maps 2026-10-24 07:19:37 +02:00
Ludovic Marie 9c134f54ac Merge pull request #1509 from ludovic111/fix/st-fishing 2026-10-24 07:17:50 +02:00
Ludovic Marie a00dc868ec Merge pull request #1471 from ludovic111/perf/glyph-cache-fxhash 2026-10-24 07:14:41 +02:00
Ludovic Marie 08306d1586 Merge pull request #1470 from ludovic111/fix/entity-shadow-height 2026-10-24 07:12:59 +02:00
Ludovic Marie 3b70dc9c16 Merge pull request #1469 from ludovic111/fix/creative-search-hotbar-keys 2026-10-24 07:11:26 +02:00
Ludovic MarieandClaude Opus 5.5 13c14563c4 Launcher adds Programmer Art and High Contrast to resources prepared without them
Resources prepared before #1538, and every version that reused them, lack vanilla's built-in
packs, so players had to click Prepare again. The launcher now copies them itself, with
horizon-prepare's `packs` (the asset index's SHA-1 checked), when it starts, installs a version
or starts the game: only for prepared versions that lack a zip, once per launcher run. Without
them in any Minecraft folder's asset store it logs one line and the game starts as before.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 07:07:28 +02:00
Ludovic Marie 99b058442c Merge pull request #1538 from ludovic111/fix/builtin-packs
🤖 Agent: merged after verify and CI parity passed, built merged with current main (goldendays-1). Prepare copies vanilla's built-in Programmer Art and High Contrast packs from the player's own launcher asset store (SHA-1 checked; nothing ships with Horizon), so packs made to sit on Programmer Art, like Golden Days, show their classic textures.
2026-10-24 06:54:08 +02:00
Ludovic MarieandClaude Opus 5.5 8cce2e2f8a Prepare: import Programmer Art and High Contrast from the asset index
Vanilla lists its built-in resource packs from minecraft:resourcepacks of
the vanilla pack (BuiltInPackSource.populatePackList), and those two zips
live only in the asset index, not in the client JAR. Preparation never
copied them, so Horizon's pack list had no Programmer Art. Packs made to sit
on it, like Golden Days ("This pack is meant to be put over Programmer
Art"), then showed the modern textures for the ~720 textures they leave to
it: furnaces, stone bricks, crafting tables, pumpkins, TNT, logs, tools,
buckets, paper.

Full preparation now imports both after extracting the JAR, like the
panorama and Unifont, and `horizon-prepare packs` adds them to prepared
resources.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 06:34:00 +02:00
Ludovic Marie 582856aac2 Merge pull request #1326 from ludovic111/feat/java-shadow-programs
🤖 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-24 06:11:42 +02:00
Ludovic Marie 0a95a78e03 Merge pull request #1290 from ludovic111/feat/java-terrain-cull
🤖 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-24 06:09:33 +02:00
Ludovic MarieandClaude Opus 5.5 5205b909f7 Villagers, llamas, bees, allays and copper golems search the full path budget
PathNavigation.setRequiredPathLength calls updatePathfinderMaxVisitedNodes, so a
villager's PathFinder visits up to max(follow range, 48) * 16 = 768 nodes, not the
256 of its follow range alone. Horizon always used follow range * 16, so a villager
gave up on any job site, bed or bell whose path needed more than 256 nodes: in the
seed 12345 village it never found the way round to the mason's stonecutter door.

Navigation now keeps PathFinder.maxVisitedNodes: the base follow range times 16 when
the mob is made, then max(follow range, required path length) * 16 after
setRequiredPathLength and after any follow range change (Mob.onAttributeUpdated).
A happy ghast that grows up or becomes a baby gets a new navigation, as in vanilla.

mob_ai: villager_job_in_box, a composter inside a stone box whose doorway faces away
from the villager.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 05:33:52 +02:00
Ludovic Marie 5ec40c8004 Merge remote-tracking branch 'origin/main' into feat/java-terrain-cull 2026-10-24 05:15:42 +02:00
Ludovic Marie 82bba24f43 Merge remote-tracking branch 'origin/fix/st-cursor-close' into feat/st-default 2026-10-24 05:14:33 +02:00
Ludovic Marie 7b0316e6a4 Merge remote-tracking branch 'origin/fix/st-hits' into fix/st-cursor-close
# Conflicts:
#	tools/lan_e2e.py
2026-10-24 05:13:15 +02:00
Ludovic Marie a29e10ef9b Merge remote-tracking branch 'origin/fix/st-player-body' into fix/st-hits
# Conflicts:
#	tools/lan_e2e.py
2026-10-24 05:11:21 +02:00
Ludovic Marie 92050712fa Merge remote-tracking branch 'origin/fix/st-maps' into fix/st-player-body
# Conflicts:
#	tools/lan_e2e.py
2026-10-24 05:09:46 +02:00
Ludovic Marie 122e3bf287 Merge remote-tracking branch 'origin/fix/st-fishing' into fix/st-maps
# Conflicts:
#	tools/lan_e2e.py
2026-10-24 05:08:17 +02:00
Ludovic Marie 65031a7739 Merge remote-tracking branch 'origin/main' into fix/st-fishing
# Conflicts:
#	tools/lan_e2e.py
2026-10-24 05:06:43 +02:00
Ganer 82218db860 Merge commit '59f018eabe26093274d34942365d31010c3e3e95' into fix/worldgen-clippy-bool-20261023 2026-10-23 21:58:59 -05:00
Ganer 040ac1bec4 Merge commit '59f018eabe26093274d34942365d31010c3e3e95' into fix/client-clippy-baseline-20261023 2026-10-23 21:58:59 -05:00
Ludovic Marie 59f018eabe Merge pull request #1535 from ludovic111/fix/dg1-mining
🤖 Agent: merged after verify and CI parity passed, built merged with current main (discord-gate-1: player reports checked against vanilla 26.3; a lan_e2e check fails on main and passes here in both modes; evidence in the PR body).
2026-10-24 04:49:25 +02:00
Ludovic MarieandClaude Opus 5.5 969bcec6c9 Villagers open and close doors on their paths
InteractWithDoor was a stub, so a villager whose path went through a
closed oak door walked into it and stayed there. In generated villages,
villagers who were inside a house never reached a job site, a bed or
the meeting point.

Like vanilla, a villager now opens the closed mob-interactable doors on
its next path step (DoorBlock.setOpen: both halves, the door sound and
the block_open game event), remembers the door it leaves and the doors
it opened, and closes them once it is more than one step past them,
unless another villager within 2 blocks is walking through. A villager
going to sleep closes them too.

The mob events apply after the mob loop, so the door opens one tick
later than in vanilla, where DoorBlock.setOpen changes the world during
the villager's brain tick.

lan_e2e's new doors check shuts a villager in a room with a closed
door and a free composter outside. Before, it never opened the door;
now it opens it, becomes a farmer, and the door is closed again on the
server and on the guest.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 04:47:39 +02:00
Ludovic Marie ca0637da80 Merge pull request #1508 from ludovic111/fix/st-wind-charge
🤖 Agent: merged after verify and CI parity passed, built merged with current main (st-default-2: server-thread gaps checked against vanilla, each with a lan_e2e check that fails before and passes after, host and guest, both modes; numbers in the PR body).
2026-10-24 04:45:07 +02:00
Ludovic Marie 47459b0b19 Merge pull request #1455 from ludovic111/chore/mob-brain-shared
🤖 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-24 04:22:35 +02:00
Ludovic Marie 9b16d37e80 Merge pull request #1452 from ludovic111/chore/dedupe-side-mask-kind
🤖 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-24 04:20:53 +02:00
Ludovic Marie 03be14bb59 Merge remote-tracking branch 'origin/fix/st-cursor-close' into feat/st-default 2026-10-24 04:12:23 +02:00
Ludovic Marie 45e5b64520 Merge remote-tracking branch 'origin/fix/st-hits' into fix/st-cursor-close 2026-10-24 04:11:25 +02:00
Ludovic Marie d65be4183f Merge remote-tracking branch 'origin/fix/st-player-body' into fix/st-hits 2026-10-24 04:10:32 +02:00
Ludovic Marie e307f03684 Merge remote-tracking branch 'origin/fix/st-maps' into fix/st-player-body 2026-10-24 04:09:39 +02:00
Ludovic Marie 26a91e3a08 Merge remote-tracking branch 'origin/fix/st-fishing' into fix/st-maps 2026-10-24 04:08:43 +02:00
Ludovic Marie 8c81cc63f7 Merge remote-tracking branch 'origin/fix/st-wind-charge' into fix/st-fishing 2026-10-24 04:07:54 +02:00
Ludovic Marie 221545e92b Merge remote-tracking branch 'origin/main' into fix/st-wind-charge 2026-10-24 04:07:01 +02:00
Ludovic Marie 88b27070bd Merge pull request #1476 from ludovic111/fix/st-held-use
🤖 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-24 03:43:11 +02:00
Ludovic Marie 499f8bddfc Merge pull request #1475 from ludovic111/fix/st-elytra
🤖 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-24 03:41:32 +02:00
Ludovic Marie 5904922865 Merge pull request #1466 from ludovic111/fix/st-books
🤖 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-24 03:39:56 +02:00
Ludovic Marie 49e706e8be Merge pull request #1465 from ludovic111/fix/st-signs
🤖 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-24 03:38:08 +02:00
Ludovic Marie 5e63b0512d Merge pull request #1464 from ludovic111/fix/st-keep-inventory
🤖 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-24 03:36:22 +02:00
Ganer d66466db81 Merge commit '22b2711cf9557d53d21080828c895dbff6027151' into fix/worldgen-clippy-bool-20261023 2026-10-23 20:25:14 -05:00
Ganer 6b55abd66a Merge commit '22b2711cf9557d53d21080828c895dbff6027151' into fix/client-clippy-baseline-20261023 2026-10-23 20:25:14 -05:00
Ludovic Marie 22b2711cf9 Merge pull request #1277 from ludovic111/feat/java-gbuffers-terrain
🤖 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-24 03:18:46 +02:00
Ganer 6ea7167cbb Merge commit 'd8dd2023dd9c6129e9e628a66d61c1b10e1435cf' into fix/worldgen-clippy-bool-20261023 2026-10-23 20:17:24 -05:00
Ganer 51de8afe95 Merge commit 'd8dd2023dd9c6129e9e628a66d61c1b10e1435cf' into fix/client-clippy-baseline-20261023 2026-10-23 20:08:07 -05:00
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
Ganer 3e04df41c8 Merge commit 'd84d7a7e6178ec1a6afab302c4b8ce80e5d2d9ee' into fix/client-clippy-baseline-20261023 2026-10-23 19:55:56 -05:00
Ganer 7371b2cccd Merge commit 'd84d7a7e6178ec1a6afab302c4b8ce80e5d2d9ee' into fix/worldgen-clippy-bool-20261023 2026-10-23 19:55:55 -05: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
Ganer 077c4c9214 Merge remote-tracking branch 'upstream/main' into fix/client-clippy-baseline-20261023 2026-10-23 19:53:00 -05: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
Ganer 8f081ae460 Merge remote-tracking branch 'upstream/main' into fix/client-clippy-baseline-20261023 2026-10-23 19:44:31 -05:00
Ganer 14461c1d96 fix(client): preserve behavior while clearing Rust 1.90 Clippy errors 2026-10-23 19:44:13 -05: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 MarieandClaude Opus 5.5 ea424ebd52 LAN and dedicated servers wait for the player's client to finish breaking a block
ServerPlayerGameMode breaks a digging player's block only when the
player's client says it is done (STOP_DESTROY_BLOCK, or the delayed
destroy), or at once when it is instant. The host ran the guest's
tick_mining with its own count and broke the block when that reached 1.
A guest whose client ran even a tick behind lost the block before it
predicted the break: no destroy particles, no break sound, and the
dropped item could appear before the block vanished.

lan_e2e's new digging check holds attack across six oak logs and hears
six breaks, then stalls the guest for a second halfway through stone:
the stone is still there on both sides when it resumes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 02:38:07 +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
Ganer fc9822b241 Merge commit 'd6dc08098a30a08be7ec168fa3e6b756aa3ac493' into fix/worldgen-clippy-bool-20261023 2026-10-23 17:12:21 -05: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
Ganer 4b1e298a44 fix(worldgen): resolve upstream Rust 1.90 Clippy boolean warnings 2026-10-23 16:55:08 -05:00
Ludovic MarieandClaude Opus 5.5 a4a101ca9c Single player runs on its server thread by default again; --in-frame is the opt-out
#1428 made the in-frame world the default while a server's players lagged
behind it (signs, books, totems, keep_inventory and the other [server-thread]
issues). With the fixes of this stack a scripted single player session (walk,
mine, place, inventory, combat, eating, boats, minecarts, chests, furnaces,
beds, chat, signs, books, totems, keep_inventory, fishing, maps, saving and
reopening) gives the same results on the server thread as in the frame.

`--in-frame`, `--no-server-thread` or HORIZON_SERVER_THREAD=0 runs the world
in the frame; `--server-thread` still works. LAN guests, dedicated servers and
the command harness are unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 23:30:14 +02:00
Ludovic MarieandClaude Opus 5.5 c6ed3112e7 Closing a menu puts a server's player's cursor stack back into their inventory
AbstractContainerMenu.removed hands the stack a closing player holds on the
cursor to placeItemBackInInventory (dropped only what doesn't fit, or for a
player who left), and broadcastChanges then empties the client's cursor. The
LAN host threw it out at the player's eyes instead, for the inventory and
every container, crafting, station, mount and merchant menu, and the client
kept showing a copy on its cursor (#1283).

The three ContainerClose arms now place it back through lan_place_back and
send the inventory, carried stack included.

tools/lan_e2e.py gains a cursor check: each player picks up a stack in their
inventory and closes it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 23:30:08 +02:00
Ludovic MarieandClaude Opus 5.5 f96101cd47 Strikes and mob effects reach a server's players: lightning, evoker fangs, poison, axolotls
Lightning (Entity.thunderHit) and evoker fangs (EvokerFangs.dealDamageTo) hit
every entity in their box, but thunder_hit and fangs_bite only looked at the
window's own player, so a server's players (guests, and single player on the
server thread) were never struck (#1480, #1481). Both now run their player
part for each guest too, through Game::each_guest_hit.

The effects a mob gives the player it acts on (cave spider and bee poison, husk
hunger, wither skeleton wither, the nautilus's breath) and an axolotl's help
after a kill stayed with the server's own player: acts_on_player now routes
MobEvent::EffectPlayer and MobEvent::AxolotlSupportPlayer to the guest the mob
sees, like its blows (#1484, #1485).

tools/lan_e2e.py gains a strikes check: a bolt hurts each player standing in
water, and a cave spider poisons them.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 23:30:08 +02:00
Ludovic MarieandClaude Opus 5.5 fa4ea80d32 A server's player's client sets its body as in frame: look pitch, use slowdown, boots on powder snow
On a server (guests, and single player on the server thread) the window ticks
its player through tick_remote_level, which copied the yaw, the Respiration
level, the wings and FAST_LAVA but not the rest of what the in-frame tick sets
from the window's own state. Both now call Game::sync_player_body:

- the look pitch: Player.travel steers sprint-swimming up and down by it (#1303)
- the slowdown of an item in use, LocalPlayer.modifyInput (#1278)
- leather boots, PowderSnowBlock.canEntityWalkOnPowderSnow (#1296)
- freeze-immune wear, LivingEntity.canFreeze
- the Auto Jump option, LocalPlayer.sendPosition (#1279)

tools/lan_e2e.py gains a body check: each player walks while drawing a bow,
stands on powder snow with leather boots and without, and sprint-swims looking
down.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 23:30:08 +02:00
Ludovic Marie 0bbe50c6e8 Merge remote-tracking branch 'origin/main' into feat/java-terrain-cull 2026-10-23 23:10:12 +02:00
Ludovic Marie c924894624 Merge remote-tracking branch 'origin/main' into feat/java-gbuffers-terrain 2026-10-23 23:08:59 +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 MarieandClaude Opus 5.5 287d7a7eb5 Maps on a server's players: each holder's map ticks on the server and its data reaches them
A map a server's player (a LAN guest, or single player's window on its
server thread) made stayed blank and saved nothing (#1382): only the
game's own player ever updated a map, and no map data went to the guests.
Each guest's turn now runs MapItem's ticks as theirs (the marker of every
carried map, the reveal of the held ones), and the server sends each guest
ClientboundMapItemDataPacket for the maps it carries, and every 10 ticks
for the maps framed in the level (ServerPlayer.synchronizeSpecialItemUpdates,
ServerEntity.sendChanges).

As in vanilla, a map keeps its holders (MapItemSavedData.HoldingPlayer):
each has its own column sweep and its own record of what changed since its
last packet, so the packet carries only the changed pixels' box, and the
decorations at most every 5th tick while they changed. Player markers are
kept per holder, so each player's marker shows on a shared map, and go
when that player stops carrying it. A server's client now applies the
packet's decorations.

lan_e2e maps: host and guest each make and hold a map, which draws the
terrain around them and their marker, in both modes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 22:58:28 +02:00
Ludovic MarieandClaude Opus 5.5 c86996c234 Fishing on a server's players: a hook per player, ticked on the server and drawn by its clients
A server's players (LAN guests, and single player's window on its server
thread) cast no lasting hook and caught nothing (#1348): the guest's cast
went into the game's own hook, which the server's tick then dropped for
want of a local player. Each guest now keeps its own hook (Player.fishing),
swapped in with the rest of its state for its turns, ticked as theirs every
server tick, and dropped when they die. Reeling in retrieves it on the
server: a catch, the rod's wear, the experience.

The server tracks every hook to its players as a fishing_bobber entity
(FishingHook.getAddEntityPacket: the owner's id; DATA_HOOKED_ENTITY and
DATA_BITING). A client takes the one its own player cast as its hook, so
the rod shows cast and the line runs to its hand, and draws the others'
bobbers on lines to their owners' hands.

A hook resting on a block stays on the ground (Entity.move keeps onGround
when nothing moves vertically); it used to flip every tick, so reeling it
in cost the rod 2 only half the time.

lan_e2e fishing: host and guest, both modes: the hook stays out and both
sides draw it, reeling in from the ground costs 2, and in open water with
Lure III a fish bites and is caught for 1.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 22:58:18 +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
rdbtCVS d7b5cd8c85 docs: make agent identity instructions user independent
🤖 Generated with Codex
2026-10-23 21:28:06 +01: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 MarieandClaude Opus 5.5 4daa91cf52 Wind charges push a server's players: the burst's packet carries each one's push
A wind charge's burst (ServerExplosion with TRIGGER_BLOCK) pushed only the
game's own player, so a LAN guest, and single player's window on its server
thread, never rose (#1386). The burst now pushes every player in reach, each
swapped in as the player, and sends ClientboundExplodePacket to the players
within 64 blocks with the push it gave them (hitPlayers), which their client
adds (Entity.pushFromExplosion); onExplosionHit's fall-damage grace goes on
the server's body. A burst from a guest's own turn (their charge landing,
their mace's wind burst) waits for the turn to end, so that it reaches the
host and the other guests too.

The packet's centre particle is the gust emitter and it throws no block
particles, as WindCharge.explode asks; the client now draws the packet's
own particle. Explosion knockback resistance lowers the player's push as in
vanilla, and a guest's body knows whether it flies (no push while flying in
creative).

lan_e2e wind: host and guest each throw a charge at their feet, rise about
six blocks and land unhurt, in both modes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 22:15:06 +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 5595db9077 Merge remote-tracking branch 'origin/main' into chore/dedupe-side-mask-kind 2026-10-23 22:10:53 +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 fe70ff90b4 Merge remote-tracking branch 'origin/main' into fix/st-wind-charge 2026-10-23 21:43:45 +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 4a442f7250 Merge remote-tracking branch 'origin/main' into perf/glyph-cache-fxhash 2026-10-23 20:53:30 +02:00
Ludovic Marie f47f6ee472 Merge remote-tracking branch 'origin/main' into fix/entity-shadow-height 2026-10-23 20:53:28 +02:00
Ludovic Marie 85c6103d45 Merge remote-tracking branch 'origin/main' into fix/creative-search-hotbar-keys 2026-10-23 20:53:27 +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 60611ed5a3 Merge remote-tracking branch 'origin/main' into mm1-1455
# Conflicts:
#	crates/horizon-client/src/mobs/villager_brain.rs
2026-10-23 20:16:35 +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 4fa5966338 Text: the glyph cache hashes chars with Fx instead of SipHash
Every drawn character looks its glyph up in the font set's cache, and with
std's SipHash the hashing was the top cost of drawing sign text (#1461:
100 text signs in view). FxHash for those char keys cuts the frame's
prepare time with 100 text signs from 0.32 ms to 0.22 ms on the Mac mini
(Metal, 1280x720): 1920 to 2250 fps.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 19:58:48 +02:00
Ludovic MarieandClaude Opus 5.5 eb79cfdb01 Entity shadows: no more z-fighting with chicken feet and other model feet
Shadow quads were lifted 0.001 above the block top and drawn in the entity
batch with depth writes. LivingEntityRenderer's translate(0, -1.501, 0)
puts a model's feet 0.001 above the ground too, so a chicken's feet and the
shadow under it were coplanar and flickered (#1440).

ShadowFeatureRenderer puts the quads on the block top itself
(relativeY + shapeBelow.minY), and RenderPipelines.ENTITY_SHADOW tests depth
without writing it. Shadows are now their own entity material (overlay 14)
drawn by such a pipeline after the entities. A small depth bias, constant
plus 1.5% of the slope, keeps them over the terrain under them, whose
vertices snap to the subpixel grid differently (without it whole shadows
vanished at some camera positions), while staying under feet 0.001 higher.
The shader pack shadow map no longer draws entity shadows as casters.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 19:58:48 +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 717f54bf28 Creative search tab: number keys over a slot move its item to the hotbar
CreativeModeInventoryScreen.keyPressed on the search tab tries the hotbar
keys on the hovered slot before the search box: a digit or keypad key bound
to key.hotbar.N swaps when the cursor is over a slot holding an item, or any
slot that is not a display slot, and the cursor stack is empty
(checkHotbarKeyPressed). Over an empty display slot, outside every slot or
with a stack held, the key still types. Horizon sent every key to the search
box (#1460).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 19:58:47 +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 MarieandClaude Opus 5.5 ad76f911bd Shields, tridents, crossbows and spyglasses end their use on a server: the client starts it too
Item.use runs on the client as well (MultiPlayerGameMode.useItem), and
only a use the client started sends RELEASE_USE_ITEM when the button
lets go. A server's client started one for bows and food only, so a
shield kept blocking after release, a charged trident never left the
hand, a loaded crossbow stayed "in use" on the server and never fired,
and a spyglass never scoped. The client now starts what the window's
own Item.use does: a bow or crossbow with ammunition, a trident, a
spyglass, a shield off cooldown, and keeps it until the button lets go.

The server's RELEASE_USE_ITEM let go of a main-hand bow only. It now
runs ServerPlayer.releaseUsingItem with the used item's hand, through
the release the window's own tick shares (bow, trident, spyglass).

A loaded crossbow reached its client without its ammunition: the
network stack gets minecraft:charged_projectiles (registry id 51,
ItemStackTemplate list), and a server's client decodes it.

lan_e2e gets a helduse check, for the host and the guest, both modes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 19:50:56 +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 5ed8181adc Elytra glides on a server's players: the client sends START_FALL_FLYING and the server checks it
A server's client never took the chest slot's Elytra into its own
movement (player.glider) and never told the server it started gliding,
so on the server thread and as a LAN guest the player just fell. The
client now sets the glider as the window's own tick does and sends
ServerboundPlayerCommandPacket START_FALL_FLYING when a glide starts.

The hub used to accept that command blindly and never clear it. The game
now answers it as handlePlayerCommand does (Player.tryToStartFallFlying,
else stopFallFlying), and each tick runs LivingEntity.updateFallFlying
for a gliding guest: the glide ends once canGlide fails (landed, riding,
flying, levitation, a glider about to break), every 20th tick wears the
Elytra, every 10th is an elytra_glide game event, and a glide that isn't
diving keeps the fall distance at 1 (checkFallDistanceAccumulation).

lan_e2e gets an elytra check, for the host and the guest, both modes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 19:12:44 +02:00
Ludovic MarieandClaude Opus 5.5 5617d702cc Books open on a server's players, and what they write and sign reaches the server
A server's player (a LAN guest, or single player's window on its server
thread) could neither write in a book and quill nor read a written book:
the server opened the screens in the guest's turn and dropped them (#1366).
As in vanilla, the client opens a book and quill's editor itself when it
uses one (LocalPlayer.openItemGui) and sends ServerboundEditBookPacket
when it saves or signs; the server writes the pages or makes the written
book, signed with the player's name, in that hotbar slot or the off hand.
A written book opens on the client when the server sends
ClientboundOpenBookPacket for its hand (ServerPlayer.openItemGui).

horizon-protocol: OpenBook and EditBook (pages a list of at most 100).
horizon-ctl: book sign presses Sign, then Sign and Close; book done on a
book and quill saves it, as its Done button does.

lan_e2e books: host and guest each write, sign, read and drop a book,
both modes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 18:10:13 +02:00
Ludovic MarieandClaude Opus 5.5 52d8ed6721 Signs open their editor on a server's players, and what they write reaches the server
A sign a server's player placed (a LAN guest, or single player's window on
its server thread) opened no editor: its turn's editor was dropped with
the rest of the guest's screens (#1380). The server now does as
ServerPlayer.openTextEdit: the block, then ClientboundOpenSignEditorPacket,
and the player may write on that sign. The client opens its editor on the
sign it has and sends ServerboundSignUpdatePacket when it closes; the
server takes the lines (formatting codes stripped) unless the sign is
waxed, the player was not the one allowed or went out of reach, and sends
the sign to everyone. The window's own sign now reaches its guests too
(sendBlockUpdated, not only a save).

horizon-protocol: OpenSignEditor and SignUpdate, with SignTextSlot as a
VarInt and the lines as a fixed list of four (26.3's stream codecs).
horizon-ctl: type and key reach an open sign editor; block shows a sign's
lines.

lan_e2e signs: host and guest each place, write and see, both modes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 18:10:13 +02:00
Ludovic MarieandClaude Opus 5.5 de358cd721 A server's players drop their inventory on death unless keep_inventory is on
The guest death path (LAN guests, dedicated players and single player's
window on its server thread) gave the experience reward but never ran
Player.dropEquipment, so a player died with keep_inventory off and kept
everything (#1284). Both paths now share one drop_inventory:
destroyVanishingCursedItems, then Inventory.dropAll from the eyes less 0.3.

ServerPlayer.die skips the loot for spectators only; the window's own
death skipped it in creative too, which vanilla doesn't.

lan_e2e keepinv: host and guest, keep_inventory off and on, both modes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 18:10:13 +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 9acc68f09f Merge feat/java-terrain-cull into feat/java-shadow-programs
The shadow samplers bind with main's custom textures (texture.* first,
then the shadow map, then the rest); the shadow layers are drawn from the
shadow list on the flat path too.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 18:00:56 +02:00
Ludovic MarieandClaude Opus 5.5 a8c21c27df Merge feat/java-gbuffers-terrain into feat/java-terrain-cull
With main's Metal culling: the pack's depth test in OpenGL's layout runs
forced early on Vulkan (fs_box_early_depth) and in the shader on Metal
(fs_box_gl, the pack's depth attached read only and bound), and the flat
path culls too (phase 1 calls, flatten_phase2 after test_gl, then
draw_phase2 with each page's group 3).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 17:55:04 +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 MarieandClaude Opus 5.5 0f3a92ac2b Merge feat/java-packs-photon-chocapic into feat/java-gbuffers-terrain
Brings main's shader api 2 (#1414) up the stack. One path for blending:
the terrain programs' blend.* now go through Pass.blend/buffer_blends like
composite and final (java_pack::pass_blends). depthtex1/depthtex2 are one
source, DepthSolid: Horizon's depth before translucent geometry, then the
pack's solid terrain's. Horizon's world goes to the pack's first terrain
target (colortex1 for MakeUp).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 17:46:12 +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 MarieandClaude Opus 5.5 a79077b30e Mob brains share Timed, compute_path, reached and the living-entity scan
Behavior's Timed was copied into the villager, animal, monster and warden
brains: one copy now, in brain.rs, drawing its duration in Java's int
arithmetic (two copies used plain +, the same for every duration they
have). MoveToTargetSink's tryComputePath and reachedTarget, copied into
the villager, monster and warden brains on the same WalkTarget and Slot
types, are the villager brain's, given each brain's
CANT_REACH_WALK_TARGET_SINCE. NearestLivingEntitySensor's scan, copied
into the villager, animal and monster brains, is brain.rs's living_near.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 16:33:59 +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 ac811d4708 One side_mask for the two datafixes, one test MobType for two mobs.rs tests
datafix: LeavesFix's getSideMask is ChunkPalettedStorageFix's, line for
line; chunk_leaves uses chunk_flatten's. mobs.rs: push_tests takes
sensor_scan_tests' 46-line cow MobType instead of its own copy.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 16:30:46 +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 3d4b63c19e wasmtime 48 LTS: one wasm-tools version for the runtime, packing and the SDK
wasmtime 36 read WebAssembly with wasm-tools 0.236, wit-component (cargo
horizon-mod, the sandbox tests) used 0.239 and the SDK's wit-bindgen 0.247:
three copies of wasmparser, wasm-encoder, wit-parser and wasm-metadata.
wasmtime 48, the next long-term-support line (fixes until August 2028),
wit-component 0.254 and wit-bindgen 0.60 all use 0.254. No code change;
horizon-mods now needs Rust 1.95.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 15:25:10 +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 Marie 4fe9d19ca3 Merge pull request #1265 from rdbtCVS/t3/revert-pr-1257-server-threading
🤖 Agent: merged after verify passed and a local run on the Mac mini. lan_e2e on the server thread passes for host and guest: boats, minecarts, frames, ladder, lightning, riding and mounts. Parity --changed passed 25 of 25, and the server-thread default is confirmed. Single player runs on its server thread again. Thanks @rdbtCVS for pushing for the root fix.
2026-10-23 03:16:47 +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 Marie 602e86e093 Merge pull request #1132 from ludovic111/feat/switch-to-0-2
🤖 Agent: merged after verify passed. Go-day step 5 (docs/versioning.md): Ludovic said go on 2026-10-23 ("tu peux envoyer la 0.2"). The bridges are out (v2026.10.22 and launcher-v0.17.0), and the latest stable is v2026.10.22.07, built after the freeze. The launcher redesign (#1269) and the mod browser are merged.
2026-10-23 02:16:49 +02:00
Ludovic Marie 8b0152bd11 Merge remote-tracking branch 'origin/main' into sw1132 2026-10-23 02:09:13 +02:00
Ludovic Marie 1c7656ee9b Merge pull request #1269 from ludovic111/feat/design-gaming
🤖 Agent: merged after verify passed. Ludovic approved the redesign on 2026-10-23. Integration run on the desktop by integ-w3 (main + the mod browser stack + this), then main merged and re-checked on the Mac mini (launcher clippy, tests).
2026-10-23 02:08:43 +02:00
Ludovic Marie 91f42fb633 Merge remote-tracking branch 'origin/main' into l269-merge 2026-10-23 02:01:27 +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 43a6fe0f7d Merge main into #1265: mount inventories for a server's players
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 01:53:53 +02:00
Ludovic Marie 92b0ee1347 Merge pull request #1325 from ludovic111/fix/server-mount-inventory
🤖 Agent: merged after verify passed and the local parity run (102 of 102 at a4e68e6a, posted on the PR); lan_e2e mounts passes in both modes.
2026-10-23 01:52:24 +02:00
Ludovic MarieandClaude Opus 5.5 91dc5b0f18 Merge main into #1265: riding for a server's players
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 01:52:02 +02:00
Ludovic Marie 014fc57b97 Merge pull request #1246 from ludovic111/feat/modbrowser-modpacks-port
🤖 Agent: merged after verify passed and the integration run on the desktop (integ-w3: main + the mod browser stack + #1269; fmt, clippy, 1224 + 131 tests, mod_browser_harness ok).
2026-10-23 01:46:33 +02:00
Ludovic Marie a9ccd5458c Merge pull request #1243 from ludovic111/feat/modbrowser-project-page
🤖 Agent: merged after verify passed and the integration run on the desktop (integ-w3: main + the mod browser stack + #1269; fmt, clippy, 1224 + 131 tests, mod_browser_harness ok).
2026-10-23 01:45:07 +02:00
Ludovic Marie 3e99730b98 Merge remote-tracking branch 'origin/main' into sw1132 2026-10-23 01:43:49 +02:00
Ludovic Marie f7b6777af6 Merge pull request #1244 from ludovic111/feat/modbrowser-e2e-harness
🤖 Agent: merged after verify passed and the integration run on the desktop (integ-w3: main + the mod browser stack + #1269; fmt, clippy, 1224 + 131 tests, mod_browser_harness ok).
2026-10-23 01:43:40 +02:00
Ludovic Marie 00b438d0ce Merge pull request #1242 from ludovic111/feat/modbrowser-java-launcher
🤖 Agent: merged after verify passed and the integration run on the desktop (integ-w3: main + the mod browser stack + #1269; fmt, clippy, 1224 + 131 tests, mod_browser_harness ok).
2026-10-23 01:43:13 +02:00
Ludovic Marie 990c609269 Merge pull request #1230 from ludovic111/feat/modbrowser-java-verdicts
🤖 Agent: merged after verify passed and the integration run on the desktop (integ-w3: main + the mod browser stack + #1269; fmt, clippy, 1224 + 131 tests, mod_browser_harness ok).
2026-10-23 01:39:39 +02:00
Ludovic Marie 9cfed309ac Merge pull request #1240 from ludovic111/fix/mod-item-network-ids
🤖 Agent: merged after verify passed and the integration run on the desktop (integ-w3: main + the mod browser stack + #1269; fmt, clippy, 1224 + 131 tests, mod_browser_harness ok).
2026-10-23 01:38:13 +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 Marie 900bba1269 Merge remote-tracking branch 'origin/main' into l269-merge
# Conflicts:
#	docs/design-system.md
2026-10-23 01:31:40 +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
Ludovic Marie 8500adab0c Merge pull request #1314 from ludovic111/fix/server-riding
🤖 Agent: merged after verify passed and the local parity run (102 of 102, posted on the PR); lan_e2e riding passes in both modes.
2026-10-23 01:29:03 +02:00
Ludovic MarieandClaude Opus 5.5 5858cb113e Java shader packs: the shadow map leaves out sections whose shadow can't reach the view
A section's shadow is its box swept away from the light; when the box
is outside one of the camera frustum's planes and the sweep never
crosses it, nothing it casts can be seen (Iris's advanced shadow
culling, conservatively). The picture is the same; BSL's 256-block
shadow distance draws far fewer sections.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 01:21:16 +02:00
Ludovic Marie 8b52929723 Merge pull request #1324 from ludovic111/feat/site-live-count
🤖 Agent: merged after verify passed; the Chromium checks are in the description.
2026-10-23 01:20:57 +02:00
Ludovic MarieandClaude Opus 5.5 a4e68e6a53 Mount inventories for a server's players, and llama inventories
A server's players (single player on its server thread, LAN guests,
dedicated-server players) get the horse inventory: shift + use on a tamed
horse, donkey, mule, camel or llama, or the inventory key while riding one
(ServerboundPlayerCommandPacket OPEN_INVENTORY), opens HorseInventoryMenu
over the mount with ClientboundMountScreenOpenPacket. Clicks run on the
menu engine's new Kind::Mount (saddle and body ArmorSlots that take what
the mount may wear, the chest, AbstractMountInventoryMenu.quickMoveStack),
and the slots go back to the mount with the item's equip sound.

The client shows a server's mount screen, predicts its clicks with the
same engine and sends OPEN_INVENTORY when the inventory key is pressed
while riding. Llamas now have their inventory in the window too: the
carpet slot and a chest of `strength` columns, no saddle slot.

lan_e2e `mounts` checks it for the host and a guest, with and without
--server-thread.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 01:20:51 +02:00
Ludovic MarieandClaude Opus 5.5 4eb7a98330 Website: "Playing now" on the home page, from the telemetry server's live count
The home page's "At a glance" list gets a fifth row with how many play right now, read from
https://telemetry.playhorizon.dev/v1/live (3 s timeout, again every minute while the tab is
visible). It stays hidden while nobody plays or the server doesn't answer. The FAQ no longer
says the "I'm playing" ping is off: the server is live since #1276.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 01:19:42 +02:00
Ludovic Marie 27bf66b354 Merge pull request #1274 from ludovic111/feat/site-releases-bench
🤖 Agent: merged after verify passed; Ludovic approved the site design. The Releases and Benchmarks pages go live with this.
2026-10-23 01:14:23 +02:00
Ludovic Marie b3e9d8ebd1 Merge pull request #1276 from ludovic111/feat/telemetry-live
🤖 Agent: merged after verify passed. telemetry.playhorizon.dev is live on Railway (service telemetry in horizon-site, volume /data): /healthz, /v1/live (a test ping counted) and /v1/benchmarks/summary (CORS for playhorizon.dev) answer. The PR's local end-to-end runs are in the description.
2026-10-23 01:14:18 +02:00
Ludovic Marie baf013dd3f Merge remote-tracking branch 'origin/main' into feat/site-releases-bench 2026-10-23 01:12:59 +02:00
Ludovic Marie e310a5ca5e Merge pull request #1259 from ludovic111/feat/website-blocks
🤖 Agent: merged after verify passed; Ludovic approved the site design.
2026-10-23 01:12:08 +02:00
Ludovic MarieandClaude Opus 5.5 c7f6903144 Merge feat/modbrowser-modpacks-port (now with main) into feat/design-gaming
launcher/src/main.rs: main (#1271) removed the Game menus setting (one menu
style now) where this branch restyled Settings. Kept the Game and About
group headings, dropped the Game menus row as main does, and renumbered the
rows after it (game arguments n+4, Horizon n+5, Launcher n+6).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 01:09:23 +02:00
Ludovic MarieandClaude Opus 5.5 05497ddd10 Merge origin/main into fix/mod-item-network-ids
No conflicts: brings main (with #1271) so CI tests this head on current main.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 01:08:34 +02:00
Ludovic MarieandClaude Opus 5.5 038e52f87e Merge feat/modbrowser-project-page (now with main) into feat/modbrowser-modpacks-port
No conflicts: carries the stack's merge of origin/main.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 01:08:33 +02:00
Ludovic MarieandClaude Opus 5.5 69887b87d1 Merge feat/modbrowser-java-launcher (now with main) into feat/modbrowser-project-page
No conflicts: carries the stack's merge of origin/main.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 01:08:33 +02:00
Ludovic MarieandClaude Opus 5.5 4786d56d5b Merge feat/modbrowser-java-launcher (now with main) into feat/modbrowser-e2e-harness
No conflicts: carries the stack's merge of origin/main.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 01:08:32 +02:00
Ludovic MarieandClaude Opus 5.5 a5d68dc890 Merge feat/modbrowser-java-verdicts (now with main) into feat/modbrowser-java-launcher
No conflicts: carries #1230's merge of origin/main up the stack.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 01:08:31 +02:00
Ludovic MarieandClaude Opus 5.5 9ae041ccd3 Merge origin/main into feat/modbrowser-java-verdicts
No conflicts: brings main (with #1271) under the mod browser stack.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 01:08:30 +02:00
Ludovic Marie 63507e7c26 Merge remote-tracking branch 'origin/main' into feat/website-blocks
# Conflicts:
#	docs/design-system.md
2026-10-23 00:58:57 +02:00
Ludovic Marie 52aa397b26 Merge pull request #1271 from ludovic111/feat/vanilla-menus
🤖 Agent: merged after verify passed. Local proof on the merged head bf274d92: fmt, clippy -D warnings, client and launcher tests, parity --changed origin/main, and test_friends.py --server-thread. CI parity passed on the earlier head. Every Horizon setting is reachable from vanilla-style screens (list in the PR body).
2026-10-23 00:56:15 +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
Ludovic MarieandClaude Opus 5.5 33dcc66379 Merge main into #1265: keep the vehicle checks, take the server-thread default
lan_e2e takes main's version: #1280 kept `boats` (passing with and
without --server-thread) and added `minecarts`, `frames` and `lightning`
next to `ladder`, and main's run() already sets HORIZON_SERVER_THREAD
explicitly as this branch does. The code keeps this branch's change:
single player runs on its server thread by default again.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 00:42:15 +02:00
Ludovic MarieandClaude Opus 5.5 525129fbe6 Riding animals for a server's players
Single player on its server thread, LAN guests and dedicated-server
players could not ride: a server player's turn had no mount input, and
its client had nothing to sit on.

The server now drives a server player's animal from that player's
PlayerInput (forward, back, strafe, jump, sneak, sprint) and rotation,
through the same tick_mount_riding the window's player uses:
- the jump bar charges per player (MountState swapped with the turn);
- sneaking gets off, with SetPassengers and the teleport beside it;
- a mount ridden by another player can't be taken or fed (isVehicle);
- an animal whose rider left stops taking its input.
The client sits where positionRider puts it on the animal it was sent,
and charges its own jump bar for the HUD.

Ridden movement (pre-existing, also in the window): Mob.setSpeed sets zza
too, so the ridden input set before the speed was lost and pigs and
striders crawled while horses walked on with no key held; the input is
now set after the speed (travelRidden). The ridden speed uses the
effective movement speed (a cold strider's modifier, potions), and a
shivering strider rides at 0.35 of it (Strider.getRiddenSpeed).

lan_e2e riding: the host and a guest saddle and ride a horse (walk and
jump), a pig and a strider with their sticks, a camel and a donkey, sit
on a llama, harness and fly a happy ghast, and get off, with and
without --server-thread. `mobs` reports ridden, controlled, saddled.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 00:40:34 +02: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 bf274d9254 Merge origin/main into feat/vanilla-menus
With #1285 Play with Friends works on the server thread: friends_view
offers sharing there, so the pause menu's Friends icon is active on both
paths and inactive only on another server.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 00:04:42 +02:00
Ludovic Marie 07e7179773 Merge pull request #1285 from ludovic111/fix/friends-server-thread
🤖 Agent: merged on verify plus local proof. tools/test_friends.py passes with the host on its server thread and in the frame (code, self-hosted relay and short code, stop). Parity --changed: 22/0. Whichever of #1271 and this lands second drops #1271's server-thread greying of the Friends icon.
2026-10-22 23:56:10 +02:00
Ludovic Marie a161487994 Merge pull request #1280 from ludovic111/fix/1222-server-vehicles
🤖 Agent: merged on verify plus local proof, since CI parity is queued behind the Mac mini runners. Parity --changed origin/main on this head: 102/0. lan_e2e passes with and without --server-thread, for the host and for a LAN guest: boats, minecarts, frames, ladder, lightning. The default is unchanged here; #1265 follows once riding animals work for a server's players too.
2026-10-22 23:55:38 +02:00
Ludovic MarieandClaude Opus 5.5 fe6ae937ef Java shader packs: the pack's shadow programs draw the shadow map
A Java pack's shadow program (shadow_cutout, shadow or shadow_solid for
the solid layer, shadow_water or shadow for the translucent one) draws
Horizon's terrain from the light after begin, as Iris's ShadowRenderer:

- shadowModelView and shadowProjection are Iris's ShadowMatrices, with
  the pack's shadowMapResolution, shadowDistance, near and far planes,
  shadowIntervalSize and sunPathRotation; shadow programs get them as
  their model view and projection.
- The renderer picks every resident section inside the light's box (no
  cave culling, no face culling) and draws it instanced, solid then
  translucent, with origins after the camera's.
- shadowtex0 holds everything, shadowtex1 the depth before translucent
  terrain, bound as depth textures for comparisons and as float copies
  for plain reads; shadowcolor0..7 take the pack's formats and clears
  (white by default).

A shadow program that fails to compile leaves the pack without its
shadow map, with a warning; its terrain programs still run.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 23:41:18 +02:00
Ludovic MarieandClaude Opus 5.5 8c2f683e0a Merge origin/main into feat/vanilla-menus
Conflict: #1237's weather now returns its own vertices; the title draws
none (the live title backdrop is gone).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 23:15:35 +02:00
Ludovic Marie 190faa4f55 Merge pull request #1268 from rdbtCVS/perf/surface-biome-cells
🤖 Agent: merged after verify passed and the contributor integration on the desktop (v5: workspace tests 1887/0, naga 0 failures, parity --changed 304/0). Rendering checked with before/after scenes: the screenshots match, and per-frame CPU and GPU time goes down.
2026-10-22 23:07:27 +02:00
Ludovic MarieandClaude Opus 5.5 c344a26e80 Java shader packs: occlusion culling while the pack draws the terrain
The pack's terrain program draws the solid sections the last test saw,
the boxes of the others are tested against the pack's depth in a pass
of their own (OpenGL's layout: y flipped, depth 0 near, so a second
depth-only culling pipeline), and the program then draws the sections
in sight from the culling's phase 2 draws. The flat path, which has no
occlusion culling, is unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 23:05:26 +02: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 Marie 36aa1ec0a2 Merge pull request #1237 from rdbtCVS/perf/cache-rain-snow-params-157e7038
🤖 Agent: merged after verify passed and the contributor integration on the desktop (v5: workspace tests 1887/0, naga 0 failures, parity --changed 304/0). Rendering checked with before/after scenes: the screenshots match, and per-frame CPU and GPU time goes down.
2026-10-22 23:00:50 +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 Marie 8ddb74031c Merge pull request #1273 from rdbtCVS/perf/end-island-volumes
🤖 Agent: merged after verify passed and the contributor integration on the desktop (v7: core 99 and worldgen 81 tests, 23 worldgen/density/noise parity harnesses 23/0; bit-exact by construction, with a test against the scalar path).
2026-10-22 22:52:05 +02:00
Ludovic MarieandClaude Opus 5.5 7f7a05cbd0 Play with Friends on single player's server thread
Sharing a world with an invite code refused with the integrated server
on its own thread ("open a singleplayer world first"): the window is a
client there. It now shares that server as Open to LAN publishes it:
friends_start publishes it when it isn't, the invite tunnel points at
its port, stopping sharing unpublishes it again, and the pause menu
shows the Friends entry with the players the server lists.

test_friends.py --server-thread runs the host's world on its server
thread; the host's own move and blocks go through /tp, /fill and
/setblock, which the server answers in both modes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 22:49:31 +02:00
Ludovic MarieandClaude Opus 5.5 e5fbba281c Releases: a title made of issue numbers reads its line from the notes
"Horizon 2026.10.22.07: #1255, mob pushes, fix, #1222" now shows "Single player runs in the frame again
until server players have vehicles and 3 more".

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 22:36:45 +02:00
Ludovic MarieandClaude Opus 5.5 63d59b26cf Benchmarks: the ThinkPad's runs, and Java's per-scene wins
- ThinkPad (i7-7500U, HD Graphics 620, Mesa): Horizon v2026.10.22.04 (two flights per scene) against vanilla
  26.3 (two games) at render distance 12. Java loads chunks faster there and flies the mountain faster; the
  page says so. Render distance 32 isn't published for it: neither game loaded the spawn in 5 minutes, and
  Horizon kept only 900 to 1,400 columns resident when given 25.
- "Where Java is ahead" also lists scenes where vanilla's average beats Horizon's by more than 5%.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 22:35:05 +02:00
Ludovic MarieandClaude Opus 5.5 72b749cdb0 Merge origin/main into feat/vanilla-menus
Conflicts: the menu blur (#1249) now runs without the Horizon style check,
chat and the tab list draw under every screen as before the blur split,
and the HUD's second range draws without the removed colour grade. Play
with Friends blurs the world behind it like vanilla's other menus.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 22:30:12 +02:00
Ludovic MarieandClaude Opus 5.5 1a8dae9f21 lan_e2e boats: the pool in a stone basin, for worlds that float
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 22:23:32 +02:00
Ludovic MarieandClaude Opus 5.5 5d77ace9a4 Merge main into #1237: keep #1234's column table and #1238's bolt quads
#1234 replaced column_size with a LazyLock table and #1238 added the
lightning quad count to gfx.rs's max chain; this branch rewrote both
places. weather.rs keeps this branch's code with #1234's table and tests;
gfx.rs takes the max over cloud, lightning and weather quads.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 22:22:19 +02:00
Ludovic MarieandClaude Opus 5.5 afd3874482 Lightning for a server's players: the bolt and the sky flash
With single player on its server thread the window drew no lightning,
and LAN guests and dedicated-server players never did: the server never
sent the bolt (it lived outside the tracked entities) and the client had
nothing to draw for a lightning_bolt AddEntity.

The server gives each bolt an entity id and tracks it like the other
entities; the client adds a visual-only bolt for it (LightningBolt.tick
off the server: sounds, the sky flash, the drawn bolt) and ticks bolts
and the flash in its level tick. `status` reports bolts and flashes, and
lan_e2e's `lightning` check summons one beside each player and asserts
both sides get the bolt and the flash.

lan_e2e: the vehicle checks put the players apart first, and run()
sets HORIZON_SERVER_THREAD explicitly either way, as #1265 does.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 22:16:05 +02:00
Ludovic MarieandClaude Opus 5.5 16b7e60074 Fix #1222 at the root: vehicles for a server's players
Single player on its server thread, LAN guests and dedicated-server
players could not board, drive or break boats and minecarts, nor use
item frames and armor stands: the client sent Interact/Attack only for
mobs, the server aimed only at mobs, and nothing carried riding over
the network.

Client (remote.rs, remote_vehicles.rs):
- Interact/Attack for boats, minecarts, item frames, armor stands and
  paintings, with the hit location (Minecraft.hitResult).
- SetPassengers seats the player; a boat ticks on the rider's client and
  goes to the server as MoveVehicle and PaddleBoat every tick; a
  MoveVehicle back puts it where the server says; a minecart moves on the
  server and the rider sits in it.

Server (lan_vehicles.rs, lan_actions.rs, hub.rs):
- look_at_entity aims at every pickable object, at the client's hit point.
- The guest's vehicle is its Entity.vehicle during its turn; tick_boats
  and tick_carts leave guest-ridden vehicles to their riders.
- handleMoveVehicle with its checks (invalid values kick, too quickly,
  moved wrongly, colliding with anything new), handlePaddleBoat.
- Player.rideTick: sneaking gets out, then SetPassengers and the
  teleport beside the vehicle (dismountTo); a minecart moves with the
  rider's input; a passenger's moves only turn it.
- SetPassengers for guests through hub.riding; one rider per vehicle.

lan_e2e: boats (host and guest board, paddle, the other side sees the
boat move, sneak out, break), minecarts and frames (item frame and armor
stand) for both, passing with and without --server-thread.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 22:16:05 +02:00
Ludovic Marie 83fa14e51b Merge pull request #1235 from rdbtCVS/t3/cache-end-sky-mesh
🤖 Agent: merged after verify passed and the contributor integration on the desktop (v5: workspace tests 1887/0, naga 0 failures, parity --changed 304/0). Rendering checked with before/after scenes: the screenshots match, and per-frame CPU and GPU time goes down.
2026-10-22 22:12:48 +02:00
Ludovic Marie 7a0bdbc05e Merge pull request #1238 from rdbtCVS/t3/cache-lightning-bolt-geometry
🤖 Agent: merged after verify passed and the contributor integration on the desktop (v5: workspace tests 1887/0, naga 0 failures, parity --changed 304/0). Rendering checked with before/after scenes: the screenshots match, and per-frame CPU and GPU time goes down.
2026-10-22 22:12:23 +02:00
Ludovic Marie 67277d4571 Merge pull request #1234 from rdbtCVS/t3/cache-column-size-table
🤖 Agent: merged after verify passed and the contributor integration on the desktop (v5: workspace tests 1887/0, naga 0 failures, parity --changed 304/0). Rendering checked with before/after scenes: the screenshots match, and per-frame CPU and GPU time goes down.
2026-10-22 22:10:50 +02:00
Ludovic MarieandClaude Opus 5.5 d3bad12383 Merge main into #1268: keep the End shortcut around the cached surface pass
#1264 wrapped build_surface in `if !self.identity_end_surface(pos)` where
this branch adds the uniform_biomes field to the same SurfaceQueries. Kept
the condition with the new field inside it; both tests are kept.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 22:04:39 +02:00
Ludovic Marie a9682c5554 Merge pull request #1264 from rdbtCVS/perf/end-identity-surface
🤖 Agent: merged after verify passed and the wave 2 integration run on the desktop (main + 16 contributor PRs, tree 71afc562: workspace tests 1887/0, launcher 113, naga translations 0 failures, parity --changed 304/0).
2026-10-22 22:02:53 +02:00
Ludovic Marie 663b11d8dd Merge pull request #1251 from rdbtCVS/t3/fix-firework-tooltip-display
🤖 Agent: merged after verify passed and the wave 2 integration run on the desktop (main + 16 contributor PRs, tree 71afc562: workspace tests 1887/0, launcher 113, naga translations 0 failures, parity --changed 304/0).
2026-10-22 22:01:25 +02:00
Ludovic Marie 4779b48d2b Merge pull request #1250 from rdbtCVS/t3/fix-slash-command-autocomplete
🤖 Agent: merged after verify passed and the wave 2 integration run on the desktop (main + 16 contributor PRs, tree 71afc562: workspace tests 1887/0, launcher 113, naga translations 0 failures, parity --changed 304/0).
2026-10-22 22:01:00 +02:00
Ludovic Marie 01f1a0360d Merge pull request #1249 from rdbtCVS/t3/fix-menu-background-blur
🤖 Agent: merged after verify passed and the wave 2 integration run on the desktop (main + 16 contributor PRs, tree 71afc562: workspace tests 1887/0, launcher 113, naga translations 0 failures, parity --changed 304/0).
2026-10-22 21:59:29 +02:00
Ludovic Marie dcb3749ed1 Merge pull request #1248 from rdbtCVS/t3/fix-block-place-animation
🤖 Agent: merged after verify passed and the wave 2 integration run on the desktop (main + 16 contributor PRs, tree 71afc562: workspace tests 1887/0, launcher 113, naga translations 0 failures, parity --changed 304/0).
2026-10-22 21:57:58 +02:00
Ludovic Marie 96a031792b Merge pull request #1247 from rdbtCVS/t3/fix-q-drop-animation
🤖 Agent: merged after verify passed and the wave 2 integration run on the desktop (main + 16 contributor PRs, tree 71afc562: workspace tests 1887/0, launcher 113, naga translations 0 failures, parity --changed 304/0).
2026-10-22 21:56:24 +02:00
Ludovic Marie 08a73694ab Merge pull request #1245 from rdbtCVS/t3/fix-instant-mined-block-delay
🤖 Agent: merged after verify passed and the wave 2 integration run on the desktop (main + 16 contributor PRs, tree 71afc562: workspace tests 1887/0, launcher 113, naga translations 0 failures, parity --changed 304/0).
2026-10-22 21:55:59 +02:00
Ludovic Marie fb7665d88e Merge pull request #1231 from rdbtCVS/perf/saved-light-loading
🤖 Agent: merged after verify passed and the wave 2 integration run on the desktop (main + 16 contributor PRs, tree 71afc562: workspace tests 1887/0, launcher 113, naga translations 0 failures, parity --changed 304/0).
2026-10-22 21:54:19 +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 6a58fc20da Java shader packs: the pack's gbuffers_terrain and gbuffers_water draw the terrain
When a Java pack has a terrain program (gbuffers_terrain, or the program
Iris falls back to), Horizon's world pass skips its own terrain and the
pack's program draws every visible section into its render targets,
tested against the depth of the rest of the world; gbuffers_water draws
the translucent layer before the first composite with the pack's blend
modes. depthtex0/depthtex1 get the depth before and after water.

- translate: a geometry mode (group 2, attributes as globals a glue
  function fills before main, alpha test) and a pass that ends switch
  cases which return, which naga's WGSL writer otherwise rejects
  (Photon).
- java_terrain: the glue decoding Horizon's terrain vertices from the
  arena pages into Iris's attributes, the block.properties id table by
  sprite name, and a 2D atlas built from the sprite arrays (animated
  sprites copied in).
- RGBA16/RG16/R16 targets are 16-bit unorm where the GPU renders them:
  Chocapic13' packs its G-buffer into them.

MakeUp Ultra Fast, Chocapic13' and BSL draw their own terrain and water
on the M4 Pro; Photon still draws black (compute and .dat textures).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 21:50:37 +02:00
Ludovic MarieandClaude Opus 5.5 be16401f90 Telemetry: send to telemetry.playhorizon.dev, a benchmarks summary, the Railway config
- DEFAULT_ENDPOINT is https://telemetry.playhorizon.dev. The heartbeat, the launcher's live count and the
  outbox upload all go there; HORIZON_TELEMETRY_URL still names another server, and HORIZON_TELEMETRY_URL=off
  sends nothing at all. CI workflows and the harnesses that start the game or the launcher set off. Consent
  is unchanged: the four switches stay opt-in, only the anonymous ping is always on.
- GET /v1/benchmarks/summary: players' benchmark uploads grouped by GPU, CPU, render distance and game, groups
  of 3+ installs only, medians per install then across installs, no install ID, day, OS version, driver,
  memory or screen. Cached 10 minutes, CORS for the website. Same rules as website/scripts/benchmarks.mjs,
  tested on the same fixtures; a deleted install leaves its groups at once.
- /v1/aggregate no longer lists the newest 50 raw benchmark reports (hardware, driver, day, one install each):
  uploads are published only through the summary.
- Behind Railway's proxy the rate limit counts X-Real-IP, else the last X-Forwarded-For entry (the one the
  proxy adds; a client could choose the first). Still in memory only, never stored or logged.
- crates/horizon-telemetry-server/railway.json: Dockerfile build from the repository root, health check
  /healthz, one replica in europe-west4 (one SQLite file on the /data volume).
- Docs: docs/telemetry.md, the server README (the summary's shape, Railway setup), the privacy page (the ping
  is on, where the data lives, how benchmark uploads are shown).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 21:46:32 +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 fd9e6a7e78 Benchmarks: the Mac mini's runs, Horizon v2026.10.22.04 against vanilla 26.3
Three runs of each game at render distance 12 and 32 (tools/perf_bench.py, seed 1234, 1280x720, VSync off).
Horizon's average sits at the 144 Hz display (macOS presents in step with it even with VSync off): marked as
capped on the page.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 21:43:37 +02:00
Ludovic MarieandClaude Opus 5.5 aae60c16ba Website: a Releases page, "Recently added" on the home page, and a Benchmarks page
- /releases: every game and launcher release on one page, newest first, Stable / Alpha / Launcher marked and
  filterable. The newest three are open; each shows what it added and fixed, the full notes, its downloads and
  a link to GitHub. Baked at build time, refreshed from GitHub when a newer release is out. /releases#release-<tag>
  opens one.
- Home: "Recently added" replaces the patch notes block: the last five releases on one line each and three
  highlights from what they added.
- /benchmarks: Horizon against vanilla Java Edition 26.3 measured on our machines (average FPS, 1% lows, p95
  frame time, start to world, chunk loads, memory, at render distance 12 and 32), saying where Java is ahead;
  a GPU + CPU picker that shows a measurement when there is one and otherwise a marked estimate with a range,
  from our own spec table (method explained on the page); players' results read live from the telemetry
  server's GET /v1/benchmarks/summary, falling back to the baked public/benchmarks.json, groups of 3+ installs
  only.
- scripts/benchmarks.mjs builds public/benchmarks.json from our reports (and, for tests, telemetry items) with
  the same rules as the server; fixtures shared with the server's test.
- Releases and Benchmarks in the nav and footer; docs/performance.md links the page.
- Pixelify Sans' "fi" ligature drew "fixes" as "Axes": common ligatures off.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 21:43:36 +02:00
rdbtCVS 4e04e97565 Reuse eligible End island candidates across bounded density volumes 2026-10-22 20:29:00 +01:00
Ludovic MarieandClaude Opus 5.5 fc8643b380 Docs: settings screenshots show the vanilla Video Settings and Advanced Graphics screens
Captured on the Mac mini with the game's offscreen capture (1280x720),
replacing the Horizon menu style's Video page.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 21:06:51 +02:00
Ludovic MarieandClaude Opus 5.5 954bb6a1fa Play with Friends: the pause icon takes controller focus, help moves to a Troubleshooting button
The pause menu's Friends icon is recorded as a widget, so a controller
reaches it and the control socket names it. The sharing screen's long help
URL ran off the screen: a Troubleshooting... button beside Done opens it,
and the hint drops out when it would reach the footer. The graphics docs
say what the two Horizon screens hold (vanilla's own settings stay on Video
Settings).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 21:06:51 +02:00
Ludovic MarieandClaude Opus 5.5 042adc674a One menu style, vanilla's: Horizon's settings join vanilla's Options
Drop the Horizon menu style (options_horizon.rs, front.rs, the live title
backdrop and its post grade, the launcher's menu-style setting, CLI and MCP
tool). Every screen is vanilla's. Horizon's own settings hang off vanilla's
Options the way mods add theirs: Horizon Settings... and Mods... take the
Telemetry/Credits slots, and Horizon Settings opens Graphics, Advanced
Graphics, Shader Packs, Controller Settings, Extras and Mods, each a vanilla
options list with tooltips. Play with friends moves behind vanilla's Friends
buttons: the pause menu's opens Play with Friends (share, copy, stop), the
title screen's opens Join a Friend, and Direct Connection takes a full code.

Old files load as before: horizon.txt's menuStyle line and
horizon-graphics.txt's level line are ignored.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 21:06:51 +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
rdbtCVS cd494e74ba Cache uniform resident biome cells during surface generation 2026-10-22 19:45:38 +01:00
Ludovic MarieandClaude Opus 5.5 f1f8170c58 Launcher: the offline name without its key, and Home before any version
The game keeps the offline name as "name:Player12345" in horizon-player.txt;
the top bar and the friends-only server list showed the whole line. Both
now read the name after the key.

With no version installed, Home has no landscape: its title starts below
the top bar like the other pages, and the sidebar says "No version
installed" instead of "Needs preparing".

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 20:33:35 +02:00
Ludovic MarieandClaude Opus 5.5 700c9ec8cd Merge main into #1264: keep the --snapshot usage line
main's warmed-timing change and this branch both edited the usage comment
at the top of examples/chunkgen_bench.rs. Kept this branch's line, which
adds [--snapshot PATH]; no code change.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 20:29:09 +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
rdbtCVS 35ffbebcdd Revert #1257: keep the integrated server thread enabled by default
Revert commit cdaf1d79a0, preserving the ladder regression check added afterward.

Server-player bugs also affect dedicated servers and Open to LAN. Running single player in the frame only hides those bugs there; keep the shared server-thread path as the default and fix the underlying issues.
2026-10-22 19:18:03 +01:00
Ludovic Marie 1ec2762fe6 Merge pull request #1263 from ludovic111/fix/1255-ladder-fall
🤖 Agent: merged on verify plus local harness proof (CI parity is queued behind about 15 runs). The new lan_e2e ladder check: on main, ten blocks of ladder took the guest from 20 to 13 health; with the fix it lands at full health. Main was merged in, keeping both the ladder and boats checks.
2026-10-22 20:14:47 +02:00
Ludovic MarieandClaude Opus 5.5 2f8d913b85 Launcher: long lists draw only the rows near the screen
Versions and Patch notes list a hundred releases, and every frame laid out
and painted all of them. Rows more than a screen away from the viewport are
now empty blocks as tall as when they were last drawn (cull.rs), so the
page keeps its length; a row being installed or showing an error is always
drawn. Scrolling Versions on the Mac mini: median frame 8.2 -> 7.0 ms.

HORIZON_LAUNCHER_PROJECT=<slug> opens a project's page on the Mods page,
for screenshots, as HORIZON_LAUNCHER_PAGE opens a page.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 20:14:29 +02:00
Ludovic Marie 6c4309d36f Merge pull request #1205 from pirut/claude/upstream-mob-pushes
🤖 Agent: merged after verify passed and the integration run on the desktop (main 34c3c1cf + the 14 contributor PRs: workspace tests 1,869/0, launcher 113, parity --changed 320/0).
2026-10-22 20:11:51 +02:00
Ludovic Marie 95d0a06ba5 Merge pull request #1228 from ItsPotatoTime/fix/entity-metadata
🤖 Agent: merged after verify passed and the integration run on the desktop (main 34c3c1cf + the 14 contributor PRs: workspace tests 1,869/0, launcher 113, parity --changed 320/0).
2026-10-22 20:05:23 +02:00
Ludovic MarieandClaude Opus 5.5 90afc3dbf4 Merge main into fix/1255-ladder-fall (lan_e2e: keep both the ladder and boats checks)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 20:04:28 +02:00
Ludovic Marie cdaf1d79a0 Merge pull request #1257 from ludovic111/fix/1222-boat-interact
🤖 Agent: merged on verify plus local harness proof. It is a release-blocker fix, and CI parity is queued behind about 15 runs on the Mac mini. lan_e2e boats fails on main with --server-thread (riding None) and passes with this PR's default (in the frame, the path that was the default until #1114). fmt and clippy -D warnings pass. Server players' vehicles are tracked in FEATURES.md 4x.
2026-10-22 20:03:55 +02:00
Ludovic Marie d68ca84275 Merge pull request #1223 from rdbtCVS/perf/chunk-palette-loads
🤖 Agent: merged after verify passed and the integration run on the desktop (main 34c3c1cf + the 14 contributor PRs: workspace tests 1,869/0, launcher 113, parity --changed 320/0).
2026-10-22 20:01:51 +02:00
Ludovic MarieandClaude Opus 5.5 270ba3fea9 Launcher: a blocky, Minecraft-like look in dark and light
Blocks palettes (dark default, light, or the system's, chosen in Settings), Pixelify Sans for
headings and controls, Pixelarticons icons, square panels with an outline, a bevel and a lip,
inventory tabs, and pixel art drawn in code: a stone texture behind the pages and a landscape
behind Home. A top bar with the pages and the account chip, and a sidebar with the instances
(Horizon shows the grass block the mod browser draws from the player's prepared textures), the
hosted servers, and the launcher's version and channel. The mod browser's pages (kinds as tabs,
project icons in slots, verdicts as badges, the project page) are restyled, not rebuilt. The
benchmark also measures scrolling the Versions list.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 20:01:13 +02:00
Ludovic Marie 4f4dc33370 Merge pull request #1214 from pirut/claude/upstream-mojang-java
🤖 Agent: merged after verify passed and the integration run on the desktop (main 34c3c1cf + the 14 contributor PRs: workspace tests 1,869/0, launcher 113, parity --changed 320/0).
2026-10-22 20:00:23 +02:00
Ludovic MarieandClaude Opus 5.5 59adff16ff Merge main into #1205: keep push_tests next to teams_tests and sensor_scan_tests
#1200 and the teams change appended test modules at the same place as this
branch's push_tests. Main's modules are kept as they are and push_tests
follows them unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 19:57:27 +02:00
Ludovic Marie 160076e15f Merge pull request #1229 from rdbtCVS/perf/aquifer-neighborhood
🤖 Agent: merged after verify passed and the integration run on the desktop (main 34c3c1cf + the 14 contributor PRs: workspace tests 1,869/0, launcher 113, parity --changed 320/0).
2026-10-22 19:54:21 +02:00
Ludovic Marie 658d54e578 Merge pull request #1225 from pirut/claude/upstream-macos-pointer-lock
🤖 Agent: merged after verify passed and the integration run on the desktop (main 34c3c1cf + the 14 contributor PRs: workspace tests 1,869/0, launcher 113, parity --changed 320/0).
2026-10-22 19:54:10 +02:00
Ludovic Marie 3576a7a1c0 Merge pull request #1208 from pirut/claude/upstream-perlin-floor
🤖 Agent: merged after verify passed and the integration run on the desktop (main 34c3c1cf + the 14 contributor PRs: workspace tests 1,869/0, launcher 113, parity --changed 320/0).
2026-10-22 19:54:05 +02:00
Ludovic Marie 0ed1bd8deb Merge pull request #1207 from pirut/claude/upstream-feature-speedups
🤖 Agent: merged after verify passed and the integration run on the desktop (main 34c3c1cf + the 14 contributor PRs: workspace tests 1,869/0, launcher 113, parity --changed 320/0).
2026-10-22 19:53:54 +02:00
Ludovic Marie f254871c1f Merge pull request #1206 from pirut/claude/upstream-perlin-climate
🤖 Agent: merged after verify passed and the integration run on the desktop (main 34c3c1cf + the 14 contributor PRs: workspace tests 1,869/0, launcher 113, parity --changed 320/0).
2026-10-22 19:53:50 +02:00
Ludovic Marie 9f687dd1ec Merge pull request #1204 from pirut/claude/upstream-furnaces
🤖 Agent: merged after verify passed and the integration run on the desktop (main 34c3c1cf + the 14 contributor PRs: workspace tests 1,869/0, launcher 113, parity --changed 320/0).
2026-10-22 19:52:49 +02:00
Ludovic Marie dc6b648f69 Merge pull request #1203 from pirut/claude/upstream-hoppers
🤖 Agent: merged after verify passed and the integration run on the desktop (main 34c3c1cf + the 14 contributor PRs: workspace tests 1,869/0, launcher 113, parity --changed 320/0).
2026-10-22 19:52:37 +02:00
Ludovic Marie 36a5696abf Merge pull request #1202 from pirut/claude/upstream-mob-scans
🤖 Agent: merged after verify passed and the integration run on the desktop (main 34c3c1cf + the 14 contributor PRs: workspace tests 1,869/0, launcher 113, parity --changed 320/0).
2026-10-22 19:52:26 +02:00
Ludovic Marie 48d6211781 Merge pull request #1201 from pirut/claude/upstream-chunk-bookkeeping
🤖 Agent: merged after verify passed and the integration run on the desktop (main 34c3c1cf + the 14 contributor PRs: workspace tests 1,869/0, launcher 113, parity --changed 320/0).
2026-10-22 19:52:15 +02:00
Ludovic Marie 1ef5f8fae5 Merge pull request #1200 from pirut/claude/upstream-mob-world
🤖 Agent: merged after verify passed and the integration run on the desktop (main 34c3c1cf + the 14 contributor PRs: workspace tests 1,869/0, launcher 113, parity --changed 320/0).
2026-10-22 19:52:03 +02:00
rdbtCVS 0928c83d4c Skip provably unchanged native End surface traversal 2026-10-22 18:40:01 +01:00
Ludovic MarieandClaude Opus 5.5 724f42355f Fix #1255: server players climbing down a ladder take no fall damage
Player's MoveSimulationType is AUTHORITATIVE_SIDE_AND_SERVER, so a
ServerPlayer travels on the server too, and every tick on a ladder or
vine LivingEntity.handleOnClimbable resets its fall distance. Horizon's
server only replayed the client's moves (ServerPlayer.doCheckFallDamage),
so the fall distance grew all the way down a ladder and hurt the player
where it landed: LAN guests, dedicated-server players and, while single
player ran on the server thread (#1114), the single player.

lan_e2e ladder: the guest slides down ten blocks of ladder and lands with
full health (before: 20 -> 13).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 19:37:21 +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 70c06224fe Website: the Blocks look, a blocky Minecraft-like design in Horizon's purple
horizon-theme gains the Blocks palettes (light and dark, WCAG AA on every panel and on the stone),
Pixelify Sans (OFL) and Pixelarticons (MIT); tokens.css now carries the Blocks colours, the pixel
font and the block edges. The site sets headings, navigation and buttons in Pixelify Sans, turns
the index lists into stacks of panels with an outline, a bevel and a lip, puts a stone texture
behind the page and Horizon's pixel landscape (a dawn or a sunset) behind the hero, both drawn in
code by scripts/art.mjs. Light and dark still follow the system, with the toggle.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 19:29:41 +02:00
Ludovic MarieandClaude Opus 5.5 c0fd8dabfd Fix #1222: single player runs in the frame again until server players have vehicles
Since #1114 single player's window is a player of its own server thread,
and a server's players can't use boats or minecarts: the client sends
Attack and Interact only for mobs (remote_start_attack, remote_use_entity),
the server aims a player's attack or use only at mobs and dragon parts
(look_at_entity), and neither side handles riding (no SetPassengers on the
client, no MoveVehicle or PaddleBoat packets). So a boat on water could not
be boarded or broken in single player.

The server thread is opt-in again (--server-thread or
HORIZON_SERVER_THREAD=1); --no-server-thread still works.

lan_e2e boats: the host right-clicks a boat on water and rides it, sneaks
out, and breaks it. Without --server-thread the host now runs the game's
default instead of forcing the frame, so the check covers what players get.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 19:15:44 +02:00
Ludovic MarieandClaude Opus 5.5 da991045f9 Modpacks: jars only CurseForge serves are left off, "CurseForge downloads coming soon"
The pack lists them with the mods left off, the port's log and fetch label say
the same, and docs/modpacks.md no longer asks for a key.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 18:54:33 +02:00
Ludovic MarieandClaude Opus 5.5 f926d1d2b1 Modpacks from the mod browser: ported in the same step, mods loaded together
Importing a modpack from the Mods page (or `mods install`) now ports it right away
for the selected version, so Play works straight after. port-pack leaves a
namespace two carried mods both claim to none of them (YUNG's mods all ship
`yungsapi`, which made the game drop three of them), and the launcher applies
the game's first-wins rule before it starts a pack, listing what it left off.

Measured on Vanilla Perfected, Slimes Adventure and All the Mods 10 (docs/modpacks.md):
2, 20 and 42 mods ported, every world loads, no crash.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 18:52:12 +02:00
Ludovic MarieandClaude Opus 5.5 b1dc23551b tools: end-to-end check of a Java mod from the mod browser
mod_browser_harness.py searches, checks and installs a mod through the launcher's
mods CLI in a scratch game folder, opens a world with it offscreen, places one of
its blocks and crafts one of its recipes on a crafting table, and times the world
load with and without it. Macaw's Fences: check 4.0 s, install 3.1 s, world load
5.4 s -> 7.3 s, hedge placed, 4 crafted.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 18:52:03 +02:00
Ludovic MarieandClaude Opus 5.5 f53998674d Launcher mod browser: project pages, search filters, project icons and gallery
A result's title opens its page in the launcher: icon, authors, downloads,
followers, licence, verdict (for a Java mod, what ports and what doesn't) and
Install, links, then Description (Markdown), Gallery and Versions tabs
(browser::details, Source::details on Modrinth's project and members).

Filters: sort (relevance, downloads, follows, newest, updated), loader, game
version, environment and categories, from the source's tag lists.

Icons and gallery images are fetched at run time over HTTPS from the sources'
image CDNs only, size-capped, decoded and scaled to PNGs in a 128 MB disk cache
(browser::images); a placeholder shows until they arrive. Vanilla's mark is a
grass block drawn from the player's prepared game files. Modrinth's logo isn't
shown: its branding is all rights reserved without written permission.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 18:51:56 +02:00
Ludovic MarieandClaude Opus 5.5 b8f79247b8 Mod browser: CurseForge is coming soon
Ludovic dropped CurseForge for now (its API needs a paid key). The Mods page shows
"CurseForge (coming soon)" beside the Modrinth switch, disabled and inert;
browser::sources() is Modrinth only, so no search or install goes to CurseForge,
and `--source curseforge` says it is coming soon. Text that asked for a key or
an environment variable now says CurseForge support is coming soon. Importing a
CurseForge zip or instance still works; jars only CurseForge serves are listed
as "CurseForge downloads coming soon".

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 18:50:25 +02:00
rdbtCVS a97d8098c7 Fix firework tooltip translations 2026-10-22 17:47:29 +01:00
rdbtCVS d67779d5a1 Make completion regression deterministic and include screenshots 2026-10-22 17:42:56 +01:00
rdbtCVS e7d42dc9d0 Fix in-world menu background blur 2026-10-22 17:41:17 +01:00
rdbtCVS ecbb0368c5 Fix hand swing prediction for rejected block placements 2026-10-22 17:37:42 +01:00
rdbtCVS 15b8514584 Fix registry-backed slash command suggestions on connected clients 2026-10-22 17:37:23 +01:00
Ludovic MarieandClaude Opus 5.5 6e83037fca end sky test: as_chunks instead of chunks_exact (clippy 1.99)
Rust 1.99's clippy denies chunks_exact with a constant size
(chunks_exact_to_as_chunks), so `cargo clippy --all-targets -D warnings`
failed on this test. Same check, written as as_chunks::<4>().

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 18:32:54 +02:00
rdbtCVS 6d36d468e7 Fix empty-hand drop animations 2026-10-22 17:29:55 +01:00
rdbtCVS d86a1b2882 Fix cooldown after instant survival breaks in multiplayer 2026-10-22 17:26:39 +01:00
rdbtCVS ca2d5e04dc Remove benchmark report from pull request 2026-10-22 17:15:46 +01:00
rdbtCVS b054c8db11 Remove benchmark report from pull request 2026-10-22 17:15:44 +01:00
rdbtCVS fba42920e4 Remove benchmark report from pull request 2026-10-22 17:15:41 +01:00
Ludovic MarieandClaude Opus 5.5 a3f81f3a30 Launcher: test a Java mod's install, dependencies, id clash and removal offline
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 18:10:20 +02:00
Ludovic MarieandClaude Opus 5.5 240e76160b Mod content: give mod items and blocks network ids, so they don't travel as air
The game and its integrated server map items and blocks to network ids through
the registries report (registries.json), which the content root left vanilla:
a mod's item in a slot, given or picked up, reached the client as air. The
content root now rewrites registries.json too, appending the new items and
blocks after vanilla's ids in plan order.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 18:07:16 +02:00
rdbtCVS c9c7cda779 docs: record the completed ten-agent optimization sequence 2026-10-22 17:06:09 +01:00
Ludovic MarieandClaude Opus 5.5 3530560581 Launcher: Java mods in the mod browser, checked before install; Modpacks moves into Mods
The Mods page gets a Mods tab: Fabric, NeoForge, Forge and Quilt mods for 26.3
(1.21 and later with Show everything). Each result's jar is downloaded (allowed
CDNs, checksum), checked with horizon-port-pack (Works, Partly, Doesn't run, with
what ports and what doesn't) and the answer cached per file. Install copies the
carry into horizon-mods/<id>/ and turns it on in horizon-mods.txt, with required
mods that port; libraries with nothing to port are listed as not ported; id or
namespace clashes are refused. Remove deletes it and turns it off.

The Modpacks page becomes the Mods page's Modpacks tab: imported packs first,
then the search. CLI: mods search --kind mod, mods check <project>.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 18:00:24 +02:00
Ludovic MarieandClaude Opus 5.5 4a350bb0a0 port-pack carry: leave out recipes for items it didn't declare
A recipe whose result is one of the mod's own items that the carry didn't declare
(its block had no vanilla template) loaded and crafted air. Vanilla refuses a
recipe for an unknown item; the carry now moves such recipes to outside/ and lists
them (recipes_unregistered), and check says "N recipes for left-out blocks".
Found by the mod browser's end-to-end run on Macaw's Fences.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 18:00:23 +02:00
Ludovic MarieandClaude Opus 5.5 da996b9fd1 port-pack: check one mod jar (Works, Partly, Doesn't run) from its content and classes
`horizon-port-pack check <jar> <out>` carries a single jar into a folder content
mod and reads its class files (constant pool, superclass, declared methods) for
what the carry can't take: block entities, menus, entities, blocks and items with
behaviour of their own, recipe types, worldgen code, Mixins, game events. The
verdict and the lists of what ports and what is skipped go to check.json, for the
launcher's mod browser to show before install.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 18:00:23 +02:00
Ludovic Marie 022600481e Merge pull request #1236 from ludovic111/feat/microsoft-coming-soon
🤖 Agent: merged. Microsoft sign-in shows "Coming soon" while builds have no client ID (Ludovic, 2026-10-22). verify passes; launcher tests 96+14+1.
2026-10-22 17:58:51 +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
rdbtCVS fa14833b41 perf(client): retain seeded lightning bolt geometry 2026-10-22 16:56:23 +01:00
rdbtCVS e4ea9e0199 Cache weather column parameters and animate atlas UVs in shader 2026-10-22 16:54:09 +01: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 93bde90aaf Launcher: Microsoft sign-in says "Coming soon" while the build has no client ID
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 17:51:51 +02:00
rdbtCVS 92b8cd692d perf(client): retain the End sky mesh on the GPU 2026-10-22 16:50:29 +01:00
rdbtCVS 05400438c7 perf(client): cache weather column direction table 2026-10-22 16:44:32 +01:00
Ludovic MarieandClaude Opus 5.5 5b44452284 fix(protocol): read PARTICLES as a list; keep every particle's data
Review fixes on top of the particle serializers:

- PARTICLES (17) is List<ParticleOptions>: a VarInt count, then each
  particle. Reading it as one particle only worked for empty lists, so a
  living entity with effect particles (LivingEntity.DATA_EFFECT_PARTICLES)
  misread everything after it. Value::Particles holds the list.
- Item particles use ItemStackTemplate.STREAM_CODEC (item, count, patch),
  not the optional slot codec.
- Vibration keeps its position source (block position or entity id and
  y offset) so it writes back the bytes it read; geyser data keeps its
  Int exactly instead of going through f32.
- Enchantment names come from the server's synced registry, not the
  builtin report (enchantment is data-driven).
- Armor stands: 26.3's DEFAULT_DEPTH_CLEAR_VALUE is 0.0 (bytecode
  dconst_0), so a marker's empty box falls back to a range of 4 and a
  visible marker stand draws; restore that. Holograms stay hidden through
  the invisible flag.

Test: particles_round_trip covers every data shape, a PARTICLES list and
the entry after it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 17:35:57 +02:00
Ludovic Marie 34c3c1cffe Merge pull request #1232 from ludovic111/feat/alpha-channel
🤖 Agent: merged. The fast channel is now "Alpha" (Ludovic, 2026-10-22); old "nightly" / beta_builds settings still read as Alpha. verify passes; launcher tests 96+14+1, release tools' Python tests 27, website build OK.
2026-10-22 17:25:47 +02:00
rdbtCVS 756cf4af07 bench: measure read-only saved chunk loading 2026-10-22 16:18:40 +01:00
rdbtCVS 0eaa13020c perf: load uniform saved light without expanding nibble layers 2026-10-22 16:18:40 +01:00
Ludovic MarieandClaude Sonnet 5.5 5eb224a909 Rename the Nightly update channel to Alpha
The whole of Horizon is in beta, so the channel that installs every build as
soon as it is built is called Alpha; Stable keeps its name. The launcher's
Settings, the update-channel command, the MCP tool, the release notes footer
and the docs say Alpha. update_channel is saved as "alpha"; "nightly" and the
old beta_builds switch still read as Alpha.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-22 17:17:33 +02:00
Ludovic Marie 15b6313292 Merge pull request #1226 from ludovic111/docs/releases-channel-posts
🤖 Agent: merged (docs only; verify passes).
2026-10-22 17:04:07 +02:00
rdbtCVS a648622b92 bench: report exact warmed generation wall timings 2026-10-22 15:46:40 +01:00
rdbtCVS 97edfec914 perf: reuse decoded aquifer neighborhoods during terrain fill 2026-10-22 15:46:40 +01:00
Ludovic Marie ca125cb028 Merge pull request #1227 from ludovic111/docs/website-current
🤖 Agent: merged (docs/website only, skip-release; verify passes).
2026-10-22 16:33:54 +02:00
Ludovic MarieandClaude Sonnet 5.5 fe5e20507a Website: docs match what ships (Mods page, shader packs page, dynamic lights, LAN workstations); the live ping is off
The Mods page (launcher 0.17) replaces "a mod browser is planned"; the Horizon
shader format is no longer called planned; Nightly-only things say Nightly.
The "I'm playing" ping has no server (DEFAULT_ENDPOINT is None), so the FAQ and
the privacy page say it is off instead of sent. The channel switch says it
needs the launcher release after 0.17. The README says what deploys the site.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-22 16:29:58 +02:00
Ludovic MarieandClaude Opus 5.5 41e6c3eca3 release-flow: #releases shows every Nightly silently, pings only for Stable
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 16:23:59 +02:00
Ludovic MarieandClaude Opus 5.5 92d2e2b86d launcher: leave Java alone when the preparer finds one itself
With a JDK installed, the launcher passed the preparation the home of the
JDK horizon-prepare finds (javac/../..) as JAVA_HOME and on the PATH.
Found through the PATH, that is often a stub: on macOS /usr/bin/javac
gives JAVA_HOME=/usr, and `JAVA_HOME=/usr /usr/bin/javac -version` never
returns (the stub calls itself), so the preparer subprocess hung on a Mac
with Temurin installed. When horizon-prepare finds a JDK by itself, run
the preparation as before, in an unchanged environment; only Minecraft's
own Java (the official launcher's, or one downloaded from Mojang) is
passed on.

Also skip the Mojang download on x86-64 Linux without glibc's loader
(NixOS without nix-ld, musl distributions): Mojang's Java can't start
there, so say so instead of downloading 115 MB first. And keep each
runtime home once (dedup only dropped neighbours).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 16:20:42 +02:00
itspotatotimeandcairn-code ad676af5eb fix(protocol): decode particle entity-data serializers so later slots survive
Serializer 17 (particles) and 16 hit a catch-all that returned Ok(None),
which stopped EntityData::read from walking the rest of the metadata list.
On armor stands that carry a level-particle effect slot, slots 2 and 3
(CustomName text and visible flag) were silently discarded, so serverside
holograms (Gamster's social-media panels and leaderboards) rendered as
nothing.

Implement both serializers as value-preserving decodes: a ParticleData
enum covering every 26.3 particle shape (state varints for ids 1/2/36/121/125,
colours, dust, dust_color_transition, effect/instant, scalar, shriek, trail,
item, vibration and geyser variants) backed by a static minecraft:particle_type
id table, with write_value arms for round-trips.

Co-Authored-By: cairn-code (GLM-5.3-Flash Free) <[email protected]>
2026-10-22 17:15:16 +03:00
itspotatotimeandcairn-code 9f6d781782 fix(client): read empty-root component names through their extra spans
Co-Authored-By: cairn-code (GLM-5.3-Flash Free) <[email protected]>
2026-10-22 17:15:16 +03:00
itspotatotimeandcairn-code a20ebde7ab fix(client): carry every server item component into tooltips
to_stack kept only damage, dyed colour, map id and a head profile, so a server item's custom name, lore, enchantments and custom data never reached the inventory tooltip. Build the vanilla `components` compound from the decoded patch and run it through the same decoder saved item NBT uses, then show lore lines between the name and the details.

Co-Authored-By: cairn-code (DeepSeek V4.1 Flash) <[email protected]>
2026-10-22 17:15:16 +03:00
Claude 71edc9d41c Prepare with Minecraft's own Java 25 when no JDK is installed
On a Mac without a JDK, preparing failed with "Java 25 is needed ...
/usr/bin/javac -version printed no version: Unable to locate a Java
Runtime": /usr/bin/javac is macOS's placeholder. Mojang's Java runtimes
are whole JDKs, javac included (java-runtime-epsilon is 25.0.1 on every
platform), and the official launcher has already downloaded one for
anyone who played 26.3.

- horizon-prepare: java::find also tries Minecraft's runtimes
  (<game dir>/runtime/<component>/<platform>/<component>, jre.bundle on
  macOS) and the launcher's pick (Options::java_home), and names the
  macOS placeholder for what it is.
- launcher: when no JDK is found, downloads the runtime the version JSON
  names (javaVersion.component) from Mojang into its own game folder,
  every file checked against its sha1, executables and links as the
  official launcher lays them out. A version's own preparer binary or
  Python script gets it through JAVA_HOME and the PATH.

Checked for real on Linux: the download is 147 files, its javac 25.0.1
compiles and runs a class with --release 25.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01AxbYUJt4ShJFCdyLNCxF21
2026-10-22 14:12:42 +00:00
itspotatotimeandcairn-code d01327862d fix(client): hide marker stands and read server entity names
A marker armor stand has an empty box, so vanilla's shouldRenderAtSqrDistance computes a zero range and the model never draws; Horizon substituted a 4-block range, which turned the stacked marker stands a lobby uses for holograms into a visible tower. Follow vanilla and let the zero edge cull the model. Server NPCs and mobs also carry CustomName, CustomNameVisible and the shared invisible flag in entity data; Horizon only read those for armor stands. Apply them to every mob, keep the styled component for the name tag, and hide the body when the flag is set.

Co-Authored-By: cairn-code (DeepSeek V4.1 Flash) <[email protected]>
2026-10-22 17:11:32 +03:00
itspotatotimeandcairn-code c139387e2a fix(client): read server map art and item profiles
A server lobby draws its signs as walls of framed maps and puts player heads on its NPCs. Horizon dropped every map_item_data packet, so framed maps stayed blank, and the item component decoder rejected attack_range (30) and profile (72), which made the whole equipment and container packets that carried them undecodable. Decode both components, carry a profile into the item data the head model reads, and apply server map patches to the map texture. The map pool now holds a wall of maps so the atlas does not re-upload every map every frame.

Co-Authored-By: cairn-code (DeepSeek V4.1 Flash) <[email protected]>
2026-10-22 17:11:32 +03:00
itspotatotimeandcairn-code a5e0645610 fix(client): draw armor stand custom names as name tags
Holograms are invisible armor stands carrying a CustomName. Armor stands parsed neither CustomName nor CustomNameVisible, and name tags came only from mobs, so a hologram drew nothing. Read both fields from NBT and entity data, keep them across a save, and put armor stands in the name tag pass at their bounding-box height even when their model is skipped.

Co-Authored-By: cairn-code (DeepSeek V4.1 Flash) <[email protected]>
2026-10-22 17:09:34 +03:00
itspotatotimeandcairn-code 399382084a fix(client): show server players' and NPCs' skins
The tab list resolved skins only for listed players and never handed them to the level's player models, so every player and NPC in a server world kept the default Steve or Alex texture. Resolve every profile's textures each frame and give each player entity its skin, including entities that are never in the tab list. Player heads read the minecraft:profile component with the insecure lookup vanilla uses, so unsigned textures from offline servers draw too.

Co-Authored-By: cairn-code (DeepSeek V4.1 Flash) <[email protected]>
2026-10-22 17:09:34 +03:00
rdbtCVS 753c071c37 docs: record early-stopped chunk optimization attempts 2026-10-22 15:09:08 +01:00
rdbtCVS 5d82b4c999 perf: load list palettes without unpacking discarded indices 2026-10-22 14:48:08 +01:00
rdbtCVS 4e46f2a57a bench: profile chunk generation and saved chunk loading 2026-10-22 14:48:08 +01:00
Claude e01296f99b Keep the pointer locked on macOS when the game captures it
winit's macOS set_cursor_position re-associates the mouse with the
pointer (CGAssociateMouseAndMouseCursorPosition(true)), so warping to
the window centre right after the Locked grab released it again and the
pointer moved freely in first person. Warp before grabbing on macOS;
Wayland still warps after, since it moves a pointer only while locked.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_0188LkUPntcZdfMF3wAqR2tK
2026-10-22 13:48:04 +00:00
Ludovic MarieandClaude Opus 5.5 ac60b337aa worldgen: chunks far apart still make a region (map past the grid)
TerrainRegion::new refused a region whose chunks bounding box passed
2^20 columns, where it took any set of distinct chunks before: the
bulk_section parity fixture puts a chunk at x = 4194304 beside the
origin and failed with InvalidChunks. Keep the grid for the usual small
box and fall back to a map past it; both refuse a position twice. The
lookup now subtracts in i64, so no coordinate can wrap into the grid.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 15:40:07 +02:00
Ludovic MarieandClaude Opus 5.5 a49cf5502d geometry: floor_i32 saturates below i32::MIN, as (int) Math.floor
Vanilla 26.3 Mth.floor(double) is (int) Math.floor(v), so a value below
i32::MIN floors to i32::MIN. The truncate-and-step version wrapped to
i32::MAX there (block_collision_scan parity failed at the chunk of
x = -2147483648.8: Rust 134217727, vanilla -134217728). Step down with
saturating_sub; the fast path is unchanged, and the test now checks
the ends of the i32 range, infinities and f64::MIN/MAX against
f64::floor as i32.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 15:37:37 +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 Marie b88218d019 Merge pull request #1209 from ludovic111/feat/update-channels
🤖 Agent: merged. Nightly/Stable update channels (Ludovic's request 2026-10-22): Settings choice, nightly train 5/20 min, one stable promotion a day at 20:00 UTC, Nightly launchers check every 30 min. verify passes on b197682d (main merged in: Settings keeps update_channel and #1185's ping_notice_seen). Ships in [email protected]; launcher.yml stays disabled until #1132.
2026-10-22 15:28:00 +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 Marie d5972490a5 Merge pull request #1193 from ludovic111/docs/modpacks-12-numbers
🤖 Agent: merged last in wave 2 (docs: modpacks-12 numbers), after the modpack PRs it describes.
2026-10-22 15:22:18 +02:00
Ludovic Marie 54f01d6bb1 Merge pull request #1192 from ludovic111/perf/linux-helpers-behind
🤖 Agent: merged after integration wave 2 on the desktop (integ/w2 @ 38307d15): fmt, clippy, workspace + launcher tests, release build and every affected parity harness pass. main merged into the branch with the same resolution as the tested tree; verify passes on that head.
2026-10-22 15:21:27 +02:00
Ludovic Marie 44e289f919 Merge pull request #1188 from ludovic111/fix/lan-guest-workstations-8
🤖 Agent: merged after integration wave 2 on the desktop (integ/w2 @ 38307d15): fmt, clippy, workspace + launcher tests, release build and every affected parity harness pass. main merged into the branch with the same resolution as the tested tree; verify passes on that head.
2026-10-22 15:21:15 +02:00
Ludovic MarieandClaude Opus 5.5 b197682dc8 Merge origin/main into feat/update-channels (Settings: keep update_channel and main's ping_notice_seen)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 15:19:31 +02:00
Ludovic MarieandClaude Opus 5.5 86e08072e4 streaming: bound ExtraCover's z range too
ExtraCover::new checked only x0/x1 against the i32 range, so two areas
near i32::MAX or i32::MIN in z built a grid whose row arithmetic
(cz + dz) overflows: a panic in debug builds, wrong cells in release.
Check z the same way, and test both ends.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 15:14:26 +02:00
Ludovic MarieandClaude Opus 5.5 c4a12964dc Merge origin/main into perf/linux-helpers-behind (game.rs: helpers behind on every run, plus main's live-count ping)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 15:13:44 +02:00
Ludovic Marie 41055168af Merge pull request #1196 from ludovic111/perf/spawn-lists-borrowed
🤖 Agent: merged after integration wave 2 on the desktop (integ/w2 @ 38307d15 = main c16b41ab + this wave): fmt, clippy -D warnings, workspace + launcher + telemetry-server tests, release build and every affected parity harness pass; the 3 harness failures and the panel test passed on rerun (known flakes).
2026-10-22 15:13:09 +02:00
Ludovic Marie 55eabf4b20 Merge pull request #1195 from ludovic111/perf/lan-entity-states
🤖 Agent: merged after integration wave 2 on the desktop (integ/w2 @ 38307d15 = main c16b41ab + this wave): fmt, clippy -D warnings, workspace + launcher + telemetry-server tests, release build and every affected parity harness pass; the 3 harness failures and the panel test passed on rerun (known flakes).
2026-10-22 15:12:58 +02:00
Ludovic Marie 4332014673 Merge pull request #1187 from ludovic111/fix/world-spawn-one-worker
🤖 Agent: merged after integration wave 2 on the desktop (integ/w2 @ 38307d15 = main c16b41ab + this wave): fmt, clippy -D warnings, workspace + launcher + telemetry-server tests, release build and every affected parity harness pass; the 3 harness failures and the panel test passed on rerun (known flakes).
2026-10-22 15:12:45 +02:00
Ludovic Marie f787bc9b75 Merge pull request #1186 from ludovic111/fix/secure-chat-leave-order
🤖 Agent: merged after integration wave 2 on the desktop (integ/w2 @ 38307d15 = main c16b41ab + this wave): fmt, clippy -D warnings, workspace + launcher + telemetry-server tests, release build and every affected parity harness pass; the 3 harness failures and the panel test passed on rerun (known flakes).
2026-10-22 15:12:31 +02:00
Ludovic Marie 1a3fb08d68 Merge pull request #1185 from ludovic111/feat/live-player-count
🤖 Agent: merged after integration wave 2 on the desktop (integ/w2 @ 38307d15 = main c16b41ab + this wave): fmt, clippy -D warnings, workspace + launcher + telemetry-server tests, release build and every affected parity harness pass; the 3 harness failures and the panel test passed on rerun (known flakes).
2026-10-22 15:12:17 +02:00
Ludovic Marie 2ae6ae2aeb Merge pull request #1184 from ludovic111/fix/dedicated-server-waits
🤖 Agent: merged after integration wave 2 on the desktop (integ/w2 @ 38307d15 = main c16b41ab + this wave): fmt, clippy -D warnings, workspace + launcher + telemetry-server tests, release build and every affected parity harness pass; the 3 harness failures and the panel test passed on rerun (known flakes).
2026-10-22 15:12:04 +02:00
Ludovic Marie 90ff954f23 Merge pull request #1182 from ludovic111/fix/online-mode-log-order
🤖 Agent: merged after integration wave 2 on the desktop (integ/w2 @ 38307d15 = main c16b41ab + this wave): fmt, clippy -D warnings, workspace + launcher + telemetry-server tests, release build and every affected parity harness pass; the 3 harness failures and the panel test passed on rerun (known flakes).
2026-10-22 15:11:37 +02:00
Ludovic Marie bdad451ab4 Merge pull request #1181 from ludovic111/perf/spread-chunk-packets
🤖 Agent: merged after integration wave 2 on the desktop (integ/w2 @ 38307d15 = main c16b41ab + this wave): fmt, clippy -D warnings, workspace + launcher + telemetry-server tests, release build and every affected parity harness pass; the 3 harness failures and the panel test passed on rerun (known flakes).
2026-10-22 15:11:15 +02:00
Ludovic Marie ba1e7e23cf Merge pull request #1179 from ludovic111/perf/render-thread-ahead
🤖 Agent: merged after integration wave 2 on the desktop (integ/w2 @ 38307d15 = main c16b41ab + this wave): fmt, clippy -D warnings, workspace + launcher + telemetry-server tests, release build and every affected parity harness pass; the 3 harness failures and the panel test passed on rerun (known flakes).
2026-10-22 15:11:03 +02:00
Ludovic Marie ec9d62858a Merge pull request #1178 from ludovic111/perf/span-cpu-time
🤖 Agent: merged after integration wave 2 on the desktop (integ/w2 @ 38307d15 = main c16b41ab + this wave): fmt, clippy -D warnings, workspace + launcher + telemetry-server tests, release build and every affected parity harness pass; the 3 harness failures and the panel test passed on rerun (known flakes).
2026-10-22 15:10:51 +02:00
Ludovic Marie 631160263f Merge pull request #1177 from ludovic111/feat/formations-fountain
🤖 Agent: merged after integration wave 2 on the desktop (integ/w2 @ 38307d15 = main c16b41ab + this wave): fmt, clippy -D warnings, workspace + launcher + telemetry-server tests, release build and every affected parity harness pass; the 3 harness failures and the panel test passed on rerun (known flakes).
2026-10-22 15:10:39 +02:00
Ludovic Marie badc91f94b Merge pull request #1176 from ludovic111/feat/ctov-compat-sky-view
🤖 Agent: merged after integration wave 2 on the desktop (integ/w2 @ 38307d15 = main c16b41ab + this wave): fmt, clippy -D warnings, workspace + launcher + telemetry-server tests, release build and every affected parity harness pass; the 3 harness failures and the panel test passed on rerun (known flakes).
2026-10-22 15:10:27 +02:00
Ludovic Marie 554932e9fd Merge pull request #1175 from ludovic111/feat/mo-structures
🤖 Agent: merged after integration wave 2 on the desktop (integ/w2 @ 38307d15 = main c16b41ab + this wave): fmt, clippy -D warnings, workspace + launcher + telemetry-server tests, release build and every affected parity harness pass; the 3 harness failures and the panel test passed on rerun (known flakes).
2026-10-22 15:10:15 +02:00
Ludovic Marie 98c77f6fc7 Merge pull request #1174 from ludovic111/fix/wayland-surface-format-test
🤖 Agent: merged after integration wave 2 on the desktop (integ/w2 @ 38307d15 = main c16b41ab + this wave): fmt, clippy -D warnings, workspace + launcher + telemetry-server tests, release build and every affected parity harness pass; the 3 harness failures and the panel test passed on rerun (known flakes).
2026-10-22 15:10:03 +02:00
Ludovic Marie 6323b4a517 Merge pull request #1173 from ludovic111/fix/launcher-says-no-sounds
🤖 Agent: merged after integration wave 2 on the desktop (integ/w2 @ 38307d15 = main c16b41ab + this wave): fmt, clippy -D warnings, workspace + launcher + telemetry-server tests, release build and every affected parity harness pass; the 3 harness failures and the panel test passed on rerun (known flakes).
2026-10-22 15:09:52 +02:00
Ludovic Marie 82122f4970 Merge pull request #1155 from ludovic111/fix/riptide-spin-attack
🤖 Agent: merged after integration wave 2 on the desktop (integ/w2 @ 38307d15 = main c16b41ab + this wave): fmt, clippy -D warnings, workspace + launcher + telemetry-server tests, release build and every affected parity harness pass; the 3 harness failures and the panel test passed on rerun (known flakes).
2026-10-22 15:08:32 +02:00
Ludovic MarieandClaude Opus 5.5 8859d7a3a3 Merge origin/main into fix/lan-guest-workstations-8 (tools/lan_e2e.py: keep both sides' checks)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 15:08:10 +02:00
Ludovic MarieandClaude Opus 5.5 d02b66fd9a Install page: say Nightly's 30-minute check under Stable and Nightly only
Keeps the Updating list as on main, so it merges cleanly with #1132.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 15:07:54 +02:00
Ludovic MarieandClaude Opus 5.5 aa65c74e0f Update channels: stable at 20:00 UTC, Nightly checks every 30 minutes
The daily promotion moves to 20:00 UTC (21:00 in Paris in winter time):
PROMOTE_AT and promote.yml's cron (20:07, retries until 21:37), with the docs
that give the time. A launcher left open on Nightly looks for new releases
every 30 minutes; Stable keeps 4 hours. The launcher README's Settings
screenshot shows the Updates choice.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 14:57:50 +02:00
Ludovic MarieandClaude Opus 5.5 ea5994a58a Update channels: Stable (daily) and Nightly (every build) in the launcher
The launcher's "Beta builds" switch becomes an Updates choice of two channels,
stored as "update_channel": "stable" | "nightly" (an old "beta_builds": true
reads as nightly). The CLI (update-channel) and MCP (update_channel) show and
set it, and list each release's channel as stable or nightly.

Nightly ships constantly: the release train waits 5 minutes for a merge wave to
settle (20 at most) instead of 15 (an hour). Stable moves once a day:
promote.yml runs at 19:00 UTC (retries until 20:37) and promotes the newest beta
that had been out 6 hours by 19:00, so late runs and retries pick the same one.
The stalled-promotion issue waits 30 hours. Release notes, docs, the website's
install and FAQ pages say Stable and Nightly.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 14:30:19 +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 Marie c16b41ab31 Merge pull request #1162 from ludovic111/feat/dynamic-lights
🤖 Agent: merged after integration wave 1 on the desktop (main e730dc14 + #1108 #1114 #1131 #1157 #1158 #1162 = ae482a72): fmt, clippy -D warnings, workspace tests (one port-race flake in horizon-server panel, passes on rerun), release build, tools/parity.py --changed origin/main 299/302 then the 3 rerun: save_roundtrip pass, world_reopen pass x2 (one early-count flake), dedicated_server = known flake also failing on main (fixed by #1183/#1184).
2026-10-22 12:51:34 +02:00
Ludovic Marie 46efecf9d2 Merge pull request #1158 from ludovic111/fix/shader-pack-folder
🤖 Agent: merged after integration wave 1 on the desktop (main e730dc14 + #1108 #1114 #1131 #1157 #1158 #1162 = ae482a72): fmt, clippy -D warnings, workspace tests (one port-race flake in horizon-server panel, passes on rerun), release build, tools/parity.py --changed origin/main 299/302 then the 3 rerun: save_roundtrip pass, world_reopen pass x2 (one early-count flake), dedicated_server = known flake also failing on main (fixed by #1183/#1184).
2026-10-22 12:51:30 +02:00
Ludovic Marie d029d0d28c Merge pull request #1157 from ludovic111/fix/shaderpack-compat
🤖 Agent: merged after integration wave 1 on the desktop (main e730dc14 + #1108 #1114 #1131 #1157 #1158 #1162 = ae482a72): fmt, clippy -D warnings, workspace tests (one port-race flake in horizon-server panel, passes on rerun), release build, tools/parity.py --changed origin/main 299/302 then the 3 rerun: save_roundtrip pass, world_reopen pass x2 (one early-count flake), dedicated_server = known flake also failing on main (fixed by #1183/#1184).
2026-10-22 12:51:27 +02:00
Ludovic Marie aa7f71a79b Merge pull request #1131 from ludovic111/feat/version-sources
🤖 Agent: merged after integration wave 1 on the desktop (main e730dc14 + #1108 #1114 #1131 #1157 #1158 #1162 = ae482a72): fmt, clippy -D warnings, workspace tests (one port-race flake in horizon-server panel, passes on rerun), release build, tools/parity.py --changed origin/main 299/302 then the 3 rerun: save_roundtrip pass, world_reopen pass x2 (one early-count flake), dedicated_server = known flake also failing on main (fixed by #1183/#1184).
2026-10-22 12:51:24 +02:00
Ludovic Marie 3704384c72 Merge pull request #1114 from ludovic111/fix/server-thread-default-v2
🤖 Agent: merged after integration wave 1 on the desktop (main e730dc14 + #1108 #1114 #1131 #1157 #1158 #1162 = ae482a72): fmt, clippy -D warnings, workspace tests (one port-race flake in horizon-server panel, passes on rerun), release build, tools/parity.py --changed origin/main 299/302 then the 3 rerun: save_roundtrip pass, world_reopen pass x2 (one early-count flake), dedicated_server = known flake also failing on main (fixed by #1183/#1184).
2026-10-22 12:51:20 +02:00
Ludovic Marie 772844362e Merge pull request #1108 from ludovic111/fix/remote-player-abilities
🤖 Agent: merged after integration wave 1 on the desktop (main e730dc14 + #1108 #1114 #1131 #1157 #1158 #1162 = ae482a72): fmt, clippy -D warnings, workspace tests (one port-race flake in horizon-server panel, passes on rerun), release build, tools/parity.py --changed origin/main 299/302 then the 3 rerun: save_roundtrip pass, world_reopen pass x2 (one early-count flake), dedicated_server = known flake also failing on main (fixed by #1183/#1184).
2026-10-22 12:51:17 +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 938a25a712 Modpacks doc: where ATM10's data mods stand, and what is missing, ranked
Pack-wide numbers after the modpacks-12 PRs (#1175, #1176, #1177, #1191):
structures that start, processors carried and dropped per mod, content,
load time and the play-through, and the gaps ranked by how much a player
notices them.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 12:05:53 +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 de8cb0dfa6 LAN guests: the lectern and the cartography table (#868)
The last two workstation menus guests couldn't use, both now on the
shared menu engine, which single player runs too:

- Lectern: LecternMenu on the engine (Kind::Lectern): the page buttons
  (1, 2, 100+) and Take Book (3, mayBuild) as ContainerButtonClick, the
  page as data slot 0 clamped to the book, the book in its one slot
  (removeItem, setItem a no-op, max 1). The host opens minecraft:lectern
  for a guest, applies the menu to the block entity (powered pulse, page
  turn level event 1043, has_book), resends changes and closes it on
  stillValid. Guests drive it from the book screen.
- Cartography table: CartographyTableMenu on the engine (Kind::Cartography,
  setupResultSlot, onTake, onCraftedBy's new map ids through a Maps env),
  replacing the old station code; guests get the same through
  guest_menu_op.
- LAN item stacks carry map_post_processing and writable/written book
  contents (they were blank for guests); map_post_processing is no longer
  saved (ItemStack.CODEC never saves it).
- A guest who enchants gets the enchant_item stat.
- Click parity: VanillaMenuOracle and menu_fixture gain both menus.
- lan_e2e: new lectern and cartography checks; enchanting checks the
  stat; server_count/server_reply ignore the guest's own operator
  broadcast echo on --server-thread; the grindstone check counts levels
  as well as points.

Fixes #868

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 11:35:45 +02:00
Ludovic MarieandClaude Opus 5.5 41a4ce4edb Linux: the game's workers go behind its render and server threads
On Linux an unprivileged thread can't raise itself, so #1179's render
thread hint does nothing there. The dedicated server already lowered its
helper threads instead (world generation, lighting, connection writers:
10 nice levels, or the idle class). The client now does the same, so the
world-generation and meshing workers (two pools with single player's
server thread) no longer hold the render thread back while terrain loads.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 11:05:19 +02:00
Ludovic MarieandClaude Opus 5.5 0ae1ab2e70 world_spawn harness: vanilla's server creates the world on one worker
Trees of neighbouring chunks overlap on chunk corners, and the one
decorated first wins. With several worker threads vanilla decorates in a
different order each run: seed -2080727421395798566 spawned at Y 64, 66
or 72 over 10 runs. The harness retried vanilla up to 3 times and passed
if any run matched, which still failed when none of them gave 72.

With -Dmax.bg.threads=1 (Util.maxAllowedExecutorThreads) vanilla
decorates in its scheduler's order, the one Horizon follows: that seed
gave 72 in 10 of 10 runs, as Horizon does. The other seeds that varied
are fixed too (10 of 10 each: -8171837717659076940 at 0 67 0,
-5296387317982839690 at -16 73 16, -211473008665507967 at 0 66 112).
The retries go, and vanilla's one spawn is compared exactly.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 11:05:00 +02:00
Ludovic MarieandClaude Opus 5.5 3aa36f95ab secure_chat harness: steps end on a barrier, players leave one at a time
Three races made the two sides differ with nothing wrong in Horizon:

- Steps ended on a fixed 1.5 s sleep. On a slow run a chat or its
  broadcast (vanilla queues the chat, then the broadcast as a later
  task) landed in the next step. Each step now also ends on a barrier:
  every connected client sends `random value 1..2` twice, and waits for
  the answer, which only the sender gets and which is left out of the
  transcript.
- The client closed every socket at once, and the server logs
  "<name> lost connection" in whatever order its network threads see
  the closed connections. The players still connected now leave one at
  a time: each waits until it has been told the previous one left
  (PlayerList.remove broadcasts that after logging). The harness waits
  for Carol's line before stopping the server.
- A player the server kicks (Bob, Mallory) is logged as
  "lost connection: <reason>", or as "Disconnected" when vanilla's
  network thread closes the connection before disconnect(reason) runs.
  The reason sent is still compared in the transcript, so the log line
  no longer compares it for kicked players.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 11:04:13 +02:00
Ludovic MarieandClaude Opus 5.5 84c7f39148 Natural spawning: borrow the spawn lists, and only the nearest distance
Natural spawning was a quarter of a 40-player server thread. Every spawn
attempt cloned the position's whole spawn list (NaturalSpawner.mobsAt) to
check the picked entry is still in it, and every group cloned it again to
pick from; mobs_at now returns the biome's or the structure's list as it is
stored (the fortress list lives in a static). The nearest player's distance,
asked on every attempt, is now the minimum of the distances instead of a
search that kept the player too. Same lists, same draws, same distances.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 10:58:39 +02:00
Ludovic MarieandClaude Opus 5.5 388f59aff4 Game: the ping's LAN flag follows is_published, every frame
Single player on the server thread publishes inside the integrated
server, so self.lan alone missed it; and the fps block runs only when a
frame is drawn.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 10:48:02 +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
Claude 5bad717213 Perlin noise without libm floor or int-to-float conversions, same values
floor_i32 is now Java's Mth.floor (truncate, step down for negative
fractions): f64::floor is a libm call on baseline x86-64, made for every
Perlin sample. Perlin's gradient table is stored as floats (every
component is -1, 0 or 1, exact either way), so the corner dot products
skip three conversions each. About 10% fewer instructions generating
terrain; values unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01M3pNrSHA2ifNrH7SAMHgSW
2026-10-22 08:43:40 +00:00
Ludovic MarieandClaude Opus 5.5 dd1525f045 dedicated_server harness: wait for what each step needs, not a fixed time
The vanilla protocol client moved on after a fixed time per step. On a
busy machine, either side's server could be slower than that: the
summoned villager was not seen yet when the trade step ran, the furnace
had not smelted the ingot when it was taken, a snowball was not thrown
yet, or the pearl had not landed. Each mode failed now and then on one
side only.

Those steps now keep the same minimum time and then also wait, for up to
30 s, until it happened: the villager is tracked, the furnace's result
slot has the ingot (plus 1 s), the snowball entity was added (plus 1 s),
the pearl teleport and the health update arrived. The respawn line also
leaves out block updates: a lava fall keeps flowing while the game runs,
and how many updates arrive before the respawn depends on timing.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 10:41:24 +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
Ludovic MarieandClaude Opus 5.5 9054acfed2 Launcher: the ping notice keeps its height; status lists the ping
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 10:37:31 +02:00
Ludovic MarieandClaude Opus 5.5 770809908d Render thread ahead of the workers on macOS and Windows
The client asks for its render thread what it already asks for the
server thread (vanilla's MinecraftServer.spin): user-interactive QoS on
macOS, above-normal priority on Windows, on machines with more than four
processors. While terrain loads at 32 chunks, the worker pools (two with
single player's server thread), the chunk encoders and other programs
outnumber the cores. A frame that gave its core up in queue.submit then
waited 10 to 200 ms to get one back: the long frames of #678.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 10:29:19 +02:00
Ludovic MarieandClaude Opus 5.5 83d001460c LAN: write each tick's entity states over the last tick's
tick_lan described every tracked entity afresh each tick: a new String for
its type, new vectors for its entity data, equipment and attributes, all
freed again at the end of the tick. The states now live in the LAN host
between ticks and each tick writes over the last tick's in place
(lan::entity::States), so those allocations are kept. The states, and so
every packet built from them, are the same as before.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 10:22:54 +02:00
Ludovic MarieandClaude Opus 5.5 8f738d35ec Live player count: an anonymous "I'm playing" ping and "N playing now"
The game sends an anonymous ping once a minute while it runs (a random
session ID kept only in memory, the version, the platform, whether a LAN
or friends world is open). The telemetry server counts sessions seen in
the last 2 minutes, in memory only, with its own per-address rate limit
and a session cap, and serves rounded counts at GET /v1/live. The
launcher shows "N playing now" on the Play page when the count is
reachable, and tells players what the ping holds on the first-run
question, in Settings, and once on the Play page for players who
answered before. docs/telemetry.md and the privacy page say the same.

Like the rest of telemetry, nothing is sent until HORIZON_TELEMETRY_URL
is set or DEFAULT_ENDPOINT gets a server.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 10:18:58 +02:00
Ludovic MarieandClaude Opus 5.5 6f1f83d07c online_mode harness: one login at a time, and vanilla's two log races
Three races made the comparison of the servers' lines about the player
fail at random:
- a login's lost connection is logged at the server's next tick, and the
  next login could start before it: each login now ends once the server
  logged its lost connection;
- vanilla's authenticator disconnects a player, then logs why, while the
  server thread may already log the lost connection: each login's lines
  are compared thread by thread;
- a player the server kicks closes the connection once it reads why; when
  that comes first vanilla logs the lost connection as "Disconnected"
  (seen once in 12 desktop runs), so a login's kick reason stands for both.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 10:08:17 +02:00
Ludovic MarieandClaude Opus 5.5 743cc8e069 Ender pearls: a server always sends the thrower the teleport
ThrownEnderpearl.onHit teleports the owner to oldPosition() (where the
pearl started its last tick) with ServerPlayer.teleport, which sends the
position packet whatever the distance. A guest's turn sent a teleport only
when the player moved more than 0.01 blocks, and the pearl took the hit
point: a pearl thrown at one's feet often lands within that, and the
dedicated_server harness saw "teleported 0 time(s)" where vanilla's server
sends 1 (about one run in four on the desktop).

The pearl now moves the player to its previous position and marks the
teleport (PlayerMotion.teleported), which the end of the guest's turn
sends even when the player stays put.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 09:51:36 +02:00
Ludovic MarieandClaude Opus 5.5 8291c8af5b Chunk packets get 1 ms a frame; the rest waits for the next frame
A client of a server (single player's server thread, LAN, multiplayer)
handled every received packet in the frame it arrived. Crossing a chunk
border at 32 chunks brings 25 to 60 columns at once, 0.1 ms each to
decode on an M4 Pro: a 3 to 8 ms frame each time. Once a frame has spent
1 ms on chunk packets, the events from the next chunk packet on wait for
the next frame, in order; events that have waited a tick are all handled
at once, so a slow machine's backlog never holds packets longer than
that. The status's pending_chunks counts the waiting events.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 09:09:47 +02:00
Ludovic MarieandClaude Opus 5.5 e6b92eb4ee Structures: Formations' fountain (its pieced structure) generates
Formations' PiecedStructure starts like its SimpleStructure, then lays out
the element's jigsaw children with vanilla's jigsaw. The game's jigsaw
assembler now exposes its child layout (add_children) for a start piece
placed by another structure type; horizon:formations reads horizon:pieced
(max_depth, max_distance_from_center), which carry writes for
formations:pieced.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 09:03:34 +02:00
Ludovic MarieandClaude Opus 5.5 2f3f1377a3 Structures: CTOV's mod-compat hooks and Repurposed's mineshaft sky view carried
ctov:modular_compat runs another processor list only when the mod it names
is loaded: carry writes that list in when the mod is ported along and takes
the hook out otherwise (142 copies in ATM10, all for Nature's Spirit).
Repurposed Structures' mineshaft_sky_view_processor (71 lists) becomes the
game's horizon:mineshaft_sky_view: nothing at or above the world's
OCEAN_FLOOR_WG height, except plain rails over a block that can occlude,
waterlogged where the world holds water. Placement contexts gain
can_occlude.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 08:46:42 +02:00
Ludovic MarieandClaude Opus 5.5 dbb1ca0907 LAN guests use crafters
A guest's use of a crafter now opens CrafterMenu (minecraft:crafter_3x3)
over the block entity. The host runs its clicks on the menu engine's
verified Kind::CrafterBlock (a disabled slot refuses items, filling one
enables it), sends the disabled slots and triggered as data slots
(CrafterBlockEntity.containerData), and handles
ServerboundContainerSlotStateChangedPacket (new in horizon-protocol) as
CrafterBlockEntity.setSlotState. The guest shows the server's crafter,
predicts its clicks and sends a slot toggle before the click as
CrafterScreen.slotClicked does.

Part of #868.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 08:43:29 +02:00
Ludovic MarieandClaude Opus 5.5 ca5404e52d Beacon on the menu engine; LAN guests use beacons
BeaconMenu now runs on horizon_client::menu (Kind::Beacon): the payment
slot (beacon_payment_items, one item), quickMoveStack and updateEffects
with BeaconBlockEntity.validateEffects, verified against the vanilla class
by test_menu_click_parity with a new SET_BEACON op (valid and invalid
powers for each pyramid level).

ServerboundSetBeaconPacket is now in the protocol. The LAN host opens
minecraft:beacon for guests with the block entity's levels and powers as
data slots (kept current while the menu is open), applies a guest's
powers to the block entity (power_select while the beam is up) and drops
the payment when the menu closes, as BeaconMenu.removed does. Guests show
the server's levels and powers and their Done button sends the packet.
Single player's beacon clicks go through the engine too.

Part of #868.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 08:43:29 +02:00
Ludovic MarieandClaude Opus 5.5 b5a92262c8 Enchanting table on the menu engine; LAN guests enchant
EnchantmentMenu now runs on horizon_client::menu: the item slot (one
item) and the lapis slot, its quickMoveStack, slotsChanged clearing the
offers without an enchantable item, and clickMenuButton's checks against
the costs, lapis and levels, verified against the vanilla class by
test_menu_click_parity (with its ten data slots and the offer buttons;
the oracle's player now has a profile for the invalid-button log line).
The offers and the enchanting itself, which need a level, stay with the
caller.

Single player routes the table's clicks, drags and double clicks through
the engine. The LAN host opens minecraft:enchantment for guests with
their own enchantment seed (saved with their player data), sends the
costs, seed and clues as data slots, and on ContainerButtonClick
enchants the item, takes the guest's level and lapis and gives a new
seed. Guests show the server's offers and send the button the client
menu accepts.

Part of #868.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 08:43:29 +02:00
Ludovic MarieandClaude Opus 5.5 3e868683ba Anvil and smithing table on the menu engine; LAN guests use them
AnvilMenu and SmithingMenu (ItemCombinerMenu) now run on
horizon_client::menu: their slot rules, createResult (the anvil's cost
data slot, repair count and rename, the smithing table's hasRecipeError),
mayPickup with the player's levels, the results' onTake and
quickMoveStack, verified against the vanilla classes by
test_menu_click_parity with repairs, merges, renames (a new RENAME op,
AnvilMenu.setItemName) and the player's levels. The harness found the
anvil treating only enchanted books as books (vanilla: anything with
stored enchantments), now fixed, and the grindstone's and anvil's result
slots now count for a double click's pick-all as in vanilla.

Single player routes both screens' clicks, drags and double clicks
through the engine; the anvil's wear and level events 1030/1029, and the
smithing table's 1044, play with vanilla's random pitch.

The LAN host opens minecraft:anvil and minecraft:smithing for guests,
handles ServerboundRenameItemPacket (new in horizon-protocol), charges the
guest's levels and wears the anvil; guests send the rename box as
AnvilScreen.onNameChanged does and show the server's cost. The control
socket's `type` now reaches the anvil's rename box.

Part of #868.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 08:43:29 +02:00
Ludovic MarieandClaude Opus 5.5 4fecf43d28 Grindstone on the menu engine; LAN guests use grindstones
GrindstoneMenu now runs on horizon_client::menu (Kind::Grindstone): its
input filter, computeResult, the result's onTake (both inputs cleared,
the experience sum handed to the caller) and quickMoveStack, verified
against the vanilla class by test_menu_click_parity with enchanted,
damaged and cursed tools and books.

Single player routes the grindstone's clicks, drags and double clicks
through the engine and rolls the orbs and level event 1042 (now playing
block.grindstone.use) where the result is taken. The LAN host opens
minecraft:grindstone for guests on the same engine and awards the orbs at
the block; guests read enchantments, stored enchantments, repair cost and
custom names from the server's stacks, and the host now sends repair
cost.

Part of #868.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 08:43:29 +02:00
Ludovic MarieandClaude Opus 5.5 c8f02d91b7 Stonecutter on the menu engine; LAN guests use stonecutters
StonecutterMenu joins horizon_client::menu (Kind::Stonecutter): the input's
recipe list (slotsChanged / setupRecipeList), selectedRecipeIndex as its data
slot, clickMenuButton (an invalid index is taken too), the result's onTake
(one input used up, awardUsedRecipes, the take sound) and quickMoveStack with
the leftover of a result move dropped. test_menu_click_parity compares it
with vanilla's StonecutterMenu, recipe buttons included.

Single player's stonecutter screen now clicks through the engine (drags and
double clicks included), and its recipe grid calls clickMenuButton as
StonecutterScreen does. A LAN guest's stonecutter opens minecraft:stonecutter
on the host's engine, as the loom does (#868).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 08:43:29 +02:00
Ludovic MarieandClaude Opus 5.5 cd37ea98a0 LAN guests use looms: workstation menus on the host's menu engine
A guest's use of a loom opens LoomMenu (minecraft:loom) for the guest. The
host runs the guest's menu on the shared menu engine (horizon_client::menu,
Kind::Loom, as single player does): clicks, the pattern buttons
(ServerboundContainerButtonClickPacket), the content and data slot sync after
each, the take-result sound at the loom, stillValid (the block gone or out of
reach) and clearContainer when it closes or the guest leaves. The guest opens
the loom screen for the server's menu, predicts its clicks and buttons on the
engine and sends them, runs the menu's client-side slotsChanged on the
server's slots and reads the selected pattern from the data slot.

The framework (game/lan_stations.rs, game/remote_stations.rs) takes the other
workstations one by one (#868).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 08:43:29 +02:00
Ludovic MarieandClaude Opus 5.5 255f1960ee Structures: Mo' Structures generate (jigsaw with its terrain check, config spacing, exclusion by spread)
Carry maps mostructures:generic to vanilla's jigsaw with horizon:start_checks
(terrain_radius 2, terrain_range = heightRange) and mostructures:type to a
random spread with the config's default spacing and separation and its sets
to avoid as horizon:exclusion_zone. The game's exclusion zone gains
"their_zones": false, which tests the avoided sets by spread and frequency
alone, as ModStructurePlacement does.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 08:33:20 +02:00
Ludovic MarieandClaude Opus 5.5 30a2cc7c61 Wayland: pin the 8-bit surface choice with a colour-managed compositor's list (#1007)
#1007's white overlay on Hyprland is #1075's washed-out swapchain: a
Wayland compositor with colour management makes Mesa list Rgba16Float
first, and the first non-sRGB format was taken. Reproduced on a real
GPU (Mesa 26.2 ANV, Intel HD 620) under headless weston 16: the old
choice washes the frame out, main's Bgra8Unorm doesn't. This test keeps
that list, as wgpu reports it, choosing an 8-bit format.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 08:26:52 +02:00
Claude bc9e5afacc Faster feature decoration: region grid, ore column fast path, lazy biome cache
TerrainRegion indexes its chunks through a grid over their bounding box.
OreFeature asks the world whether any column under its blob reaches the
floor heightmap in one call, and tracks tested positions in a bitset.
BiomeTemperature no longer preallocates its map and queue. Same output.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01M3pNrSHA2ifNrH7SAMHgSW
2026-10-22 06:26:36 +00:00
Ludovic MarieandClaude Opus 5.5 f8d0ef3cfe Merge main into fix/remote-player-abilities
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 08:15:44 +02:00
Ludovic MarieandClaude Opus 5.5 16738c9f26 Launcher: say when a prepared version has no sounds (#1035)
A version prepared while the sound copy failed (Mojang's republished 26.3
asset index, #1076, fixed in #1091), or one that reused such a version's
resources when installed, stays silent and showed as plain Prepared. The
Play page now says it is prepared without its sounds and makes Prepare
again the visible fix; versions --installed and the MCP list say so too.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 08:10:31 +02:00
Ludovic MarieandClaude Opus 5.5 e70b520e5d Render thread ahead of the workers on macOS and Windows
The client asks for its render thread what it already asks for the
server thread (vanilla's MinecraftServer.spin): user-interactive QoS on
macOS, above-normal priority on Windows, on machines with more than four
processors. While terrain loads at 32 chunks, the worker pools (two with
single player's server thread), the chunk encoders and other programs
outnumber the cores. A frame that gave its core up in queue.submit then
waited 10 to 200 ms to get one back: the long frames of #678.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 07:57:47 +02:00
Ludovic MarieandClaude Opus 5.5 c2b7f3a4ea Slow-frame and slow-tick lines show the CPU time of each span
HORIZON_SPIKES spans keep the thread's CPU time beside their wall time,
and the slow-frame line gives the render thread's CPU time in the frame.
A span that mostly waited (for the GPU, a lock or a core) prints both
(`submit 30.2/0.1`), so a frame that waited reads apart from one that
worked. Found the cause of #678's long frames: the render thread losing
its core in queue.submit.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 07:55:54 +02:00
Ludovic MarieandClaude Opus 5.5 8e700aa683 Riptide: run vanilla's auto-spin attack after the launch
Releasing a Riptide trident now starts LivingEntity.startAutoSpinAttack(20,
8.0): for 20 ticks the player takes the SPIN_ATTACK pose (0.6x0.6, eye
height 0.4), hits the first living entity its swept box touches for the
spin's damage and bounces back (velocity x -0.2), and stops on a horizontal
collision. The player model spins (setupRotations: rotX(-90 - xRot),
rotY(age * -75)), SpinAttackEffectLayer draws the riptide boxes, and the
first-person hand uses ItemInHandRenderer's spin transform. LAN guests see
the host's spin through living flag bit 4.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 07:43:55 +02:00
Ludovic Marie e730dc1426 Merge pull request #1164 from ludovic111/fix/moving-piston-tint-pos
🤖 Agent: merged. Verify passes. This fixes main's build (E0061, where #1113 and #1115 crossed).
2026-10-22 07:38:39 +02:00
Ludovic Marie d9560d46e6 Merge remote-tracking branch 'origin/fix/moving-piston-tint-pos' into HEAD 2026-10-22 07:24:39 +02:00
Ludovic MarieandClaude Opus 5.5 89747692d2 Moving pistons: pass the block position to particle_sprite (main build fix)
#1113 (moving pistons) and #1115 (break particles, which gave particle_sprite
a position for its biome tint) each passed alone; together main no longer
compiled. A moving block now takes its tint at its own block position, as
vanilla's moving-block renderer reads the level's tint there.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 07:24:24 +02:00
Ludovic Marie 26142a9c40 Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	tools/lan_e2e.py
2026-10-22 07:18:55 +02:00
Ludovic Marie ee3ac16594 Merge pull request #1113 from ludovic111/fix/moving-piston-collision
🤖 Agent: merged. Verify and CI parity pass (the earlier online_mode failure was a race and passed on rerun). Moving blocks are drawn and solid; entity pushing is in FEATURES.md LATER 4h.
2026-10-22 07:03:19 +02:00
Ludovic Marie 52c24d1ac9 Merge pull request #1160 from ludovic111/integ/b5-lan-rest
🤖 Agent: merged. Verify passes. Proof: the B5 integration of these eight heads (parity --changed 102/102, lan_e2e 46–47/48 with only order-dependent flakes), and the conflict resolution in tools/lan_e2e.py checked def by def against each branch.
2026-10-22 07:03:15 +02:00
Ludovic Marie b56b6e824e Merge pull request #1159 from ludovic111/fix/axolotl-idle-swim
🤖 Agent: merged. Verify passes; local parity --changed origin/main passes (101 harnesses; #1159's one online_mode diff reran clean and passes on main). Screenshots and tick-by-tick results in the body.
2026-10-22 06:25:18 +02:00
Ludovic Marie 0840b1f8ca Merge pull request #1156 from ludovic111/fix/sulfur-bubbles
🤖 Agent: merged. Verify passes; local parity --changed origin/main passes (101 harnesses; #1159's one online_mode diff reran clean and passes on main). Screenshots and tick-by-tick results in the body.
2026-10-22 06:25:14 +02:00
Ludovic Marie 95cee53d13 Merge remote-tracking branch 'origin/fix/guest-predicts-door-use' into integ/b5-lan-rest
# Conflicts:
#	tools/lan_e2e.py
2026-10-22 06:17:27 +02:00
Ludovic Marie 195403a6e1 Merge remote-tracking branch 'origin/fix/lan-player-arrows-hit-players' into integ/b5-lan-rest
# Conflicts:
#	tools/lan_e2e.py
2026-10-22 06:17:27 +02:00
Ludovic Marie 48c8975097 Merge remote-tracking branch 'origin/fix/lan-guest-kills-are-theirs' into integ/b5-lan-rest
# Conflicts:
#	tools/lan_e2e.py
2026-10-22 06:17:27 +02:00
Ludovic Marie 705da8b248 Merge remote-tracking branch 'origin/fix/lan-guest-worn-armour' into integ/b5-lan-rest
# Conflicts:
#	tools/lan_e2e.py
2026-10-22 06:17:27 +02:00
Ludovic Marie 214d32865c Merge remote-tracking branch 'origin/fix/explosions-every-player' into integ/b5-lan-rest
# Conflicts:
#	tools/lan_e2e.py
2026-10-22 06:17:26 +02:00
Ludovic Marie a558b046cd Merge remote-tracking branch 'origin/fix/piglin-barter-nearest-player' into integ/b5-lan-rest
# Conflicts:
#	tools/lan_e2e.py
2026-10-22 06:17:26 +02:00
Ludovic Marie 2159aa2205 Merge remote-tracking branch 'origin/fix/ghast-fireball-deflect' into integ/b5-lan-rest
# Conflicts:
#	tools/lan_e2e.py
2026-10-22 06:17:19 +02:00
Ludovic Marie 4002595331 Merge remote-tracking branch 'origin/fix/small-fireball-fire' into integ/b5-lan-rest 2026-10-22 06:16:35 +02:00
Ludovic MarieandClaude Opus 5.5 832f879b27 Dynamic lights: held, dropped and burning light sources (Horizon Extras)
A new Horizon Extras option, Lighting > Dynamic lights (`dynamicLights`,
off by default): a torch, lantern or other light-emitting item held by a
player or mob, dropped as an item, or any entity on fire lights the blocks,
entities and hand around it, as LambDynamicLights and OptiFine do.

Client-only and at render time: the light engine, chunk meshes and saves
never see it. Each frame the game picks the 16 sources nearest the camera
(dynamic_light::select) and passes them in Globals; terrain.wgsl and
entity.wgsl raise each vertex's block light to `level - distance` when that
is brighter. Off, the count in globals.time.z is 0, nothing is gathered and
the shaders' loop runs zero times, so no pipeline is rebuilt either way.

An item glows like its block's default state (torch 14, lantern and
glowstone 15, lit redstone torch 7); a lava bucket 15; burning entities 15.

Fixes #814

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 05:40:09 +02:00
Ludovic MarieandClaude Opus 5.5 f59f6e3e84 lan_e2e portals: take the portals away afterwards
The guest came home into its Nether portal's frame, with an obsidian
ceiling three blocks up, and the host onto the End portal it had set at
spawn; both stayed there for the rest of the run. Under that ceiling a
double jump rises about 1.2 blocks at most, and whether the flight check
saw it depended on timing: `flight` failed in full --server-thread runs
(portals runs only there) and passed alone. The check now clears the
frame and the End portal in the overworld, and puts the guest back where
it started, also when it fails.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 05:16:51 +02:00
Ludovic MarieandClaude Opus 5.5 b7453b129b agent_control_e2e survival: wait for the log's drop or its pickup
On single player's server thread the broken log's drop and its pickup
both come from the server, a few ticks after `break` answers: the
pickup could land before the scenario looked for the drop, which it
then never found (StopIteration). Step 3 now waits up to 20 ticks for
the logs in the inventory or the drop to walk to.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 05:16:51 +02:00
Ludovic MarieandClaude Opus 5.5 05e169f4e3 agent_control_e2e pick: wait for the server to clear the cursor
Step 2 clones a stack in the creative tab and closes the screen. The
clone lived only on the client: ItemPickerMenu's carried stack is the
inventoryMenu's, and AbstractContainerMenu.removed clears the carried
stack only for a ServerPlayer, so vanilla's client still holds it when
the survival inventory opens. With a stack in hand the middle button's
release sends CLONE (AbstractContainerScreen.mouseReleased); the server
finds the carried stack it was told about wrong and sends the empty one
(broadcastChanges). On single player's server thread that answer takes
a round trip, as in vanilla, so step 3 now waits up to 20 ticks for it.
The in-frame world puts the clone away when the menu closes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 05:16:51 +02:00
Ludovic MarieandClaude Opus 5.5 8b75ac3cdb lan_e2e and agent_control_e2e on the server thread
From b9f3821f (fix/server-thread-default): the host window draws what
it was sent on the server thread, and agent_control_e2e waits two ticks
where the server answers a click.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 05:16:51 +02:00
Ludovic Marie 55891f9c95 Wait for the server's block in the agent's place, try hidden spots
On a client of a server (single player's server thread, a LAN guest) the
placed block comes with the server's answer: `place` waits up to a second
for it before reporting no change. agent_control_e2e tries the next free
spot when the tree's trunk above the broken log hides a spot's faces.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 9821bb7ef5)
2026-10-22 05:16:51 +02:00
Ludovic Marie 2f883b5cdb Keep the command harness in the frame; compare finished border chunks
- The command harness (HORIZON_COMMAND_SCRIPT) drives the world itself,
  without the event loop, so it does not start the server thread.
- `status` lists the minecarts a client of a server was sent.
- save_roundtrip: single player's server thread finishes the chunks its
  player tracks, as vanilla's integrated server does, including some that
  vanilla's server (view distance 4) left before their terrain or light.
  Those had no blocks, light or heightmaps to compare yet.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 2818c89f24)
2026-10-22 05:16:51 +02:00
Ludovic Marie 0468b75c43 Run single player on its server thread by default
Single player's world now runs on the integrated server thread unless
asked otherwise, as vanilla's does, so a world tick no longer holds up
a frame. `--no-server-thread` or `HORIZON_SERVER_THREAD=0` keeps the
world in the frame as an escape hatch; `--server-thread` stays.

lan_e2e.py sets the host's mode explicitly either way.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 4ff23b686d)
2026-10-22 05:16:51 +02:00
Ludovic Marie 3668973807 Merge pull request #1133 from ludovic111/fix/openal-stereo-panning
🤖 Agent: merged. Verify passes; CI parity's only failure is the known dedicated_server pearl flake. Measured panning against OpenAL Soft's stereo output in the body.
2026-10-22 05:04:32 +02:00
Ludovic Marie 1ec77ad997 Merge pull request #1117 from ludovic111/fix/thrown-potion-render
🤖 Agent: merged. Verify passes. CI parity's only failure is server_player_targets, a server harness this client rendering change can't touch; it has flaked before (its log said '109 lines identical' on #1118). Screenshots in the body.
2026-10-22 05:04:29 +02:00
Ludovic Marie e6e18b8e4c Merge pull request #1154 from ludovic111/feat/mod-browser-page
🤖 Agent: merged. Verify passes (launcher only). Mod browser part 3/3: the launcher's Mods page, with screenshots in the body.
2026-10-22 05:04:25 +02:00
Ludovic Marie 6f5a9b61e7 Merge pull request #1153 from ludovic111/feat/mod-browser-install
🤖 Agent: merged. Verify passes (launcher only). Mod browser part 2/3: install, update and remove.
2026-10-22 05:04:00 +02:00
Ludovic Marie 99aaccc977 Merge pull request #1141 from ludovic111/feat/mod-browser-sources
🤖 Agent: merged. Verify and CI parity pass. Mod browser part 1/3: Modrinth source and checksum verification. The CurseForge source stays inactive until a key is configured.
2026-10-22 05:03:13 +02:00
Ludovic MarieandClaude Opus 5.5 86b58b71fa Client of a server: abilities from the server, flight sent back
A player who joined already in creative could not fly: the remote
client kept only instabuild and the flying speed of
ClientboundPlayerAbilitiesPacket, and the motion step clears flying for
a player that may not fly. The packet also comes before the client has
a player (it is made at the first teleport). Single player creative on
its server thread could not fly at all; a LAN guest only after a game
mode change.

- The remote client keeps the last abilities and applies flying, may
  fly, invulnerable and the flying speed to the player, also when the
  player is made (ClientPacketListener.handlePlayerAbilities).
- When its flight turns on or off (double jump, spectator, landing) it
  sends ServerboundPlayerAbilitiesPacket (LocalPlayer.onUpdateAbilities).
- The hub keeps it as the guest's flying when the guest may fly
  (handlePlayerAbilities), so the save has it, and a game mode change
  sets it as GameType.updatePlayerAbilities does.
- lan_e2e `flight`: the guest, and on the server thread the host's
  window, fly in creative, stay up, and stop in survival.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 04:43:05 +02:00
Claude dfe795bc09 Faster Perlin volumes and climate search, same results
PerlinNoise::add_volume worked out each Y coordinate's cell, offset and
fade once per X/Z column. Every column walks the same Y coordinates, so
they are now computed once per volume. The arithmetic is unchanged, so
the output is bit for bit the same.

ClimateIndex kept its tree as nodes with a Vec of child ids each. The
built tree is now renumbered breadth first into flat arrays, so a
branch's children are one contiguous run. The search visits the same
nodes in the same order, so ties resolve exactly as before.

terrain_bench (seed 12345, 8x8 chunks, 1 thread, medians of 3 runs):
terrain 14.5 -> 12.4 ms/chunk, biomes 1.20 -> 1.06 ms/chunk.
Checked against vanilla 26.3 in-process: 2304 chunks on seeds 12345, -1
and 987654321987 identical, and the biome-source and biome-generation
parity suites (2.1M lines) match.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01M3pNrSHA2ifNrH7SAMHgSW
2026-10-22 02:42:52 +00:00
Ludovic MarieandClaude Opus 5.5 56591efa5f A guest hears its door at once, and only once
Vanilla plays a block use's sound with `level.playSound(player, ...)`: the
player who clicked hears it from their own client's use, and the server
sends it to everyone else. Horizon's guest waited for the server's sound,
one round trip after its predicted door had already opened.

The guest now plays the sounds of the use it predicts (doors, trapdoors,
fence gates, levers, buttons), and the server sends the sounds of such a
use to the other players only. The control socket's `status` counts the
positional sounds each game played (`heard`).

lan_e2e `predict`: with the host frozen, the guest already hears its door;
once the server answers, the guest and the host have each heard it once.

Fixes #869

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 04:06:39 +02:00
Ludovic MarieandClaude Opus 5.5 ceff635615 A guest draws a bow, and lets go to shoot
A client of a server sent the bow's use but never started using the item
itself, so letting go of the button sent no RELEASE_USE_ITEM: the server
kept the guest drawing and never shot. BowItem.use runs on the client too
(startUsingItem, with an arrow or in creative), and Minecraft.handleKeybinds
releases the item in use once the button is up.

The guest now draws on its own side as well (held until the button lets go
or another slot is picked), so the server shoots when it lets go.

lan_e2e: `arrows` passes both ways now (the guest's arrow hurts the host);
the `pvp` and `arrows` checks allow the victim natural regeneration while
pvp is off (it healed 19.3 -> 20 and failed the exact check).

Fixes #484

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 04:06:39 +02:00
Ludovic MarieandClaude Opus 5.5 a4247e9323 A guest's door, trapdoor, fence gate, lever and button use is predicted
Vanilla's MultiPlayerGameMode.useItemOn runs the use on the client's own
level inside startPrediction, records each block it changes under the
packet's sequence in BlockStatePredictionHandler, and keeps them until
ClientboundBlockChangedAckPacket settles them (a use the server refused
goes back). A Horizon guest only sent the use: its door opened one round
trip later.

The guest already ran the use on a dry copy of its level to know whether
the click ends there; the blocks that use changes are now kept on its
level as predictions under the use's sequence, as the break prediction
does, when they are all hand-operated blocks: a wooden or copper door,
trapdoor or fence gate, a lever or a button (an iron door has no hand use
and changes nothing). The sound still comes from the server.

lan_e2e `predict`: with the host process frozen, the oak door the guest
clicks is already open on the guest's side (both halves), stays open on
both sides once the server answers, and an iron door stays shut.

Fixes #869

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 04:06:39 +02:00
Ludovic MarieandClaude Opus 5.5 5763854b0d A player's arrow hurts the other players on a served world
A remote player's projectiles never hit other players: a player's shots
only looked for mobs and the host, a guest's flew as if the host had shot
it (so it could not hit the host until it left the host's box), and a hit
on a guest from another guest ran as the shooter, who has no hub to reach
the victim. No bow PvP on a server or between LAN players.

A player's arrows, snowballs and eggs now meet every player, a guest's
flying from that guest (Projectile.getOwner). A hit on another player runs
as the victim's hurt, refused when the pvp game rule is off or the teams
are allies without friendly fire (ServerPlayer.canHarmPlayer), which turns
the arrow back as a raised shield does (AbstractArrow.onHitEntity).
Pearls, potions and wind charges fly past players as before.

lan_e2e `arrows`: the guest shoots the host and the host the guest with a
bow, both hurt; with pvp off, no harm.

Part of #484

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 04:06:39 +02:00
Ludovic MarieandClaude Opus 5.5 def242ecc1 A mob a guest kills is the guest's kill
LivingEntity.die credits the kill to lastHurtByPlayer: the killed
statistic, the mob_kills custom statistic, the kill score criteria and the
player_killed_entity trigger belong to that player. Horizon kept only
whether a player had hurt the mob, and the death ran as the host, so a LAN
or server guest's kills counted in the host's statistics and advancements.

The mobs a player hurt now remember which player (the acting guest's
connection, or the host), and the killer's side of the death runs as that
guest.

lan_e2e `kills`: a pig the guest kills shows in the guest's
minecraft.killed:minecraft.pig score, not the host's.

Part of #484

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 04:06:38 +02:00
Ludovic Marie 67569ef984 A LAN guest's worn armour protects it
The hub's players are server players too, and in vanilla the armour they
wear counts against every hit (LivingEntity.collectEquipmentChanges puts
each piece's attribute modifiers on the player, getDamageAfterArmorAbsorb
reads them). Horizon only put them on the host: a guest who put armour on
in its inventory menu (the hub moves it), joined wearing it, respawned with
it kept or came through a portal (a new body each time) was hurt as if it
wore nothing. Iron armour, 15 points, should take a 10-damage blow down to
6; a guest took the full 10.

Each guest now keeps its own list of applied equipment modifiers, swapped
in with the rest of its state for its turn, and its armour and main hand
are put on its attributes whenever they differ from what they were put on
for (a new body starts with none).

lan_e2e `worn`: the guest shift-clicks iron armour on in its inventory and
takes 10 mob-attack damage: 6 with it, 10 without (failed before: 10 and 10).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit e0549f88ce)
2026-10-22 04:06:37 +02:00
Ludovic Marie 73c5a1c5d6 Explosions hurt and push every player, through a raised shield
ServerExplosion.hurtEntities takes every entity in range, players
included, but only the host player was hurt and pushed: LAN guests stood
in a blast untouched. The player's part is now its own function, run for
the host and then for each guest in the level, swapped in as the player.

It also follows Player.hurtServer more closely for everyone: the damage
goes through a raised shield (an explosion is blocked like any attack
from where the blast is, the shield takes wear), the push is reduced by
explosion knockback resistance, and a spectator or a creative player
flying is not pushed.

tools/lan_e2e.py gains a `blast` check: primed TNT five blocks from a
survival guest hurts it (20 -> 4.6) and pushes it away; facing it behind
a raised shield, the guest takes nothing. Before this change the guest
took no damage and no push.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 0782a598fe)
2026-10-22 04:06:37 +02:00
Ludovic Marie 8df160f63f Piglins throw their barter toward the nearest player, guests included
PiglinAi.throwItemsTowardPlayer aims at NEAREST_VISIBLE_PLAYER: the
nearest living, non-spectator player within 16 blocks. Only the host was
considered, so a LAN guest who bartered saw the loot fly to the host.

lan_e2e's new barter check: the guest, in gold boots, drops a gold ingot
to a piglin in a glass corridor with the host on the far side; the
piglin stays calm and the barter reaches the guest. It fails on the old
build (the loot lands on the host's side).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 896f12f732)
2026-10-22 04:06:36 +02:00
Ludovic Marie ba72a097fa A player's blow turns a ghast's fireball back
Player.attack on a #minecraft:redirectable_projectile (a ghast's fireball,
a wind charge) deflects it with ProjectileDeflection.AIM_DEFLECT: its
movement becomes the attacker's unit view vector and the attacker its
owner, with the no-damage sound. The crosshair finds these projectiles a
block round their box (AbstractHurtingProjectile.getPickRadius), a remote
client sends the attack for them, and the host turns one a guest strikes
along that guest's view. A fireball a player turned back kills the ghast
it hits (Ghast.isReflectedFireball: no fire immunity, 1000 damage).

lan_e2e.py --check deflect: the host strikes a caged ghast's fireball and
the ghast dies; before, the fireball hit the host.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 6ba02f5205)
2026-10-22 04:06:36 +02:00
Ludovic Marie cb4ecfbbe9 A small fireball's fire is placed like vanilla's, and goes out
SmallFireball.onHitBlock places fire with Level.setBlockAndUpdate and
BaseFireBlock.getState (soul fire on soul soil and soul sand), and a mob's
fireball only when mobGriefing is on. Horizon wrote a plain fire state
straight into the chunk: no block updates and no scheduled fire tick, so
the fire never aged and never went out, even on stone or nether bricks
where vanilla's goes out within about half a minute (FireBlock.tick). Every
blaze fireball that missed left a fire that stayed for good: fortress
floors round a spawner filled with fire that kept everyone near it burning.

The fire is now the level's fire state for the spot, set with updates
(the fire's tick is scheduled on place), and only with mobGriefing when a
mob shot it.

lan_e2e `fireball`: a dispenser shoots a fire charge onto stone; both
players see the fire and it goes out by itself (failed before: still burning
after 1200 ticks; now out after about 480).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 5b7235c24b)
2026-10-22 04:06:35 +02:00
Ludovic Marie c1b5fe95d2 Merge pull request #1152 from ludovic111/perf/advancement-index-async-log
🤖 Agent: merged. Verify passes. B5's local integration of all its PRs on main: parity --changed 102/102, plus lan_e2e (flakes listed in FEATURES.md LATER 4l).
2026-10-22 04:05:36 +02:00
Ludovic Marie 635faf6d27 Merge pull request #1150 from ludovic111/fix/locator-bar-per-level
🤖 Agent: merged. Verify passes. B5's local integration of all its PRs on main: parity --changed 102/102, plus lan_e2e (flakes listed in FEATURES.md LATER 4l).
2026-10-22 04:05:15 +02:00
Ludovic Marie 229f2139d6 Merge pull request #1145 from ludovic111/fix/hurtlog-guest-respawn
🤖 Agent: merged. Verify passes. B5's local integration of all its PRs on main: parity --changed 102/102, plus lan_e2e (flakes listed in FEATURES.md LATER 4l).
2026-10-22 04:05:02 +02:00
Ludovic Marie 615aa8f926 Merge pull request #1143 from ludovic111/fix/ominous-item-spawner-save
🤖 Agent: merged. Verify passes. B5's local integration of all its PRs on main: parity --changed 102/102, plus lan_e2e (flakes listed in FEATURES.md LATER 4l).
2026-10-22 04:04:50 +02:00
Ludovic Marie 96ae26d865 Merge pull request #1136 from ludovic111/fix/summon-tracked-ids
🤖 Agent: merged. Verify passes. B5's local integration of all its PRs on main: parity --changed 102/102, plus lan_e2e (flakes listed in FEATURES.md LATER 4l).
2026-10-22 04:04:37 +02:00
Ludovic Marie affb823466 Merge pull request #1135 from ludovic111/fix/world-spawn-before-save
🤖 Agent: merged. Verify passes. B5's local integration of all its PRs on main: parity --changed 102/102, plus lan_e2e (flakes listed in FEATURES.md LATER 4l).
2026-10-22 04:04:25 +02:00
Ludovic Marie c690303948 Merge pull request #1144 from ludovic111/fix/lan-players-lie-in-bed
🤖 Agent: merged. Verify passes. B5's local integration of all its PRs on main: parity --changed 102/102, plus lan_e2e (flakes listed in FEATURES.md LATER 4l).
2026-10-22 04:03:23 +02:00
Ludovic Marie dbeebefbda Merge pull request #1134 from ludovic111/fix/loot-set-potion
🤖 Agent: merged. Verify passes. B5's local integration of all its PRs on main: parity --changed 102/102, plus lan_e2e (flakes listed in FEATURES.md LATER 4l).
2026-10-22 04:03:02 +02:00
Claude 9019d708d0 Streaming: answer "near a player?" from a grid of the players' squares
On a server every player is an extra area, and streaming asks of every
loaded and wanted column, several times a tick (unloading, dropping
protos, finishing, decoration, the generation scan), whether one of
those squares reaches it: a scan of every player's square per question.
The squares are now one grid of distances, made again when a player
changes chunk, and in_extra reads it. Differential test against the scan
over 60 random layouts, every margin.

At 50 players, 6081 columns, 3 questions each: 0.45 ms -> 0.04 ms, plus
0.02 ms to make the grid.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01UxXpf7d4RgpespL2G2UsGE
2026-10-22 01:40:47 +00:00
Claude 186777110f Hoppers: skip the item scan when no item reaches the hopper's column
HopperBlockEntity.suckInItems scanned every item entity for each hopper
that ticked without a container above. During the block-entity pass the
game now keeps the set of block columns item boxes reach; a hopper whose
column is not in it has nothing to suck, and the scan (same order, same
item taken) runs only for the rest. Items do not move within the pass:
removals leave a superset, and additions (pushed at the end with a new
id) or removing the last item rebuild the set. It is dropped at the end
of the pass.

  500 hoppers,  200 items:  1.36 -> 0.05 ms per tick
  2000 hoppers, 500 items: 13.52 -> 0.26 ms per tick
  2000 hoppers, 2000 items: 60.6 -> 3.3 ms per tick

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01UxXpf7d4RgpespL2G2UsGE
2026-10-22 01:40:38 +00:00
Ludovic MarieandClaude Opus 5.5 ac25d41578 Shader Packs page: show the first line of a pack's error
Translator errors go on with a drawn source excerpt that the row can't show.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 03:32:23 +02:00
Ludovic MarieandClaude Opus 5.5 acbfb8ceca docs/shader-packs.md: the ten packs tested and what stops the rest
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 03:32:23 +02:00
Ludovic MarieandClaude Opus 5.5 6ea7a51ff3 Shader Packs page: the folder's path, Open Folder, a switch per pack
The Shader Pack setting cycled through what shaderpacks/ held, so with the
folder empty (or the packs in the launcher's version folder, as brabus_ had
them) it silently stayed Off. The row now opens a Shader Packs page: a
sidebar section in the Horizon menus, Video Settings > Shader Packs... in
the Classic ones. It shows the game home's shaderpacks/ path and an Open
Folder button (made if missing), says when there is no pack, and lists one
switch per pack. A pack that fails to load says why on its row. The list is
read again every second while the page shows.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 03:32:23 +02:00
Ludovic MarieandClaude Opus 5.5 e570561db9 Shader packs: write narrow fragment outputs through a vec4
GL fills the channels a fragment shader leaves out; wgpu refuses a pipeline
whose output has fewer channels than its target. Steadfast's final pass
writes `out vec3` and failed to build in the game.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 03:32:23 +02:00
Ludovic MarieandClaude Opus 5.5 21998a7e6a Shader packs: read pack sources as Iris's JCPP does; legalise what naga refuses
Preprocessor (GLSL and shaders.properties), as JCPP does:
- a #define replaces an earlier macro (Bloop, Steadfast: DefineRedefined);
- in #if, a float counts as its integer part (Steadfast's
  shaders.properties float token), and a macro always defined empty counts
  as 0 (Iris's MC_GL_VENDOR_OTHER, Bloop's TAA_USE_YCOCG);
- an #if may run to the end of the input (Bloop's taa.fsh);
- #error "..." lexes in skipped blocks (Photon);
- shaders.properties keeps its empty lines and hides backslashes from the
  preprocessor (IRIS_PASSTHROUGHBACKSLASH), so a continued value no longer
  swallows the next directive or line (Photon, Bliss, Steadfast's custom
  uniforms; Complementary's isCold uniform is now complete).

Legaliser: % through overloads (naga won't take ivec2 % int: Noble),
sqrt of a negative constant left to the GPU (Bloop's NaN), constants naga
can't fold (?:, mix: Bliss) become globals set in main, and 16/8-bit types
(u16vec2 & co., Noble on NVIDIA) become their 32-bit ones.

horizon-client: real_packs_load runs the game's own load over the packs in
HORIZON_TEST_SHADERPACKS (the compatibility table's source).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 03:32:23 +02:00
Ludovic MarieandClaude Opus 5.5 cf96b7c097 Mod browser (3/3): the launcher's Mods page
A Mods page between Versions and Modpacks: resource packs, shader packs and
modpacks as tabs, a search field, Show everything, and Modrinth / CurseForge
once CurseForge has a key. Results are index rows with summary, author,
downloads, source, licence and how well each runs (with the reason), Page and
Install (Import for a modpack, which then shows on the Modpacks page).
Installed packs list their version, source, file and the checksum they were
checked against, with Update and Remove. Searches, installs and update checks
run on threads; stale search answers are dropped.

Fixes #577

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 03:27:33 +02:00
Ludovic Marie 5aff29807a Merge pull request #1149 from ludovic111/fix/others-breaking-progress
🤖 Agent: merged. Verify passes. B5's local integration of all its PRs on main: parity --changed 102/102 (the explosion-dependent 22 rerun after rebasing onto #1101), and lan_e2e passes except checks that fail only after earlier checks and pass alone (in FEATURES.md LATER 4l).
2026-10-22 03:24:35 +02:00
Ludovic Marie d4739f31b2 Merge pull request #1137 from ludovic111/fix/dragon-server-levels
🤖 Agent: merged. Verify passes. B5's local integration of all its PRs on main: parity --changed 102/102 (the explosion-dependent 22 rerun after rebasing onto #1101), and lan_e2e passes except checks that fail only after earlier checks and pass alone (in FEATURES.md LATER 4l).
2026-10-22 03:24:32 +02:00
Ludovic MarieandClaude Opus 5.5 9346ac04ad Mod browser (2/3): install, update and remove, with horizon-launcher mods
browser::installed installs a pack and its required dependencies into
resourcepacks/ or shaderpacks/, every file checked before any download
(a dependency that is a Java mod stops it) and checksum-verified on the
way in; mod-browser.json records source, version, file and checksum.
Update swaps the file, remove deletes only what the browser installed (a
player's own file of the same name is never overwritten). A modpack is
downloaded, checked, and imported through the Modpacks page's pipeline.
The CLI gains `mods list|search|install|update|remove`.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 03:22:39 +02:00
Ludovic MarieandClaude Opus 5.5 784e8f0648 Mod browser (1/3): Modrinth and CurseForge sources, checksum-checked downloads
The launcher's core gains `browser`: search Modrinth for what Horizon loads
(resource packs, Iris/OptiFine shader packs, modpacks; never Java mod jars),
say how well each result runs, pick the newest version that runs, and
download a file only from Modrinth's or CurseForge's file servers, with a
plain file name, kept only when its size and SHA-512 (SHA-1 on CurseForge)
match. CurseForge is a pluggable source that stays off until an API key
exists (HORIZON_CURSEFORGE_API_KEY at run time or build time; the launcher
workflow passes the repository secret). docs/mod-browser.md.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 03:17:25 +02:00
Ludovic MarieandClaude Opus 5.5 49a224df9f Axolotl: free water paths, flat walk targets and the attack sound
Three differences from vanilla's Axolotl kept the brain from matching:

- The constructor's setPathfindingMalus(WATER, 0) was missing, so every
  swim target in water failed DefaultRandomPos's malus check and idle
  axolotls never picked a walk target.
- Axolotl.getWalkTargetValue returns 0 everywhere; Horizon used Animal's
  light-based value, which picked the brightest candidate instead of the
  first.
- Axolotl.playAttackSound (entity.axolotl.attack at pitch 1) never played.

The mob AI harness gets #64's two scenarios, in a pool sunk into the ground
so no water flows out of it: hunting a cod, and playing dead after repeated
hits. Both now match vanilla on every tick (393/393 mob-ticks, was 26).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 03:15:12 +02:00
Claude cb0bbe7d19 Sculk: skip the loaded-chunk diff while no chunk came or went
sculk_sync_index collected every loaded chunk key into a set and diffed
it against the known chunks every ten ticks, thousands of keys on a busy
server. It now remembers the chunk map's membership revision and skips
the diff while it is unchanged; a replaced chunk keeps its key and was
not rescanned before either.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01UxXpf7d4RgpespL2G2UsGE
2026-10-22 01:10:06 +00:00
Claude 863a309161 Allays and piglins: hoist per-tick lookups out of the per-mob loop
tick_allays scanned every block entity of the level for each allay,
every tick, to find a playing jukebox within 10 blocks. The playing
jukeboxes are now collected once, at the first allay (no allay starts
or stops one).

piglin_pickups looked up the piglin_loved tag and hashed every item
entity's id once per piglin. Each item's flag is now computed once and
kept until an item is added (always pushed at the end under a new id)
or removed.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01UxXpf7d4RgpespL2G2UsGE
2026-10-22 01:10:06 +00:00
Claude 3917f35e3e Natural spawning: answer mob box queries from a grid after a few scans
GameLevel's isUnobstructed, noCollision (living shulkers) and the
spawners' entity count each scanned every mob, computing each 1.5 KB
mob's box. A spawn pass holds the game immutably, so after eight
queries it builds a grid of the mobs' boxes once and reads candidates
from it; the first queries still scan, as the build costs a few scans.

  100 box tests, 2000 mobs: 3.48 -> 0.31 ms (grid build included)

Also gives the mob_ai_probe example the new others_index field.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01UxXpf7d4RgpespL2G2UsGE
2026-10-22 01:10:05 +00:00
Claude a893fa6125 Villagers: count the others' POI tickets once per search
pois_in_range (AcquirePoi and the job, bed and meeting point searches)
asked, for every candidate POI in range, how many tickets it had, and
each answer scanned every other mob for villagers holding it: POIs times
mobs per search. The others' tickets are now counted into a map at the
first candidate and read from it; this villager's own memories are
still checked per POI, as before.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01UxXpf7d4RgpespL2G2UsGE
2026-10-22 01:10:05 +00:00
Claude 3b21a975bb Mob goals: read nearby mobs from the tick's grid in four more places
FollowParentGoal (every baby, every tick), the pufferfish's puff check,
the enderman's block placing and the iron golem's DefendVillage scanned
every mob. They now read candidates from the position grid around the
area they test (one block wider where they test positions, as a box
whose bottom face holds the position meets it), in snapshot order, and
test exactly as before.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01UxXpf7d4RgpespL2G2UsGE
2026-10-22 01:10:05 +00:00
Claude 67ca65bb3b Mob tick: drop gone mobs' guest bindings with a set, not a scan per binding
On a server every ticked mob is bound to its nearest player, and each tick
the bindings of mobs that are gone were dropped by scanning every mob for
each binding. Mob is 1.5 KB, so the scan misses the cache on every mob:
with 1000 loaded mobs that was 0.44 ms a tick, with 2000 1.86 ms; a set of
the live ids makes it 0.02 and 0.04 ms (stand-in structs of Mob's size).

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01UxXpf7d4RgpespL2G2UsGE
2026-10-22 01:10:05 +00:00
Claude 6216b84669 Mob tick: build the mob world once a tick, index the snapshot
tick_creature_entities called mob_world_base for every mob. That rebuilt
the player view (its allies set scans every mob UUID) and, with any
scoreboard team, an owned copy of every mob's team, each a clone of the
team's player set: cubic in the mobs on a team. The base is now built
once and cloned per mob, with the teams borrowed behind an Arc.

  100 mobs, half on a team:  37.7 ms -> 0.06 ms per tick
  500 mobs, half on a team:  4297 ms -> 0.31 ms per tick

MobWorld::seen looked mobs up by scanning the snapshot; the mob ticks
now pass an id index (first slot wins, as the scan did): 2000 lookups
over 2000 mobs 1.09 -> 0.01 ms. The nearest-living-entity sensors, the
look-at goal, the ravager's roar and MobWorld::nearby read candidates
from the tick's position grid instead of every mob, in the same order:
2000 mobs 0.39 -> 0.11 ms per tick.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01UxXpf7d4RgpespL2G2UsGE
2026-10-22 01:10:05 +00:00
Ludovic Marie 1fda49b9b0 Merge pull request #1119 from ludovic111/fix/boat-water-patch
🤖 Agent: merged. Verify passes. CI parity's failures are only the known flakes: world_spawn (vanilla gives several spawn heights for one seed across runs) and dedicated_server (play-pearl). This client rendering change can't move either. B2's integration passed every rendering harness, and before/after screenshots are in the body.
2026-10-22 03:03:27 +02:00
Ludovic Marie cb5355b3ff Merge pull request #1116 from ludovic111/fix/end-crystal-spin
🤖 Agent: merged. Verify passes. CI parity's failures are only the known flakes: world_spawn (vanilla gives several spawn heights for one seed across runs) and dedicated_server (play-pearl). This client rendering change can't move either. B2's integration passed every rendering harness, and before/after screenshots are in the body.
2026-10-22 03:03:23 +02:00
Ludovic MarieandClaude Opus 5.5 c4d6b34172 Other players' block cracks are drawn
On a server or a world open to LAN, a player saw only their own crack.
The server sent each new stage to the others
(ClientboundBlockDestruction), but a client of a server ignored the
packet, and the host of a LAN world neither drew its guests' cracks nor
sent its own to them.

Vanilla keeps the cracks per breaker (ClientLevel.destroyBlockProgress:
a progress outside 0..9 removes it, 400 ticks without news drops it) and
draws, within 32 blocks, the furthest one on each block
(LevelExtractor.extractBlockDestroyAnimation). Horizon now does the
same: a client of a server takes the packet, a LAN host records its
guests' stages and sends its own (each new stage, then -1 when it
stops), and every crack is drawn with its own stage. A guest's crack
also goes when the block it digs is gone before its STOP_DESTROY_BLOCK
(its own tick broke it), as ServerPlayerGameMode.tick clears it on air:
the others kept its last stage for 400 ticks. Each crack vertex carries
its stage sprite's atlas origin in its light bytes (the crumbling pass
is unlit); the shader keeps the shared sprite size.

lan_e2e `cracks`: each side watches the block the other mines and sees
its crack grow; no crack is left once the blocks break. The screenshots
from both sides show the cracks.

Fixes #1096

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 03:02:56 +02:00
Ludovic Marie 9ad152d2b8 Merge pull request #1130 from ludovic111/fix/website-semver-tags
🤖 Agent: merged. Website only; verify passes and the website tests pass (2/2). The site shows nothing about 0.2 until a 0.2 release exists.
2026-10-22 03:02:53 +02:00
Ludovic Marie 8c99b1099a Merge pull request #1126 from ludovic111/docs/versioning-0-2
🤖 Agent: merged. Docs only; verify and CI parity pass. The review's corrections are in (a56317f9).
2026-10-22 03:02:50 +02:00
Claude 2ecbdd57eb Mob pushes: one list per mob once a crowd is pushing
Pushes between mobs wait in a list until their target ticks; each mob's
tick scanned the whole list, and take_mob_pushes removed each push from
the events in place, shifting the rest. In a pen where every mob pushes
its neighbours both grow with the square of the crowd per mob.

The pending pushes stay a list while short (64) and become one list per
mob past that, each in push order, so every velocity adds up the same
values in the same order. take_mob_pushes splits the events in one pass,
keeping the order of the others. A test checks velocities bit for bit
and the remaining events against the old code.

  30 mobs pushing each other:   0.11 -> 0.04 ms per tick
  100 mobs pushing each other:  1.73 -> 0.37 ms
  200 mobs pushing each other:  9.07 -> 2.61 ms

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01UxXpf7d4RgpespL2G2UsGE
2026-10-22 01:02:34 +00:00
Claude 62874d8dc4 Furnaces: look up the cooking recipe in an index, without cloning it
Every lit furnace, smoker and blast furnace called Recipes::cook each
tick, which scanned all cooking recipes (string compares and hash
lookups) and the furnace then cloned the recipe found, its ingredient's
item set included. Recipes now keeps, built at the first lookup, the
first recipe of each kind for each item, which is what the scan
returned; the furnace borrows it.

  1000 lookups over 320 recipes: 2.07 -> 0.08 ms

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01UxXpf7d4RgpespL2G2UsGE
2026-10-22 01:02:00 +00:00
Claude da314fa658 Server tick: cheaper view requeue and chunk ticket levels
Two costs that grow with players on the server thread:

- Hub::update_view rebuilt a player's chunk queue on every chunk crossing
  by scanning the queue (a Vec) once for each chunk of the view. It now
  checks a set. Same queue, same order (differential test against the old
  scan). 111 -> 16 us per crossing at view 10, 6.5 ms -> 0.13 ms at 32.
- ChunkTickets rebuilt a hash map of every ticketed chunk whenever any
  player changed chunk, and every block entity, mob and ticking chunk
  hashed into it. Levels are now a grid over the tickets' bounding box,
  with the map kept for tickets spread too far apart. At 50 players:
  build 0.23 -> 0.08 ms, lookups about 2x faster (differential test over
  200 random ticket layouts).

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01UxXpf7d4RgpespL2G2UsGE
2026-10-22 01:02:00 +00:00
Ludovic Marie fd0473605b Merge pull request #1125 from ludovic111/fix/station-quick-move
🤖 Agent: merged. Verify passes. Local integration of #1121–#1125 on main 2d586fac: parity --changed, all 101 affected harnesses pass. Screenshots in the body.
2026-10-22 03:00:02 +02:00
Ludovic Marie 24ce07cb93 Merge pull request #1124 from ludovic111/fix/menu-style-test-race
🤖 Agent: merged. Verify passes. Local integration of #1121–#1125 on main 2d586fac: parity --changed, all 101 affected harnesses pass. Screenshots in the body.
2026-10-22 02:59:58 +02:00
Ludovic Marie 194bad0b08 Merge pull request #1123 from ludovic111/fix/book-title-edit-box
🤖 Agent: merged. Verify passes. Local integration of #1121–#1125 on main 2d586fac: parity --changed, all 101 affected harnesses pass. Screenshots in the body.
2026-10-22 02:59:54 +02:00
Ludovic Marie f0b2083ca8 Merge pull request #1122 from ludovic111/fix/classic-extras-pairs
🤖 Agent: merged. Verify passes. Local integration of #1121–#1125 on main 2d586fac: parity --changed, all 101 affected harnesses pass. Screenshots in the body.
2026-10-22 02:59:50 +02:00
Ludovic Marie d87fe66e3e Merge pull request #1121 from ludovic111/fix/session-lock-release
🤖 Agent: merged. Verify passes. Local integration of #1121–#1125 on main 2d586fac: parity --changed, all 101 affected harnesses pass. Screenshots in the body.
2026-10-22 02:59:46 +02:00
Ludovic MarieandClaude Opus 5.5 52eb549aec Particles: add 26.3's sulfur bubbles over potent sulfur
PotentSulfurBlock.animateTick sends two sulfur_bubbles up through a water
source above a wet potent sulfur block, and one time in ten plays
block.potent_sulfur.noxious_gas. SulfurBubbleParticle rises at -0.04
gravity with 0.85 friction, wiggles sideways, grows from 0.02 to 0.15 and
pops when it leaves a water source, stops rising or travels 3 blocks. Its
provider passes yAux as the Z speed, which is kept.

The particle providers harness gets water worlds for sulfur_bubbles,
bubble and bubble_column_up.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 02:21:36 +02:00
Ludovic Marie afd9e820f5 Merge pull request #1127 from ludovic111/fix/server-update-semver-tags
🤖 Agent: merged. Verify and CI parity pass, and the independent review is OK: 12 update tests; v0.2.0.1 ranks below every 0.2 tag; a 0.2 install is never offered a dated tag.
2026-10-22 02:16:01 +02:00
Ludovic MarieandClaude Sonnet 5.5 0ee03671ba Website: the fallback releases link matches launcher@ and launcher-v tags
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-22 02:15:09 +02:00
Ludovic MarieandClaude Sonnet 5.5 a56317f943 docs/versioning.md: servers don't update themselves, a decision on v2026.10.21, bridge soaks first, irreversible list
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-22 02:15:07 +02:00
Ludovic MarieandClaude Opus 5.5 fde383c6e8 Website: read 0.2 tags (v0.2.0, [email protected]) as newer than the dated ones
From 0.2 the game is tagged vX.Y.Z and the launcher [email protected] (docs/versioning.md). The site
took only vYYYY.MM.DD as a game release and launcher-v as a launcher, so it would keep offering
launcher 0.16 and showing the last dated patch notes.

src/versions.js holds the tag test and order (every 0.2 tag above every tag from before, semver
within, a pre-release before its release), shared by build.mjs and main.js; the download button
picks the newest launcher by tag, the notes the newest stable game releases, and the launcher
lookup falls back to launcher@ tags before launcher-v. Nothing shows "0.2" until a 0.2 release
exists. `npm test` runs its tests (node --test).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 02:13:33 +02:00
Ludovic MarieandClaude Opus 5.5 385abe5f0c docs/versioning.md: the 0.2 numbering, the way over for old installs, the go-day steps
Ludovic decided (2026-10-21) that the next release is 0.2 for both the game and the launcher, with one
clear scheme instead of vYYYY.MM.DD(.N) and launcher-vX.Y.Z. This page is the design the other 0.2
pull requests implement:

- tags vX.Y.Z and [email protected] (launcher-v0.2.0 is taken), on a line in .github/release-line.txt;
  betas stay pre-releases under their final tag; semver pre-release tags reserved for previews;
- one order for every reader: each 0.2 tag above every tag from before;
- how release.yml, launcher.yml and promote.yml map onto it (HORIZON_VERSION, HORIZON_LAUNCHER_TAG);
- what launchers and servers from before 0.2 do with it, and the bridges that move them
  (launcher-v0.17.0, the last dated game release made latest before v0.2.0);
- the orchestrator's steps for the day Ludovic says go, freeze off included.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 02:13:33 +02:00
Ludovic Marie fcfcd408a4 Merge pull request #1129 from ludovic111/feat/release-line
🤖 Agent: merged. Verify and CI parity pass, and an independent review found no blocking issue. The release, launcher and promote workflows stay disabled with RELEASES_PAUSED=1. Without .github/release-line.txt the train decides exactly as before (checked against every existing tag).
2026-10-22 02:12:49 +02:00
Ludovic Marie 65276e7ef6 Merge pull request #1128 from ludovic111/fix/launcher-semver-tags
🤖 Agent: merged. Verify and CI parity pass, and an independent review found no blocking issue. The release, launcher and promote workflows stay disabled with RELEASES_PAUSED=1. Without .github/release-line.txt the train decides exactly as before (checked against every existing tag).
2026-10-22 02:12:45 +02:00
Ludovic Marie 3ca2fc8c8b Merge pull request #1115 from ludovic111/fix/break-particle-sprite
🤖 Agent: merged. Verify passes. CI parity's only failure is world_spawn seed -2080727421395798566, where vanilla itself gives several heights across runs (also on main; see #1112). Screenshots in the body.
2026-10-22 02:03:42 +02:00
Ludovic Marie e935f5dc55 Merge pull request #1118 from ludovic111/fix/biome-fog-blend
🤖 Agent: merged. Verify and CI parity pass, with before/after screenshots in the body.
2026-10-22 02:03:21 +02:00
Ludovic Marie 323cc49198 Merge pull request #1120 from feeldev12/fix/benchmark-world-folder-race
🤖 Agent: merged. Thank you @feeldev12! Verify passes. A 2-line fix to benchmark tooling: the world folder is created before the marker is written, so slow devices no longer race the async world open.
2026-10-22 02:03:17 +02:00
Ludovic MarieandClaude Opus 5.5 3fdf210ccb Advancement index: measured on vanilla's tree
An ignored test reads vanilla's advancements and item tags from the
prepared resources (HORIZON_RESOURCES) and reports what an inventory
change and a recipe unlock test now. On the Mac mini (release):
inventory_changed has 1914 criteria, the index is built once in 0.75 ms
with the tree (off the tick), and a nine-stack inventory tests 67 of them
(found in 16 us); recipe_unlocked has 1739, an iron pickaxe tests 1.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 01:57:27 +02:00
Ludovic MarieandClaude Opus 5.5 cc84150f6c A player leaving a level loses its waypoints there
ServerWaypointManager.removePlayer disconnects every waypoint the player
was receiving: a player who goes to another level gets an untrack for each
player it tracked in the one it left. Horizon's per-level managers tick
with the players in their own level only, so the untrack had no connection
to go to and the player kept the old level's markers.

Each level's manager now also gets the connections of the players in the
other levels, for these untracks.

The player-targets and dimension-players harnesses compare the waypoints
as each step leaves them (the last packet for each waypoint): arriving in a
level, vanilla sends the same waypoint up to three times within the step,
and the packet by packet order is test_locator_bar_parity's. Both pass:
123 and 40 lines identical.

Part of #843

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 01:57:27 +02:00
Ludovic MarieandClaude Opus 5.5 815625e547 Advancement triggers test only the criteria that can pass; the log writes on its own thread
Two main-thread costs from the hub tick (found on the companion branch with
HORIZON_SPIKES, commit 060c795e; its companion and metering parts stay there):

- Every inventory change tested all ~1,900 inventory_changed criteria, and
  every recipe unlocked (an iron ingot unlocks dozens) all ~1,300
  recipe_unlocked ones: 1.5-5 ms on such a tick. The tree now indexes
  inventory_changed criteria by the items that can pass them (tags resolved;
  the few without an item list are always tried) and recipe_unlocked
  criteria by recipe, so a change tests the 20-40 that could match. Same
  answers: a criterion with an item-limited predicate can only pass with one
  of those items held.
- Each log line was an unbuffered stderr write plus a latest.log write under
  a mutex on the calling thread; a full pipe or a slow disk held the tick.
  The logger now formats the line and keeps it for crash reports on the
  calling thread and hands the I/O to a "Log writer" thread; flush() waits
  for it (crash report, exit).

(cherry picked from commit 060c795e, companion parts left out)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 01:57:27 +02:00
Ludovic MarieandClaude Opus 5.5 369179f331 The locator bar has one waypoint manager per level
Vanilla keeps a ServerWaypointManager per level: a player in another level
is neither a transmitter nor a receiver there. Horizon passed every player
to one manager whatever their dimension, so a player who went to the
Nether stayed tracked at their Nether coordinates by the players left in
the Overworld (`track Bob block 0 100 0`), and nothing was re-tracked when
they came back.

Each level now has its own manager, ticked with the players in that level:
a player changing level leaves the old one (untracked for everyone there,
as ServerLevel.removePlayerImmediately) and joins the new one
(addDuringTeleport). The player-targets and dimension-players harnesses
now compare the waypoints too, instead of leaving them out.

Fixes #843

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 01:57:27 +02:00
Ludovic Marie 169be6374c hurtlog keeps a LAN guest's hurts across a respawn
A guest's body is rebuilt when it respawns, and its hurt and attack logs
went with the old one: the control socket's hurtlog lost the hurts that
killed it whenever the death and the respawn fell between two reads.
The logs now pass to the new body, and a dead guest's are read too.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit df9fa5f018)
2026-10-22 01:57:26 +02:00
Ludovic Marie 33183952d5 Hurt and attack logs per player, read with hurtlog on the control socket
Every hurt a player takes (PlayerMotion::hurt_blocking, which all player
damage goes through) is kept: the damage type, the amount before blocking
and armour, what a raised blocking item stopped, the health and absorption
lost, the armour and armour toughness it met, the fire ticks, the health
left and, for a mob's blow, the knockback it gave. Every melee blow on a
mob is kept too: the damage after the attack strength
(getAttackStrengthScale), the strength, a critical hit, the mob, whether it
landed and killed. The last 64 of each are kept.

`hurtlog` on the control socket hands them out, per player (the host and
each LAN guest), and clears them: a harness can read what a fight did hit
by hit and compare it with vanilla's numbers.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit fdfa952379)
2026-10-22 01:57:26 +02:00
Ludovic MarieandClaude Opus 5.5 d7701e7e26 Ominous item spawners are saved with their chunk
OminousItemSpawner.addAdditionalSaveData keeps its item and
spawn_item_after_ticks; Horizon saved neither, so an ominous trial
spawner's key or potion floating mid-wave vanished when the world closed
or its chunk unloaded. They are now written with the chunk's entities
(UUID and unmodelled fields kept), read back as live spawners whose tick
count starts again from 0 (Entity.tickCount isn't saved), and leave the
level with their chunk.

Fixes #462

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 01:57:25 +02:00
Ludovic MarieandClaude Opus 5.5 4f461b27ed Other players are drawn lying in their beds
A sleeping guest stood on top of its bed for everyone else: the server
sent its pose but never its bed (LivingEntity.SLEEPING_POS goes only to
the sleeper itself), Horizon's clients had no sleeping pose for other
players, and the host's own models of its guests ignored the bed. A
player paired with the sleeper later (the host walking up from afar)
saw the same.

The tracked state of a sleeping player (the host or a guest) now holds
its bed, so pairing and updates send it, and its removal sends the empty
position back; the clients and the host's guest models lie the player in
the bed as LivingEntityRenderer.setupRotations does for Pose.SLEEPING
(the villagers' sleeping pose, by the bed's facing).

lan_e2e `sleep`: the host draws the sleeping guest lying, the guest the
sleeping host, and neither once they are up.

Fixes #1045

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 01:57:25 +02:00
Ludovic MarieandClaude Opus 5.5 4b4b88b061 dragon: FightWho's doc names LAN and server players only
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 01:57:24 +02:00
Ludovic Marie 1845cc73af fix: end crystals take their entity ids when the fight places them
Like the dragon, a spike's crystal gets its id from the entity counter as
EndCrystal's constructor does, instead of only once a client tracks it: a
player the level serves without a client can then aim at it. The fight's
control status lists each crystal's id and position.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit e32e5031c1)
2026-10-22 01:57:24 +02:00
Ludovic Marie ac06314bd9 fix: the dragon fights every player in the End, not only the window's
The dragon read the window's own player for everything: network players
(LAN guests, players on a server) were never targeted, strafed, charged,
knocked back, bitten, breathed on or hit by its fireballs.

- It sees the level's players (the window's and every guest in the level)
  through `TargetingConditions.forCombat` as each phase asks
  (`Level.getNearestPlayer`): alive, neither creative nor spectator, none on
  Peaceful. The holding pattern strafes the enemy nearest the egg, the
  landing approach comes in opposite it, the perched dragon scans within 20
  blocks (10 up or down) and charges within 150.
- The strafe keeps its own target (`attackTarget`) and goes back to the
  holding pattern when it leaves the level.
- A destroyed crystal sends a holding dragon after the player who destroyed
  it, else the nearest player (`EnderDragon.onCrystalDestroyed`), when it can
  attack them.
- Wing knockback and the head and neck bites, the breath and fireball clouds
  and its fireballs act on every player alike; what hits a guest runs as
  that guest, its knockback sent to its client.
- The dragon takes its id and its parts' when it is created, so a guest can
  attack them in a world without network players too.

A unit test covers the targeting rules (spectators and creative players
passed over, ranges, Peaceful, the nearest to the egg).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 2eef4e4bf5)
2026-10-22 01:57:24 +02:00
Ludovic Marie 02ba66b721 /summon reports the entities that take their id when first tracked
Primed TNT (and falling blocks, projectiles) spawned from /summon get
their entity id only when first tracked, so the summon found no new id
and answered "Unable to summon entity" though the entity was there and
went off. The summon now also counts those lists, and names the type:
"Summoned new Primed TNT", as vanilla.

Test: a TNT summon in the command execution corpus (world group:
125/126 match vanilla; the one left, /waypoint list, was there before).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit dd733684bc)
2026-10-22 01:57:23 +02:00
Ludovic Marie f16c0318b7 fix: a new world knows its world spawn before its first save
The world spawn was only filled in when the world was first saved, from
the player's spawn point at that moment: a bed slept in before then became
the world spawn, and anything asking for the world spawn earlier (a
guest's respawn) fell back on the host's spawn point instead.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 03e621ed3a)
2026-10-22 01:57:23 +02:00
Ludovic Marie 2adfd75193 Loot tables apply set_potion
SetPotionFunction was skipped, so the potions a loot table gives came
out with no contents: a piglin's barter never held fire resistance (the
drinkable and splash ones, 16 of 469), only uncraftable potions. The
merchant and /loot code had it; the shared loot evaluator now does too.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 1b2767122d)
2026-10-22 01:57:23 +02:00
Ludovic Marie f5688883ad fix: dragon damage follows vanilla (perched arrows, cooldown, explosions)
- A perched dragon (scanning, attacking, flaming, hovering) shrugs arrows
  off as `AbstractDragonSittingPhase.onHurt` does: no damage, and the arrow
  is set alight for a second and turns back instead of being spent. Arrows
  that hit a part or a crystal deal the arrow's damage with its Power bonus,
  and turn back when the hit is refused (a dying dragon, an invulnerable
  crystal).
- The damage cooldown is `LivingEntity.hurtServer`'s: within it a stronger
  hit lands its excess over the last one instead of being dropped; the hurt
  sound plays only for a new hit.
- Explosions hit as `ServerExplosion.hurtEntities` does: the dragon itself
  (its body) and then each part, each by its distance and its exposure
  (`getSeenPercent`, so cover protects it), through the cooldown; and every
  end crystal in reach blows up, not only the first.

`Dragon::hurt` and `Dragon::explosion_hits` are pure, with unit tests for
the perched arrows, the cooldown's excess, the parts' quarter plus one,
the take-off after a quarter of its health, and exposure.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit c730ed9ed8)
2026-10-22 01:57:23 +02:00
Ludovic Marie f2f3fb6ede fix: the ender dragon fight runs on server levels
On a dedicated server (and single player's server thread) the End is ticked
with no local player, and `tick_fight` returned before spawning the dragon:
players in the End found crystals but never a dragon.

- The fight finds its players as `EnderDragonFight.updatePlayers` does, every
  20 ticks: every player alive within 192 blocks of (0, 128, 0), the host and
  the network players in the level alike. Scanning the state, the respawn
  sequence, `createNewDragon` and the crystal count run while it has one;
  the dragon and crystals tick whenever the End does.
- The dragon is tracked to network players (`minecraft:ender_dragon`, its
  health and `DATA_PHASE` as vanilla pairs them, no attribute instances) and
  reserves its eight part ids after its own, as `EnderDragon`'s constructor
  does, so clients can attack the parts; a network player's attack on a part
  or an end crystal is aimed at it.

tools/test_server_dragon_parity.py sends a lone vanilla-codec client to the
End of vanilla's dedicated server and of Horizon's: the fight starts and the
dragon is tracked with the same spawn data, synched data entries and
attributes. VanillaProtocolClient's entity mode answers a scene's teleport
with the acceptance alone, as the 26.3 client does.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 618db927d9)
2026-10-22 01:57:23 +02:00
Ludovic MarieandClaude Opus 5.5 6127790456 Switch releases to 0.2 (merge only when Ludovic says go)
.github/release-line.txt = 0.2: the release train's next runs publish v0.2.0 (a beta, as always)
and [email protected] at once, "the first release of 0.2" (docs/versioning.md). Merge it only after
the bridges (the last dated game release, made stable, and launcher-v0.17.0) are out: the go-day
steps are in docs/versioning.md.

Also, because they say 0.2 is out: the site's FAQ explains the numbers, Install says an old launcher
updates twice, hosting.md gives the one command for a server that says it is up to date, and the
launcher's Cargo.toml says 0.2.0 so builds from source (packages) are on the 0.2 line.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 01:24:30 +02:00
Ludovic MarieandClaude Opus 5.5 30cdd6298a Version sources: a release build says its version (0.2.3), not 0.1.0 or a folder name
release.yml builds every game release with HORIZON_VERSION, the tag without its v
(docs/versioning.md), as launcher.yml already does for the launcher: no version-bump commits.

- horizon_core::release::{RELEASE, VERSION}: the built-in version, or Cargo.toml's for a build from
  source. (horizon_core::version stays Minecraft's, generated by gen_version_consts.py.)
- horizon-server and horizon-prepare --version, the server's /api/v1/status, horizon-ctl's MCP
  serverInfo and the "Horizon/<version>" user agents use it; they all said 0.1.0.
- The client's crash reports and telemetry say v0.2.3 from the built-in version wherever the game
  is installed; releases from before 0.2 (no version built in) still read the launcher's folder name.
- The launcher's benchmark telemetry used Cargo.toml's version instead of the release's.
- release.yml checks that horizon-prepare and horizon-server --version say the release's version.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 01:20:13 +02:00
Ludovic Marie b31cb42242 Merge pull request #1111 from ludovic111/fix/occlusion-camera-above-world
🤖 Agent: merged. Verify passes. Local integration of #1109–#1112 on main ae21bd54: parity --changed 142/142. After the rebase onto #1094: 101/101, with screenshots in the body.
2026-10-22 01:15:59 +02:00
Ludovic Marie 7672c3d86c Merge pull request #1112 from ludovic111/fix/loading-screen-square
🤖 Agent: merged. Verify passes. Local integration of #1109–#1112 on main ae21bd54: parity --changed 142/142. CI parity's world_spawn diff (seed -2080727421395798566) also happens on main: vanilla itself gives y64/65/66/72 across runs. The other failure is the known dedicated_server pearl flake.
2026-10-22 01:15:55 +02:00
Ludovic Marie ec257d331e Merge pull request #1110 from ludovic111/fix/item-merge-falling-landing
🤖 Agent: merged. Verify passes. Local integration of #1109–#1112 on main ae21bd54: parity --changed 142/142, with before/after item counts in the body.
2026-10-22 01:15:50 +02:00
Ludovic MarieandClaude Opus 5.5 cfd4ac33bf Station shift-clicks follow vanilla's quickMoveStack; the brewing stand runs on the menu engine
The station screens' hand-written quick move put a payment stack's first item
into the beacon, fell back to the other inventory half when an enchanting,
grindstone, stonecutter, cartography or anvil rule's own slots were full, and
sent every input back hotbar-last-first. Follow each menu's quickMoveStack
(EnchantmentMenu, BeaconMenu, GrindstoneMenu, StonecutterMenu,
CartographyTableMenu, ItemCombinerMenu with SmithingMenu's
canMoveIntoInputSlots), repeated as doClick's QUICK_MOVE does.

A local brewing stand's clicks now run on the verified menu engine, as a
server's already did, so its shift-clicks, drags (quick craft) and double
clicks are BrewingStandMenu's, which test_menu_click_parity covers.

Part of #275

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 01:12:43 +02:00
Ludovic MarieandClaude Opus 5.5 aaf6384751 Tests: the menu style is per test thread, so parallel tests can't flip it
The menu style is one process-wide flag, and options tests switch it to lay
out a page in Horizon's or Classic's style. Running in parallel, one test's
switch moved another's widgets (classic_graphics_presets_set_the_distances
failed 4 of 200 runs of the options tests). Under cfg(test) the flag is a
thread local, Classic first; the game keeps the shared atomic.

Fixes #802

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 01:12:43 +02:00
Ludovic MarieandClaude Opus 5.5 cb20d7056c Book title: draw Select All's highlight, as BookSignScreen's EditBox
The title box is an unbordered, centred EditBox (black, no shadow) in
vanilla. Horizon kept Ctrl/Cmd+A's selection but never drew it, and centred
the title together with its cursor. Draw it with ui::append_field (now with
a text shadow flag): the value centred alone, the cursor after it, and the
inverted highlight. horizon-ctl gets the super (Cmd) and F6 key names, and
A/C/V/X with the edit modifier held act on the open book.

Part of #275

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 01:12:42 +02:00
Ludovic MarieandClaude Opus 5.5 7e82252f12 Extras: classic switches two to a row; settings nest under their switch
Classic Extras laid every setting out as a full-width button. Pair each
section's buttons as vanilla's OptionsList.addSmall does (the same layout as
Video Settings), and shorten "Lower sensitivity while zoomed" to "Slower
turning" so it fits a half-width button.

Horizon's Extras page shows a switch's own settings (Zoom key, Minimap size,
FPS corner...) only while that switch is on, so it stays one row per extra
as the list grows. Classic keeps every button, as vanilla screens do.

Fixes #812
Fixes #815

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 01:12:42 +02:00
Ludovic MarieandClaude Opus 5.5 782d93bf99 Release the session lock before closing it, as DirectoryLock.close does
A world's session.lock is an flock held by the open file. Closing the file
releases it only once every copy of the descriptor is gone, and a process
spawned on another thread holds a copy until it execs. Under a parallel
cargo test (crash reports spawn uname/sw_vers), deleting a world right after
dropping its lock could then fail with "already open". Unlock explicitly
in Drop, as vanilla releases the FileLock before closing the channel.

Fixes #749

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 01:12:42 +02:00
Ludovic Marie e1d39b9787 Merge pull request #1109 from ludovic111/tools/chunk-harness-postprocess
🤖 Agent: merged. Verify and CI parity pass (chunk harness tooling).
2026-10-22 01:03:10 +02:00
Ludovic MarieandClaude Opus 5.5 082cc278af Audio: pan like OpenAL Soft's stereo output, not at full gain on both sides
A centred mono source played at full gain on each channel; OpenAL Soft
(vanilla's LWJGL build, HRTF off) renders stereo pairwise: 0.596 per side
ahead, 0.404 behind, 0.5 above or below. Explosions were 4.5 dB louder than
vanilla. Pan with the camera's forward and up vectors, play a mono output
unpanned, and release the limiter over 0.2 s like OpenAL Soft's.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 00:52:08 +02:00
Ludovic MarieandClaude Opus 5.5 9a3237c5ac Release train: a release line (0.2) for vX.Y.Z and [email protected] tags
docs/versioning.md: with .github/release-line.txt (MAJOR.MINOR) the train tags game releases
vMAJOR.MINOR.PATCH and launcher releases [email protected], the patch after the newest
release on the line; the line's first release goes out at once (still not while RELEASES_PAUSED is
set). Without the file nothing changes: vYYYY.MM.DD[.N] and launcher-vX.Y.Z. The file is not added
here; the switch to 0.2 is a separate one-line pull request for the day Ludovic says go.

- tag_key: every 0.2 tag ranks above every dated or launcher-v tag; semver order within, a
  pre-release before its release. release_channels.py uses the same key and tag test, so promotion
  and "latest only moves forward" work across the switch.
- A line's first release says so in its notes; 0.2.0's explains the numbering and how a server
  that says it is up to date takes it (horizon-server update --version v0.2.0).
- launcher.yml strips either prefix for HORIZON_LAUNCHER_VERSION and passes HORIZON_LAUNCHER_TAG,
  which the launcher uses to know its own line.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 00:47:35 +02:00
Ludovic MarieandClaude Opus 5.5 0d747f1a86 Launcher: 0.2 tags (v0.2.0, [email protected]) follow every date and launcher-v tag
From 0.2 the game is tagged vX.Y.Z and the launcher [email protected] (docs/versioning.md). The launcher
compared every tag number by number, so it would never offer game v0.2.0 over v2026.10.19.15 nor
update itself to 0.2.0 from 0.16.0 (launcher-v0.2.0 is also taken by an old release).

- release_order(): tags from before 0.2 (vYYYY.MM.DD[.N], launcher-vX.Y.Z, bare versions) compare as
  before; every 0.2 tag follows them, in semver's order (a pre-release before its release).
- own_tag(): the tag this launcher was released as. launcher.yml will pass HORIZON_LAUNCHER_TAG; a
  release with only HORIZON_LAUNCHER_VERSION was a launcher-v one; a build from source is on 0.2.
  So the last launcher-v release (the bridge old launchers can see) updates itself to launcher@.
- The matching-refs fallback looks for launcher@ tags first, then launcher-v.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 00:38:37 +02:00
Ludovic MarieandClaude Opus 5.5 0dd31c0757 horizon-server update: semver tags (v0.2.0) follow every date tag
From 0.2 Horizon's game releases are tagged vMAJOR.MINOR.PATCH (docs/versioning.md). newer() compared
tags number by number, so v0.2.0 looked older than v2026.10.19.15 and an installed server would never
offer or install it. Every semver tag now outranks every date tag; within semver, patch and minor
compare as numbers and a pre-release (v0.3.0-rc.1) comes before its release. Pruning old releases
sorts with the same order.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 00:29:34 +02:00
Ludovic Marie 2d586fac52 Merge pull request #1107 from ludovic111/fix/beehive-break-releases-bees
🤖 Agent: merged. Verify passes. Local integration of #1101–#1107 on main caeffffb: parity --changed 302/304, and the two failures (block_particles, entity_model) fail identically on plain main. CI parity's secure_chat failure on #1101 logged 'transcripts identical' (flaky).
2026-10-22 00:17:45 +02:00
Ludovic Marie ef1142ab49 Merge pull request #1101 from ludovic111/fix/explosion-knockback-resistance
🤖 Agent: merged. Verify passes. Local integration of #1101–#1107 on main caeffffb: parity --changed 302/304, and the two failures (block_particles, entity_model) fail identically on plain main. CI parity's secure_chat failure on #1101 logged 'transcripts identical' (flaky).
2026-10-22 00:17:41 +02:00
feeldev c833fbe212 fix(benchmark): create the world folder before writing its marker
`open` replies once the world starts opening on a worker, so the save
folder may not exist yet when the benchmark writes horizon-benchmark.txt
into it. On a slower device (an Android phone) the write failed with
"No such file or directory" and the run ended before the first scene.
2026-10-21 17:04:34 -05:00
Ludovic MarieandClaude Opus 5.5 4eaa0251e2 Cave culling: see the world from above or below its height
With the camera above the build height (or below the bottom), vanilla's
SectionOcclusionGraph.initializeQueueForFullUpdate seeds the search with
the top (bottom) layer of sections around the camera instead of the
camera's own section. Horizon started from the camera's section and could
only step one section past the world's edge, so from y 352 up (two
sections above the top) it reached nothing and every chunk vanished.

Fixes #1037

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-21 23:09:57 +02:00
Ludovic Marie b7390d783b Merge pull request #1106 from ludovic111/fix/attack-knockback
🤖 Agent: merged. Verify passes; CI parity passes except dedicated_server, which fails the same way on main (play-pearl teleported 0 vs 1, the known flake in FEATURES.md LATER).
2026-10-21 23:02:44 +02:00
Ludovic Marie aac5ff91e7 Merge pull request #1104 from ludovic111/fix/elytra-hop
🤖 Agent: merged. Verify passes; CI parity passes except dedicated_server, which fails the same way on main (play-pearl teleported 0 vs 1, the known flake in FEATURES.md LATER).
2026-10-21 23:02:40 +02:00
Ludovic Marie 7f4de08a9a Merge pull request #1102 from ludovic111/fix/no-use-while-using-item
🤖 Agent: merged. Verify passes; CI parity passes except dedicated_server, which fails the same way on main (play-pearl teleported 0 vs 1, the known flake in FEATURES.md LATER).
2026-10-21 23:02:37 +02:00
Ludovic Marie b8261b3270 Merge pull request #1105 from ludovic111/fix/attribute-swapping
🤖 Agent: merged. Verify and CI parity pass.
2026-10-21 23:02:07 +02:00
Ludovic Marie 81042a8155 Merge pull request #1103 from ludovic111/fix/elytra-fall-distance
🤖 Agent: merged. Verify and CI parity pass.
2026-10-21 23:02:03 +02:00
Ludovic MarieandClaude Opus 5.5 1cca55b667 Draw and collide blocks while a piston moves them
Moving blocks are minecraft:moving_piston blocks with a block entity.
The client drew nothing for them and gave them no collision, so pistons
on a clock flickered and players walked through them (#935).

The world now keeps a copy of the level's moving-piston block entities.
SceneCollisions::moving_piston_shape returns their shape
(PistonMovingBlockEntity.getCollisionShape, already in piston_shape),
and game/moving_pistons.rs ports PistonHeadRenderer: the moved block at
its interpolated offset, the moving head's short state, and a retracting
piston's head and extended base.

Entity pushing (moveCollidedEntities) is not ported yet.

Fixes #935

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-21 22:34:13 +02:00
Ludovic MarieandClaude Opus 5.5 e3cea08736 Boats: the water patch hides water inside the hull; the riding camera turns smoothly
BoatRenderer draws its water_patch part with RenderTypes.waterMask (depth
only) unless the boat is under water, so translucent water behind it fails
the depth test inside the hull. Horizon loaded the part but never drew it:
the water surface showed through the boat. The patch now draws with a
depth-only Water mask pipeline after the entities and before translucent
terrain (rafts have none, as RaftRenderer).

Turning in a boat stepped the camera at the tick rate: positionRider adds
the boat's deltaRotation to the passenger's yRot after its yRotO was kept,
and LocalPlayer.getViewYRot interpolates a passenger's yaw between them.
The camera now spreads the turn over the tick's frames.

Fixes #1016

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-21 22:33:08 +02:00
Ludovic MarieandClaude Opus 5.5 7404c2b94e Sky, fog and water fog colours blend across biomes (GaussianSampler)
The camera's biome alone gave the sky, fog and water fog colours, so they
jumped at every biome boundary (most visibly the Nether fog). Vanilla's
EnvironmentAttributeProbe weights the noise biomes of a 6x6x6 quart
neighbourhood around the camera with GaussianSampler's kernel and lerps
the spatially interpolated attributes between them
(SpatialAttributeInterpolator, ARGB.srgbLerp); unloaded chunks count as
plains.

Fixes #797

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-21 22:33:08 +02:00
Ludovic MarieandClaude Opus 5.5 08fdafc4fc Thrown potions: drawn as their item, facing the camera (ThrownItemRenderer)
A thrown splash or lingering potion or experience bottle was a stand-in
terrain particle spawned at its tick position: darkened by the terrain
particle colour, without the potion's colour, two overlapping copies and
no interpolation between ticks. Draw it as vanilla's ThrownItemRenderer
does: the item model (GROUND, with the potion tint) facing the camera,
interpolated, lit at its light probe, hidden for its first two ticks near
the camera (ThrowableProjectile.shouldRenderAtSqrDistance). Witch potions
draw as the splash potion of their effect instead of a bare bottle.

Fixes #818

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-21 22:33:07 +02:00
Ludovic MarieandClaude Opus 5.5 621b791a33 End crystals spin and bob in every dimension
EndCrystal.tick advances the crystal's time wherever it is; only the fire
at its block needs a dragon fight. Horizon advanced it only during the End
fight, so a crystal placed elsewhere kept a fixed time and jumped back
every tick (it flickered instead of turning).

Fixes #1051

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-21 22:33:07 +02:00
Ludovic MarieandClaude Opus 5.5 38b16f44cb Terrain particles: the model's particle sprite and vanilla tint sources
Breaking, mining, landing and other block particles took the block's up
face and a fixed grass or foliage colour, so grass blocks crumbled as
tinted grass tops (moss-like). Vanilla's TerrainParticle uses the model's
particle material (first variant / first multipart selector) and tint
source 0's colorAsTerrainParticle: the biome-blended in-world colour,
none for grass blocks (BlockTintSources.grassBlock) or blocks without a
source. The in-wall screen overlay and block markers use the particle
material too.

Fixes #1034

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-21 22:33:07 +02:00
Ludovic Marie 252f877f45 Merge pull request #1100 from ludovic111/fix/agent-e2e-wait-loaded
🤖 Agent: merged. Test-only fix (agent_control_e2e waits for the world to load); verify and CI parity pass.
2026-10-21 22:02:46 +02:00
Ludovic MarieandClaude Opus 5.5 8b0872f901 Elytra: a landing glide lasts one more tick, so hops keep their speed
The glide flag is the server's: LivingEntity.updateFallFlying clears it when
canGlide fails (the player is on the ground), from the position the client
sent, and the client gets the change on its next tick. So in vanilla a glide
that lands goes on for one tick on the ground, with travelFallFlying's drag
instead of ground friction, and a jump on that tick (with its sprint boost)
keeps the speed: the elytra hop. Horizon ended the glide on the landing tick,
so each hop lost about half of the speed to ground friction.

Fixes #1056

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-21 21:06:54 +02:00
Ludovic MarieandClaude Opus 5.5 d32fd5457a Beehives: nests grown in the live level get their block entity and bees
BeehiveDecorator stores 2 or 3 bees in the nest's block entity. World
generation kept them, but trees placed in the live level (a sapling grown next
to flowers, which picks the *_bees_005 trees, and /place feature) dropped them:
LevelFeatureWorld had no store_beehive_occupants, so the nest had no block
entity at all. Breaking it released nothing and bees could not enter it.

LevelAccess gets load_block_entity, the game loads the occupants into the
nest's block entity, and the occupant NBT is shared with world generation.

Fixes #1097

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-21 21:06:32 +02:00
Ludovic MarieandClaude Opus 5.5 9d9f259f94 Combat: sprint and yaw knockback (Player.causeExtraKnockback); vanilla knockback on the player
Player.attack adds causeExtraKnockback after the hurt: the weapon's knockback
enchantment (halved) plus 0.5 for a sprinting full-strength hit, pushed along
the attacker's yaw (sin, -cos) rather than away from it, which lets a player
steer a hit target by turning while attacking (knockback displacement).
Horizon had only the enchantment part, on mobs; sprinting hits added nothing
and players (LAN PvP) got no extra knockback at all. Now:
- player hits on mobs and players: one extra knockback of that strength with
  LivingEntity.knockback (knockback resistance, lift only from the ground),
  the attack.knockback sound, and the attacker's sprint stops;
- the knockback a player takes (mob and PvP hits, dealDefaultKnockback with
  0.4) reads its knockback resistance (netherite) and only lifts from the
  ground: an airborne player kept being thrown up by every hit.

Fixes #504

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-21 21:06:32 +02:00
Ludovic MarieandClaude Opus 5.5 c01739300a Combat: attack damage and speed come from last tick's main hand (attribute swapping)
The player's ATTACK_DAMAGE and ATTACK_SPEED modifiers change in
LivingEntity.detectEquipmentUpdates, once a tick, while Player.attack reads
the weapon's enchantments and smash bonus from the stack in hand at the
attack. So swapping hotbar slots just before an attack hits with the old
item's damage and charge and the new item's enchantments (a Wind Burst or
Breach mace after another weapon): attribute swapping. Horizon read both from
the item in hand at the attack.

Also Player.tick's reset: switching to a different item now resets the attack
strength the damage uses (only the indicator's ticker reset before), for the
host and for LAN guests.

Fixes #505

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-21 21:06:32 +02:00
Ludovic MarieandClaude Opus 5.5 9d96bb6ec7 Explosions: knockback reads explosion_knockback_resistance; armor enchantment modifiers stack per slot
Two Blast Protection pieces now cancel explosion knockback (#1049):
- ServerExplosion.hurtEntities scales knockback by EXPLOSION_KNOCKBACK_RESISTANCE
  for living entities. The player's knockback ignored it, and mobs used
  KNOCKBACK_RESISTANCE (iron golems were never pushed; vanilla pushes them,
  every type's explosion resistance base is 0).
- EnchantmentAttributeEffect.idForSlot: the modifier id ends with the slot,
  so the same enchantment on two armor pieces adds up. The id was
  <id>.<level>, so the second piece replaced the first.
- hitPlayers skips creative players who are flying: no knockback for them.

Fixes #1049

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-21 21:06:31 +02:00
Ludovic MarieandClaude Opus 5.5 2c47e79cc7 Use: no block placing or item use while an item is in use
Minecraft.handleKeybinds only calls startUseItem while the player is not
using an item: use clicks are consumed and held use doesn't repeat. Horizon
repeated held use every 4 ticks regardless, so with a shield raised in the off
hand, aiming at a block placed the main hand's block (or spawn egg, boat...).

Fixes #1050

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-21 21:06:31 +02:00
Ludovic MarieandClaude Opus 5.5 705be141d7 Elytra: a glide that isn't diving keeps the fall distance at 1
LivingEntity.updateFallFlying starts with Entity.checkFallDistanceAccumulation:
while gliding and falling slower than 0.5 blocks a tick, the fall distance is
capped at 1. Horizon kept accumulating it for the whole glide, so landing after
gliding down (or after climbing and gliding back) dealt the full drop's damage.

Fixes #1046

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-21 21:06:31 +02:00
Ludovic Marie caeffffb94 Merge pull request #1099 from ludovic111/fix/lan-e2e-xp-host-back
🤖 Agent: merged. Test-only fix to tools/lan_e2e.py (root cause in the body); verify and CI parity pass.
2026-10-21 21:02:45 +02:00
Ludovic Marie a0c3d531c2 Merge pull request #1094 from ludovic111/fix/cave-culling-vanilla-rules
🤖 Agent: merged. Verify and CI parity pass. The body has the frame-time table and screenshot masks against no-culling for the visibility fix.
2026-10-21 21:02:36 +02:00
Ludovic Marie 4d22eb0fbf Merge pull request #1084 from ludovic111/fix/weather-frame-cost
🤖 Agent: merged. Verify and CI parity pass on the rebased head (b591c9dc), with before/after frame times in the body (rain 561 → 759 FPS, snow 663 → 981 on the Mac mini).
2026-10-21 21:02:30 +02:00
Ludovic MarieandClaude Opus 5.5 1bd1d57fb3 agent_control_e2e survival: a tree whose log is hidden is skipped
Low leaves can hide every face of the trunk's bottom log from where the
walk stops ("no face of block ... is visible from here"). The scenario
now breaks inside the tree loop and moves on to the next tree, as it
already did for one it cannot reach.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-21 20:55:20 +02:00
Ludovic MarieandClaude Opus 5.5 5f0e924221 agent_control_e2e: wait for the world, and start below a tree's crown
- survival and ui asserted that the Create New World click itself
  answered `playing`. Since 7f4193f8 the in-frame world is prepared off
  the UI thread behind the title screen, so the click answers `title`
  and both scenarios failed in the frame (main's default) every time.
  They now wait for `loaded`, as the other scenarios do.
- The survival spawn is random within the spawn radius, on the first
  full top face below the MOTION_BLOCKING height
  (PlayerSpawnFinder.getLevelRespawnPos), so a tree's leaves too: from
  there no tree was reachable within goto's three-block drops. The
  scenario now drops the player to the ground under the leaves first.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-21 20:46:21 +02:00
Ludovic MarieandClaude Opus 5.5 32a48282cc Loading screen: fill the chunk map square by square, as vanilla
Vanilla's spawn chunks load by ticket level, so the level loading screen's
chunk map fills in Chebyshev rings. While the loading screen is up,
Horizon now picks chunks ring by ring (nearest first inside a ring)
instead of by Euclidean distance, which grew a disc.

Fixes #770

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-21 20:31:43 +02:00
Ludovic MarieandClaude Opus 5.5 5c3b05938f lan_e2e: put the host back after the xp check
check_xp makes the host a spectator, out of the orbs' way, and left it
one for the rest of the run. A spectator sinks (Player.travel keeps 0.6
of its vertical speed each tick, about 0.2 blocks from standing), so the
next check that made it survival again, swings, landed it inside the
floor with the guest teleported beside it: the guest's feet were in the
floor's stone, which hid the dirt it tills from every angle ("no face of
block ... is visible from here"). It failed only after the earlier
checks, never alone.

check_xp now ends in survival where the host stood, as check_trials
already does.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-21 20:19:33 +02:00
Ludovic MarieandClaude Opus 5.5 b4100ed752 Items merge with their neighbours; falling blocks land in their own tick
ItemEntity.tick merges a dropped stack with mergeable items whose boxes
meet its own grown by half a block sideways, every 2 ticks while it moves
between blocks and every 40 otherwise (mergeWithNeighbours, tryToMerge,
merge). Horizon never merged, so fresh chunks, where water and falling
blocks break leaf litter, moss and plants, filled up with single items.

FallingBlockEntity.tick also places (or breaks) the block within the
entity's own tick. Horizon moved every falling block first and placed them
after, so a block falling right behind another landed in the same cell and
broke into an item: stacked sand and gravel in fresh chunks dropped.

Fixes #996

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-21 20:01:42 +02:00
Ludovic MarieandClaude Opus 5.5 e725e5257f Chunk generation harness: compare postprocessing marks too
The oracle prints every ProtoChunk postprocessing position and chunk_dump
prints Horizon's, so a chunk that schedules different fluid or shape
updates after generation (the blocks that later tick and drop items in
fresh chunks, #996) fails the harness.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-21 19:39:37 +02:00
Ludovic Marie ae21bd5464 Merge pull request #1095 from ludovic111/fix/hub-default-game-mode
🤖 Agent: merged. Verify passes. The change runs only on the server thread (opt-in today), and the PR body's proof shows lan_e2e --server-thread going from 35/39 to 38/39. #1072's switch needs it.
2026-10-21 19:38:28 +02:00
Ludovic Marie 297843939c Merge pull request #1093 from ludovic111/fix/prepare-finds-jdks
🤖 Agent: merged after integration C: the desktop ran 322/325 harnesses on main + #1088–#1093, and its 3 failures were the known dedicated_server flake plus the two display harnesses; world_reopen and save_roundtrip pass on the Mac mini (main + the six branches).
2026-10-21 19:37:48 +02:00
Ludovic Marie e6842705b0 Merge pull request #1090 from ludovic111/fix/missingno-warning
🤖 Agent: merged after integration C: the desktop ran 322/325 harnesses on main + #1088–#1093, and its 3 failures were the known dedicated_server flake plus the two display harnesses; world_reopen and save_roundtrip pass on the Mac mini (main + the six branches).
2026-10-21 19:37:44 +02:00
Ludovic Marie c6dbb61759 Merge pull request #1089 from ludovic111/fix/video-cost-labels
🤖 Agent: merged after integration C: the desktop ran 322/325 harnesses on main + #1088–#1093, and its 3 failures were the known dedicated_server flake plus the two display harnesses; world_reopen and save_roundtrip pass on the Mac mini (main + the six branches).
2026-10-21 19:37:41 +02:00
Ludovic Marie b58f1b156f Merge pull request #1088 from ludovic111/fix/tick-server-thread
🤖 Agent: merged after integration C: the desktop ran 322/325 harnesses on main + #1088–#1093, and its 3 failures were the known dedicated_server flake plus the two display harnesses; world_reopen and save_roundtrip pass on the Mac mini (main + the six branches).
2026-10-21 19:37:36 +02:00
Ludovic Marie e4fb401c3c Merge pull request #1092 from ludovic111/fix/prepare-unifont
🤖 Agent: merged after integration C: the desktop ran 322/325 harnesses on main + #1088–#1093, and its 3 failures were the known dedicated_server flake plus the two display harnesses; world_reopen and save_roundtrip pass on the Mac mini (main + the six branches).
2026-10-21 19:37:33 +02:00
Ludovic Marie 7d512ed829 Merge pull request #1091 from ludovic111/fix/republished-asset-index
🤖 Agent: merged after integration C: the desktop ran 322/325 harnesses on main + #1088–#1093, and its 3 failures were the known dedicated_server flake plus the two display harnesses; world_reopen and save_roundtrip pass on the Mac mini (main + the six branches).
2026-10-21 19:37:21 +02:00
Ludovic MarieandClaude Opus 5.5 6f6354c2c0 Cave culling: vanilla's graph rules (every entry face, distant ray test)
SectionOcclusionGraph.runUpdates, both rules Horizon left out:
- a section leads on through whatever any face it was entered through
  connects to (Node.sourceDirections), not only the first one;
- beyond 3 sections from the camera, a step is taken only if the line
  from the section's corner towards the camera, in 28-block steps while
  farther than 60 blocks, stays in sections the search has reached.

The search takes the camera position for the ray test. Exact equivalent
of vanilla's, faster: one ray per axis per node (both steps along an
axis share the corner; while the camera stays in the searched-from
section no point falls in a neighbour the loop adds), an answer per
section corner during a full search (neighbours share corners; reached
sections are only added, so a line that stayed in them still does, and
one that failed is checked again from the sample it failed at, stepping
over the earlier ones with the same additions), and an integer floor
instead of libm's on x86-64. Tests compare every cell with a literal
port of runUpdates (map of nodes, FIFO, JOML arithmetic), and a resumed
line with one checked from its corner.

Fixes #782

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-21 19:00:46 +02:00
Ludovic MarieandClaude Opus 5.5 b591c9dc64 Weather: rain heights from a cached MOTION_BLOCKING heightmap, no per-column allocations
WeatherEffectRenderer reads level.getHeight(MOTION_BLOCKING, x, z) for
every column within the weather radius. Horizon scanned each column block
by block from 10 blocks above the radius down to the ground, with a
string search per block, every frame: about half a millisecond of CPU per
frame in rain on a Ryzen, more on laptops (#899). Client chunks now keep
a MOTION_BLOCKING map next to their WORLD_SURFACE one, built on the first
read and dropped by writes that can move it, from the facts world
generation already uses (blocks_motion_in_heightmap or a fluid).

This is also vanilla's rule where the scan differed: slabs, stairs and
other motion-blocking blocks without a full collision box now stop rain,
and a roof more than 20 blocks above the camera does too.

Tiling a column at each texture repeat no longer allocates two vectors
per column.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-21 18:57:30 +02:00
Ludovic Marie e66c35b2ef Merge pull request #390 from ludovic111/feat/version-port-agent
🤖 Agent: merged (version-port tooling, rebased on main). Ludovic asked on 10-21 to clear the PR queue.
2026-10-21 18:53:08 +02:00
Ludovic Marie dacdab490e Merge pull request #389 from ludovic111/feat/version-port-publish
🤖 Agent: merged (version-port tooling, rebased on main). Ludovic asked on 10-21 to clear the PR queue.
2026-10-21 18:52:42 +02:00
Ludovic Marie 90b675a882 Merge pull request #388 from ludovic111/feat/version-port-harnesses
🤖 Agent: merged (version-port tooling, rebased on main). Ludovic asked on 10-21 to clear the PR queue.
2026-10-21 18:52:16 +02:00
Ludovic Marie ac4a63fd22 Merge pull request #359 from ludovic111/feat/version-port-regen
🤖 Agent: merged (version-port tooling, rebased on main). Ludovic asked on 10-21 to clear the PR queue.
2026-10-21 18:51:15 +02:00
Ludovic Marie a96bfb39fd Merge pull request #357 from ludovic111/feat/tools-follow-lock
🤖 Agent: merged (version-port tooling, rebased on main). Ludovic asked on 10-21 to clear the PR queue.
2026-10-21 18:50:46 +02:00
Ludovic MarieandClaude Opus 5.5 87d5cd27c1 Server thread: the owner's commands run with the hub in place
owner_console_command ran the window's commands as the world's owner with
the hub taken out, so a selector saw only the owner (`team join … @a` left
the guests out, and friendly fire still hurt) and `defaultgamemode` never
reached the hub (guests joined in the world's old mode). It now puts the
hub back while the command runs, as run_guest_commands does for a guest.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-21 18:32:41 +02:00
Ludovic Marie 7e0b202374 Merge pull request #1087 from ludovic111/fix/backdrop-worker
🤖 Agent: merged. Title backdrop built on a worker (fixes #896). Integration proof (main + #1079 #1081 #1082 #1083 #1085 #1086 #1087), 2026-10-21: desktop parity 103/105; the 2 others (world_reopen, save_roundtrip) need a display and pass on the Mac mini. Verify passed.
2026-10-21 18:05:47 +02:00
Ludovic Marie d235e760c3 Merge pull request #1086 from ludovic111/fix/creative-middle-click
🤖 Agent: merged. Creative pick block in inventories (fixes #1048, Visivel). Integration proof (main + #1079 #1081 #1082 #1083 #1085 #1086 #1087), 2026-10-21: desktop parity 103/105; the 2 others (world_reopen, save_roundtrip) need a display and pass on the Mac mini. Verify passed.
2026-10-21 18:05:41 +02:00
Ludovic Marie 3f0e3230d2 Merge pull request #1083 from ludovic111/fix/firework-tooltips
🤖 Agent: merged. Firework tooltips (fixes #1047, Visivel). Integration proof (main + #1079 #1081 #1082 #1083 #1085 #1086 #1087), 2026-10-21: desktop parity 103/105; the 2 others (world_reopen, save_roundtrip) need a display and pass on the Mac mini. Verify passed.
2026-10-21 18:05:36 +02:00
Ludovic Marie 0f22b49f74 Merge pull request #1082 from ludovic111/fix/cobweb-falling-blocks
🤖 Agent: merged. Cobwebs slow falling blocks (fixes #933). Integration proof (main + #1079 #1081 #1082 #1083 #1085 #1086 #1087), 2026-10-21: desktop parity 103/105; the 2 others (world_reopen, save_roundtrip) need a display and pass on the Mac mini. Verify passed.
2026-10-21 18:05:31 +02:00
Ludovic Marie b1d15653b2 Merge pull request #1079 from ludovic111/fix/night-sky-stars-cost
🤖 Agent: merged. Stars built once (fixes #1054, Visivel and rdbtCVS). Integration proof (main + #1079 #1081 #1082 #1083 #1085 #1086 #1087), 2026-10-21: desktop parity 103/105; the 2 others (world_reopen, save_roundtrip) need a display and pass on the Mac mini. Verify passed.
2026-10-21 18:05:26 +02:00
Ludovic Marie 29b3aff942 Merge pull request #1085 from ludovic111/fix/place-blocked-by-entities
🤖 Agent: merged. Blocks can't be placed into entities (fixes #1052, Visivel). Integration proof (main + #1079 #1081 #1082 #1083 #1085 #1086 #1087), 2026-10-21: desktop parity 103/105; the 2 others (world_reopen, save_roundtrip) need a display and pass on the Mac mini. Verify passed.
2026-10-21 18:05:21 +02:00
Ludovic Marie db4eb06784 Merge pull request #1081 from ludovic111/fix/shears-carve-pumpkin
🤖 Agent: merged. Shears carve pumpkins (fixes #1067, reported by Tydragon). Integration proof (main + #1079 #1081 #1082 #1083 #1085 #1086 #1087), 2026-10-21: desktop parity 103/105; the 2 others (world_reopen, save_roundtrip) need a display and pass on the Mac mini. Verify passed.
2026-10-21 18:05:02 +02:00
Ludovic MarieandClaude Opus 5.5 57cc113be4 Client logs: no warning for minecraft:missingno; say when no sounds loaded
- sprites: vanilla models name the missing texture on purpose (block/air.json's
  particle is minecraft:missingno), so using it is not worth a warning (#853).
- audio: when the resources hold no sounds.json, the game plays nothing on a
  working output and the log said only which device opened. It now says the
  sounds were not copied and how to fix it (#1035, #1076).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-21 17:37:45 +02:00
Ludovic MarieandClaude Opus 5.5 1fdc003d52 Prepare: find installed JDKs; launcher shows the preparation error up top (#666)
A player installed Java 25 but JAVA_HOME still named an older JDK, so
Prepare stopped with "Java 25 is needed", and the launcher showed that
error at the foot of the page, out of sight until the window was enlarged.

- horizon-prepare java::find: after JAVA_HOME, the PATH and macOS's
  java_home, look in the folders JDK installers use (Program Files vendors
  on Windows, /usr/lib/jvm on Linux, JavaVirtualMachines on macOS, ~/.jdks,
  SDKMAN), JDKs named for the pinned major first.
- launcher: the preparation's progress and error sit right under the
  buttons instead of below the rows.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-21 17:37:44 +02:00
Ludovic MarieandClaude Opus 5.5 b5c7c5789a Prepare: import the Unifont glyphs from the asset index (#1076)
The client JAR ships an empty font/include/unifont.json; the real provider
list and font/unifont.zip, unifont_jp.zip and unifont_pua.zip (1.9 MB) come
only from the asset index, which preparation never copied. Every character
the bitmap fonts lack showed as a missing-glyph box (a server's "▸").

Full preparation now imports them after extracting the JAR, like the title
panorama, and `horizon-prepare fonts` repairs prepared resources.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-21 17:34:05 +02:00
Ludovic MarieandClaude Opus 5.5 3ac4d26cd0 Prepare: accept Mojang's republished 26.3 asset indexes (sounds, panorama)
Mojang republished the 26.3 version JSON again (sha1 61fff131), with a new
asset index 34 (0c8f43b2) that changes only 142 translation files. A launcher
keeps one indexes/34.json, so every install made since then, official
launcher or Prism, failed the "pinned asset index" check: no sounds and no
title panorama.

- research-lock.json: accept the new version JSON, and list the asset
  indexes of every accepted version JSON (accepted_asset_index_sha1).
- horizon-prepare: an index the lock does not know is still used, with a
  line in the log, since every object copied from it is checked against its
  own SHA-1; the sounds now require minecraft/sounds.json in the index.
- tools/lab.py: the same for the dev tools.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-21 17:32:00 +02:00
Ludovic MarieandClaude Opus 5.5 a197adaed0 Video settings: cost labels from measured frame times, and "Saves time" for cave culling
Measured on an Intel HD 620 (#897): rain and snow cost about 3 ms of GPU
per frame there, most of it from the weather radius; FXAA about 0.8 ms;
cave culling saves about 0.6 ms; mipmaps cost nothing measurable and
clouds about 0.1 ms. A new Cost::Saves tag ("Saves time", in the success
colour) marks a setting that makes frames faster when on.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-21 17:19:43 +02:00
Ludovic MarieandClaude Opus 5.5 be8b2d54fc /tick on a server thread: sprint, query, step and a smooth frozen camera
The integrated and dedicated server loops paced every tick at the tick rate,
so /tick sprint ran at 20 tps and never reported, and the integrated server
recorded no tick times for /tick query. They now share
MinecraftServer.runServer's pacing: no wait while sprinting, the sprint's
report sent to the operators as the server's, and tick times recorded.

The hub broadcasts ClientboundTickingStepPacket when a step starts or stops
(ServerTickRateManager.updateStepTicks) and sends a joining player the
steps left, not 0.

The camera, hand, view bobbing and the player's own model use
Camera.getCameraEntityPartialTicks (getGameTimeDeltaPartialTick(true)), so
they stay smooth while the level is frozen instead of moving at 20 Hz.

Fixes #1038

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-21 17:07:10 +02:00
Ludovic MarieandClaude Opus 5.5 0a1959077c Title backdrop: build its generator on a worker
start_backdrop built VanillaTerrain on the event loop, so the title stopped
drawing for the whole build: 2.1 s cold and 0.86 s warm on an M4 Pro (fast
profile), far longer on slow CPUs. The worker now builds it and finds the
spot; the title draws its panorama until the generator is installed.

Fixes #896

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-21 16:53:54 +02:00
Ludovic MarieandClaude Opus 5.5 403fead15d Blocks can't be placed into mobs and other building blockers (Level.isUnobstructed)
BlockItem.canPlace now checks every entity whose blocksBuilding is set: mobs,
armor stands (not markers), boats, minecarts, end crystals, primed TNT and
falling blocks, not only players.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-21 16:41:17 +02:00
Ludovic Marie d128289ca6 Merge pull request #1006 from ludovic111/perf/server-helpers-behind
🤖 Agent: merged. Linux dedicated servers lower their world-generation and packet-writer threads behind the server thread; nothing changes elsewhere. verify passed; parity: every harness passes except dedicated_server, which is flaky on main with the same failure modes (7-run A/B against main in the last comment).
2026-10-21 16:34:41 +02:00
Ludovic Marie fd9eb84bea Merge pull request #356 from ludovic111/feat/version-consts
🤖 Agent: merged (version-port tooling, rebased on main). Ludovic asked on 10-21 to clear the PR queue.
2026-10-21 16:16:03 +02:00
Ludovic MarieandClaude Opus 5.5 f18f6d58ac Creative pick block in inventories: full stacks and drags (AbstractContainerScreen cloning)
The middle button (keyPickItem) over an inventory or the creative screen no
longer clicks like the left button: with infinite materials it clones the
hovered stack at full size (CLONE) or, holding a stack, drags full stacks into
slots (quick craft type 2); otherwise it does nothing. Agents can middle-click
widgets (click ID middle), and creative slots are ui widgets now.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-21 16:14:41 +02:00
Ludovic MarieandClaude Opus 5.5 8f13b2be8c Firework tooltips: flight duration and explosions (Fireworks.addToTooltip)
Rockets list their flight duration and each run of equal explosions (shape,
colours, fade colours, trail, twinkle); firework stars list their explosion.
Both honour TooltipDisplay. New harness: tools/test_firework_tooltip_parity.py.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-21 16:14:41 +02:00
Ludovic MarieandClaude Opus 5.5 8a7ebee269 Cobwebs hold falling blocks (WebBlock.entityInside)
A falling block's move now runs vanilla's checkInsideBlocks along its path: a
cobweb it passes or lands in makes it stuck (0.25, 0.05, 0.25), which scales
its next move and zeroes its velocity, and resets its fall distance.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-21 16:14:41 +02:00
Ludovic MarieandClaude Opus 5.5 8186baba8b Shears carve pumpkins (PumpkinBlock.useItemOn)
Right-clicking a pumpkin with shears now carves it like vanilla: a carved
pumpkin facing the clicked side (the side facing the player on the top and
bottom), four pumpkin seeds from carve/pumpkin popped out of that side, the
carve sound, one point of wear, the shear game event and the used
statistic. A pumpkin carved on a golem base builds the golem, as
CarvedPumpkinBlock.onPlace does; the dispenser shares that placement.

use_item_on parity: pumpkin cases on any face with the player turned any
way (the oracle and fixture take an optional yaw and face).

Fixes #1067

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-21 16:14:41 +02:00
Ludovic Marie 85f65a69b7 Merge pull request #1021 from ludovic111/feat/mod-vanilla-tags
🤖 Agent: merged. Verify passed. Integration proof (main + #1041 #1043 #1021 #1020 #1032 #659 #1029 #1066), 2026-10-21: 97 of 100 harnesses passed on the desktop. world_reopen and save_roundtrip need a display there; they pass on the Mac mini with #1041/#1043. dedicated_server is a pre-existing flake: it fails on plain main too, and is pass/fail in 2 runs on the integration.
2026-10-21 15:38:16 +02:00
Ludovic Marie edcf8b26f3 Merge pull request #1032 from ludovic111/feat/rs-waterlog-when-replacing-water
🤖 Agent: merged. Verify passed. Integration proof (main + #1041 #1043 #1021 #1020 #1032 #659 #1029 #1066), 2026-10-21: 97 of 100 harnesses passed on the desktop. world_reopen and save_roundtrip need a display there; they pass on the Mac mini with #1041/#1043. dedicated_server is a pre-existing flake: it fails on plain main too, and is pass/fail in 2 runs on the integration.
2026-10-21 15:38:11 +02:00
Ludovic Marie a69baf681b Merge pull request #659 from ludovic111/perf/collision-scan
🤖 Agent: merged. Verify passed. Integration proof (main + #1041 #1043 #1021 #1020 #1032 #659 #1029 #1066), 2026-10-21: 97 of 100 harnesses passed on the desktop. world_reopen and save_roundtrip need a display there; they pass on the Mac mini with #1041/#1043. dedicated_server is a pre-existing flake: it fails on plain main too, and is pass/fail in 2 runs on the integration.
2026-10-21 15:38:06 +02:00
Ludovic Marie 921a9f4588 Merge pull request #1043 from rdbtCVS/t3/async-world-save-exit
🤖 Agent: merged, thanks @rdbtCVS! World saves are spread over frames and keep the session lock until every write is durable (with my small clippy fix). Integration proof (main + #1041 #1043 #1021 #1020 #1032 #659 #1029 #1066), 2026-10-21: 97 of 100 harnesses passed on the desktop. world_reopen and save_roundtrip need a display there; they pass on the Mac mini with #1041/#1043. dedicated_server is a pre-existing flake: it fails on plain main too, and is pass/fail in 2 runs on the integration.
2026-10-21 15:38:01 +02:00
Ludovic Marie 914dbf0402 Merge pull request #1041 from rdbtCVS/t3/move-world-init-off-ui
🤖 Agent: merged, thanks @rdbtCVS! Local worlds are prepared on a worker; the loading screen keeps drawing. Integration proof (main + #1041 #1043 #1021 #1020 #1032 #659 #1029 #1066), 2026-10-21: 97 of 100 harnesses passed on the desktop. world_reopen and save_roundtrip need a display there; they pass on the Mac mini with #1041/#1043. dedicated_server is a pre-existing flake: it fails on plain main too, and is pass/fail in 2 runs on the integration.
2026-10-21 15:37:56 +02:00
Ludovic Marie bc8904213d Merge pull request #1078 from ludovic111/docs/java-perf-mods-survey
🤖 Agent: merged (docs only: the Java perf-mod survey). Fixes #991.
2026-10-21 15:05:46 +02:00
Ludovic Marie c104ceff4a Merge pull request #1077 from ItsPotatoTime/fix/hud-legacy-formatting
🤖 Agent: merged, thanks @ItsPotatoTime! The action bar, title, subtitle and boss bar names now resolve legacy § codes, like the sidebar since #1003 and like vanilla's StringDecomposer.
2026-10-21 15:05:36 +02:00
Ludovic Marie 870d2f4297 Merge pull request #1020 from ludovic111/feat/close-off-air-sources
🤖 Agent: merged. Verify and CI parity passed; local proof on main + the four modpacks PRs is in the body.
2026-10-21 15:05:14 +02:00
Ludovic Marie 1bcf90def5 Merge pull request #1014 from ludovic111/feat/verify-signed-updates
🤖 Agent: merged. Every merge condition holds: the latest stable game release (v2026.10.19.15) and launcher release (launcher-v0.16.0) verify against the release key, legacy_releases.py --check passes, and launcher and horizon-server built from this branch install signed, pinned-legacy and beta releases and refuse an edited SHA256SUMS, an altered archive and a missing signature. verify passed; parity proof in the last comment.
2026-10-21 14:47:43 +02:00
Ludovic MarieandClaude Opus 5.5 1a89c52173 docs: survey of the Java performance mods' techniques for Horizon
Lithium, FerriteCore, ImmediatelyFast, Gnetum, ScalableLux, Structure
Layout Optimizer, AsyncParticles and Ixeris: per technique, the vanilla
cost it targets, whether Horizon already avoids it, and whether it is
worth measuring. From public descriptions only; no mod code read.

Fixes #991

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-21 14:46:03 +02:00
Ludovic Marie 8ee29c7844 Merge pull request #213 from ludovic111/feat/macos-signing
🤖 Agent: merged inert. Without the MACOS_SIGN_IDENTITY variable (not set: Ludovic's Developer ID and notary setup don't exist yet), every new signing/notarizing step is skipped and the release, launcher, sign and promote pipelines are unchanged; verify and parity passed on this head, a launcher dry run passed with signing skipped, and today's ad hoc launcher bundle passes the new self-update signature check. Setting it up is in FEATURES.md LATER (4b).
2026-10-21 14:28:32 +02:00
Ludovic Marie 3ceb8e4307 Merge pull request #1029 from ludovic111/perf/lod-worker-inputs
🤖 Agent: merged. Verify and CI parity (`parity / parity`) passed; before/after numbers are in the body.
2026-10-21 14:03:30 +02:00
itspotatotimeandcairn-code 41c4caac25 fix(hud): apply legacy formatting codes to action bar and titles
Servers send § codes inside a component's text. Vanilla's font applies them to every literal, but the action bar, title, subtitle and boss bar names drew the component raw, so the codes showed as glyphs. Resolve them before measuring and drawing, as the sidebar already does.

Co-Authored-By: cairn-code (DeepSeek V4.1 Flash) <[email protected]>
2026-10-21 14:48:01 +03:00
Ludovic Marie 02bd3cbc72 Merge pull request #1075 from ItsPotatoTime/fix/washed-title-screen-colors
🤖 Agent: merged, thanks @ItsPotatoTime! Prefers an 8-bit non-sRGB surface (Bgra8Unorm/Rgba8Unorm) instead of RADV's fp16 ExtendedSrgb one, so colours are no longer washed out on Wayland. Mac and Windows already got Bgra8Unorm. Verify passed; parity doesn't cover surface formats.
2026-10-21 13:32:03 +02:00
Ludovic MarieandClaude Opus 5.5 50a5e64012 Stars: build the star buffer once and draw it with the sky's rotation
SkyRenderer.buildStars builds the 1500 star quads once into a static
buffer; Horizon regenerated them on the CPU and uploaded about 200 KB of
vertices every frame while stars were out, which made nights cost more
than days (#1054). The stars now live in a static vertex buffer built
when the renderer starts, drawn after the sun and moon with one small
uniform: the view projection times the sky's rotation, and the star
brightness.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-21 13:14:39 +02:00
itspotatotimeandcairn-code 6bb6009133 fix(gfx): keep the swapchain off the linear float format
Rgba16Float is the first non-sRGB format RADV advertises on Wayland, and wgpu gives it the linear ExtendedSrgb color space. The renderer writes gamma-encoded values, so the compositor read them as linear light and washed every colour out.

Prefer Bgra8Unorm or Rgba8Unorm and keep the old choice as the fallback. A screenshot on a surface that is not 32-bit now reports an error instead of copying with an 8-bit row stride, which aborted wgpu validation.

Co-Authored-By: cairn-code (DeepSeek V4.1 Flash) <[email protected]>
2026-10-21 13:46:55 +03:00
Ludovic Marie 7756660d6e Merge pull request #1066 from ludovic111/docs/servers-page-released
🤖 Agent: merged. Docs only (README and website/docs): the Servers page is in launcher 0.15+, and releases go from beta to stable. Verify passed; parity doesn't cover docs. This also redeploys playhorizon.dev, which still showed the 10-19 14:29 build.
2026-10-21 12:37:48 +02:00
Ludovic MarieandClaude Opus 5.5 d8eafd7dc0 version-port-daily.sh: no fleet-load priority change (it lowered the calling session); launchd's Nice and LowPriorityIO do it
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-21 12:22:21 +02:00
Ludovic MarieandClaude Opus 5.5 70e058142c version_port.py publish: the report's work items as GitHub issues, a dry run unless --yes
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-21 12:22:21 +02:00
Ludovic MarieandClaude Opus 5.5 c82d3f25c4 docs/version-ports.md: the harness stage replayed both ways
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-21 12:22:21 +02:00
Ludovic MarieandClaude Opus 5.5 b3bc3c0768 docs/version-ports.md: regen, its replay, status
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-21 12:22:21 +02:00
Ludovic MarieandClaude Opus 5.5 94b5d2dbc4 tools/ci: the daily version-port agent (detect, port, harnesses, report, publish dry run), not installed
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-21 12:22:21 +02:00
Ludovic MarieandClaude Opus 5.5 fec2e20f72 port_harness: horizon-prepare's javac errors from the log it names (also when classifying again)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-21 12:22:21 +02:00
Ludovic MarieandClaude Opus 5.5 3b5eabaf5f regen: command_parse_fixture exports Horizon's registration order; javac errors summarized
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-21 12:22:21 +02:00
Ludovic MarieandClaude Opus 5.5 1cd72f1ba7 port_harness: horizon-prepare writes the data generator's reports into the candidate, not through regen's link to the version store
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-21 12:22:21 +02:00
Ludovic MarieandClaude Opus 5.5 938e6b1191 version_port.py regen: a candidate worktree pinned to the new version, every table regenerated
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-21 12:22:21 +02:00
Ludovic MarieandClaude Opus 5.5 285593e531 docs/version-ports.md: the candidate harness run, status
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-21 12:22:21 +02:00
Ludovic MarieandClaude Opus 5.5 ec0725aeb0 version_port.py harnesses: horizon-prepare and every harness in the candidate, newly failing ones in the report
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-21 12:22:21 +02:00
Ludovic MarieandClaude Opus 5.5 9ff74a58a1 lan_e2e.py: the prepared resources of the pinned version (lab)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-21 12:22:20 +02:00
Ludovic MarieandClaude Opus 5.5 ef8b045d79 Tests main added since: prepared resources under the generated version
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-21 12:22:20 +02:00
Ludovic MarieandClaude Opus 5.5 eee7786609 Generated version facts through lab.version_info; options and world harnesses take the data version from the jar
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-21 12:22:20 +02:00
Ludovic MarieandClaude Opus 5.5 605b9f8511 datafix: the version name through horizon_core::version, in the harness's Rust scope
horizon_core::VANILLA_VERSION is a re-export, which left the datafix harness's
PARITY_RUST_SCOPE open: its six shards reran on any Rust change.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-21 12:22:20 +02:00
Ludovic MarieandClaude Opus 5.5 ce87bfb8c7 Tools read the pinned Minecraft version from research-lock.json
lab.VERSION, lab.GAME (.local/minecraft/<version>) and lab.RESOURCES
(.local/horizon-client/<version>) come from the lock, and lab.version_info()
reads the pinned client jar's version.json. Every harness, generator and
tool that named 26.3 (resource folders, jar and source paths, result
records, the vanilla client's --version and asset index, the bare login
client's protocol, the benchmark's options.txt data version, the packet id
table's header) uses them, so a candidate worktree pinned to another version
runs them unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-21 12:22:20 +02:00
Ludovic MarieandClaude Opus 5.5 326f69d326 lan_bot: the status's protocol is the generated constant
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-21 12:22:20 +02:00
Ludovic MarieandClaude Opus 5.5 9f385280ef Version facts in one generated file: crates/horizon-core/src/version.rs
tools/gen_version_consts.py writes what the pinned client jar's version.json
states (name, id, protocol, data version, pack formats, Java, stable, build
time) into horizon_core::version, and every Rust site reads it from there:
PROTOCOL_VERSION, GAME_VERSION, DATA_VERSION, the save's and the datafixer's
version name, /version, the crash report, the legacy ping, the dedicated
server's start line, the title screens, the default resource folder, the
resource pack format, and the launcher's and horizon-ctl's fallbacks. CI
checks the file against the pinned jar.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-21 12:22:20 +02:00
Ludovic MarieandClaude Opus 5.5 b928dc3c9b Merge origin/main into perf/server-helpers-behind
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-21 12:20:38 +02:00
Ludovic MarieandClaude Opus 5.5 846f681b9a Signed updates: releases from before signing stay installable with pinned files; tests need a release key
- crates/horizon-release-sign/legacy-releases.txt pins the SHA-256 of every archive of the 131 releases published
  before signing (GitHub's upload digests, written by tools/ci/legacy_releases.py). The launcher and
  horizon-server update install those releases only with exactly those files, so rolling back to one works and
  nobody can swap its files later; every other release needs its signed SHA256SUMS.
- the_launcher_ships_with_a_release_key and horizon_server_ships_with_a_release_key fail while
  release-keys.txt lists no key, so this can't ship refusing everything.
- Test tags moved to v2099.* so they never collide with a pinned release.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-21 12:20:36 +02:00
Ludovic MarieandClaude Opus 5.5 36908858a3 Launcher and horizon-server update: install only releases whose signed checksums match
The launcher (game installs and its own updates) and `horizon-server update` fetch a release's
SHA256SUMS and SHA256SUMS.sig, check the signature against the keys built in from
crates/horizon-release-sign/release-keys.txt and the archive against the signed SHA-256, and
refuse anything unsigned, signed for another tag or by another key, or that doesn't match, with a
message saying why. Merge only once release-keys.txt lists the key and a signed stable release
exists: until then every download is refused.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-21 12:20:36 +02:00
Ludovic MarieandClaude Opus 5.5 3ffbb1cb9f Sign and notarize the macOS builds with Developer ID
With the repository variable MACOS_SIGN_IDENTITY set, the launcher's Horizon.app and .dmg and the
game's three programs are signed with the hardened runtime and a timestamp, notarized and (app,
dmg) stapled, from a keychain of the Mac mini's own that keychain.sh unlocks with its password from
1Password. Unset, everything is signed ad hoc as before. The launcher checks an updated bundle's
signature before and after swapping it in, and puts the old one back when it doesn't hold.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-21 12:20:34 +02:00
Ludovic Marie 4d2e05dda7 Merge pull request #1074 from ludovic111/fix/input-capture-fixture
🤖 Agent: merged on verify. Test-only (a Python test fixture and a temp-folder name inside a #[test] fn), so no harness can see it; it turns main's verify green again after #235 and removes a horizon-server test flake.
2026-10-21 12:20:25 +02:00
Ludovic MarieandClaude Opus 5.5 386cb5203a horizon-server tests: settings and config no longer share a temp folder
a_stopped_server_saves_and_keeps_comments (settings.rs) and
writes_properties_and_syncs_back (config.rs) both used
horizon-server-config-<pid>, so when they ran at the same time one
removed or rewrote the other's horizon.toml and
writes_properties_and_syncs_back failed now and then. The settings test
now has its own folder.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-21 12:11:23 +02:00
Ludovic MarieandClaude Opus 5.5 b67b3c23ed tests: the input capture fixture models #235's cursor recentring
#235 made Game::capture read the surface size, warp the pointer to the
window's centre and set the HUD cursor, so the fixture's copy of the method
no longer compiled and verify failed on main. The fixture now stubs
winit's PhysicalPosition/PhysicalSize, Window::inner_size and
set_cursor_position, HudBatch and Game's renderer and cursor, and checks
the recentring: one warp on grab, two on release, the HUD cursor at the
centre, and nothing when releasing an already released pointer.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-21 12:01:11 +02:00
Ludovic MarieandClaude Opus 5.5 f137a8d3cc Merge origin/main into perf/server-helpers-behind
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-21 11:52:33 +02:00
Ludovic Marie 1225f89e57 Merge pull request #235 from ludovic111/fix/cursor-centre-on-screen-open
🤖 Agent: merged. Recentres the cursor like vanilla's MouseHandler.grabMouse/releaseMouse. CI verify and parity passed; Ludovic asked on 10-21 to clear the PR queue autonomously. If the cursor misbehaves on a Wayland compositor, file it and we'll follow up.
2026-10-21 11:40:44 +02:00
Ludovic Marie 34443128ba Merge pull request #1061 from rdbtCVS/t3/fix-issue-1058
🤖 Agent: merged, fixes #1058. Options we can't support yet stay greyed out in Classic UI with an explanation. Thanks @rdbtCVS!
2026-10-21 11:35:04 +02:00
Ludovic Marie a717ff012a Merge pull request #1044 from Yvonne-Aizawa/feat/mod-storage-capability
🤖 Agent: merged. Reviewed the storage host: keys are one [a-z0-9_.-] segment, ids are [a-z0-9_], and the quota, key cap and per-call write budget all hold. Thanks @Yvonne-Aizawa!
2026-10-21 11:34:36 +02:00
Ludovic Marie 30740f902f Merge pull request #1065 from rdbtCVS/fix/pr-ready-validation-scope
🤖 Agent: merged. pr_ready now checks the touched crates and their dependents instead of the whole workspace; CI keeps its workspace-wide checks. The earlier verify failure was an unrelated supervisor test. Thanks @rdbtCVS!
2026-10-21 11:34:07 +02:00
Ludovic Marie 04abab60ab Merge pull request #1042 from rdbtCVS/t3/optimize-startup-handoff 2026-10-21 11:30:59 +02:00
Ludovic MarieandClaude Opus 5.5 ab9e0cdea4 Use if let for the integrated server stop request
Clippy's unnecessary_unwrap rejected the is_some check followed by unwrap.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-21 11:30:52 +02:00
Ludovic Marie a55da8d210 Merge pull request #1062 from rdbtCVS/t3/fix-issue-1057
🤖 Agent: merged. Follows ClientLevel.getMarkerParticleTarget with a 432-case vanilla harness; fixes #1057. Thanks @rdbtCVS!
2026-10-21 11:29:52 +02:00
Ludovic Marie 9ee99c632a Merge pull request #1063 from rdbtCVS/t3/add-launcher-menu-scroll
🤖 Agent: merged, the launcher's long pages scroll properly now. Thanks @rdbtCVS!
2026-10-21 11:29:07 +02:00
Ludovic Marie 9f114e1de8 Merge pull request #1064 from rdbtCVS/t3/fix-settings-subheadings
🤖 Agent: merged, thanks @rdbtCVS!
2026-10-21 11:28:38 +02:00
Ludovic Marie 71826576e8 Merge pull request #1059 from rdbtCVS/fix/spectator-item-drop-1053
🤖 Agent: merged. Matches Minecraft.handleKeybinds (no drops in spectator) with a 128-snapshot vanilla harness. Thanks @rdbtCVS!
2026-10-21 11:28:09 +02:00
Ludovic Marie 04854e96b5 Merge pull request #1036 from rdbtCVS/fix/1033-creative-hunger
🤖 Agent: merged. Follows Player.causeFoodExhaustion and hurtServer for invulnerable abilities; fixes #1033. Thanks @rdbtCVS!
2026-10-21 11:27:42 +02:00
Ludovic Marie 8e360e377c Merge pull request #1039 from rdbtCVS/t3/reset-spectator-fly-speed
🤖 Agent: merged. Matches vanilla's mode switch (flying speed back to 0.05 when leaving spectator). Thanks @rdbtCVS!
2026-10-21 11:27:13 +02:00
Ludovic MarieandClaude Opus 5.5 0d9c964711 Docs: the Servers page is released (launcher 0.15), and releases go beta → stable
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-21 11:05:13 +02:00
Ludovic Marie 5513361783 Merge pull request #1027 from ludovic111/fix/carry-vine-template
🤖 Agent: merged after verify and CI parity passed (modpacks-11: Chipped's 11 vines carried as vines; the 11 blockstate errors are gone from the game log).
2026-10-21 11:02:04 +02:00
rdbtCVS a7941a8eca Scope PR validation to affected crates and avoid unrelated example builds 2026-10-21 00:56:37 +01:00
rdbtCVS 91b5256fc2 Add pointer-operated scrollbars throughout the launcher 2026-10-21 00:24:17 +01:00
rdbtCVS 800fbc757e Fix classic video search section headings 2026-10-21 00:20:52 +01:00
rdbtCVS 01fcfc9095 Match vanilla accessibility controls and hide unavailable Horizon options 2026-10-20 16:39:58 +01:00
rdbtCVS cadc20cd58 Fix creative barrier and light block marker particles 2026-10-20 16:30:34 +01:00
rdbtCVS 2469b92e4f Restore missing accessibility settings and disable unfinished features 2026-10-20 16:29:50 +01:00
rdbtCVS a29d293e8e fix: prevent spectators from dropping hotbar items 2026-10-20 16:20:54 +01:00
yvonneandClaude Sonnet 5 c3f23d9452 Mods: close three storage gaps Codex's review found
- Reject a second running mod with an id already in use: two mods
  sharing an id would also share capability storage's data_dir,
  letting one read, overwrite or delete the other's state.
- Cap storage to 64 keys per mod, not just 4 MiB: zero- or near-zero-
  byte values could otherwise flood the directory with files the
  byte quota never sees.
- Add a 1 MiB per-call write budget, separate from the 4 MiB on-disk
  quota: a loop replacing one key stays under the quota on every
  check (the total on disk never grows) while still doing unbounded
  synchronous disk I/O per call, which fuel never meters.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-10-19 19:56:12 +02:00
yvonne 8d55af7cae Mods: capability storage, a quota'd per-mod key/value folder
A mod with capability storage gets get(key)/set(key, value)/delete(key),
backed by real files beside horizon-mods/ (.horizon-mod-data/<id>/, 4 MiB
quota per mod). Keys are a single path segment so they can't leave the
mod's own directory. Unlike in-memory mod state, this survives a
cargo-horizon-mod dev reload, since it's keyed by the mod's path and id
rather than its Store.
2026-10-19 19:43:29 +02:00
rdbtCVS d5d3f5b3c7 Prepare world save snapshots incrementally and poll durable completion 2026-10-19 18:32:40 +01:00
rdbtCVS 4ffb073b59 Prepare startup handoff CPU resources on the loader worker 2026-10-19 18:31:52 +01:00
rdbtCVS 7f4193f88e Move local world initialization off the UI thread 2026-10-19 18:26:38 +01:00
rdbtCVS 20579bf638 fix: reset spectator fly speed on game mode changes 2026-10-19 17:55:13 +01:00
rdbtCVS 8d8d8ab221 Fix creative and spectator hunger exhaustion and starvation 2026-10-19 17:39:53 +01:00
Ludovic Marie defaa7cd2a Merge pull request #1025 from ludovic111/fix/command-exec-dimension-flake
🤖 Agent: merged after verify and CI parity passed (modpacks-11).
2026-10-19 17:23:10 +02:00
Ludovic Marie 9e50fd8df2 Merge pull request #1028 from ludovic111/fix/carry-enter-block-advancements
🤖 Agent: merged after verify and CI parity passed (modpacks-11).
2026-10-19 17:23:05 +02:00
Ludovic Marie 8b53bd8a47 Merge pull request #1022 from ludovic111/fix/carry-rs-structure-void
🤖 Agent: merged after verify and CI parity passed (modpacks-11).
2026-10-19 17:23:00 +02:00
Ludovic Marie f441acf2f8 Merge pull request #1019 from ludovic111/fix/carry-untyped-loot-numbers
🤖 Agent: merged after verify and CI parity passed (modpacks-11).
2026-10-19 17:22:55 +02:00
Ludovic Marie 9d555dcdd0 Merge pull request #988 from ludovic111/feat/simple-template-structures
🤖 Agent: merged after verify passed and a local check on main + #988: chunk_generation, command_exec and mob_ai (the three harnesses that failed in CI on its old head, under load) all pass; modpacks-10's integration ran 301/301. Formations' structures generate from the noise column.
2026-10-19 16:46:40 +02:00
Ludovic MarieandClaude Opus 5.5 0eda0b0aaa docs/modpacks.md: Repurposed's waterlogging fix is carried
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 16:34:07 +02:00
Ludovic MarieandClaude Opus 5.5 032446fb68 Structures: Repurposed Structures' waterlogging fix, as horizon:waterlog_in_world_water
WaterlogWhenReplacingWaterProcessor (16 lists: the bottom pillars of its
villages and outposts) waterlogs a block that can be where the world holds
water, makes it dry where it does not, and gives a block placed in water a
block tick. Carry used to drop it; it is now the game's
horizon:waterlog_in_world_water, in the chunk being generated as the mod's.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 16:30:52 +02:00
Ludovic Marie 2ff8f5277d Merge pull request #1012 from ludovic111/feat/release-channels
🤖 Agent: merged after verify passed, security-1's dry runs on the branch, and an independent review whose findings were all fixed. Every release goes out as a beta and is promoted to stable after 6 hours if its smoke test passed and no release-blocker issue is open; launcher-v0.14.0 (stable by default) is already out.
2026-10-19 16:29:16 +02:00
Ludovic Marie 9414c55d22 Merge pull request #1015 from ludovic111/ci/codeowners
🤖 Agent: merged after verify passed, security-1's dry runs on the branch, and an independent review whose findings were all fixed. CODEOWNERS for the release pipeline and update code. The protection itself (tools/ci/protect_pipeline.sh --apply) waits for the switch to the GitHub App.
2026-10-19 16:29:12 +02:00
Ludovic Marie 19898a0d77 Merge pull request #1018 from ludovic111/docs/security
🤖 Agent: merged after verify passed, security-1's dry runs on the branch, and an independent review whose findings were all fixed. docs/security.md: the security model in plain words, with what is live and what still waits.
2026-10-19 16:29:07 +02:00
Ludovic Marie d93c739edc Merge pull request #1024 from ludovic111/ci/release-clean-checkout
🤖 Agent: merged after verify passed, security-1's dry runs on the branch, and an independent review whose findings were all fixed. Release builds on the Mac mini start from a clean checkout.
2026-10-19 16:29:03 +02:00
Ludovic Marie 2b756b7957 Merge pull request #1031 from ludovic111/feat/readme-front-page
🤖 Agent: merged after verify passed (docs and website only; the site build fails on broken links, anchors or images). The README is a shorter front page that links to the new docs.
2026-10-19 16:28:22 +02:00
Ludovic Marie df1155b8b5 Merge pull request #1030 from ludovic111/feat/site-docs
🤖 Agent: merged after verify passed (docs and website only; the site build fails on broken links, anchors or images). The website gets a Docs section with 11 pages; /server redirects to /docs/hosting.
2026-10-19 16:28:18 +02:00
Ludovic MarieandClaude Opus 5.5 ea6ad5165f README: a clear front page
One-paragraph pitch, download and install, what works today with links to the
site's documentation, hosting and modpacks in two lines each, then build from
source, contributing and the project status table (moved lower, unchanged in
form so the site and tests/test_parity.py still read it). The stale release
notes section and launcher version are gone: the release notes are linked.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 16:25:33 +02:00
Ludovic MarieandClaude Opus 5.5 a13e6b40ce Website: a documentation section for the whole project, replacing the server page
/docs has a left navigation and eleven pages written for players first, then
hosters, then contributors: what Horizon is, install, settings, multiplayer,
logs and bug reports, hosting (the launcher's Servers page and the command
line), modpacks, mods and data packs, performance, contributing and the FAQ.
/server redirects to /docs/hosting. The build checks every link between the
pages and every image.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 16:24:23 +02:00
Ludovic Marie d500f28ee4 Merge pull request #1026 from ludovic111/feat/launcher-servers-page
🤖 Agent: merged after verify passed (74 + 9 + 1 launcher tests; launcher-only). The Servers page: create, start/stop, players, settings, backups, updates, run at login, and Friends over the internet with Tailscale (device sharing, an optional rule limiting shared friends to the game port, never Funnel). Ludovic's real-tailnet check is described in the PR.
2026-10-19 16:22:08 +02:00
Ludovic Marie c95ab6c26e Merge pull request #1017 from ludovic111/feat/launcher-servers-backend
🤖 Agent: merged after verify passed (launcher-only; no parity harness depends on it). The launcher's server backend and `horizon-launcher servers …` CLI.
2026-10-19 16:22:02 +02:00
Ludovic Marie c94b6a0972 Merge pull request #1009 from ludovic111/feat/server-config-command
🤖 Agent: merged after verify and CI parity passed (40 server tests; the whole create → start → join → whitelist → backup → restore → update → stop flow ran on the Mac mini and the ThinkPad). Adds `horizon-server config`, `backups --json`, `update --check --json`; fixes whitelisting on offline servers and updates as root in containers.
2026-10-19 16:21:56 +02:00
Ludovic MarieandClaude Opus 5.5 399178174d port-pack: Repurposed's structure voids keep the world's block
repurposed_structures:structure_void_processor returns null for a
template's structure_void blocks, which is what vanilla's block_ignore
does with blocks: [structure_void]. Carry writes it that way, so the
82 lists that use it (its mineshafts, ocean village and temples, the
jungle pyramid's hidden room, swamp village) no longer place structure
voids over the terrain.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 16:19:24 +02:00
Ludovic MarieandClaude Opus 5.5 9adeb682cb docs/modpacks.md: Repurposed's air fix is carried
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 16:18:43 +02:00
Ludovic MarieandClaude Opus 5.5 4459ed9dd5 Structures: Repurposed Structures' air fix, as horizon:close_off_air_sources
CloseOffAirSourcesProcessor walls off the air and the other liquids beside
a template's fluids, so its ocean mineshafts keep their water in. The game
runs it as horizon:close_off_air_sources (the same weighted blocks and the
mod's random, neighbours written in their sections as the mod does), and
carry maps the mod's 4 uses to it. The weighted block list is shared with
the fluid fix.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 16:18:31 +02:00
Ludovic MarieandClaude Opus 5.5 72515b99a7 Content mods: shared tags merge like a data pack's, replace honoured with a warning
A mod's file for a vanilla, convention or other mod's tag adds its values in
load order; one with "replace": true now drops vanilla's entries and the
earlier mods' first, as a higher data pack does, and the log says which mod
replaced what (kept with the cached root, so a reused root warns too).
Carry keeps such tags instead of setting them aside: Towns and Towers'
seven minecraft:has_structure biome tags load. Mods still own no other
minecraft content.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 16:18:12 +02:00
Ludovic MarieandClaude Opus 5.5 8011d274eb port-pack: enter_block criteria name their block as blocks
26.3's EnterBlockTrigger reads a block list from 'blocks' and skips
1.21.1's 'block', so a carried criterion matched any block the player
entered: Regions Unexplored's Downer, From the Tree Tops and Light as a
Frog were granted on the first dive into the sea. Carry renames the
field (20 advancements in Regions Unexplored, with its boat recipes'
in-water criteria).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 16:16:54 +02:00
Ludovic MarieandClaude Opus 5.5 6b5694d2b7 port-pack: blocks with sides and up that are true or false are vines, not walls
Without the oracle, a block's template is guessed from its blockstate's
property names, and a vine's (east, north, south, up, west) fit
cobblestone_wall. The game then refused Chipped's 11 vine blockstates:
a wall's sides are none, low or tall, so 'true' is an unknown multipart
property value. block_template looks at the sides' values and makes those
vines; Luminax's 32 walls stay walls.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 16:11:16 +02:00
Ludovic MarieandClaude Opus 5.5 5e90e26de0 launcher: the Tailscale panel reads invitations and access controls again every 30 s
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 16:05:33 +02:00
Ludovic Marie 59a8aa2cd5 Merge pull request #984 from ludovic111/feat/carry-sea-level-processors
🤖 Agent: merged after verify passed and a local check on main + #983 + #984 (clippy clean; chunk_generation and locator_bar, the two harnesses that failed in CI on the old head, both pass; modpacks-10's integration ran 301/301). Structures built under the sea are flooded and waterlogged; Better Ocean Monuments' processors carried.
2026-10-19 16:02:52 +02:00
Ludovic Marie 84e9dd59fd Merge pull request #983 from ludovic111/feat/sea-level-processors
🤖 Agent: merged after verify passed and a local check on main + #983 + #984 (clippy clean; chunk_generation and locator_bar, the two harnesses that failed in CI on the old head, both pass; modpacks-10's integration ran 301/301). Structures built under the sea are flooded and waterlogged; Better Ocean Monuments' processors carried.
2026-10-19 16:02:45 +02:00
Ludovic MarieandClaude Opus 5.5 76b151d2b9 launcher README: Servers page screenshots
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 15:59:42 +02:00
Ludovic MarieandClaude Opus 5.5 373742573f command_exec harness: come back from the Nether to terrain that is already loaded
The relative-dimension check teleported the player from the Nether's
80 100 -40 to the overworld's 644 100 -316, fresh ocean chunks at seed 1.
On some runs vanilla broke some of their kelp and seagrass while they
loaded (its chunk loading is asynchronous), which rolled those blocks'
loot tables, so /random reset * later counted 12 or 13 sequences
against Horizon's 11. 1 230 0 in the Nether lands at 12 230 4, terrain
loaded since the start: the scaling is still checked, and 10 runs in a
row pass (5 of the 16 runs before failed this way).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 15:53:41 +02:00
Ludovic Marie c719d215ef Merge remote-tracking branch 'origin/main' into integ-659 2026-10-19 15:51:09 +02:00
Ludovic MarieandClaude Opus 5.5 47bfa67f49 launcher: keyboard and contrast tests for the Servers page; readable pre-update backup names
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 15:50:11 +02:00
Ludovic MarieandClaude Opus 5.5 1ea04a2b10 docs/security.md: where things stand today, and the Mac mini runs forks' code too once labelled
- A table of what is live, waiting to merge, or waiting for Ludovic, so the page doesn't read as if it all were.
- Forks: their checks need a maintainer's approval, and run-parity runs them on the Mac mini; the Mac mini
  section no longer says forks don't run there, and says what a clean release checkout does and doesn't fix.
- Blockers hold every beta while open; releases from before signing stay installable with pinned files.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 15:45:24 +02:00
Ludovic MarieandClaude Opus 5.5 c05a00092e docs/security.md: the security model in plain words
What an attacker could do before (instructions hidden in reports, a skipped check that counted as
passed, releases straight to every player, downloads trusting GitHub alone, pipeline files changed
like any other), what stops it now (untrusted text, verify always running, beta then stable,
signed updates, code owners, the agents' own GitHub App), what still needs Ludovic, and what it
doesn't solve. Written for the website's Contributing page; CONTRIBUTING.md links it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 15:44:29 +02:00
Ludovic MarieandClaude Opus 5.5 622b9a3a95 Releases: the Mac mini builds them from a clean checkout
Its work folder is shared with pull requests' parity jobs; with clean: false, whatever they left (target/, an
untracked .cargo/config.toml, a build script's output) could end up in a release. release.yml and launcher.yml
now always check out clean; target/ is rebuilt from scratch, and vanilla-data.sh relinks .local/ from its
preparations outside the checkout.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 15:43:49 +02:00
Ludovic MarieandClaude Opus 5.5 19220000be CODEOWNERS: the launcher's downloader, links.json and the Cargo.toml files; Ludovic alone may bypass the review
- backend.rs downloads, installs and self-updates; links.json says where from; launcher/Cargo.toml and
  crates/horizon-server/Cargo.toml pick the crates that check downloads.
- protect_pipeline.sh: ludovic111 is the only owner and can't approve his own pull requests, so he is the one
  bypass_pull_request_allowances user. enforce_admins stays on, so verify is still required for everyone.
  --apply reads the protection back and fails if GitHub didn't keep it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 15:42:17 +02:00
Ludovic MarieandClaude Opus 5.5 1c341d58a6 CODEOWNERS for the release pipeline, and the protection that makes it count
.github/, tools/ci/, the release tools, the signing crate and the code that checks updates are
Ludovic's to review. tools/ci/protect_pipeline.sh prints (and with --apply sets) main's protection
with code owners' reviews required and no other approvals, keeping the required verify check;
it is for the orchestrator to apply once agents work as the GitHub App.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 15:42:09 +02:00
Ludovic Marie 7d5261345a Merge pull request #1023 from ludovic111/ci/release-public-key
🤖 Agent: merged after verify passed. The secret was set first (RELEASE_SIGNING_KEY in the release environment), so the next release is signed with this key.
2026-10-19 15:40:55 +02:00
Ludovic MarieandClaude Opus 5.5 07a7a26d7d launcher: older servers show no backup toggle; no empty line under a player without a ping
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 15:39:40 +02:00
Ludovic MarieandClaude Opus 5.5 a307bd2e3d Release channels: blockers hold every beta, latest only moves forward, a stalled promotion opens an issue
- A release-blocker issue's text is never read: its author can edit it after a maintainer labels it, so an
  open blocker holds every beta, and one closed as completed still holds the betas published before it closed.
- Even --force never promotes a beta that isn't newer than the stable release: horizon-server update and
  install.sh download GitHub's latest.
- When the oldest beta newer than the stable release has waited 24 hours, promote.yml opens a "Promotion to
  stable has stalled" issue with the last reason; the next promotion closes it.
- promote.yml: persist-credentials: false; issues: write for the stall issue.
- Website: the newest 100 releases, plus GitHub's latest (the stable one) when a day of betas pushes it out.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 15:38:05 +02:00
Ludovic MarieandClaude Opus 5.5 9b1e413edf launcher: a server whose Horizon predates config says so instead of empty backups; a note after an update
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 15:36:55 +02:00
Ludovic MarieandClaude Opus 5.5 2a7303f76f Releases: beta and stable channels, promoted after 6 hours
Game releases go out as betas (GitHub pre-releases, never "latest") with the Mac mini's smoke
report attached. promote.yml (every 30 minutes, or by hand from a phone) runs
tools/release_channels.py, which makes the newest beta stable and latest once it has been out 6
hours, passed its smoke test and no release-blocker issue covers it, with notes rewritten to cover
everything since the last stable release. release_train.py counts merges from the newest release of
either channel, and a beta's notes say it is one.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 15:34:03 +02:00
Ludovic MarieandClaude Opus 5.5 64c6c292ef Release signing: add the first public key
The private key was generated by tools/ci/new_release_key.sh straight into
the release environment's RELEASE_SIGNING_KEY secret (never on disk or
printed); from now on every release's SHA256SUMS is signed with it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 15:33:08 +02:00
Ludovic Marie e2ea3014d5 Merge pull request #1013 from ludovic111/feat/launcher-beta-builds
🤖 Agent: merged after verify passed, with security-1's dry runs on the branch and an independent review (no regression: parity still runs on every PR from this repo; releases go out unsigned with a warning until the signing key is set).
2026-10-19 15:32:14 +02:00
Ludovic MarieandClaude Opus 5.5 c42ae71384 launcher: no update check while another action runs
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 15:32:12 +02:00
Ludovic Marie 0a1d03b4b7 Merge pull request #1011 from ludovic111/feat/signed-release-checksums
🤖 Agent: merged after verify passed, with security-1's dry runs on the branch and an independent review (no regression: parity still runs on every PR from this repo; releases go out unsigned with a warning until the signing key is set).
2026-10-19 15:32:09 +02:00
Ludovic Marie e17e8b9d36 Merge pull request #1010 from ludovic111/ci/verify-always-runs
🤖 Agent: merged after verify passed, with security-1's dry runs on the branch and an independent review (no regression: parity still runs on every PR from this repo; releases go out unsigned with a warning until the signing key is set).
2026-10-19 15:32:04 +02:00
Ludovic MarieandClaude Opus 5.5 a4b7ac4a9b horizon-server update: unpack as the user, not the archive's owners
As root in a user namespace (an unprivileged LXC or rootless container), GNU tar
keeps the archive's owners by default and fails on IDs it can't map, so the update
stopped at "tar couldn't unpack". Found driving the launcher's Update now from one.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 15:31:55 +02:00
Ludovic MarieandClaude Opus 5.5 a8dbaaabd4 launcher: refused values show under their field; a restore says what it did; Checking… while an update check runs
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 15:21:56 +02:00
Ludovic Marie d468e1e78d Merge pull request #974 from ludovic111/feat/carry-extra-namespaces
🤖 Agent: merged after verify and CI parity passed, plus modpacks-10's local integration (parity 301/301, structure-start oracle 150/150 and 134/134). Towns and Towers' namespaces carried in its manifest.
2026-10-19 15:20:55 +02:00
Ludovic Marie 9a0077794c Merge pull request #969 from ludovic111/feat/mod-extra-namespaces
🤖 Agent: merged after verify and CI parity passed, plus modpacks-10's local integration (all nine branches on main: parity 301/301, structure-start oracle 150/150 and 134/134). Content mods may own namespaces besides their id; two mods may not share one.
2026-10-19 15:20:30 +02:00
Ludovic Marie 418e22a4b5 Merge pull request #975 from ludovic111/fix/mod-loot-tables
🤖 Agent: merged after verify and CI parity passed, plus modpacks-10's local integration (all nine branches on main: parity 301/301, structure-start oracle 150/150 and 134/134). Mod loot tables from every namespace, so modded structure chests are filled.
2026-10-19 15:20:26 +02:00
Ludovic MarieandClaude Opus 5.5 2193559cb1 Far terrain: chunk summaries and mesh inputs on the worker
The render thread summarized every observed chunk (256 columns, seven
light reads per block) and gathered each mesh job's inputs (neighbour
edges, visited columns, cloned spans) itself, within a budget of about
1 ms and 2 ms a frame but at least one chunk or tile: a large tile or a
chunk on a slow machine still cost tens of milliseconds in one frame.

Both now run on the far-terrain worker. Lod::observe hands the chunk
over (the latest per column, queued once), and the worker returns its
summary with its reductions (Done::Observed): an unchanged one is
dropped, a new one is kept and applied as a reduction was. A mesh job
takes a snapshot of the active tiles, their samples and the visited
summaries (Arc maps, cheap to clone) and gathers every tile's inputs
on the worker, as it did before inputs moved to the render thread.
At most OBSERVE_AHEAD resident chunks wait at once, since an edit
copies a chunk the worker still holds.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 15:07:28 +02:00
Ludovic MarieandClaude Opus 5.5 1b17bef90d launcher: the Check Minecraft accounts note wraps in the Create dialog
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 15:05:51 +02:00
Ludovic MarieandClaude Opus 5.5 621cea18ca docs/modpacks.md: carry types bare number ranges
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 15:04:18 +02:00
Ludovic MarieandClaude Opus 5.5 d21b122379 launcher: the suggested server name is selected (typing replaces it); Space types a space everywhere
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 15:01:16 +02:00
Ludovic MarieandClaude Opus 5.5 fd5360533b launcher: a focus ring with a gap; the empty Servers page mentions the EULA
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 14:58:19 +02:00
Ludovic MarieandClaude Opus 5.5 32439053c9 launcher: an override Tailscale path that isn't there means not installed
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 14:56:04 +02:00
Ludovic MarieandClaude Opus 5.5 8f51e68d2d launcher: draw the keyboard focus ring (a shadow needs some blur in GPUI)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 14:54:28 +02:00
Ludovic MarieandClaude Opus 5.5 76e3827545 release.yml sign step: a block scalar, so the line break isn't folded into an escaped space
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 14:47:39 +02:00
Ludovic MarieandClaude Opus 5.5 f94dfe1f08 Carry: number providers written as a bare {min, max} get 26.3's uniform type
1.21.1 read a {min, max} without a type as a uniform number provider; 26.3
dispatches on type alone. The game read such rolls and counts as 0, so
chests of 304 carried ATM10 tables (When Dungeons Arise 140, Repurposed
Structures 50, Structory and its towers 78, ...) came out empty or short.
Carry now types them in pools (rolls, bonus_rolls) and in the functions
that take one, nested bounds too; ranges such as limit_count's and item
predicates' counts stay ranges.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 14:39:18 +02:00
Ludovic Marie a43620023d Merge pull request #995 from ludovic111/perf/lod-integrated-gpu
🤖 Agent: merged after verify passed and CI parity passed on the stack head (#995; local 98/98). perf-frame-9's terrain LOD follow-ups: band and black cliffs fixed, far quads −36 %, per-frame budgets, LOD cap on non-Apple integrated GPUs. LODs off matches main (screenshots, FPS).
2026-10-19 14:38:25 +02:00
Ludovic Marie 4d164deb16 Merge pull request #994 from ludovic111/perf/lod-frame-spikes
🤖 Agent: merged after verify passed and CI parity passed on the stack head (#995; local 98/98). perf-frame-9's terrain LOD follow-ups: band and black cliffs fixed, far quads −36 %, per-frame budgets, LOD cap on non-Apple integrated GPUs. LODs off matches main (screenshots, FPS).
2026-10-19 14:38:10 +02:00
Ludovic MarieandClaude Opus 5.5 182a8e1a15 horizon-release-sign keygen: PKCS#8 v1 keys, which OpenSSL 3.0 reads
ring writes Ed25519 keys as PKCS#8 v2 (with the public key), which Ubuntu's OpenSSL 3.0 refuses
("unsupported"); verify caught it on ubuntu-latest. keygen now writes v1, the 32-byte seed alone,
checked to give the same public key as ring's document.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 14:37:57 +02:00
Ludovic Marie 1c5e5b96aa Merge pull request #993 from ludovic111/perf/lod-buried-gaps
🤖 Agent: merged after verify passed and CI parity passed on the stack head (#995; local 98/98). perf-frame-9's terrain LOD follow-ups: band and black cliffs fixed, far quads −36 %, per-frame budgets, LOD cap on non-Apple integrated GPUs. LODs off matches main (screenshots, FPS).
2026-10-19 14:37:56 +02:00
Ludovic Marie fa0b364f9d Merge pull request #992 from ludovic111/fix/lod-visual-bugs
🤖 Agent: merged after verify passed and CI parity passed on the stack head (#995; local 98/98). perf-frame-9's terrain LOD follow-ups: band and black cliffs fixed, far quads −36 %, per-frame budgets, LOD cap on non-Apple integrated GPUs. LODs off matches main (screenshots, FPS).
2026-10-19 14:37:42 +02:00
Ludovic MarieandClaude Opus 5.5 43287c18c2 horizon-server whitelist add: the offline UUID on a running offline server
The game resolves names as vanilla does, Mojang first, so on an offline server a name someone owns got that
account's UUID and the player, who joins under the offline UUID, was refused. horizon-server now writes the
offline entry and has the game reload the whitelist.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 14:24:15 +02:00
Ludovic MarieandClaude Opus 5.5 d38c07c2fa Releases: sign in a job of its own that runs only sha256sum and OpenSSL
Any repository code running earlier in the job that holds the key could reach it (a build patched
through Cargo.toml, or a step writing $GITHUB_ENV or $GITHUB_PATH for the next one). The `sign`
job checks out only tools/ci/sign_release.sh, which signs with the runner's OpenSSL; the publish
job, without the key, checks the result with horizon-release-sign as launchers will.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 14:18:54 +02:00
Ludovic MarieandClaude Opus 5.5 d2898fcba1 launcher README: the Servers page
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 14:18:33 +02:00
Ludovic MarieandClaude Opus 5.5 a8a00aac94 launcher: Friends over the internet: Tailscale status, invitations and a safe access rule
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 14:16:53 +02:00
Ludovic MarieandClaude Opus 5.5 10d76ad89d Launcher: stable builds by default, a "Beta builds" switch, and each version's channel
Game releases now go out as betas and become stable 6 hours later (promote.yml). The launcher
installs new versions from the stable channel unless "Beta builds" is on in Settings; turning it
on checks again at once. The Versions page, `versions` and the MCP server show each build's
channel. The website's patch notes list stable releases only.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 14:15:20 +02:00
Ludovic MarieandClaude Opus 5.5 4768d3f65b launcher: a Servers page that creates, runs and manages horizon-server servers (WIP)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 14:09:09 +02:00
Ludovic MarieandClaude Opus 5.5 f32a91d793 Releases: signed checksums (SHA256SUMS, SHA256SUMS.sig) for game and launcher releases
crates/horizon-release-sign signs a release's checksums with an Ed25519 key that exists only as a
secret of the `release` environment, and checks them as launchers will, against the public keys
built in from release-keys.txt. Dry runs sign with a throwaway key. Until release-keys.txt lists a
key, releases keep going out with SHA256SUMS alone and a warning; once it does, a missing or
unknown key fails the release before anything is published. tools/ci/new_release_key.sh pipes a
new key straight into the secret and prints only the public key. ring and base64 are already in
the lockfile.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 14:03:03 +02:00
Ludovic MarieandClaude Opus 5.5 cef652d026 checks: verify always runs, so a skipped job can never satisfy main's required check
Labels started `checks` runs whose verify job was skipped, and GitHub counts a skipped required
check as passed: auto-merge merged #823 and #837 before the real verify finished. Labels no longer
start `checks` (the fork-only run-parity label has its own workflow), and verify has no condition.
The parity job moves into a reusable workflow both call.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 13:58:27 +02:00
Ludovic Marie 2bfe3391a8 Merge branch 'perf/collision-scan' of https://github.com/ludovic111/horizon into integ-659 2026-10-19 13:50:47 +02:00
Ludovic Marie 266aff190e Merge pull request #1005 from ludovic111/perf/spawn-structure-overrides
🤖 Agent: merged after verify passed and local parity (perf-server-12: `tools/parity.py --changed origin/main` 98/98 with the three branches together, sculk / natural_spawn / spawn_rules harnesses pass; before/after numbers in the PR). CI parity was still queued on a busy runner.
2026-10-19 13:50:35 +02:00
Ludovic Marie cde54369a0 Merge pull request #1004 from ludovic111/perf/sculk-chunk-ticks
🤖 Agent: merged after verify passed and local parity (perf-server-12: `tools/parity.py --changed origin/main` 98/98 with the three branches together, sculk / natural_spawn / spawn_rules harnesses pass; before/after numbers in the PR). CI parity was still queued on a busy runner.
2026-10-19 13:50:30 +02:00
Ludovic MarieandClaude Opus 5.5 627837e8c3 Launcher: servers hosted through horizon-server (backend and CLI)
horizon_launcher::servers keeps the list of servers and drives horizon-server
for them: create (init, then config set), start, stop, backups, settings, the
service, and the running supervisor's control API for the live status, the log
and console commands. The programs come from an installed, prepared version,
laid out as install.sh does (server-runtime/releases, current, game-files), so
horizon-server update and its rollback work on them unchanged; the game files
are a copy of the player's own prepared files.

Problems come back in plain words with the fix to offer (a busy port with a
free one, missing game files with Prepare, the EULA).

horizon-launcher servers list|create|set-up|run|forget does the same from a
terminal.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 13:48:12 +02:00
Ludovic MarieandClaude Opus 5.5 42bb310df9 horizon-server config: read and change server.toml's settings, live where vanilla can
`config [--json]` prints the settings; `config set KEY=VALUE...` writes them through
toml_edit, checked as server.toml is checked (nothing is written if one is wrong). A
running server takes the difficulty, the default game mode and the whitelist at once
through its console, and the backup settings because the supervisor reads them each
time; the rest waits for the next start, and the command says which.

Also `backups --json` and `update --check --json`, for the launcher's Servers page.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 13:29:51 +02:00
Ludovic Marie ddc5aaed0e Merge pull request #1008 from ludovic111/fix/java-pack-fullscreen-inputs
🤖 Agent: merged after verify passed; cloud_render, environment_render, weather_render and 923 client tests pass, with before/after screenshots of Complementary Unbound. Java shader packs get real eye brightness, custom uniforms, Iris's sun path and tilt, and clouds=off/sun/moon flags. Part of #944 (reported by brabus_); shadows and gbuffers still to come.
2026-10-19 13:24:02 +02:00
Ludovic Marie 79f0ddee2c Merge pull request #990 from ludovic111/feat/server-prune-releases
🤖 Agent: merged after the verify rerun passed (the first run's failure was a flake; horizon-server tests pass 39/39 locally) and CI parity passed. Old releases are pruned automatically after an update.
2026-10-19 13:20:54 +02:00
Ludovic Marie adb1476582 Merge pull request #989 from ludovic111/feat/carry-formations
🤖 Agent: merged after verify and CI parity passed (modpacks-10, reviewed with local harness proof in the agent's report). Carries Formations' structures.
2026-10-19 13:20:30 +02:00
Ludovic Marie f910be28c8 Merge pull request #982 from ludovic111/feat/carry-pillar-processors
🤖 Agent: merged after verify and CI parity passed (modpacks-10, reviewed with local harness proof in the agent's report). Carries the mods' pillar and fluid-fix processors in ported packs.
2026-10-19 13:20:14 +02:00
Ludovic Marie 36c4ceb723 Merge pull request #981 from ludovic111/feat/pillar-processors
🤖 Agent: merged after verify and CI parity passed (modpacks-10, reviewed with local harness proof in the agent's report). Structure pillars and fluid fixes placed outside the template, like the mods do.
2026-10-19 13:19:58 +02:00
Ludovic Marie 0231d2890c Merge pull request #1003 from ludovic111/fix/sidebar-legacy-codes
🤖 Agent: merged after verify passed and the HUD harness with new §-coded cases (34/220 on main → all pass). The sidebar applies § formatting codes like vanilla. Fixes #937.
2026-10-19 13:17:46 +02:00
Ludovic Marie 494afc8dc3 Merge pull request #1002 from ludovic111/fix/player-equip-sound
🤖 Agent: merged after verify passed and local proof (menu_click fails on main 7978/8000, passes with the fix; place_recipe passes). The player's armour equip sound and event follow every vanilla path. Fixes #986 (reported by famescare).
2026-10-19 13:17:32 +02:00
Ludovic MarieandClaude Opus 5.5 0431f12a6f Give Java packs' full-screen passes Iris's sky, eye light and custom uniforms
Complementary Unbound drew clouds and light shafts inside terrain and a
second sun because the full-screen passes got wrong inputs:

- eyeBrightness was a constant (0, 240), so packs saw open sky
  underground. It is now the block and sky light of the camera entity's
  eye block (CommonUniforms.getEyeBrightness), and eyeBrightnessSmooth
  follows it with the pack's eyeBrightnessHalflife (SmoothedFloat).
- Custom uniforms (uniform.* / variable.* in shaders.properties) were all
  0: Complementary's eyeBrightnessM, isEyeInCave, starter, framemod8 and
  the rest. horizon-shaderpack's uniforms.rs parses and evaluates them
  each frame like Iris (stareval priorities, IrisFunctions, SmoothFloat).
- sunPosition, moonPosition, shadowLightPosition, sunAngle and
  shadowAngle now follow CelestialUniforms: the sky's sun and moon angles
  turned by Y -90, the pack's sunPathRotation about Z, then the angle. The
  old sun vector pointed north-south instead of east-west.
- While a Java pack runs, sunPathRotation tilts Horizon's sun, moon and
  stars as Iris tilts vanilla's, and clouds = off | fast | fancy, sun and
  moon = false apply. Complementary and Sildur's set clouds = off; the
  vanilla clouds Horizon drew anyway wrote depth the packs' light shafts
  and fog took for terrain.

Fixes #944

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 13:14:48 +02:00
Ludovic Marie 572addb2ff Merge pull request #578 from Online5277/fix/classic-menu-default
🤖 Agent: merged, thanks Online5277! Ludovic decided: Classic menus by default for fresh profiles (saved choices and HORIZON_MENU_STYLE still win). We merged current main in; fmt, clippy -D warnings and 1,022 client tests pass, and verify passes. Fixes #548.
2026-10-19 13:09:39 +02:00
Ludovic MarieandClaude Opus 5.5 7c06feec74 Keep the equip list out of the place_recipe oracle's state line
VanillaPlaceRecipeOracle prints VanillaMenuOracle.state, so the equips
field the menu_click harness compares broke its lines. Only the
menu_click oracle's own lines carry it now.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 12:48:49 +02:00
Ludovic Marie 451318860e Merge remote-tracking branch 'origin/main' into pr578 2026-10-19 12:48:48 +02:00
Ludovic MarieandClaude Opus 5.5 7426aed33c Play the equip sound when the player puts on armour
LivingEntity.onEquipItem now runs for the player wherever vanilla calls
it: the inventory menu's ArmorSlot and off-hand slot (setByPlayer, in the
menu engine, so clicks, shift-clicks, number keys, drags and LAN guests'
clicks), right-click equipping (Equippable.swapWithEquipmentSlot), the
creative screen (every changed slot, as handleSetCreativeModeSlot sets it),
the swap-hands key, dispensers (now also a shield into the off hand) and
/item and /loot on armour slots. It plays Equippable.equipSound (the
generic armour sound by default) on PLAYERS when the item lands in its own
slot, and emits equip/unequip game events for armour; nothing for
spectators, on a client, or when the item is unchanged.

The menu click harness's oracle now records vanilla's onEquipItem calls.

Fixes #986

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 12:43:01 +02:00
Ludovic MarieandClaude Opus 5.5 24abdb664d Apply legacy formatting codes in the scoreboard sidebar
Vanilla's Font draws every literal through StringDecomposer.iterateFormatted:
a § code styles what follows and is not drawn, a lone trailing § is
dropped. The sidebar drew owner names, team affixes, scores and the title
raw, so servers' sidebars showed § and the code letters. They now go
through Text::resolve_legacy, as the tab list already does.

The HUD harness's sidebar cases gain legacy-coded owners, affixes, titles
and fixed score formats.

Fixes #937

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 12:24:00 +02:00
Ludovic MarieandClaude Opus 5.5 c7f9fdd706 Dedicated server on Linux: world generation and packet writers behind the server thread
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 12:10:33 +02:00
Ludovic MarieandClaude Opus 5.5 85e5ba9d85 Natural spawning: look up only the structures that override spawns
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 12:10:33 +02:00
Ludovic MarieandClaude Opus 5.5 24428c4f83 Sculk: ask each chunk once a tick whether its sensors tick
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 12:10:32 +02:00
Ludovic Marie 5c88c46c51 Merge pull request #1001 from ludovic111/fix/guest-projectile-owner
🤖 Agent: merged after verify passed. Remote players' projectiles record their owner: ender pearls teleport and hurt the thrower, mobs target the shooter; docs/server.md row updated; part of #484.
2026-10-19 11:52:38 +02:00
Ludovic Marie f7d2a0b311 Merge pull request #1000 from ludovic111/fix/pvp-friendly-fire
🤖 Agent: merged after verify passed. Teammates with friendly fire off no longer hurt each other on a server (lan_e2e pvp); part of #484.
2026-10-19 11:52:26 +02:00
Ludovic Marie 97993634c0 Merge pull request #999 from ludovic111/fix/guest-use-item-on
🤖 Agent: merged after verify passed. A remote player's item used on a block is used once (a snowball thrown at the ground flew 2× main hand / 3× off hand); part of #484.
2026-10-19 11:50:11 +02:00
Ludovic Marie 75573c292a Merge pull request #998 from ludovic111/fix/dead-guest-commands
🤖 Agent: merged after verify passed. Chat commands from a dead player run, as in vanilla's handleChatCommand (fixes #973); dedicated_server transcript 604/604 lines identical.
2026-10-19 11:49:58 +02:00
Ludovic MarieandClaude Opus 5.5 a480d10157 docs/server.md: what remote players get right on a dedicated server now (#484)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 11:12:42 +02:00
Ludovic MarieandClaude Opus 5.5 092bf87012 Server: a guest's projectiles are theirs (ender pearls take them)
A projectile a guest shot was recorded as the local player's (Who::Player), which on a
server means the host, or nobody on a dedicated server. So a guest's ender pearl
teleported the host on a LAN world and did nothing on a dedicated server, what it hit
answered to the host, and clients were told the projectile had no owner.

Projectiles now remember the guest who shot them (Projectile.guest, set when the
guest's turn ends). Clients get the guest as the owner (Projectile.getAddEntityPacket),
and the hit runs as that guest: their pearl teleports and hurts them, and the mob hit
answers to them. ThrownEnderpearl's endermite also needs monsters to spawn, and comes
where the player was.

The dedicated-server harness checks that the snowballs' owner is the player and throws
an ender pearl at the player's feet: vanilla teleports it and takes 5 health, main did
neither.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 11:11:44 +02:00
Ludovic MarieandClaude Opus 5.5 ca1e3a17c8 Server: a right click with an item on a block uses it once
A vanilla client sends a right click as one packet per hand and part
(Minecraft.startUseItem): UseItemOn for the block's part and ItemStack.useOn, then
UseItem for Item.use when that passed, the main hand first. The server ran the whole
click, both hands, for UseItemOn, then the item again for UseItem: a snowball thrown
at the ground flew twice, three times from the off hand.

Each packet now does its own part with its own hand
(ServerGamePacketListenerImpl.handleUseItemOn / handleUseItem): UseItemOn stops at
useOn (no throwing, drinking, eating, raising, equipping, buckets or an ender eye off a
frame), and neither touches entities (Interact does). Horizon's own client, which
counted on the server trying the off hand, now sends the off hand's packets when the
main hand's use passes, as vanilla's does. UseItem's rotation is the player's now
(absSnapRotationTo) without sending the client a teleport back.

The dedicated-server harness right-clicks the ground with a snowball in the main hand,
then in the off hand, and counts the snowballs against vanilla's (2 and 3 on main,
1 and 1 now).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 11:11:19 +02:00
Ludovic MarieandClaude Opus 5.5 b0c8f964b2 Server: players on one team without friendly fire don't hurt each other
Player attacks between the host and guests, or two guests, only checked the pvp game
rule. ServerPlayer.canHarmPlayer also asks the victim's team (Player.canHarmPlayer):
an ally on a team with friendlyFire false takes no damage, and the attacker hears the
no-damage sound.

lan_e2e's pvp check puts both players on such a team and checks that neither hurts
the other.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 10:34:03 +02:00
Ludovic MarieandClaude Opus 5.5 4953305c7b Server: run a dead player's chat commands, as vanilla does (#973)
A dead guest's packets were all dropped but the respawn request, so a command typed on
the death screen never ran. ServerGamePacketListenerImpl.handleChatCommand and
handleSignedChatCommand don't look at the player's health: the commands now run.

The dedicated-server harness sends `/trigger nope` from the death screen and compares
the reply with vanilla's before respawning.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 10:34:02 +02:00
Ludovic MarieandClaude Opus 5.5 ee3e874eb8 horizon-server: delete old releases after an update
An install kept every release it ever unpacked in releases/. Once the game is up on the
new release (or a stopped server is switched), the update now deletes the old ones,
keeping the release in use, the one `previous` names (rollback's target), the one the
running horizon-server came from, and the `updates.keep` newest of the others (1 by
default, a new server.toml setting). A release that can't be deleted is reported and
left for the next update.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 09:50:27 +02:00
Ludovic Marie ee59df13a9 Merge pull request #980 from ludovic111/feat/launcher-dx12-after-renderer-crash
🤖 Agent: merged after verify passed. If the game crashes natively after its Vulkan adapter line and before its first frame, the launcher switches the Graphics API setting from Auto to DX12 and tells the player; it doesn't relaunch by itself.
2026-10-19 09:46:01 +02:00
Ludovic Marie 5229857dab Merge pull request #979 from ludovic111/feat/dx12-fallback
🤖 Agent: merged after verify passed. DirectX 12 for Windows: Auto tries Vulkan first and falls back to DX12 when Vulkan has no adapter or its device request fails; Video Settings > Graphics API (Windows only) and HORIZON_GRAPHICS_API override it. We merged main in (kept #771's LOD row and lodDistance next to graphicsApi); clippy -D warnings and 1,021 client tests pass, including the WGSL→HLSL translation of every shader. Not yet run on real Windows hardware.
2026-10-19 09:45:49 +02:00
Ludovic MarieandClaude Opus 5.5 5148b2ee2c Far terrain: capped on integrated GPUs, held back on 4-thread machines
- Integrated GPUs other than Apple's draw far terrain to 128 chunks at
  most, whatever LOD Distance says; its description says so, and the
  status reports the distance in use (`lod.effective_distance`).
- On machines with four threads or fewer, far terrain starts no detail and
  one sample job per 500 ms while the near terrain loads, so the near
  chunks and the frame get the cores first.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 09:34:32 +02:00
Ludovic MarieandClaude Opus 5.5 c468938a66 Far terrain: bounded render-thread work while tiles generate
- Upload far meshes within about 2 ms a frame (at least one); the rest
  wait, with their removals and the cutout, and apply in order.
- Gather mesh inputs across frames within about 2 ms, from a snapshot of
  the active tiles' samples, instead of hundreds at once.
- Select tiles only when the plan, samples, covered columns or dirty tiles
  changed; add the covered stand-ins only after a replan.
- Find the tiles touching an invalidated chunk per level, not by scanning
  every active tile; share tile samples with mesh jobs (Arc) instead of
  cloning them.
- Recompute the full-detail columns only when their completeness, the
  center or the radius changed.
- Observe chunks for the far summaries within about 1 ms a frame (at least
  one), unloaded ones first, with each block state's kind read once instead
  of matching block names per block.

LODs off is unchanged: none of this runs without far terrain.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 09:34:32 +02:00
Ludovic MarieandClaude Opus 5.5 895dc26848 Far terrain: no faces for caves nothing can see into
Far tiles mesh every gap in their columns: caves and aquifers in visited and
refined chunks, and the gaps kelp and seagrass leave in water. Most are closed
on every side, so their floors, ceilings and walls cost vertices and never
show.

Before meshing, a tile fills each gap below a column's top that no
neighbouring column's open air looks into (every neighbour's top is at or
above the gap's top) with the material below it, or with the fluid above it
(kelp in water). Cave mouths stay open; unknown neighbours and cells meeting
full-detail or finer terrain count as open.

Seed 1234, RD12, LOD 64, overview: 4.93 M to 3.17 M far-terrain quads.

Follow-up to Online5277's terrain LODs (#771).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 09:34:32 +02:00
Ludovic MarieandClaude Opus 5.5 bf8d84930f Far terrain: no dark band at the full-detail edge, no black cliffs
Full-detail sections leaving the render distance stopped being drawn at once,
while the distant tiles still left their columns out until the next mesh job
finished: for a few seconds after moving (longer on slow machines) a hole
opened there, lined with the tiles' seal walls. Those took the light recorded
for buried blocks, none, so the hole showed as a dark band.

Lod::take_cutout hands the renderer the columns the finished meshes leave
out. Their resident sections stay drawn, even past the render distance, and
sections unloaded while still left out are released only once the tiles
cover them. With LODs off the cutout is empty and nothing changes.

Side faces above the top of the column they face open onto air. Reduced
columns and the seals expose sides that were buried, so those parts take at
least the sky light over that top (black cliffs in far tiles). Faces into a
cave keep their recorded light.

Follow-up to Online5277's terrain LODs (#771).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 09:34:32 +02:00
Ludovic MarieandClaude Opus 5.5 af9d888410 Far terrain: time each step of Lod::update
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 09:34:32 +02:00
Ludovic Marie 272414cc05 Merge remote-tracking branch 'origin/main' into feat/dx12-fallback
# Conflicts:
#	crates/horizon-client/src/graphics.rs
#	crates/horizon-client/src/graphics_ui.rs
2026-10-19 09:31:58 +02:00
Ludovic Marie dbfaf2e003 Merge pull request #976 from ludovic111/fix/wgpu-hal-empty-descriptor-pool
🤖 Agent: merged after verify passed and CI parity passed 322/323; the one failure, dedicated_server (two extra block updates at respawn, a server-only path this GPU change can't reach), passed 2/2 locally on main + this PR and 2/2 on main. Vendors wgpu-hal 30.0.1 with the upstream fix for gfx-rs/wgpu#9845; should fix #746 on HD 520/530, to be confirmed by the reporters on the release.
2026-10-19 09:29:37 +02:00
Ludovic Marie 1612c86c15 Merge pull request #985 from ludovic111/feat/server-bench-paper-fabric
🤖 Agent: merged after verify passed (benchmark data and docs only). Four-way equal-work benchmark: Horizon uses 3.8–4.8× less RAM and starts in ~8 s; Paper's average tick is shorter at every count, which is now perf work.
2026-10-19 08:41:06 +02:00
Ludovic MarieandClaude Opus 5.5 8beeea55ff Carry Formations' structures
Its formations:simple becomes the game's horizon:formations (its pool and
placement), and its single pool element vanilla's with its ground_level
under horizon:ground_level. Its pieced structure (one) stays out; its
processors are still dropped.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 08:35:08 +02:00
Ludovic MarieandClaude Opus 5.5 a6b183f7f5 Generate Formations' structures from the noise column
The structure context gets ChunkGenerator.getBaseColumn's blocks
(StartContext::noise_column: air, water, lava, solid or other, from the
noise's minimum Y, air past its ends), which Formations' placements test.

horizon:formations is Formations' SimpleStructure: one element of its pool
at a random spot of the chunk and rotation, at the height its placement
finds under the element's corners and centre (surface, ceiling, on water
or lava, in caves on the floor, under the ceiling, buried, on lava), as
the mod draws and checks them. Single pool elements take a ground level
(`horizon:ground_level`, 1 by default as vanilla's), which moves the piece
and its terrain adaptation as Formations' does.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 08:34:23 +02:00
Ludovic Marie 6af77dfbe4 Merge remote-tracking branch 'origin/main' into feat/server-bench-paper-fabric
# Conflicts:
#	docs/server.md
2026-10-19 08:33:52 +02:00
Ludovic Marie 56fcc91e4b Merge pull request #967 from ludovic111/feat/site-host-a-server
🤖 Agent: merged after verify passed. The site's nav links Host a server; README gains a hosting paragraph; tools/server.py and docs/server-quickstart.txt retired.
2026-10-19 08:33:15 +02:00
Ludovic Marie aac0dc7f60 Merge pull request #966 from ludovic111/feat/server-update
🤖 Agent: merged after verify and CI parity passed. horizon-server update (SHA-256 check, world backup, switch, health wait, automatic rollback) and rollback; 6-hourly update notice in log, status and panel. Proven end to end on the Mac mini and the ThinkPad (healthy update, crash rollback, timeout rollback, manual rollback, real GitHub download); 37 unit tests.
2026-10-19 08:33:03 +02:00
Ludovic Marie fcb37f05e9 Merge pull request #965 from ludovic111/fix/server-relative-dir
🤖 Agent: merged after verify and CI parity passed. horizon-server run --dir <relative> no longer crash-loops the game; new test fails without the fix.
2026-10-19 08:32:51 +02:00
Ludovic MarieandClaude Opus 5.5 adb07c96ff Carry Better Ocean Monuments' processors and YUNG's legs
Better Ocean Monuments' ten processors (no fields; their behaviour is in
its Java) become vanilla's rule, the game's horizon:pick (one new
nextFloat per chance), horizon:flood_air, horizon:waterlog,
horizon:keep_world_block and horizon:yung_pillar; Better Witch Huts' log
and fence legs become horizon:yung_pillar. Its monuments are no longer
dry.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 08:29:23 +02:00
Ludovic MarieandClaude Opus 5.5 d983b47db6 Carry the mods' pillar and fluid fix processors
Repurposed Structures' and Moog's pillar_processor, Repurposed's
bottom_pillar_processor and close_off_fluid_sources_processor, YUNG's
pillar_processor (Better Fortresses, Better Jungle Temples) and Better
Fortresses' stair_pillar_processor become the game's horizon:pillar,
horizon:bottom_pillar, horizon:close_off_fluid_sources,
horizon:yung_pillar and horizon:stair_pillar. A pillar's own processor
list is written in, with the Java processors in it dropped.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 08:29:09 +02:00
Ludovic MarieandClaude Opus 5.5 0a733cec55 Carry: the namespaces a mod's own files name, as its manifest's namespaces
Towns and Towers ships its 60 structures under kaisyn and towns_and_towers,
not its mod id t_and_t, so carry left them outside. A namespace is now the
mod's when its own files (its id's, its tag additions, the namespaces
already claimed) name ids in it and no mod of the pack has it as its id.
Compat files for other mods and pack mods' configs stay outside; of ATM10's
42 jars only T&T claims any. A replacing tag of its own is an addition.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 08:29:00 +02:00
Ludovic MarieandClaude Opus 5.5 303aa35a6f Flood and waterlog the structures built under the sea
Adds the game's side of Better Ocean Monuments' Java processors:

- horizon:flood_air: the template's air below sea level becomes water
  (its AirProcessor; the monuments' templates are mostly air, so they
  stood dry);
- horizon:waterlog: below sea level, blocks that can be waterlogged are,
  and water and lava on the edge of the chunk being generated are marked
  for postprocessing (its WaterlogProcessor);
- horizon:keep_world_block: a placeholder takes the world's block (its
  StructureVoidProcessor);
- horizon:pick gains `redraw` (a new nextFloat for each chance, as its
  random processors draw) and `keep_properties` (Block.withPropertiesOf,
  for its copper stairs and slabs);
- horizon:yung_pillar gains `keep_properties`, for Better Witch Huts'
  fence legs.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 08:28:42 +02:00
Ludovic MarieandClaude Opus 5.5 df8bc603ff Content mods: namespaces besides the mod id, with no namespace shared by two mods
A content mod's manifest may list namespaces its files use besides its id
(Towns and Towers ships kaisyn and towns_and_towers under mod id t_and_t).
They are the mod's as its id is: files load, loaders read them. minecraft,
c and horizon cannot be listed. Two mods that are on owning one namespace
is an error naming both; the game keeps the first and leaves the other's
content out.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 08:28:42 +02:00
Ludovic MarieandClaude Opus 5.5 6008be0a68 Loot: tables, modifiers, predicates, tags and recipes of every namespace
The loot loader read data/minecraft/ only, so every chest in a carried
mod's structure that names its mod's loot table opened empty. It now reads
every namespace (a vanilla root has only minecraft). Integers written as
floats (rolls 3.0, as Towns and Towers writes them) read as vanilla's int
codecs read them. 534 of the 675 non-block tables in ATM10's carried mods
now fill a chest; vanilla's chest fills are byte-identical.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 08:28:42 +02:00
Ludovic MarieandClaude Opus 5.5 ffa46a50af Place pillars and fluid fixes that reach outside the template
Adds the game's side of the mods' Java processors that write blocks around
a structure while its template is processed:

- horizon:pillar and horizon:bottom_pillar: Repurposed Structures' and
  Moog's PillarProcessor and BottomPillarProcessor, a column from a trigger
  block (or the bottom layer) down to the ground, with their nested lists
  and Moog's height randomizer;
- horizon:close_off_fluid_sources: their fluid fix, which walls off source
  blocks next to the structure's open blocks;
- horizon:yung_pillar and horizon:stair_pillar: YUNG's PillarProcessor
  (Better Fortresses, Better Jungle Temples) and Better Fortresses' stair
  supports.

They write through PlacementContext::set_chunk_block, the
ProtoChunk.setBlockState the mods call (heightmaps kept), and only inside
the chunk being generated, as the mods check WorldGenRegion's centre.
Processor lists nested in pillars are capped at 16 levels.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 08:28:42 +02:00
Ludovic MarieandClaude Opus 5.5 aec5c20698 hosting.md: rewrap the tick bullet
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 08:25:15 +02:00
Ludovic MarieandClaude Opus 5.5 a64877f1a1 Server benchmark: Horizon against vanilla, Paper and Fabric + Lithium, equal work (2026-10-19)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 08:25:07 +02:00
Ludovic MarieandClaude Opus 5.5 731b4256db server_bench: turn Paper's connection throttle off (the bots share one address)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 08:23:27 +02:00
Ludovic MarieandClaude Opus 5.5 0001363aa0 server_bench: one tested pattern for every server's Done line
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 08:23:27 +02:00
Ludovic MarieandClaude Opus 5.5 86cf1542d1 server_bench: Paper and Fabric + Lithium, dev-only downloads checked against their hashes
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 08:23:27 +02:00
Ludovic Marie 0af3dd3900 Merge pull request #959 from ludovic111/feat/carry-yung-modifiers
🤖 Agent: merged after verify and CI parity passed, and a combined check with rdbt's #796 on current main (fmt, clippy, 359 tests, 14 world-gen harnesses). YUNG/Moog terrain adaptation around structures (Better Fortresses netherrack inside bridges 39–41 % → 2.5–7 %), its carry, and YUNG modifiers.
2026-10-19 08:10:05 +02:00
Ludovic Marie 0def8931cb Merge pull request #956 from ludovic111/feat/carry-terrain-adaptation
🤖 Agent: merged after verify and CI parity passed, and a combined check with rdbt's #796 on current main (fmt, clippy, 359 tests, 14 world-gen harnesses). YUNG/Moog terrain adaptation around structures (Better Fortresses netherrack inside bridges 39–41 % → 2.5–7 %), its carry, and YUNG modifiers.
2026-10-19 08:09:53 +02:00
Ludovic Marie 39b74fb468 Merge pull request #955 from ludovic111/feat/enhanced-terrain-adaptation
🤖 Agent: merged after verify and CI parity passed, and a combined check with rdbt's #796 on current main (fmt, clippy, 359 tests, 14 world-gen harnesses). YUNG/Moog terrain adaptation around structures (Better Fortresses netherrack inside bridges 39–41 % → 2.5–7 %), its carry, and YUNG modifiers.
2026-10-19 08:09:41 +02:00
Ludovic Marie b464f977f1 Merge pull request #796 from rdbtCVS/t3/optimize-noise-aquifers
🤖 Agent: merged, thanks rdbt! Bulk density arithmetic, the shared Beardifier kernel and Perlin column reuse: 12.685 → 10.069 ms/chunk Overworld terrain (−20.6 %, one worker). We merged current main in (density/tests.rs: kept both test sets) and checked it together with #955/#956/#959: fmt, clippy -D warnings, 359 core/world/port-pack tests, and 14 world-gen harnesses (density, noise, structure_density, server_world_gen, world_spawn, decorated_region, …) all pass.
2026-10-19 08:09:26 +02:00
Ludovic Marie c5656493cd Merge pull request #977 from ludovic111/feat/renderer-startup-steps
🤖 Agent: merged after verify passed. The renderer logs each start-up step once per device (driver, requested features and limits, surface, pipelines, first acquire/submit/present), so a native crash's last log line names the step.
2026-10-19 08:02:21 +02:00
Ludovic Marie 99b07dc810 Merge remote-tracking branch 'origin/main' into pr/796
# Conflicts:
#	crates/horizon-core/src/density/tests.rs
2026-10-19 07:58:32 +02:00
Ludovic Marie d2e8e5fe28 Merge pull request #771 from Online5277/feat/terrain-lod
🤖 Agent: merged, thanks Online5277! Terrain LOD lands off by default. With LODs off it matches main (0 differing pixels in 6 RD16 views, same FPS/GPU ms on the HD 620, same time to full render distance); CI verify and parity pass. We merged main in three times and adapted it to #905/#939 and the packed texture coordinates. The follow-ups from the review (edge band, integrated-GPU cost, lighter far meshes) we're taking on ourselves.
2026-10-19 07:49:12 +02:00
Ludovic MarieandClaude Opus 5.5 c2665ee7bd Launcher: switch the game to DirectX 12 after a Vulkan crash at its start (#746)
A native crash in a graphics driver leaves no crash report, only the game's
last lines. When the game ends with no report after its Vulkan adapter line and
before its first frame (Windows only), set graphicsApi:dx12 in its graphics
settings (the game home's and the run's own, only where it was auto) and tell
the player to press Play again, instead of the crash notice.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 07:43:12 +02:00
Ludovic MarieandClaude Opus 5.5 8aacf93d8f DirectX 12 on Windows: the fallback when Vulkan fails, and a setting (#746)
The Windows client builds wgpu's dx12 backend. With graphicsApi:auto (the
default), the renderer opens Vulkan and falls back to DirectX 12 when Vulkan
has no adapter (no Vulkan driver) or its device request fails; vulkan and dx12
pick one (horizon-graphics.txt, Video Settings > Graphics API on Windows, or
HORIZON_GRAPHICS_API; WGPU_BACKEND still overrides). Other systems keep their
one backend.

DirectX 12 keeps wgpu's indirect-draw validation: it is what passes each
draw's first instance to shaders there. Occlusion culling stays Vulkan-only and
flat draws Metal-only, as before; multi-draw and the GPU timer use DirectX 12's
own. gpu-allocator now resolves to the windows crate wgpu-hal's dx12 code
uses (0.62): with cpal's 0.54 it did not compile. A test translates every WGSL
shader to HLSL as the dx12 backend does for FXC.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 07:34:16 +02:00
Ludovic MarieandClaude Opus 5.5 70de685d2b Log each step of the renderer's start (#746)
A native crash in the graphics driver leaves no panic or crash report: the
last log line is all a player can send. After the adapter line, log the
driver, the features and limits the device is requested with, the surface
configuration, the pipelines (with multi-draw, occlusion culling and the GPU
timer on or off) and the first frame's acquire, submit and present, once per
device.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 07:27:44 +02:00
Ludovic MarieandClaude Opus 5.5 4992d877c7 Fix the Intel HD 520/530 crash at device creation (#746)
wgpu 30 creates a Vulkan descriptor pool with no pool sizes while it creates
the device. Vulkan allows that only since 1.3.215; Intel's Windows driver for
6th-10th gen integrated GPUs (igvk64.dll, HD 520/530, UHD 620/630) reads
past the empty array and crashes with 0xc0000005 inside request_device, right
after the game logs its renderer (gfx-rs/wgpu#9845). Apply the upstream fix
(gfx-rs/wgpu#10124: one sampler entry when the pool would be empty), which no
wgpu release has yet, through [patch.crates-io].

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 07:22:12 +02:00
Ludovic MarieandClaude Opus 5.5 79f78f904b Vendor wgpu-hal 30.0.1 unchanged (examples left out)
The crates.io sources of wgpu-hal 30.0.1, minus its examples, lock file and
VCS metadata, so the next commit can apply one upstream fix that has no
release yet. Excluded from the workspace: it keeps its own lints.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 07:22:12 +02:00
Ludovic Marie 21ac62a085 Merge pull request #972 from ludovic111/perf/sculk-unload-once
🤖 Agent: merged after verify passed and local proof (sculk harness). Sculk drops every unloaded chunk's blocks in one pass: worst tick 583–609 → 38–44 ms with 40 bots.
2026-10-19 07:15:24 +02:00
Ludovic Marie cf5a979b35 Merge pull request #971 from ludovic111/perf/heightmaps-on-helpers
🤖 Agent: merged after verify passed and local proof (heightmap, chunk_stage, lan, …). Chunk-packet heightmaps are built on the warm-up threads; bytes identical; lan_chunks −11–13 %.
2026-10-19 07:15:15 +02:00
Ludovic Marie d157880712 Merge pull request #970 from ludovic111/perf/server-thread-first
🤖 Agent: merged after verify passed and local proof on the integration commit (8 harnesses, lan_e2e both modes). The server thread runs ahead of the workers on macOS and Windows, like vanilla's Thread.setPriority(8): 40 bots tick p99 101.5/77.7 → 32.8/32.7 ms, ticks over 50 ms 166/146 → 1/2.
2026-10-19 07:15:05 +02:00
Ludovic Marie f95a279a92 Merge pull request #968 from ludovic111/fix/lan-e2e-hazards-tidy
🤖 Agent: merged after verify passed. The host clears the hazards box, so a dead guest no longer leaves it behind for pvp/hurt (8/10 → 0/3 failures under --server-thread).
2026-10-19 07:10:53 +02:00
Ludovic Marie 14c6a197e4 Merge pull request #854 from Online5277/fix/multiplayer-chat-input
🤖 Agent: merged, thanks Online5277! Server chat now goes through the shared chat component, so the wheel and Page Up/Down scroll it. We merged main into the branch (main had moved the atlas to self.gui.atlas); clippy -D warnings, the 6 chat tests and the LAN chat check (both modes) pass, and verify passes. Fixes #810.
2026-10-19 07:06:30 +02:00
Ludovic Marie 9e98c1902b Merge remote-tracking branch 'origin/main' into pr854
# Conflicts:
#	crates/horizon-client/src/game/remote.rs
2026-10-19 06:40:04 +02:00
Ludovic Marie 78e8556e5a Merge pull request #800 from LookImEcho-Rick/codex/issue-592-nether-teleport
🤖 Agent: merged, thanks LookImEcho-Rick! It was still marked draft, but its description was complete and our checks were done, so we marked it ready. /execute in <dimension> now moves the player across dimensions like vanilla (fixes #592). With main merged in and the #947 fix (#962), command_exec, command_parse, server_dimension_players and dedicated_server pass locally, and verify passes. If you meant to add more, a follow-up PR is welcome.
2026-10-19 06:29:47 +02:00
Ludovic Marie 1f16677a44 Merge pull request #963 from ludovic111/feat/processor-random-and-condition
🤖 Agent: merged after verify passed and parity --changed 311/311 locally. Lithostitched apply_random, condition and set_block in the game: CTOV farms grow carrots, potatoes and beetroot, paths and mossy cobblestone appear.
2026-10-19 06:20:58 +02:00
Ludovic Marie 0ceede079e Merge pull request #964 from ludovic111/feat/carry-apply-random-condition
🤖 Agent: merged after verify passed and parity --changed 311/311 locally. Carry for Lithostitched apply_random, condition and set_block (130 dropped → 0).
2026-10-19 06:20:48 +02:00
Ludovic Marie 9ed2f482af Merge pull request #960 from ludovic111/feat/carry-processor-references
🤖 Agent: merged after verify passed and parity --changed 311/311 locally. Lithostitched reference processors carried: 83 dropped → 0.
2026-10-19 06:20:39 +02:00
Ludovic Marie 995187d82d Merge pull request #958 from ludovic111/feat/yung-modifiers
🤖 Agent: merged after verify passed and parity --changed 311/311 locally on the integration commit. YUNG modifiers, piece_in_range and dead-end pools (game side).
2026-10-19 06:20:28 +02:00
Ludovic Marie f3a3dd22bd Merge pull request #957 from ludovic111/fix/carry-json-comments
🤖 Agent: merged after verify and CI parity passed. The carry reads data files with comments as Minecraft does; six Better Fortresses pools come back (265 → 3,765 pieces over seed 1234's Nether centre).
2026-10-19 06:20:19 +02:00
Ludovic Marie cd4f8ba849 Merge pull request #954 from ludovic111/fix/stronghold-ring-biome-search
🤖 Agent: merged after verify passed and local proof on one integration commit of modpacks-9's PRs: parity --changed 311/311, structure-start oracle 3,153/3,153. CI parity's only failure was secure_chat, which this change doesn't touch. Stronghold rings 3/128 off → 0/128 on seeds 1234, 2, 3, 42, -7.
2026-10-19 06:20:09 +02:00
Ludovic Marie 874d62b49b Merge remote-tracking branch 'origin/main' into pr800 2026-10-19 05:55:51 +02:00
Ludovic Marie ee4b4c7834 Merge pull request #962 from ludovic111/fix/level-switch-entities
🤖 Agent: merged after verify and CI parity passed (command_exec 764/764 three times locally, 20 affected harnesses). /summon no longer spawns other pending entities and takes their id; the harness settle waits for entities like vanilla's. Fixes #947.
2026-10-19 05:55:41 +02:00
Ludovic MarieandClaude Opus 5.5 0bc6c689da Site: link the host-a-server page from the nav and the README; retire the Fabric server starter
tools/server.py and docs/server-quickstart.txt are gone (horizon-server replaces them);
build_release.py builds only the client starter, and the docs point to docs/hosting.md.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 05:34:34 +02:00
Ludovic MarieandClaude Opus 5.5 3ce89ebb8a horizon-server update: report a switch without an install instead of staying busy
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 05:31:08 +02:00
Ludovic MarieandClaude Opus 5.5 bf2c5e7946 horizon-server update: wait for the old game after a rollback; document updates
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 05:30:21 +02:00
Ludovic MarieandClaude Opus 5.5 2e730b9213 lan_e2e: tidy the hazards box from the host
The void can kill the guest before the hazards check tidies its stone box,
and the LAN server drops a dead guest's packets, so the box stayed: the
guest suffocated in it during pvp, and the hurt check's pig with it. The
host runs the tidying fills instead.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 05:28:27 +02:00
Ludovic MarieandClaude Opus 5.5 0e025df8e1 Server thread: ahead of the workers on macOS and Windows, as vanilla's spin
MinecraftServer.spin raises its server thread (Thread.setPriority(8)) when
there are more than four processors. The new horizon-threads crate makes the
same request for the dedicated server's thread and the integrated server's:
user-interactive QoS on macOS, above-normal priority on Windows, nothing on
Linux (an unprivileged thread can't raise itself).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 05:27:50 +02:00
Ludovic MarieandClaude Opus 5.5 857a88351f horizon-server update and rollback: switch releases, roll back by itself when the game doesn't come up
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 05:21:26 +02:00
Ludovic MarieandClaude Opus 5.5 a8682eb4c5 horizon-server: give the game its folders whole when --dir is relative
`horizon-server run --dir srv` (what init prints) started the game in srv/ with
--control-socket srv/.horizon/engine.json, which the game resolved inside srv/,
so it crashed at every start. spawn now passes absolute paths.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 05:20:36 +02:00
Ludovic MarieandClaude Opus 5.5 cd99c1c683 Sculk: drop the blocks of every unloaded chunk in one pass
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 05:01:18 +02:00
Ludovic MarieandClaude Opus 5.5 3667bb963c Chunk batches: make the columns' heightmaps on the warm-up threads too
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 05:01:18 +02:00
Ludovic Marie c569bbe291 Merge pull request #961 from ludovic111/fix/readback-alignment
🤖 Agent: merged after verify passed. GPU readbacks (occlusion results, GPU timer) no longer assume the mapping's alignment; fixes the Vega 11 launch crash in #953.
2026-10-19 04:47:48 +02:00
Ludovic MarieandClaude Opus 5.5 4b28390f72 docs/modpacks.md: Lithostitched's processors carried, and what they change
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 04:42:29 +02:00
Ludovic MarieandClaude Opus 5.5 f62e07c36a Read GPU readbacks without assuming the mapping's alignment
A Vega 11 on Windows (Vulkan) panicked at launch in bytemuck's cast_slice
with TargetAlignmentGreaterAndInputNotAligned, starting with the release
that shipped occlusion culling. The section-test results and the GPU
timer's timestamps were cast in place from the mapped range, which isn't
guaranteed to be 4- or 8-byte aligned. Copy the words out instead.

Fixes #953

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 04:40:00 +02:00
Ludovic MarieandClaude Opus 5.5 bb24420df2 Carry: Lithostitched's apply_random, condition and set_block as the game's
apply_random becomes horizon:apply_random: its weighted lists, or its set
of lists at weight 1, with the inline lists carried like any other (their
Java processors dropped and counted). condition becomes horizon:condition
when the game runs every condition in it (true, all_of, any_of, not,
random_chance, matching_blocks with plain property values), with its then
and else lists carried. set_block with a simple or weighted state provider
and no block entity change becomes horizon:set_block. Anything else stays
Java and is dropped as before.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 04:29:23 +02:00
Ludovic MarieandClaude Opus 5.5 2647ed7ed0 Structure processors: Lithostitched's apply_random, condition and set_block as data
horizon:apply_random runs one processor list drawn by weight
(WeightedList.getRandom); horizon:condition runs `then` or `else` by a
test of the block (true, all_of, any_of, not, random_chance and
matching_blocks on the placed or the existing block). Both take their
random from Lithostitched's RandomSettings: seeded with the world seed
plus the name's hash, forked positionally at the block, the piece or the
structure's pivot. process_block now gets the piece's position for it.

horizon:set_block is Lithostitched's set_block with a simple or weighted
state provider: the block becomes the state (drawn at the chosen position
from the unsalted seed), keeping the properties it shares with the old one
unless preserve_state is off.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 04:28:57 +02:00
Ludovic Marie 8993bd4594 Merge pull request #949 from rdbtCVS/t3/reuse-terrain-sampler-scratch
🤖 Agent: merged, thanks rdbt! Terrain sampler scratch buffers are reused between chunks; byte-identical output across the 22 world-gen parity harnesses on the combined commit.
2026-10-19 04:21:05 +02:00
Ludovic Marie 764b4e0803 Merge pull request #948 from rdbtCVS/t3/fix-issue-945
🤖 Agent: merged, thanks rdbt! Spectator scroll-wheel flight speed now follows vanilla (6,791 oracle cases); verified on the combined commit with local_input and the client tests. Fixes #945.
2026-10-19 04:20:53 +02:00
Ludovic Marie ea8964506c Merge pull request #946 from rdbtCVS/t3/dedup-neighbour-computation
🤖 Agent: merged, thanks rdbt! Checked on one commit with #948 and #949: fmt, workspace clippy -D warnings, 896 world/core/client tests, and 22 parity harnesses (biome, carving, decoration, density, structure_density, server_world_gen, world_spawn, …) all pass.
2026-10-19 04:20:41 +02:00
Ludovic MarieandClaude Opus 5.5 8e095b88fa Carry: write Lithostitched's processor references in
lithostitched:reference runs the processors of other lists in turn.
Carry now writes their processors in its place, in order and references
in them too, from the mod's own processor lists. A reference to a tag or
to a list the mod lacks stays, and is dropped and counted as before.
ChoiceTheorem's Overhauled Village builds most of its lists this way.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 04:17:42 +02:00
Ludovic MarieandClaude Opus 5.5 ca790e8dba Far terrain: a third of the render-thread work per frame
Lod::update sorted every unsampled plan node each frame to take at most
four (none while every sample slot is busy), and woke the workers every
frame. It now skips the selection while the slots are full, keeps only
the best 2 * (SAMPLE_JOBS + pending) entries before sorting (a key is
listed at most twice, so the same tiles are taken in the same order),
takes the best fallback with min(), and wakes the workers only when a job
was queued. A finished chunk's detail now goes straight to the tile at
each level 0-4 holding it instead of scanning every ready tile (the others
never overlapped it).

Mac mini, RD12, LOD 256 while far terrain generates, `sample` of the main
thread over 30 s, alternating: Lod::update 10.4 % and 12.3 % of the
samples before, 3.4 % and 3.5 % after; update_world 3497 and 3373
samples before, 668 and 864 after.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 04:08:00 +02:00
Ludovic MarieandClaude Opus 5.5 a19a6b5bcc docs/modpacks.md: YUNG's modifiers and dead-end pools, Towns and Towers' namespaces
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 04:06:59 +02:00
Ludovic MarieandClaude Opus 5.5 9a836c50a8 Docs: carried terrain kernels and what they change in seed 1234
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 04:06:59 +02:00
Ludovic MarieandClaude Opus 5.5 a906bb50ba Structures: YUNG's and Moog's enhanced terrain adaptation, as terrain kernels
YUNG's API and MoogsStructureLib add their own kernel to the Beardifier:
a size and falloff of its own, carving or burying above and below a piece's
floor, around rigid pieces and junctions. Better Strongholds buries its
stronghold, Better Fortresses carves the netherrack over its bridges and
Moog's mega fortress carves a band around its corridors. A structure or a
pool element holding `horizon:enhanced_terrain_adaptation` now gets that
kernel (horizon_core's TerrainKernel), summed after vanilla's density as
the libraries' mixins do; YUNG's also grows the start's box by its radius.
Vanilla data never has the key, so vanilla worlds are unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 04:06:59 +02:00
Ludovic MarieandClaude Opus 5.5 be85b81d13 YUNG's assembler: modifiers, piece_in_range and dead-end pools
Better Nether Fortresses relies on three parts of YUNG's API the game did
not have:

- piece_in_range: another piece (of some templates, or any) inside the
  piece's box grown sideways, below and above.
- Modifiers, applied once the layout is done, in piece order and with the
  assembler's random: a condition, then actions on the piece itself.
  transform swaps its template for one picked by setFeatureSeed(x, y, x)
  and moves it by an offset turned with the piece; delay_generation moves
  it to the end of the piece list.
- Dead-end pools: a piece that got no child though it has other jigsaws
  is taken out (its parent's junction, its room and its count given back)
  and a piece of its dead-end pool is chosen for the same parent jigsaw.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 04:06:59 +02:00
Ludovic MarieandClaude Opus 5.5 bb3ec12ed0 Carry: read data files with comments, as Minecraft does
Minecraft reads data JSON with Gson's lenient reader, which skips `//`,
`#` and `/* */` comments. Six of YUNG's template pools have them (Better
Fortresses' bridges, halls and hall ends; Better Jungle Temples' floors),
so carry refused them as unreadable and the structures lost those pieces:
seed 1234's seven Better Fortresses had 265 pieces in all, now 3,765, and
five Better Jungle Temples 170, now 196. A file that parses once its
comments are gone is carried as plain JSON.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 04:06:59 +02:00
Ludovic MarieandClaude Opus 5.5 cd30aae4a8 Strongholds: sample each ring search through one biome resolver
ChunkGeneratorStructureState's ring searches call findBiomeHorizontal,
which samples its 57x57 quarts in order through one resolver; vanilla's
climate index starts each lookup from the previous result and keeps it on
a tie. Horizon built a fresh resolver for every quart, so ties went the
other way: three of seed 1234's 128 strongholds were up to 6 chunks off
(the first one 96 blocks from vanilla's). StartContext::noise_biomes
samples a batch through one resolver and the search hands it the square.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 04:06:59 +02:00
Ludovic MarieandClaude Opus 5.5 8246171ae9 Carry: YUNG's dead-end pools, modifiers and piece_in_range
YUNG's single elements keep their dead-end pool and their modifiers under
horizon:yungsapi, which the game's assembler now reads: the modifier's
condition, and its transform and delay_generation actions on the piece
itself. piece_in_range carries as is. A modifier aimed at another target
than self, or another action, is counted lost.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 04:06:59 +02:00
Ludovic MarieandClaude Opus 5.5 44b8aeab0a Carry: YUNG's and Moog's terrain adaptation as the game's terrain kernel
YUNG's API's `enhanced_terrain_adaptation` (its presets spelled out, NONE
as size 0) and MoogsStructureLib's (with its band) were counted lost. Carry
now writes them as `horizon:enhanced_terrain_adaptation` on the structure
and on the pool elements that have their own: YUNG's with every piece's
junctions and the start's box grown by the radius, Moog's with only the
terrain-matching pieces' junctions, as each library does.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 04:06:59 +02:00
Ludovic Marie ab8d803d5c Merge pull request #952 from ludovic111/perf/heightmap-scan-from-surface
🤖 Agent: merged after verify and CI parity passed. tick_blocks finds the motion-blocking height from the top of the terrain: 1.98 → 1.36 ms per tick with 10 bots on the Mac mini (−31%).
2026-10-19 04:05:57 +02:00
Ludovic Marie 07f2913d53 Merge pull request #951 from ludovic111/fix/entity-ticks-ticking-area
🤖 Agent: merged after verify and CI parity passed. Non-living entities (TNT, falling blocks, boats, frames…) now tick only in entity-ticking chunks, as in vanilla.
2026-10-19 03:14:23 +02:00
Ludovic Marie 737841550f Merge pull request #950 from ludovic111/fix/natural-spawn-every-player
🤖 Agent: merged after verify and CI parity passed; 301/301 harnesses locally on the combination with #951/#952. Dedicated servers now spawn mobs around every player (0 → ~70 per bot, 139–144 for two, matching vanilla's caps).
2026-10-19 03:14:15 +02:00
Ludovic MarieandClaude Opus 5.5 4f992028bf Merge origin/main into feat/terrain-lod
Keeps both new generator tests (far surface sampling, worker-owned carver cache).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 03:09:16 +02:00
Ludovic MarieandClaude Opus 5.5 c0f0c17ed4 Keep restored chunk entities out of /summon's result after a level switch
Coming back to a level, the entities saved with its chunks wait in
World::loaded_entities until the next tick spawns them, as vanilla's
PersistentEntitySectionManager.processPendingLoads does. /summon pushed its
compound onto that queue and drained all of it, so the first waiting
entity took the id counted as the summon's root: after a Nether round trip
`/summon minecraft:armor_stand ...` reported "Summoned new Pig". Summon now
spawns only its own stack and leaves the queue to the tick.

The command harness's settle now waits for the entities of the chunks
around the player as well, like the oracle's settle
(areEntitiesActuallyLoadedAndTicking), and a `portal` op sends the player
through the nether portal it stands in (Entity.handlePortal:
NetherPortalBlock.getPortalDestination, teleportToPortalDestination). The
corpus's new `levels` group makes the round trip and checks the Overworld's
pig and a summon's feedback on the way back.

Fixes #947

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 03:07:17 +02:00
rdbtCVS 2fe7cb3a4b Reuse terrain sampler scratch buffers between chunks 2026-10-19 01:23:23 +01:00
rdbtCVS 0ab07f7209 Fix spectator scroll wheel flight speed 2026-10-19 01:10:17 +01:00
rdbtCVS 6a4bed4744 Coordinate concurrent neighbour cache misses by chunk key 2026-10-19 00:52:21 +01:00
Ludovic MarieandClaude Opus 5.5 c74948d24c Uploads: main's path again when no upload budget is set
The soft 4 ms yield per frame (also with Unlimited budgets) made a saved
RD32 world reach full render distance about 1 s later on the Mac mini
(streaming time after launch, 4 alternating pairs: main 1.9 s mean, yield
2.8 s; t100 from a first batch of 3 pairs: 6.1 s vs 7.1 s) without better
loading frame times (max 133-210 ms vs 31-150 ms). Without a budget every
finished mesh uploads at once again, as on main. The budgeted path keeps
this branch's fixes: only loaded chunks' sections wait, and sizes count the
24-byte terrain vertex.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 01:45:38 +02:00
Ludovic Marie ba3a0fcf01 Merge pull request #943 from rdbtCVS/t3/reduce-carver-cache-locks
🤖 Agent: thanks rdbt! Verify passes. A per-worker memo of the carver biome lookups in front of the shared mutex: the resolved biome per origin is a pure function of seed, dimension and position, so results can't change, and the test checks eviction leaves the terrain identical. Paired benchmarks: +3.3–3.9 % terrain throughput at six workers (16 pairs, two sites), up to +5.9 % at size 32, no single-worker regression.
2026-10-19 01:36:09 +02:00
Ludovic MarieandClaude Opus 5.5 e805e9886c Scan for the motion-blocking top from the world surface, not the build limit
getHeightmapPos(MOTION_BLOCKING) walked every column from y 319 down,
one block lookup per y. Each precipitation roll (3 in 48 per ticking
chunk per tick) and each isRainingAt paid for it: with the chunks of 10
players ticking it was most of tick_blocks. Only air lies above the
WORLD_SURFACE heightmap the chunk already keeps, and air neither blocks
motion nor holds a fluid, so the scan starts there. LevelAccess gains
world_surface (the build limit by default, so other levels scan as before).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 01:35:44 +02:00
Ludovic MarieandClaude Opus 5.5 c755ca04f5 Tick paintings, frames, boats, armor stands, falling blocks and TNT only in entity-ticking chunks
ServerLevel.tick ticks an entity only where the chunk tickets make it
entity ticking (inEntityTickingRange, and EntityTickList holds only the
entities of entity-ticking chunks). Mobs, items and minecarts already
checked it (#931); these six entity kinds ticked wherever they were
loaded, so TNT went off and sand fell past the simulation distance.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 01:35:44 +02:00
Ludovic MarieandClaude Opus 5.5 08c77ad785 Spawn mobs around every player, and despawn from the nearest of them
Natural spawning returned without a local player, so a dedicated or
--server-thread server spawned no mobs and a LAN host spawned them only
around itself. ServerChunkCache.tickChunks now takes its spawning chunks
from every player in the level (ChunkMap.collectSpawningChunks), the
global caps from the natural spawn chunk tracker's size, and the local
caps from each player (LocalMobCapCalculator). Inhabited time and
thunder follow the same chunks (tickSpawningChunk), the wandering trader
picks a random living player (getRandomPlayer), and mobs despawn by
their distance to the nearest non-spectator of any kind.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 01:35:44 +02:00
Ludovic MarieandClaude Opus 5.5 471a8fd2f9 Merge origin/main into #800 (execute in: source dimension)
main's #880 added Source::level and Source::elsewhere for server players
teleported into another level; this branch adds Source::dimension and
moves the local player. Kept dimension (it covers what elsewhere was read
for) and level; /tp tries main's teleport_across for a server's players
first, then this branch's local-player level switch.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 01:34:25 +02:00
Ludovic Marie a9bcaa6ed5 Merge pull request #942 from ludovic111/fix/village-villagers
🤖 Agent: reviewed; verify passes. Ports removeWhenFarAway = false for Villager, WanderingTrader and AbstractGolem (iron/snow/copper golems, shulkers). Seed 1234 plains village: main 0 villagers, this PR 14, vanilla 26.3 server 14. 95/96 affected harnesses pass; world_spawn timed out on a busy machine, then 59/60 seeds alone (the one off is the known chunk-order flake #308). Fixes #917.
2026-10-19 01:31:13 +02:00
Ludovic Marie eaf6b03f86 Merge pull request #927 from ludovic111/fix/lazy-pair-recheck
🤖 Agent: reviewed; verify passes. Re-checked all four perf-server-10 PRs merged on current main (after #887 #931 #912 #916): fmt/clippy clean, horizon-client tests 893 passed, lan, dedicated_server, server_world_gen, chunk_stage, world_reopen, server_entities, server_player_targets, server_dimension_players pass, lan_e2e growth/plates/eating/hurt/chat/light pass in both modes (eating with --server-thread failed once in the long sequence, then passed twice alone). Numbers in the PR.
2026-10-19 01:24:19 +02:00
Ludovic Marie cdd55e54a5 Merge pull request #914 from ludovic111/perf/guest-tick-stats
🤖 Agent: reviewed; verify passes. Re-checked all four perf-server-10 PRs merged on current main (after #887 #931 #912 #916): fmt/clippy clean, horizon-client tests 893 passed, lan, dedicated_server, server_world_gen, chunk_stage, world_reopen, server_entities, server_player_targets, server_dimension_players pass, lan_e2e growth/plates/eating/hurt/chat/light pass in both modes (eating with --server-thread failed once in the long sequence, then passed twice alone). Numbers in the PR.
2026-10-19 01:23:58 +02:00
Ludovic Marie fdf4b16e72 Merge pull request #934 from ludovic111/perf/chunk-encode-workers
🤖 Agent: reviewed; verify passes. Re-checked all four perf-server-10 PRs merged on current main (after #887 #931 #912 #916): fmt/clippy clean, horizon-client tests 893 passed, lan, dedicated_server, server_world_gen, chunk_stage, world_reopen, server_entities, server_player_targets, server_dimension_players pass, lan_e2e growth/plates/eating/hurt/chat/light pass in both modes (eating with --server-thread failed once in the long sequence, then passed twice alone). Numbers in the PR.
2026-10-19 01:23:39 +02:00
Ludovic Marie d79b394086 Merge pull request #915 from ludovic111/perf/paletted-pack
🤖 Agent: reviewed; verify passes. Re-checked all four perf-server-10 PRs merged on current main (after #887 #931 #912 #916): fmt/clippy clean, horizon-client tests 893 passed, lan, dedicated_server, server_world_gen, chunk_stage, world_reopen, server_entities, server_player_targets, server_dimension_players pass, lan_e2e growth/plates/eating/hurt/chat/light pass in both modes (eating with --server-thread failed once in the long sequence, then passed twice alone). Numbers in the PR.
2026-10-19 01:23:19 +02:00
rdbtCVS 745c756df5 perf(worldgen): cache carver biomes per worker 2026-10-19 00:21:20 +01:00
Ludovic MarieandClaude Opus 5.5 a3f38b76aa Merge origin/main into feat/terrain-lod
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 01:19:15 +02:00
Ludovic MarieandClaude Opus 5.5 1935cfc7e5 Translucent draws: main's order when no distant tile has water
Sections arrive back to front (visible is sorted by distance, then key), so
the merged list only needs sorting when a distant tile adds translucent
draws, and ties now break the same way as main's reversed visible order:
with LODs off the draw order and cost are exactly main's.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 01:13:12 +02:00
Ludovic Marie 8fcae5043b Merge pull request #932 from ludovic111/feat/lithostitched-block-swap
🤖 Agent: reviewed; verify passes. Main merged into the stack (processors.rs tests kept from both sides; port-pack tests pass on each layer, clippy clean). Parity: all 269 game-side harnesses on modpacks-8's integration commit, structure-start oracle 35/35.
2026-10-19 01:07:45 +02:00
Ludovic Marie c1d2eddb5d Merge pull request #930 from ludovic111/feat/yung-chest-cap
🤖 Agent: reviewed; verify passes. Main merged into the stack (processors.rs tests kept from both sides; port-pack tests pass on each layer, clippy clean). Parity: all 269 game-side harnesses on modpacks-8's integration commit, structure-start oracle 35/35.
2026-10-19 01:07:21 +02:00
Ludovic Marie 3215ee942e Merge pull request #929 from ludovic111/feat/yung-placeholder-blocks
🤖 Agent: reviewed; verify passes. Main merged into the stack (processors.rs tests kept from both sides; port-pack tests pass on each layer, clippy clean). Parity: all 269 game-side harnesses on modpacks-8's integration commit, structure-start oracle 35/35.
2026-10-19 01:04:55 +02:00
Ludovic Marie cbd051a1d7 Merge pull request #926 from ludovic111/feat/carry-trial-spawners-vaults
🤖 Agent: reviewed; verify passes. Main merged into the stack (processors.rs tests kept from both sides; port-pack tests pass on each layer, clippy clean). Parity: all 269 game-side harnesses on modpacks-8's integration commit, structure-start oracle 35/35.
2026-10-19 01:04:30 +02:00
Ludovic MarieandClaude Opus 5.5 bc549713bb Merge feat/yung-chest-cap (with main) into feat/lithostitched-block-swap
processors.rs tests: keep both sides' tests.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 00:57:59 +02:00
Ludovic MarieandClaude Opus 5.5 63044f339c Merge feat/yung-placeholder-blocks (with main) into feat/yung-chest-cap
processors.rs tests: keep both sides' tests.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 00:57:52 +02:00
Ludovic MarieandClaude Opus 5.5 c3595f1a9c Merge feat/carry-trial-spawners-vaults (with main) into feat/yung-placeholder-blocks
processors.rs tests: keep #929's desert temple test and #920's rule-test test.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 00:57:45 +02:00
Ludovic MarieandClaude Opus 5.5 03c423daac Merge origin/main into feat/carry-trial-spawners-vaults
processors.rs tests: keep both #926's trial spawner and vault tests and
#920's Repurposed rule-test test.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 00:57:14 +02:00
Ludovic MarieandClaude Opus 5.5 3d0b76400b Merge origin/main into feat/terrain-lod
Keeps #905's single command buffer and #939's occlusion culling: distant
tiles draw in phase 1, so phase 2 stays exactly the deferred sections the
culling test reads back, and the flat path's origins include the tiles in
main's relocated upload block.

Adapts to main's APIs: block sprites and models now live in RenderAssets
(WorkerContext::render), BlockFacts behind Resources::facts(), and
terrain vertices hold the UV as 16-bit fixed point (#df58a474). Merged
distant faces repeat their sprite once per block, which the [0, 2) section
UVs cannot hold, so tile vertices now store UVs from the face's corner in
1/32-block steps (TerrainVertex::tiled, faces split at 1024 blocks) and the
instance's origin.w carries the UV step: 1/32768 for sections (decodes
exactly as before), 1/32 for tiles.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 00:52:42 +02:00
Ludovic Marie 6da2963080 Merge pull request #924 from ludovic111/fix/carry-dead-vertical-distance
🤖 Agent: reviewed; verify passes. Parity: all 269 game-side harnesses on modpacks-8's integration commit (main 573f3a64 + the six game-side PRs), vanilla structure-start oracle 35/35. Merged in order #919 → #920 → #922 → #923 → #924, then the stack #926 → #929 → #930 → #932.
2026-10-19 00:52:33 +02:00
Ludovic Marie 5b9c75c017 Merge pull request #923 from ludovic111/feat/carry-small-nether-dungeon
🤖 Agent: reviewed; verify passes. Parity: all 269 game-side harnesses on modpacks-8's integration commit (main 573f3a64 + the six game-side PRs), vanilla structure-start oracle 35/35. Merged in order #919 → #920 → #922 → #923 → #924, then the stack #926 → #929 → #930 → #932.
2026-10-19 00:52:19 +02:00
Ludovic Marie d04c266edb Merge pull request #922 from ludovic111/feat/repurposed-mineshafts
🤖 Agent: reviewed; verify passes. Parity: all 269 game-side harnesses on modpacks-8's integration commit (main 573f3a64 + the six game-side PRs), vanilla structure-start oracle 35/35. Merged in order #919 → #920 → #922 → #923 → #924, then the stack #926 → #929 → #930 → #932.
2026-10-19 00:52:05 +02:00
Ludovic Marie 5f0fea0bd6 Merge pull request #920 from ludovic111/feat/carry-rule-tests
🤖 Agent: reviewed; verify passes. Parity: all 269 game-side harnesses on modpacks-8's integration commit (main 573f3a64 + the six game-side PRs), vanilla structure-start oracle 35/35. Merged in order #919 → #920 → #922 → #923 → #924, then the stack #926 → #929 → #930 → #932.
2026-10-19 00:51:52 +02:00
Ludovic Marie 0c933cc9c5 Merge pull request #919 from ludovic111/feat/structure-rule-tests
🤖 Agent: reviewed; verify passes. Parity: all 269 game-side harnesses on modpacks-8's integration commit (main 573f3a64 + the six game-side PRs), vanilla structure-start oracle 35/35. Merged in order #919 → #920 → #922 → #923 → #924, then the stack #926 → #929 → #930 → #932.
2026-10-19 00:51:38 +02:00
Ludovic Marie 8625ec35e0 Merge pull request #939 from ludovic111/perf/occlusion-cull-cheap
🤖 Agent: reviewed; verify passes. HD 620 (Vulkan/Mesa) culling off → on: RD16 +22–42 % FPS (peak unchanged, culling paused), RD32 +14–109 %; 1 % lows and CPU ms equal or better except RD32 peak's 1 % low (38.8 → 37.4). Screenshots: static views within main-vs-main noise, 21 frames after 30–40° camera jumps identical (no popping). parity section_mesh, translucency, light, hud locally; CI parity covers the rest. On only on Vulkan devices with SHADER_EARLY_DEPTH_TEST, INDIRECT_FIRST_INSTANCE and fragment-writable storage; Metal draws as before. Not yet checked on NVIDIA/AMD or Windows Vulkan: HORIZON_GPU_CULL=0 turns it off.
2026-10-19 00:39:08 +02:00
Ludovic MarieandClaude Opus 5.5 13cd89578a Terrain: occlusion culling of solid sections (Vulkan)
Sections that terrain in front hides entirely are drawn with an instance
count of 0. Phase 1 draws the sections the last test read back saw; in the
same pass every other section's box (and an eighth of phase 1's) is drawn
against phase 1's depth, front faces only, with a fragment shader forced
past an early depth test that marks the section seen. Phase 2, in a second
pass, draws what the test saw. The test is exact per pixel and needs no depth
read or Hi-Z, and every solid draw stays one entry of one list, so the CPU
records as many draws as before (Mesa unrolls a multi-draw per entry on
Intel Gen9). Culling pauses while the last result hid under a tenth of the
solid indices, with one frame in 64 testing everything.

HD 620 at RD16: +23-42% FPS in five of perf-frame-6's six views, the peak
view unchanged; RD32: +14-109%. Pixels unchanged, also on camera jumps.
Vulkan only: naga's MSL has no early fragment tests. HORIZON_GPU_CULL=0
turns it off, `all` culls every frame; `section-cull on|off` in a game.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 23:53:53 +02:00
Ludovic Marie 25f2e2396d Merge pull request #916 from ludovic111/feat/server-headless-build
🤖 Agent: reviewed; verify passes. Release dry run on this branch (run 39167763008): linux, macos, windows builds and publish all succeed; horizon-server-linux-x86_64.tar.gz (28 MB) built and import-checked (no libasound/libudev). Agent's proof: 880/879 client tests in both builds, dedicated_server/server_entities/lan on the server build, install → init → start → bot join on the ThinkPad without the libraries.
2026-10-18 23:42:28 +02:00
Ludovic Marie 204a887ca2 Merge pull request #936 from ludovic111/feat/server-bench-equal-work
🤖 Agent: reviewed; verify passes. Equal-work server benchmark: fixed exploring speed, 300 s warm-up, chunk-work comparison per run. Horizon 1.8/4.7/11.7/23.8 ms per tick at 1/10/25/50 bots vs vanilla 11.3/34.3/50.5/stopped by its watchdog; single runs on the ThinkPad, caveats in the README.
2026-10-18 23:27:56 +02:00
Ludovic Marie 76d2985865 Merge pull request #921 from ludovic111/feat/vanilla-client-macos
🤖 Agent: reviewed; verify and parity pass. tools/vanilla_client.py runs on macOS (vsync off so a hidden window doesn't block the join); the owner's vanilla 26.3 client joined a Horizon server and stayed connected 4 min.
2026-10-18 23:25:45 +02:00
Ludovic MarieandClaude Opus 5.5 32a37e8937 Benchmark: Horizon's server against vanilla 26.3 with mixed bots, equal work
Explore bots paced at 0.8 blocks a second and a 300-second warm-up, so both servers serve the same
paths; the work table shows it. Horizon ran as horizon-engine (the server build, from its archive).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 23:22:07 +02:00
Ludovic Marie e293132dda Merge pull request #912 from rdbtCVS/t3/fix-grimac-false-flags
🤖 Agent: thanks rdbt! Verify passes. Checked locally merged with current main: fmt and clippy clean, cargo test -p horizon-client (885 passed), 14 harnesses including your two new oracles (player_sync, interaction_input, local_input, sprint_input, item_use, item_use_start, item_use_lifecycle, use_item_on, teleport_consume, lan, dedicated_server, server_player_combat, server_player_targets, server_entities), and lan_e2e pvp, hurt, eat, eating, clicks, chat, respawn in both modes: all pass. Fixes #903.
2026-10-18 23:12:59 +02:00
Ludovic MarieandClaude Opus 5.5 875773a08f Villagers, traders and golems never despawn for distance
Village villagers are placed from the jigsaw templates without
PersistenceRequired (the village's iron golem has it), so Mob.checkDespawn
discarded every villager of a village generated more than 128 blocks from
the player, and the rest after a while beyond 32 blocks. Vanilla's
Villager, WanderingTrader and AbstractGolem (iron, snow and copper golems,
shulkers) override removeWhenFarAway to false.

Fixes #917.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 23:10:31 +02:00
Ludovic Marie 18f9ae5413 Merge pull request #931 from ludovic111/fix/server-block-ticks-no-player
🤖 Agent: reviewed; verify passes. parity --changed 95/95 before the rebase; the 9 block-tick harnesses (dedicated_server byte-identical, 591 lines) and lan_e2e plates/growth/time in both modes after it; growth fails on main. Fixes #890: blocks tick from vanilla's ticket levels around every player. MSPT with 10 bots rises from ~1.6 to ~14 ms because blocks now tick at all on a server (vanilla: 40).
2026-10-18 22:52:12 +02:00
Ludovic MarieandClaude Opus 5.5 a9ad185287 Lithostitched's block swaps, with the properties both blocks have
ChoiceTheorem's Overhauled Village swaps blocks in its templates through
Lithostitched's block_swap processor (137 lists): a map of block to
block, the new one taking Block.withPropertiesOf the old state, and the
block left alone when the new one is not registered. Carry dropped it.

- Game: horizon:block_swap, the same map; the new block's default state
  with each property of the old one it has too (by name, when the value
  is one of its own).
- Carry: lithostitched:block_swap becomes it.

ATM10: processors carried 136 -> 273 of 1,427 (dropped 1,291 -> 1,154).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 22:45:28 +02:00
Ludovic MarieandClaude Opus 5.5 50215b484a Better Dungeons' small dungeons: one or two chests, as the mod caps them
Better Dungeons' small dungeon templates hold several chests; its Java
chest processor keeps the first, then each other one on nextFloat <= 0.2
until two have stayed, and turns the rest into cave air. Carry dropped it,
so every chest stayed.

- Game: horizon:count_cap. Of `blocks`, the first `min` a placement meets
  stay, then each stays on settings.getRandom(pos).nextFloat() <= chance
  until `max` have; the others become `replacement`. Counted per
  placement (PlaceSettings, reset in process_infos), like the mod's
  DungeonContext, set up per StructureTemplate.placeInWorld.
- Carry: betterdungeons:small_dungeon_chest_processor becomes it with the
  mod's config defaults (1, 2, 0.2; chest and trapped chest).

Seed 1234, five small dungeons decorated: 12 chests -> 6.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 22:39:03 +02:00
Ludovic MarieandClaude Opus 5.5 c7985974e7 Better Desert Temples' placeholder blocks: suspicious sand, sherds, swaps
Better Desert Temples builds its temples with placeholder blocks (orange,
red and white glass, blue and yellow concrete, red wool, polished diorite,
blank pots) that its Java processors turn into suspicious sand, sand and
sandstone, upside-down stairs, cobwebs, terracotta, TNT and pots with
sherds. Carry dropped them, so the placeholders stayed in the world.

- Game: a horizon:pick processor. A block of `block` takes the first
  pick whose `below` is past one settings.getRandom(pos).nextFloat(),
  else `default`; an outcome is a state or one of several by nextInt
  (Util.getRandom), a new block entity or the block's own, and NBT lists
  of picked strings (one float each).
- Carry: the fixed swaps become vanilla's rule; the random ones
  horizon:pick with the mod's bounds (BlockStateRandomizer's sums as
  floats). The processors that read or change the world around (pillars,
  Diorite's waterlogging) stay dropped.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 22:23:18 +02:00
Ludovic MarieandClaude Opus 5.5 a0bcc13f1c Tick blocks around every player, from vanilla's chunk tickets (#890)
Without a local player (dedicated server, --server-thread) no block ticked:
tick_blocks returned early, so crops, fluids, redstone, pistons and pressure
plates stood still, and a LAN host ticked only around itself.

ticket_levels ports DistanceManager's levels: PLAYER_LOADING at 31 within the
view distance and PLAYER_SIMULATION at 31 - simulation distance for every
player in the level (the local player, LAN guests, a server's players), and
FORCED at 31. Random ticks run in simulation entity-ticking chunks that are
ticking chunks, scheduled ticks, block events and block entities in block
ticking ones, entities in entity ticking ones; the sculk blocks follow the
same rule. Saved ticks unpack when a chunk starts ticking
(startTickingChunk), not when it loads.

The hub keeps the tick-rate state current while nobody is in the game, so a
player joining a frozen server is told once (updateJoiningPlayer).

dedicated_server parity froze nothing while the client was sent its chunks:
saved fluid ticks there (a lava fall that never settles) ran before or after
the chunk was sent depending on each server's loading speed, and it failed on
main too. It now freezes the game until the client has its chunks.

lan_e2e gets a growth check: water flows and wheat grows next to a guest
1000 blocks from the host.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 22:20:39 +02:00
Ludovic MarieandClaude Opus 5.5 96822cd5f8 Moog's trial spawners and vaults: the saved state out, the configs in
Moog's trial spawner and vault randomizing processors keep the block and
change its block entity. The templates were saved with state that breaks
them in a new world: a vault's server_data holds
state_updating_resumes_at far in the future (it never opens) and a
spawner's spawn_data an empty entity (it spawns nothing of its own).

- Game: a horizon:edit block entity modifier takes `remove`'s keys out,
  then merges `data` in. append_static now merges as CompoundTag.merge
  does (compounds in both are merged), not a shallow replace.
- Carry: each processor becomes vanilla's rule processor, one rule per
  block state (12 trial spawner, 32 vault), with horizon:edit: the
  spawner's normal and ominous configs by id, the vault's loot table and
  key (the ominous ones on ominous vaults). trial_spawner/ files get their
  mobs' 1.21.1 ArmorItems/HandItems as 26.3's equipment.

ATM10: 6 more processors carried (125 of 1,427).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 22:12:37 +02:00
Ludovic MarieandClaude Opus 5.5 3cd155792d Entity tracking: check a pair again only when one side moves, as vanilla's ChunkMap
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 22:11:47 +02:00
Ludovic Marie 24bcef42be Merge pull request #887 from ludovic111/fix/lan-remote-item-use
🤖 Agent: verify passes; merged with main and checked locally (see the comment above). Part of #803: players see each other eat and use items.
2026-10-18 21:59:12 +02:00
Ludovic MarieandClaude Opus 5.5 4b18b79d15 vanilla_client.py on macOS: the real vanilla 26.3 client joins Horizon's server
The tool picks this machine's natives (macOS arm64 or Linux), adds -XstartOnFirstThread on macOS
and turns vsync off there: with it on, an unseen window (a locked session) blocks in
SDL_GL_SwapWindow before the client joins. docs/server.md records the join.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 21:46:09 +02:00
Ludovic MarieandClaude Opus 5.5 1e800099e9 Carry: vertical_distance_from_start_piece is a dead field, not a lost rule
Repurposed Structures' jungle fortress and End stronghold still write
vertical_distance_from_start_piece, but no codec of the mod (or of Moog's
Structure Lib) reads it any more: its own structures ignore it. Carry
drops it without counting it in rules_lost, which now lists only rules
the mods do run.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 21:43:58 +02:00
Ludovic MarieandClaude Opus 5.5 e3909f4370 Carry: Better Dungeons' small Nether dungeon, as YUNG's jigsaw
betterdungeons:small_nether_dungeon is YUNG's jigsaw structure (the same
fields and YungJigsawManager assembly) behind the mod's
smallNetherDungeons config switch, which ships on. Carry maps it as it
maps yungsapi:yung_jigsaw: minecraft:jigsaw with horizon:yungsapi.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 21:43:45 +02:00
Ludovic MarieandClaude Opus 5.5 82c8de262f Repurposed's mineshafts: laid out under the terrain, carried as jigsaws
Repurposed Structures' 10 overworld mineshafts (birch, desert, icy,
jungle, ocean...) are its generic jigsaw with one change in
MineshaftStructure.postLayoutAdjustments: the layout is not centred on
the chunk; its top goes 15 blocks under the lowest ocean floor at the
chunk's middle and 16 blocks to each side (taking one off after each
side, as the mod does), never up, within y_allowance.

- Game: "layout": "mineshaft" in horizon:piece_limited runs that step.
- Carry: repurposed_structures:generic_mineshaft becomes minecraft:jigsaw
  with the piece-limited layout and that key, as its generic jigsaw does.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 21:43:37 +02:00
Ludovic Marie b77891dfe0 Merge pull request #918 from ludovic111/ci/cancel-on-close
🤖 Agent: verify passes; workflow-only change (cancel a PR's unfinished runs on close).
2026-10-18 21:43:12 +02:00
Ludovic MarieandClaude Opus 5.5 41bd91f295 CI: cancel a pull request's unfinished checks when it closes
A merged or closed pull request's queued runs held the self-hosted runners
until someone cancelled them by hand. pull_request_target lets the token
cancel runs of fork pull requests too; the job checks out nothing. It
matches the branch and the head commit, so a fork's branch named like
another doesn't cancel that one's runs.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 21:35:47 +02:00
Ludovic MarieandClaude Opus 5.5 00343ad2e2 Chunk packets: pack a paletted container a long at a time
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 21:21:34 +02:00
Ludovic MarieandClaude Opus 5.5 f1d5bf5209 server_bench: compare each bot count's work across servers, and flag runs that aren't equal
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 20:57:07 +02:00
Ludovic MarieandClaude Opus 5.5 1f2f2a9f6d Chunk batches: encode each column once a tick, its new sections on warm-up threads
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 20:53:09 +02:00
Ludovic MarieandClaude Opus 5.5 5fcea03fb8 Carry: Repurposed's rule tests as the game's, and the chain as the iron chain
Two things kept Repurposed Structures' mineshaft processor lists from
loading in 26.3, and with them the mod's 5 Nether mineshaft pools:

- Its three rule tests written in Java. Carry renames them to the game's
  horizon:matter_phase, horizon:piece_origin_axis_aligned_linear_pos and
  horizon:y_value (213 in 75 lists), counted in the report's
  rule_tests_carried.
- 1.21.1's minecraft:chain, which 26.3 calls minecraft:iron_chain (its
  datafixers rename it in worlds). Carry renames it in recipes, loot
  tables and worldgen files: 112 files in 5 of ATM10's mods.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 20:49:23 +02:00
Ludovic MarieandClaude Opus 5.5 d634870f15 Structures: Repurposed's matter phase, piece origin and y value rule tests
Repurposed Structures' mineshafts and Nether monument test blocks with
three rule tests of their own. Vanilla, and the game, refuse a processor
list naming a type they do not know, and with it every pool naming the
list: the 5 Nether mineshaft start pools in a ported pack never load.

The game now runs them under its own types, with the mod's fields:

- horizon:matter_phase (MatterPhaseRuleTest): solid (not air, no fluid,
  canOcclude), liquid, air, and air or liquid with rails and chains,
  optionally inverted. Structure processors now bind the block
  properties to read canOcclude.
- horizon:piece_origin_axis_aligned_linear_pos: vanilla's axis-aligned
  chance over the block's distance from its template's origin.
- horizon:y_value: the block's world height in a range.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 20:48:34 +02:00
Ludovic MarieandClaude Opus 5.5 b18df1fd6b Merge origin/fix/lan-remote-item-use (#887) into main
Conflicts: the hub's player state keeps both main's attributes (#857) and
#887's living_flags; remote.rs and lan.rs debug JSON keep swinging and
using; the guests' model tick keeps both the swing update and the item
in use; lan_e2e.py checks unioned. remote_player_eating reads main's
Option<Consumable> (#799): any consumable with particles, its own use
time and should_emit, as the local player does.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 20:36:43 +02:00
Ludovic Marie 573f3a64f7 Merge pull request #913 from ludovic111/integ/bl4
🤖 Agent: verify passes; local checks in the PR body. Merges bugs-lan-4's #881–#886.
2026-10-18 20:27:03 +02:00
Ludovic Marie 20cc48b48f Merge pull request #905 from ludovic111/perf/render-encode
🤖 Agent: reviewed; verify passes. Mac mini RD32 standing, 4 alternating pairs: 116.1 → 163.3 FPS, CPU 7.09 → 4.98 ms/frame. Screenshots of 6 views plus HUD and inventory: no pixel differs beyond main-vs-main noise. parity --changed 95/95 locally. No early return after the copies encoder is taken, so staged section copies can't be dropped.
2026-10-18 20:26:51 +02:00
Ludovic MarieandClaude Opus 5.5 519ec1217c Server build: the game without sound or controllers, so Linux servers need neither ALSA nor udev
horizon-client's cpal output and its controllers (gilrs, the DualSense's HID output) move behind the
audio and controllers features, on by default, so the game is unchanged. Built with
--no-default-features it links neither libasound.so.2 nor libudev.so.1. release.yml packages that
build as horizon-engine in horizon-server-linux-x86_64.tar.gz (xtask package-server, checked by
check_imports.py --server); install.sh installs it on Linux (and --archive installs a local one);
horizon-server already prefers horizon-engine. The Dockerfile uses it too, and CI lints it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 20:17:21 +02:00
Ludovic Marie def901b06f Merge remote-tracking branch 'origin/main' into integ/bl4 2026-10-18 20:09:06 +02:00
Ludovic Marie 51c49e11f4 Merge pull request #911 from ludovic111/feat/carry-repurposed-jigsaw-types
🤖 Agent: reviewed; verify passes. Parity: 298/298 harnesses on the integration commit 234bd101 (main + #907/#908/#910); vanilla structure-start oracle 35/35 with all five modpacks-7 changes. Merged in the required order #907 → #908 → #909 → #910 → #911.
2026-10-18 20:08:30 +02:00
Ludovic Marie 7a78d15c9e Merge pull request #910 from ludovic111/feat/repurposed-start-checks
🤖 Agent: reviewed; verify passes. Parity: 298/298 harnesses on the integration commit 234bd101 (main + #907/#908/#910); vanilla structure-start oracle 35/35 with all five modpacks-7 changes. Merged in the required order #907 → #908 → #909 → #910 → #911.
2026-10-18 20:08:04 +02:00
Ludovic Marie 36775db223 Merge pull request #909 from ludovic111/feat/carry-spawner-processors
🤖 Agent: reviewed; verify passes. Parity: 298/298 harnesses on the integration commit 234bd101 (main + #907/#908/#910); vanilla structure-start oracle 35/35 with all five modpacks-7 changes. Merged in the required order #907 → #908 → #909 → #910 → #911.
2026-10-18 20:07:39 +02:00
Ludovic Marie 567a68dd4f Merge pull request #908 from ludovic111/feat/spawner-mobs-processor
🤖 Agent: reviewed; verify passes. Parity: 298/298 harnesses on the integration commit 234bd101 (main + #907/#908/#910); vanilla structure-start oracle 35/35 with all five modpacks-7 changes. Merged in the required order #907 → #908 → #909 → #910 → #911.
2026-10-18 20:07:15 +02:00
Ludovic Marie 00a2ea23f0 Merge pull request #907 from ludovic111/fix/processor-ids-namespace
🤖 Agent: reviewed; verify passes. Parity: 298/298 harnesses on the integration commit 234bd101 (main + #907/#908/#910); vanilla structure-start oracle 35/35 with all five modpacks-7 changes. Merged in the required order #907 → #908 → #909 → #910 → #911.
2026-10-18 20:06:49 +02:00
rdbtCVS 4de112fe16 Fix multiplayer tick ordering and interaction target parity 2026-10-18 19:06:34 +01:00
Ludovic Marie 87cf8cb9e9 Merge pull request #904 from ludovic111/fix/server-log-lines
🤖 Agent: reviewed; verify passes. Refused logins name Server thread in latest.log; console, supervisor.log and the panel print local time.
2026-10-18 19:56:57 +02:00
Ludovic Marie 2a9d67bd68 Merge pull request #902 from ludovic111/docs/server-benchmark-mixed
🤖 Agent: reviewed; verify passes.
2026-10-18 19:52:37 +02:00
Ludovic Marie a714a9b6f8 Merge pull request #900 from ludovic111/fix/server-light-border
🤖 Agent: reviewed; verify passes.
2026-10-18 19:52:34 +02:00
Ludovic Marie e046036869 Merge pull request #898 from ludovic111/fix/server-linux-libraries
🤖 Agent: reviewed; verify passes (client --help exits 0 without loading the game, so the start check only catches a program the system can't load). Linux installs now name the missing ALSA/udev libraries; services run through the installer's current link.
2026-10-18 19:52:31 +02:00
Ludovic Marie 2d1dd54b33 Merge pull request #893 from ludovic111/fix/server-find-resources
🤖 Agent: reviewed; verify passes. Fresh installs now find game files prepared without sounds (horizon-resources.json).
2026-10-18 19:52:22 +02:00
Ludovic MarieandClaude Opus 5.5 02a6ab7429 Guest ticks: count the per-tick statistics without making strings, and date a criterion only when one is granted
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 19:29:23 +02:00
Ludovic MarieandClaude Opus 5.5 bdc758c5e5 Server log: vanilla's thread in latest.log, and local time on the console
A dedicated server printed refused logins on its console as vanilla
does ("[Server thread/INFO]: Disconnecting <name> ..."), but wrote them
to logs/latest.log under the connection thread's own name
("[horizon-lan-conn/INFO]"): the file logger used the OS thread's name,
not the one vanilla logs the line from. The engine's lines now go to the
file under the same thread as on the console.

The console's times were UTC while logs/latest.log and vanilla's
console (Log4j's %d{HH:mm:ss}) use local time, so the two disagreed by
the time zone, as did the supervisor's own lines in
logs/supervisor.log and the web panel's console. Both now print local
time. Backup names keep their UTC time, as documented.

Found by the end-to-end install check on the Mac mini and the ThinkPad.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 19:27:23 +02:00
Ludovic MarieandClaude Opus 5.5 40972de77a Benchmark: Horizon's server against vanilla 26.3 with mixed bots, as installed
The first published benchmark for the working version (docs/server.md
section 10, item 4). It ran the v2026.10.18.15 release, as install.sh
installs it, against vanilla 26.3 with 1, 10, 25 and 50 mixed bots. The
servers ran on the ThinkPad (quiet, under nice 19) and the bots on the
Mac mini over the tailnet. Vanilla's default game rules applied on both;
Horizon's server runs the locator bar now.

Both servers kept 20 TPS throughout. Horizon used 3.7 to 5.2 times less
memory. Its average tick was shorter with 1, 10 and 25 bots and 18 %
longer with 50, while it generated about twice the new terrain a second
(the exploring bots fly as fast as each server sends chunks). Its P99
was lower at every count.

benchmarks/server-mixed-2026-10-18/ holds the README, the report and the
machine's load; docs/server.md links it from sections 9 and 10.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 19:26:26 +02:00
Ludovic Marie d4d46b1bb7 Merge remote-tracking branch 'origin/fix/lan-paused-clock' into integ/bl4
# Conflicts:
#	tools/lan_e2e.py
2026-10-18 19:22:08 +02:00
Ludovic Marie faba097ff2 Merge remote-tracking branch 'origin/fix/lan-swings' into integ/bl4
# Conflicts:
#	crates/horizon-client/src/game/lan_actions.rs
#	tools/lan_e2e.py
2026-10-18 19:22:07 +02:00
Ludovic Marie c234641ead Merge remote-tracking branch 'origin/fix/lan-damage-events' into integ/bl4
# Conflicts:
#	crates/horizon-protocol/src/packets/play.rs
#	tools/lan_e2e.py
2026-10-18 19:17:43 +02:00
Ludovic Marie bff5121f6e Merge remote-tracking branch 'origin/fix/lan-guest-pressure-plates' into integ/bl4
# Conflicts:
#	tools/lan_e2e.py
2026-10-18 19:16:57 +02:00
Ludovic Marie 40fbae13b6 Merge remote-tracking branch 'origin/fix/lan-guest-brewing' into integ/bl4
# Conflicts:
#	crates/horizon-client/src/game/lan_crafting.rs
#	tools/lan_e2e.py
2026-10-18 19:16:56 +02:00
Ludovic Marie feb85f6d15 Merge remote-tracking branch 'origin/fix/lan-guest-machine-screens' into integ/bl4
# Conflicts:
#	tools/lan_e2e.py
2026-10-18 19:16:35 +02:00
Ludovic Marie f422cbb79a Merge pull request #901 from ludovic111/integ/sh7
🤖 Agent: Merged after the real verify passed; checks in the description.
2026-10-18 19:15:06 +02:00
Ludovic MarieandClaude Opus 5.5 ac2547c24a lan_e2e light: wait for the border chunk's light before comparing
On the server thread, lan_e2e --check light failed its border step (#891):
the guest's border chunk still had block light 0 next to glowstone placed
in the chunk past it. The server was right; the check was not.
same_light() waits until the host's light equals the guest's, but on the
server thread the host's game is a client too, and both are dark until
the light update arrives. They agreed at once, same_light returned, and
the next line read the guest's light before the update came.

The check now waits up to 100 ticks for the guest's border light, then
compares the host's with it.

With temporary logging on the server thread: the glowstone lands in a
finished chunk that isn't lit (the ring past what the guest tracks), so
the edit queues a full light job for its neighbours. The border chunk's
job then relights sections 15 to 17, and Hub::broadcast_light_changes
sends them to both clients as a tracked border chunk, as
ChunkMap.isChunkOnTrackedBorder does. Nothing in the engine changes.

Fixes #891

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 19:03:29 +02:00
Ludovic Marie b0b4414397 Merge pull request #752 from rdbtCVS/perf/biome-zoom-shared-mix
🤖 Agent: Merged. Bit-exact against the per-corner oracle (unit test over a dense grid, 7 seeds, wrapping extremes), CI parity passed, and the surface pass is about 10% faster in chunkgen_bench. Thanks rdbt!
2026-10-18 18:37:12 +02:00
Ludovic MarieandClaude Opus 5.5 80dacdba68 lan_attributes: read items through Resources::items() after #878
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 18:36:16 +02:00
Ludovic Marie 453ef077f4 Merge remote-tracking branch 'origin/main' into integ/sh7 2026-10-18 18:30:16 +02:00
Ludovic MarieandClaude Opus 5.5 4ea2243e4d horizon-server on Linux: say which libraries the game needs, and follow updates
On a Linux without ALSA's library (most minimal server images), the
game exits at once with the loader's "error while loading shared
libraries: libasound.so.2". The supervisor took that for a crash and
restarted it five times over two and a half minutes before giving up
with "see logs/latest.log", which holds nothing about it.

Now:
- `run` starts the game with --help first (it loads no game files) and,
  if the system can't load it, stops at once with the loader's line and
  the packages to install on Debian/Ubuntu, Fedora, Arch and NixOS;
- `init` prints the same as a warning after "Game:";
- install.sh lists the libraries `ldd` can't find, with the same advice;
- docs/server.md says so in the install section.

Also, a service installed without the installer's launcher on the PATH
ran `<dir>/releases/<tag>/horizon-server`: Linux reports the running
program with links resolved, so the unit pinned that release and an
update never reached it. The fallback now goes through `<dir>/current`
while it points at that release.

Found by the end-to-end install check on the ThinkPad (NixOS, no
nix-ld libraries) with v2026.10.18.15.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 18:16:14 +02:00
Ludovic Marie 73787507d1 Merge pull request #895 from ludovic111/fix/world-rest-no-timeout
🤖 Agent: Merged after the real verify passed.
2026-10-18 18:11:00 +02:00
Ludovic MarieandClaude Opus 5.5 1b141ef330 Startup: no time limit on loading the world resources
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 18:04:38 +02:00
Ludovic Marie f2a323a2d9 Merge pull request #878 from rdbtCVS/startup/combined-progressive-loading-20261018
🤖 Agent: Merged. Thanks rdbt for the consolidation and for merging main yourself! On de6f15e6: fmt, workspace clippy, horizon-client tests and the client build pass, and 19/19 covering harnesses pass (options, world_reopen, lan, dedicated_server, block_removal, section_mesh, resource_pack, menu_click, screen_layout, the pack, world-options, chat and container screens, and the consumable ones). We'll handle the two follow-ups noted in the comment above (the 120 s fatal timeout, and the backdrop terrain built on the event loop).
2026-10-18 17:52:54 +02:00
Ludovic MarieandClaude Opus 5.5 44fba3bf9d horizon-server: find game files prepared without sounds
init and run looked for asset-index.json to recognise a prepared folder,
but horizon-prepare writes that file only with the optional sounds
(`horizon-prepare sounds`). A server prepared the usual way, without
sounds, was never found: init said "no prepared game files" and listed
the folder that held them. They are now recognised by
horizon-resources.json, the manifest every preparation writes and the
one the game itself reads.

Found by installing v2026.10.18.15 with install.sh on the Mac mini and
preparing from the release folder.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 17:50:52 +02:00
Ludovic Marie 163bcdd604 Merge pull request #889 from rdbtCVS/fix/811-main-menu-debug-overlay
🤖 Agent: Merged. Matches 26.3's KeyboardHandler.keyPress: the F3 toggle isn't conditional on a world or the absence of a screen, and the title counts as an open screen so it gets no gameplay keys. Fixes #811. Thanks rdbt!
2026-10-18 17:35:02 +02:00
Ludovic Marie e7fded804e Merge remote-tracking branch 'origin/feat/flight-server-tick-ms' into integ/sh7 2026-10-18 17:23:49 +02:00
Ludovic Marie dd1fd6d3c5 Merge remote-tracking branch 'origin/fix/remote-chat-events' into integ/sh7
# Conflicts:
#	tools/lan_e2e.py
2026-10-18 17:23:49 +02:00
Ludovic Marie 56c352679d Merge remote-tracking branch 'origin/feat/server-thread-agent-commands' into integ/sh7 2026-10-18 17:23:24 +02:00
Ludovic Marie e30527c800 Merge remote-tracking branch 'origin/fix/server-respawn-repair' into integ/sh7 2026-10-18 17:23:23 +02:00
Ludovic Marie 52298d9a37 Merge remote-tracking branch 'origin/fix/server-crouch-flags' into integ/sh7
# Conflicts:
#	crates/horizon-client/src/lan/entity.rs
2026-10-18 17:23:22 +02:00
Ludovic MarieandClaude Opus 5.5 aea18f245f Renderer: one command buffer per frame, uploads before the first pass
wgpu-core ends its Metal command buffer at every pass: each render or
compute pass costs two command buffers (the pass and its barriers), each
run of copies between passes one more, and Metal creates and commits every
one on its own. The frame made about 18: the section copies in their own
encoder, the queue's pending writes for the HUD's fire animation, copies
before and after the flatten pass, and the screen effects' and HUD's
vertex uploads between later passes. At render distance 32 that cost 2-5 ms
of submit per frame, mostly waiting on the command queue's lock.

The frame now records after the section copies in their encoder, stages
the fire animation and the HUD's glyph patches through the frame's belt
instead of Queue::write_texture, and records every upload (screen
effects, HUD, flatten inputs, indirect draws) before the first pass.
The commands and their order within each pass are unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 17:22:43 +02:00
Ludovic Marie eed2d8ddff Merge remote-tracking branch 'origin/feat/server-entity-attributes' into integ/sh7 2026-10-18 17:17:54 +02:00
Ludovic Marie f5d81e883c Merge remote-tracking branch 'origin/feat/server-player-targets' into integ/sh7 2026-10-18 17:17:53 +02:00
Ludovic MarieandClaude Opus 5.5 be161f100b Report the world's tick time in flight summaries
A `fly` benchmark replied with frame times only. With single player on its
server thread, the world's ticks no longer show in the frames, so a flight
could not tell how long the server took.

The summary now has tick_ms and tick_ms_max: the world's smoothed tick time
(the F3 server line) averaged and at its highest over the flight. On the
server thread it is the server's.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 17:15:24 +02:00
Ludovic MarieandClaude Opus 5.5 159a5dfa5f LAN: pressure plates, tripwires and detector rails see the guests
Block triggers counted only the host's own player and the minecarts, so a
guest stepping on a plate did nothing. The guests in the level, not
spectators, now trigger them through Entity.checkInsideBlocks and are
counted by the plates' entity queries. On the server thread no block
ticks run at all yet (#890).

lan_e2e.py plates: a stone plate under the guest, on both sides.

Part of #848

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 16:59:56 +02:00
rdbtCVS de6f15e664 Merge latest main and preserve surface-height and consumable changes 2026-10-18 15:53:28 +01:00
Ludovic Marie 6c078b2852 Merge pull request #874 from ludovic111/perf/server-tick-spikes
🤖 Agent: Merged. HORIZON_SPIKES logs slow server ticks with thread CPU time and spans; without it the tick runs exactly as before.
2026-10-18 16:50:21 +02:00
Ludovic Marie 2c9bfbb70a Merge pull request #888 from ludovic111/fix/integrated-owner-movement
🤖 Agent: Merged. Fixes #861: a ctl tp/fly move is sent even while the destination chunk isn't loaded, the single-player owner is exempt from the move check as in vanilla, and wait-ready waits for the level around the player. lan, dedicated_server, server_entities and server_dimension_players pass.
2026-10-18 16:50:18 +02:00
Ludovic Marie a9bf5dc8e5 Merge pull request #873 from ludovic111/perf/spawn-surface-heightmap
🤖 Agent: Merged. Natural spawning reads a WORLD_SURFACE heightmap kept on the chunk, as vanilla: column-height lookups 333/183 → 0/3 samples, natural spawning per tick 1.36/0.78 → 0.66/0.49 ms. 9 harnesses pass (natural_spawn, spawn_rules, heightmap, chunk_generation, chunk_stage, spawner, finalize_spawn, world_spawn, raid).
2026-10-18 16:50:15 +02:00
Ludovic Marie c5b6eed173 Merge pull request #872 from ludovic111/perf/structure-start-shards
🤖 Agent: Merged. Game thread waiting on the structure-start cache lock while exploring: 194/57 → 1/0 samples; structures_reaching 260/127 → 15/50. structure_density, worldgen, chunk_stage and chunk_generation pass.
2026-10-18 16:50:12 +02:00
Ludovic Marie e0b75a86b7 Merge pull request #799 from ludovic111/fix/consume-effects-vanilla
🤖 Agent: Merged. CI parity and verify passed, and on current main with this branch, horizon-client clippy is clean and all its tests pass (873 lib + integration). Consumables now finish as in vanilla 26.3.
2026-10-18 16:48:54 +02:00
rdbtCVS 378739703b Fix F3 debug overlay toggling in the main menu 2026-10-18 15:45:06 +01:00
Ludovic MarieandClaude Opus 5.5 7a6a4d9249 Server thread: horizon-ctl tp and fly make the server load the new chunks (#861)
With the world on its server thread, a horizon-ctl `tp` or `fly` moved the
window's player to chunks the server had not sent. The client skips its
tick while its own chunk isn't loaded, so it never sent the move, and the
server kept streaming the old area: "loading terrain..." forever.

- The client sends a move made where nothing is loaded (by horizon-ctl;
  a player who walks there has already sent it), so the server streams
  the chunks there.
- The integrated server doesn't check its owner's moves, as vanilla's
  `shouldCheckPlayerMovement` (`isSingleplayerOwner`); a long `tp` was
  "moved too quickly".
- `wait-ready` on a server also waits for the level around the player
  (`PlayerLoaded`); the status shows it as `server.loaded`.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 16:42:47 +02:00
Ludovic MarieandClaude Opus 5.5 5b47d53dbf LAN: guests use brewing stands
BrewingStandBlock opens BrewingStandMenu for a guest: the host clicks it on
the menu engine over the stand's slots (one bottle per slot, ingredient and
fuel slots, quick moves by item) and sends its four data slots
(BrewingStandBlockEntity.dataAccess). The guest opens minecraft:brewing_stand
menus, maps their slots, predicts clicks on Kind::Brewing and shows the
times. Reported by Tydragon00.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 16:41:07 +02:00
Ludovic MarieandClaude Opus 5.5 76e659c74b LAN: players see each other use items (eating, bows, shields)
Neither side sent LivingEntity.DATA_LIVING_ENTITY_FLAGS for players, so
no one saw another player eat, draw a bow or raise a shield. The host now
sends the item in use and its hand for itself and every guest (always:
the trackers send only the entries that changed, and stopping is a change
back to 0), and draws its guests' bodies using their items. A guest's
client starts and stops the other players' item use from the flags and
drops their eating crumbs at their mouth (Consumable.onConsumeTick); the
eating sounds already come from the server.

lan_e2e.py eating: the host and the guest each see the other eat bread,
and stop.

Part of #803

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 16:41:06 +02:00
Ludovic MarieandClaude Opus 5.5 89cbe03ebe LAN: everyone sees players' arm swings
The host ignored ServerboundPunchPacket, so a guest mining or hitting
swung only on its own screen; only the first swing a guest's use ever
started went out, the host's own swings never did, the host drew no swing
on guests' bodies and a guest's client ignored
ClientboundSwingAnimationPacket.

The host now swings the guest's hand on a punch (handlePunch), sends each
tick's guest swings to the players around and the host's own to the
guests, and swings and ticks the guests' bodies it draws. A use the server
ran that swung goes back to the guest too: Horizon's client does not run
uses itself, so it never predicted that swing (the hoe). The guest's
client swings itself or the other entity on the packet.

lan_e2e.py swings: mining swings seen from each side, and the guest's own
hoe swing.

Part of #848

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 16:41:06 +02:00
Ludovic MarieandClaude Opus 5.5 0bc19687aa LAN: guests see mobs flash when hit
The host told guests of a hurt mob with EntityEvent 2, which 26.3's
clients read as a kinetic hit, not a hurt. It now sends
ClientboundDamageEventPacket (ServerLevel.broadcastDamageEvent) with the
damage type and the attacker, and the guest's client restarts the hurt
flash and jolts the walk animation (LivingEntity.handleDamageEvent).

lan_e2e.py hurt: a pig the guest hits and one the host hits flash on
both sides.

Part of #848

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 16:41:06 +02:00
Ludovic MarieandClaude Opus 5.5 a49f93f9ef LAN: guests open hoppers, dispensers and droppers
The guest's client closed every menu it had no screen for, so hoppers
(MenuType.HOPPER) and dispensers and droppers (GENERIC_3x3) did nothing.
They open as their 5 and 9 slot screens now, drawn by the menu's type
and under the server's title.

lan_e2e.py machines: each opens with its slots and keeps a stack the guest
shift-clicks in.

Part of #848

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 16:41:06 +02:00
Ludovic MarieandClaude Opus 5.5 567cc33576 LAN: the guest's clock stops with /time pause and keeps up with the server's game time
The host sent the overworld clock's rate even while /time pause held it
(ServerClockInstance.packNetworkState sends 0 when paused), so a guest's
day kept running. A guest also moved its clock by the rounded rate each
client tick and never caught up when the server's game-time sync moved
game_time on, so a guest whose game lagged fell behind for good.
ClientClockManager.tick advances the clock by the game time passed, with
its partial tick; the guest now does the same.

lan_e2e.py time: the paused clock and a guest stalled for 4 seconds.

Part of #848

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 16:41:05 +02:00
Ludovic MarieandClaude Opus 5.5 164f1dfd52 Feed a remote client's chat lines to the agent's event log
A client joined to a server (and the window on the server thread) keeps the
server's chat lines in its own list. The event log only counted the local
chat's lines, so `wait-for chat` never saw a line from the server.

The remote client counts the lines it receives. The event log reads that
count and those lines while connected.

lan_e2e gets a `chat` check: a line the host sends reaches the guest's
event log, and on the server thread the host window's too.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 16:39:37 +02:00
rdbtCVS c296578a17 Remove startup verification screenshots 2026-10-18 15:38:01 +01:00
Ludovic MarieandClaude Opus 5.5 7b5a57be7c Carry: Repurposed's Nether city, Nether shipwreck and End stronghold
They are Repurposed's generic jigsaw with a start check of their own:
carried as minecraft:jigsaw with the piece-limited layout and the
check named in horizon:start_checks.repurposed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 16:37:24 +02:00
Ludovic MarieandClaude Opus 5.5 f3099e8340 Start checks: Repurposed's Nether city, Nether shipwreck and End stronghold rules
Repurposed Structures' city_nether, shipwreck_nether and stronghold_end
types are its generic jigsaw with one more start check each. The
`repurposed` key of horizon:start_checks names the check:
- city_nether: no block from sea level + 20 to sea level + 45 at the
  corners of the eight chunks around;
- shipwreck_nether: no solid, filled block in a 33x30x33 grid above the start;
- stronghold_end: land at least 45 above the bottom at 70 and 35 blocks
  in each direction and at the start.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 16:36:52 +02:00
Ludovic MarieandClaude Opus 5.5 db13bd7df5 Carry: the structure mods' spawner processors, as data the game runs
Carry used to take every processor written in Java out of its list (#822),
so spawners kept what the template has: an empty spawner in Moog's
templates, a placeholder pig in Better Dungeons'. The new processors module
rewrites the ones with a data form before that:

- Moog's Structure Lib's and Repurposed Structures' spawner randomizing
  processors become horizon:spawner_mobs, with Repurposed's rs_spawners list
  written in (weight-0 entries left out, as its loader does);
- YUNG's Better Dungeons and Better Desert Temples spawner processors (a
  spawner or a placeholder block becomes a spawner of a fixed mob) become
  vanilla's rule processor with an append_static block entity, the entity in
  26.3's shape (equipment, drop_chances);
- Moog's vanilla loot swap is a no-op while its "replace vanilla" preset is
  off, as they ship, and is taken out.

Each is counted in carry-report.json's processors_carried. The game's own
horizon: types stay in the lists.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 16:36:52 +02:00
Ludovic MarieandClaude Opus 5.5 2ebc1c44b3 Structures: horizon:spawner_mobs, the mods' spawner randomizing processors
Moog's Structure Lib and Repurposed Structures register a spawner
randomizing processor in Java: each spawner of the template gets one mob of
a weighted list, picked with settings.getRandom(pos).nextFloat(), and the
spawner NBT they write (Delay ... SpawnRange as shorts, the mob as the only
potential and the next spawn, optional custom spawn rules). With nothing to
pick the spawner becomes the replacement block.

horizon:spawner_mobs does the same from data; horizon-port-pack writes it in
place of theirs. Without it carry drops the processors, and Moog's templates
keep their spawners empty.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 16:36:46 +02:00
Ludovic MarieandClaude Opus 5.5 ae3622eb92 Processors: read rule ids without a namespace, as vanilla does
A rule processor's position predicate and block entity modifier types are
identifiers: vanilla reads "always_true" as minecraft:always_true. Horizon
matched the full string, so a list writing the short form failed, and with it
every pool naming the list. ChoiceTheorem's Overhauled Village writes
"always_true" in four village pools (plains, plains fortified, halloween), whose
houses then never generated.

The two decoders move out of Rule's decode into PosRule::decode and
Modifier::decode, which normalize the type first.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 16:36:46 +02:00
Ludovic MarieandClaude Opus 5.5 60bc15a63e Run the agent's commands, give and time on the server thread
With single player on its server thread, the window is a client of the
server. The agent's `command` went out as the owner's chat and returned no
output, so harnesses that read a command's feedback (lan_e2e trials) failed.
Control `give` and `time` changed only the window's copy of the world.

The server now runs them as single player's console does without a server
thread (`as_console`): as the world's owner, in the owner's level. The
feedback goes to the owner's chat and comes back to the agent. `give` runs
`give @s`, and `time` sets the server's clock too.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 16:28:48 +02:00
rdbtCVS 70c4b205ba Merge main and adapt progressive startup to current client APIs 2026-10-18 15:28:40 +01:00
Ludovic Marie 2b6ae71fb7 Merge pull request #877 from ludovic111/fix/launcher-fmt
🤖 Agent: Merged after the real verify passed (formatting only).
2026-10-18 16:25:48 +02:00
Ludovic Marie 720240feac Merge pull request #875 from ludovic111/fix/container-break-drops
🤖 Agent: Merged after #855 and the real verify. block_removal 4000/4000 including 319 mining cases; dispenser, crafter, composter, mace and use_item_on pass.
2026-10-18 16:25:33 +02:00
Ludovic MarieandClaude Opus 5.5 3a474c2060 Launcher: rustfmt modpacks.rs
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 16:20:32 +02:00
Ludovic Marie 176da24f2e Merge pull request #855 from Online5277/fix/loaded-container-drops
🤖 Agent: Merged. Fixes #844: mined containers spill their contents again (block_removal 4000/4000 with #875's mining cases). Thanks Online5277!
2026-10-18 16:20:26 +02:00
Ludovic Marie f257131bde Merge pull request #858 from ludovic111/fix/prism-prepare-libraries
🤖 Agent: Merged after the real verify passed. Prism's metadata renames native jars (same sha1), so prepare now also looks under Prism's name, still checked against Mojang's sha1, with nothing downloaded. Launcher and horizon-prepare tests pass.
2026-10-18 16:20:22 +02:00
rdbtCVS 409a014ff0 Report the progressive title RSS comparison and its limits 2026-10-18 15:20:09 +01:00
rdbtCVS 55cd4e90f4 Record startup phase, early-entry and memory verification 2026-10-18 15:20:09 +01:00
Ludovic MarieandClaude Opus 5.5 fcf3a3e267 spans: test that the thread CPU clock counts work, not sleep
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 16:17:15 +02:00
Ludovic MarieandClaude Opus 5.5 be03fec7fc Server ticks: HORIZON_SPIKES logs slow ticks with their CPU time and spans
With HORIZON_SPIKES=MS, a dedicated or integrated server tick longer than
MS ms is logged with the server thread's CPU time over the tick and the
tick's spans (tick_mobs, streaming, ...). A tick with much more wall time
than CPU time waited for a lock or for a core.

The spans recorded on a server thread were never taken before, so they
grew for as long as the server ran with HORIZON_SPIKES set.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 16:17:15 +02:00
Ludovic MarieandClaude Opus 5.5 e8247df5f0 Natural spawning reads a kept WORLD_SURFACE heightmap
Every spawn attempt (`NaturalSpawner.getRandomPosWithin`) asks for the
WORLD_SURFACE height of a column. Horizon scanned the column down for it
each time, a chunk-map lookup and a block-name compare per block. Vanilla
reads the chunk's heightmap.

Chunks now keep WORLD_SURFACE: built on the first read, and dropped by a
`set` that can move a column's surface (`Heightmap.update` leaves writes
two or more below it alone). Natural spawning, the spawn rules' bat check
and beacons read it.

The unused `heights` field (never written after `Chunk::empty`) is gone.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 16:16:27 +02:00
Ludovic MarieandClaude Opus 5.5 20d9c4c984 Pair a respawned player again for the other players
ServerLevel.removePlayerImmediately and addRespawnedPlayer: when a player
respawns, every other player tracking it gets RemoveEntities and the next
tick pairs it again as a new entity. The hub only cleared the respawned
player's own tracker, so the others kept the dead body, lying down, wherever
the player went (the host window too, on the server thread).

The in-frame LAN host's death now reaches its guests (LivingEntity.die's
EntityEvent DEATH), and its respawn goes through the same path.

lan_e2e gets a `respawn` check: the host dies and respawns (and on the
server thread the guest too), and the other side's client draws it dead,
then alive. `status` lists the other players a client draws dead.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 16:11:08 +02:00
Ludovic MarieandClaude Opus 5.5 62cacfa607 Mining: the block-removal harness mines blocks as a survival player
test_block_removal_parity gains a "mine" removal: VanillaDispenserOracle
calls the real ServerPlayerGameMode.destroyBlock for a survival player holding
a pickaxe, axe, shovel, sword, shears, a mace or nothing, and Horizon's
dispense fixture calls break_block, the path every player's mining takes (the
single player game, the server thread and LAN guests). It compares the drops,
the spilled contents, experience, sounds, events, statistics and the held
stack. On main it shows every mined container losing its contents (#844, fixed
by #855) and three gaps of Horizon's own, fixed here:

- ItemStack.mineBlock: an item with a tool component counts as used, and wears
  its damage_per_block (2 for swords, the mace and the trident), also the mace
  and trident, whose tool component has no rules. Shears wear on every block
  but fire (ShearsItem.mineBlock).
- DecoratedPotBlock.playerWillDestroy: a pot mined with an item in
  breaks_decorated_pots, unless an enchantment prevents it, cracks first and
  shatters.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 16:10:10 +02:00
Ludovic MarieandClaude Opus 5.5 43223c34e2 Structure starts: a sharded cache, so the server thread stops waiting on workers
The generator's structure-start cache was one Mutex<HashMap>. Each
generated chunk reads the 289 start chunks around it for its references,
and the server thread reads the starts of the chunks its mobs and spawn
attempts are in (`structures_reaching`). While workers generated, the
server thread spent most of its start lookups waiting for that lock.

The cache is now 64 RwLock shards keyed by a hash of the chunk: a lookup
read-locks one shard. Vanilla keeps the starts on each chunk
(`StructureManager.getStartForStructure` -> `chunk.getStartForStructure`),
so its lookups share no lock either. The first starts built for a chunk
are still the ones everyone keeps.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 15:58:20 +02:00
Ludovic Marie 6e09dbe10a Merge pull request #871 from ludovic111/perf/waypoint-pairs
🤖 Agent: Merged after the real verify passed. Waypoints::tick −33% (166 → 111 µs median), with byte-identical packets (530,834, same hash). locator_bar, lan, dedicated_server and server_dimension_players pass.
2026-10-18 15:50:09 +02:00
Ludovic Marie 2fd82a0c2c Merge pull request #864 from ludovic111/perf/tracking-sections
🤖 Agent: Merged. Entities::tick −37% (112–116 → 71–75 µs median per tick at 40 bots + 570 entities), with the same 14,159,154 packets before and after. 9 covering harnesses pass (server_entities, lan, dedicated_server, server_dimension_players, locator_bar and the entity ones); main has no lan/ changes since.
2026-10-18 15:46:03 +02:00
Ludovic Marie 7b8d8a604d Merge pull request #870 from ludovic111/integ/mp6
🤖 Agent: Merged after the real verify passed. Combined check on this branch: fmt, clippy, workspace tests, and the worldgen, structure_density, chunk_generation and world_spawn harnesses pass. modpacks-6's own integration passed 298/298 harnesses with the start oracle at 35/35.
2026-10-18 15:41:42 +02:00
Ludovic Marie ac287c78c5 Merge pull request #867 from ludovic111/perf/mesh-queue-columns
🤖 Agent: Merged. Mesh scheduling while flying at RD32: 0.62/0.72 → 0.36/0.22 ms per frame; at rest with ~12,000 waiting sections: 0.30–0.91 → 0.06–0.22 ms. Same checks as #865.
2026-10-18 15:36:44 +02:00
Ludovic Marie 6ad66df8d0 Merge pull request #866 from ludovic111/perf/light-dirty-sections
🤖 Agent: Merged. Light changes mark dirty sections without the per-frame sort and dedup (0.05–0.09 ms → 0). Same checks as #865.
2026-10-18 15:36:40 +02:00
Ludovic Marie 5620e4f1d1 Merge pull request #865 from ludovic111/perf/compact-terrain-vertex
🤖 Agent: Merged. Terrain vertices go 28→24 B: vertex memory −14%, Metal GPU 0.62→0.54 ms. HD 620 FPS is unchanged (vertex size isn't its bottleneck). Screenshots: the only differing pixels are on water, at main-vs-main noise level, with identical drawn sections. 95/95 covering harnesses on an integration with #866 and #867.
2026-10-18 15:36:37 +02:00
Ludovic MarieandClaude Opus 5.5 8bf73f89d4 Locator bar: look up each connection and its two players once
A moving player updates its connection with every other player, three
times a tick (updateWaypoint, then updatePlayer at the position change and
at ServerChunkCache.move), so with 40 players there are about 4800
updates a tick. Each looked the connection up twice and both players up
four to six times. Now an update finds the connection's place once and
resolves the two players once, then checks, updates or remakes it there.

The same packets go out, in the same order.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 14:57:06 +02:00
Ludovic MarieandClaude Opus 5.5 b171508c0f Server: check each entity's range for every client from one compact pass
Entity tracking still asks every client about every entity each tick
(vanilla re-checks each pair whenever the player sends a move, so for
moving players it does the same), but the check is cheaper:

- each entity's position, id, range and chunk are read once a tick into
  a compact list, not from its EntityState once per client;
- each client looks up each of the tick's chunks once in the chunks it
  holds, not once per entity in it;
- an entity with no changes and no client starting to track it skips the
  loop over the clients.

The decisions and the packets are the same, in the same order.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 14:49:31 +02:00
Ludovic Marie 6d238a89c5 Merge pull request #862 from ludovic111/fix/streaming-meshes-remote-lit
🤖 Agent: Merged after the real verify passed; the streaming_meshes tests pass locally with resources.
2026-10-18 14:45:55 +02:00
Ludovic MarieandClaude Opus 5.5 dd9f8a9088 Server: send a player standing up again, and draw other players' crouch
A value back at its default is left out of an entity's state, so the
tracker never told clients that a player stopped crouching or a mob stopped
burning: Entity's shared flags and pose now go back to 0. On a client of a
server (the host window on the server thread), another player's crouch flag
now reaches their model and name tag.

From #731's server-thread list; lan_e2e's walls check passed in-frame and
failed with --server-thread.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 14:43:55 +02:00
Ludovic MarieandClaude Opus 5.5 6bbcf8e3d6 streaming_meshes test: remote columns arrive lit since #824
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 14:39:34 +02:00
Ludovic Marie 4f9df73831 Merge pull request #860 from ludovic111/perf/save-compress-helpers
🤖 Agent: Merged after the real verify passed. Output is byte-identical (palettes / region files), and 17 covering harnesses passed with both PRs on main 256f7c20 (since then main has only #751, a settings UI change).
2026-10-18 14:10:03 +02:00
Ludovic Marie 47aaf3a015 Merge pull request #859 from ludovic111/perf/palette-ids-table
🤖 Agent: Merged after the real verify passed. Output is byte-identical (palettes / region files), and 17 covering harnesses passed with both PRs on main 256f7c20 (since then main has only #751, a settings UI change).
2026-10-18 14:09:58 +02:00
Ludovic MarieandClaude Opus 5.5 3e825eaa40 Prepare: find native jars where Prism Launcher keeps them (#849)
Prism's metadata moves a native jar's classifier into its artifact name
(com.mojang:jtracy:1.14.38:natives-windows becomes
com.mojang:jtracy-natives-windows:1.14.38) and Prism stores libraries at their
name's path, so every native jar of a Prism 26.3 install is in
com/mojang/jtracy-natives-windows/1.14.38/jtracy-natives-windows-1.14.38.jar,
not at the version JSON's path. The bytes and sha1 are Mojang's. Preparing
from a linked Prism folder stopped on the first one (jtracy on Windows).

horizon-prepare now looks there when the version JSON's path is missing; the
copy is still checked against the version JSON's sha1. The missing-file hints
no longer tell players of other launchers to install the official one.

Reported by famescare on Discord.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 13:51:33 +02:00
Ludovic Marie 831cd657de Merge remote-tracking branch 'origin/docs/modpacks-structure-libraries' into integ/mp6 2026-10-18 13:23:07 +02:00
Ludovic Marie 7dacd43706 Merge remote-tracking branch 'origin/feat/carry-piece-limited' into integ/mp6 2026-10-18 13:23:06 +02:00
Ludovic Marie 643dccfa42 Merge remote-tracking branch 'origin/feat/piece-limited-jigsaw' into integ/mp6 2026-10-18 13:23:06 +02:00
Ludovic Marie 6a5e3a836d Merge remote-tracking branch 'origin/feat/carry-yung-placements' into integ/mp6 2026-10-18 13:23:06 +02:00
Ludovic Marie d5ab26f1d9 Merge remote-tracking branch 'origin/feat/yung-placements' into integ/mp6 2026-10-18 13:23:06 +02:00
Ludovic Marie 32be8bac0a Merge remote-tracking branch 'origin/feat/carry-yung-jigsaw' into integ/mp6 2026-10-18 13:23:06 +02:00
Ludovic Marie eae7688378 Merge remote-tracking branch 'origin/feat/yung-jigsaw-game' into integ/mp6 2026-10-18 13:23:06 +02:00
Ludovic Marie 6d15bad156 Merge remote-tracking branch 'origin/fix/large-structure-templates' into integ/mp6 2026-10-18 13:23:05 +02:00
Ludovic Marie 2d8d55942c Merge remote-tracking branch 'origin/feat/carry-structure-set-additions' into integ/mp6 2026-10-18 13:23:05 +02:00
Ludovic Marie 2efa0d65b5 Merge remote-tracking branch 'origin/feat/mod-structure-set-additions' into integ/mp6 2026-10-18 13:23:05 +02:00
Ludovic MarieandClaude Opus 5.5 bfa622bbde Server: send entity attributes to clients
ClientboundUpdateAttributesPacket, as vanilla's ServerEntity sends it: every
client-syncable instance when an entity is paired, the changed ones with the
dirty entity data, and a player's dirty set to the player itself.

- Mobs and armor stands report the instances vanilla would have created
  (movement speed, finalizeSpawn's bases and modifiers, the baby modifier,
  effects', equipment's and /attribute's), players their LiveAttributes.
- Guests get the per-tick movement speed instance and the creative reach
  modifiers (ServerPlayer.updatePlayerAttributes).
- cube::set_size sets the attribute bases as AbstractCubeMob.setSize does,
  so a summoned slime has its size's health and speed.
- server_entities compares attributes again, the player's own too, with a
  slime and a scaled pig in its scene.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 13:21:13 +02:00
Ludovic MarieandClaude Opus 5.5 0bc5f4263c save_bench: time the region writer on a saved world's chunks
Reads every chunk of a saved dimension, then times SaveDir's save_later and
flush against the same encoding and compression done on one thread, and
checks the written chunks byte for byte.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 12:38:04 +02:00
Cosmic 8f8db8096b Preserve non-heart block entities until normal removal 2026-10-18 06:26:49 -04:00
Ludovic MarieandClaude Opus 5.5 2ec71e5387 Mesh scheduling looks at dirty sections column by column
Sections waiting to be remeshed were one hash set that the render thread
walked section by section every frame, though every check (column loaded,
in view, its 3x3 lit, its region) holds for a whole column. At RD 32 about
12,000 sections wait at the edge of the lit area at any time. They are now
kept per column (a bit per section index), so each frame looks at each
column once and meshes all of its ready sections together.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 12:23:32 +02:00
Ludovic MarieandClaude Opus 5.5 6bf479ec0b Light changes dirty sections by section, not cell by cell
Relighting block edits and relit chunks collected every changed cell,
sorted and de-duplicated them, then marked the sections around each cell
for remeshing. Vanilla's light engine reports changed sections to
LevelRenderer instead. LightChanges now keeps, per changed section, which
of 27 border classes (low face, inside, high face per axis) its changed
cells fall in; that gives exactly the same dirty sections and light log
without keeping or sorting the cells.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 12:23:32 +02:00
Cosmic 323002cf98 Fix multiplayer chat scrolling through the shared chat component 2026-10-18 06:20:57 -04:00
Ludovic MarieandClaude Opus 5.5 df58a474fb Terrain vertices in 24 bytes: the UV as 16-bit fixed point
TerrainVertex goes from 28 to 24 bytes: the UV becomes two u16 in 32768
steps per unit, which hold model UVs (multiples of 1/16 and their halves)
exactly and decode exactly in terrain.wgsl. The position stays a float:
fixed point moves the edges of offset plants and rotated elements by a
fraction of a pixel.

Section vertex memory drops 14% (RD 16: 380 -> 325 MB, arena 503 -> 369 MB);
pixels are unchanged on Metal and Vulkan outside animated water.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 12:09:07 +02:00
Ludovic Marie e508a5ce1d Merge pull request #751 from LookImEcho-Rick/codex/issue-582-settings-search
🤖 Agent: Merged. CI parity passed 94 of 95. The one miss, tooltip_display, passes locally on main with this branch, and so does options. Thanks LookImEcho-Rick!
2026-10-18 12:04:59 +02:00
Ludovic MarieandClaude Opus 5.5 d1db8c4a54 Region writer: encode and compress a batch's chunks on helper threads
The writer thread encoded (NBT) and zlib-compressed every chunk of a batch
one after another before writing them. A batch of at least 8 chunks is now
shared out to scoped helper threads (at most half the hardware threads),
which take the next chunk as they finish one. The results are put back in
submission order and written exactly as before, so the region files are
byte-identical: each chunk's bytes depend only on the chunk, and sector
allocation follows the same write order.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 12:01:28 +02:00
Ludovic MarieandClaude Opus 5.5 fdee6576c6 Paletted containers: build palettes without hashing
palette_ids (behind PalettedContainer::pack, every saved and sent section,
and from_values, every generated one) scanned the palette for each of the
4096 values while it had up to 16 entries, then hashed every value with
SipHash. A run of one value is now matched once, and past 16 entries a
thread-local table indexed by value (block states and biomes are below
65536) replaces the hash map; only the palette's entries are set, and they
are cleared before returning. The output is unchanged: palette in
first-occurrence order and the same ids.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 12:01:12 +02:00
Ludovic MarieandClaude Opus 5.5 1b6a3cf535 Docs: modpacks, the structure libraries Horizon now runs
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 11:41:27 +02:00
Ludovic Marie 256f7c200a Merge pull request #842 from ludovic111/perf/waypoint-lookups
🤖 Agent: Merged. Waypoints::tick is 3.5× cheaper with the same packets in the same order. Same checks as #841.
2026-10-18 11:36:36 +02:00
Ludovic Marie 96a1e1cb24 Merge pull request #841 from ludovic111/perf/tracker-dirty-data
🤖 Agent: Merged. tick_lan is 40–55% cheaper and MSPT about 20% lower (40 bots, ~600 entities). The agent ran 95/95 harnesses (--changed) with #842 on main 4b43242a. The orchestrator then ran clippy and the lan tests on current main with both: clean.
2026-10-18 11:36:33 +02:00
Ludovic Marie 798249a212 Merge pull request #781 from rdbtCVS/t3code/2a9d6cd1
🤖 Agent: Merged. light_chunk is 28% faster, light parity shows 0 differing values against vanilla 26.3, and CI parity passed on a merge with main after #824's light changes. Thanks rdbt!
2026-10-18 11:36:23 +02:00
Cosmic dde52bc1e9 Merge remote-tracking branch 'origin/main' into feat/terrain-lod
# Conflicts:
#	crates/horizon-client/src/gfx.rs
2026-10-18 05:31:37 -04:00
Cosmic fde95ff20e Build distant terrain from sparse seed samples and layered voxel columns 2026-10-18 05:30:56 -04:00
Ludovic Marie f0c7df2000 Merge pull request #836 from LookImEcho-Rick/codex/falling-block-collision-parity
🤖 Agent: Merged. Reviewed against vanilla: landing comes from Entity.move's vertical collision, the landing position is blockPosition(), velocity is zeroed per axis, and placement goes through Level.isUnobstructed with the collision shape. Locally on this head, fmt and clippy are clean, and contextual_collision, level_place, placement, powder_snow_collision and world_movement all PASS. Thanks LookImEcho-Rick!
2026-10-18 11:16:57 +02:00
Ludovic MarieandClaude Opus 5.5 512962e3ca Carry: Repurposed Structures' and Moog's jigsaws keep their layout rules
Their jigsaw structures, the Nether variants too, get a
horizon:piece_limited key for the game's piece-limited assembler:
y_allowance, burying_type, cannot_spawn_in_liquid, the pools whose
fallbacks ignore the bounds, the Nether variants' land search and ledge,
and each structure's piece counts from the mod's rs_pieces_spawn_counts
or msl_pieces_spawn_counts files. use_bounding_box_hack becomes vanilla's
use_expansion_hack. Lithostitched's jigsaw stays vanilla's.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 11:11:40 +02:00
Ludovic MarieandClaude Opus 5.5 aab43c46ae Carry: YUNG's placements, and the vanilla structures its mods turn off
YUNG's enhanced random spread becomes vanilla's random spread with its
enhanced exclusion zone as horizon:exclusion_zone; the desert and jungle
temples' placements add horizon:avoid_biomes (no ocean or river within 48
blocks of the chunk's middle at y 120); Better Strongholds' adds
horizon:ring_sections. A YUNG's structure the game can start gets
horizon:disables with the vanilla type its mod's mixin turns off; Better
Mineshafts, a Java structure type, leaves vanilla's mineshafts in place.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 11:10:58 +02:00
Ludovic MarieandClaude Opus 5.5 f57c59a971 Structures: Repurposed Structures' and Moog's piece-limited jigsaw assembler
A jigsaw structure with "horizon:piece_limited" is laid out as
PieceLimitedJigsawManager does (the same code in both mods): a fresh
random seeded by the chunk, the start piece drawn uniformly and moved to
the surface at its center, a volume 120 blocks down and 180 up, children
drawn by weight from the pools' entries in order, pieces capped per
template and some required (retried up to 40 times), pools whose
fallbacks ignore the bounds, y_allowance clipping. After the layout, the
structure's postLayoutAdjustments: the pieces centred on the chunk corner,
then buried (lowest corner, average land, lowest side) or, for the Nether
variants, moved onto the highest or lowest land or a fixed height.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 11:08:03 +02:00
Ludovic Marie 845a122faa Merge pull request #758 from rdbtCVS/perf/portal-section-mask
🤖 Agent: Merged. CI parity passed 88 of 89 harnesses. The one miss, dedicated_server (a known intermittent failure), passes locally on this head. Thanks rdbt!
2026-10-18 11:01:13 +02:00
Ludovic MarieandClaude Opus 5.5 54bae6131b feat(server): commands act on other players (names, UUIDs, @a/@p/@r/@s)
EntitySelector.findPlayers and EntityArgument's player targets on a
hosted world or dedicated server: every guest other than the one in place
is Target::Guest, found by name, UUID or selector, and what a command does
to them (give, gamemode, tp, kill, effect give/clear, data) runs with
their state swapped in, their own messages going to their chat. The
console, operators and command blocks can all target them; execute
as/at reads their position, rotation and eye height; a teleport into
another level (execute in, the console's Overworld, a destination in
another level) moves them there.

as_guest splits into enter_guest/leave_guest so that guest turns nest,
and a guest's commands run with the hub back in self.lan (queued per
packet), so they reach the other players (and /kick works for ops).
Item and advancement names and player names in feedback take vanilla's
component shape; /kill on a guest sends vanilla's death message.

Harness: tools/test_server_player_targets_parity.py.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 10:39:02 +02:00
Ludovic MarieandClaude Opus 5.5 4db511079d Locator bar: update each connection in place, without clones
ServerWaypointManager's per-tick work for every moving player copied each
connection out of its row (its icon's style string too), scanned the row
again to put it back, copied the lists of players and waypoints it walked,
and looked players up through SipHash. Connections are now updated where
they are (the same row order and the same packets), the lists are walked
by index and the maps use FxHash.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 10:28:17 +02:00
Ludovic MarieandClaude Opus 5.5 c63fa74239 Server: track each entity once for all its clients, as vanilla's ServerEntity
Every guest had its own copy of what it was last told about every entity,
so each tick compared each entity's data and built and encoded its update
packets once per guest. A level now keeps one ServerEntity state per
entity (ChunkMap.entityMap), whose changes are worked out once a tick,
encoded once and sent to every guest tracking it; a guest's Tracker is the
set of entities it is paired with. The update cadence is the entity's own
tickCount, as vanilla's, and late clients are paired with the data the
others hold. The per-tick copy of every entity state, the id lists and the
packet lists are gone too.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 10:28:16 +02:00
Ludovic MarieandClaude Opus 5.5 c7c9725942 Structures: YUNG's placements and their vanilla counterparts turned off
Three keys a carried mod's data can hold, which vanilla data never has:
- "horizon:ring_sections" on a random spread (BetterStrongholdsPlacement):
  starts only in the odd rings around the origin, with Java's int math.
- "horizon:avoid_biomes" (BetterDesertTemplePlacement and
  BetterJungleTemplePlacement): no start when one of the biomes is found
  around the chunk's middle block, as findBiomeHorizontal samples it.
- "horizon:disables" on a structure: every structure of the listed types
  fails to start, as YUNG's DisableVanilla*Mixin makes
  ChunkGenerator.tryGenerateStructure return false; in a set of several
  structures the next one is tried, as in vanilla.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 10:22:22 +02:00
Ludovic Marie 4b43242a61 Merge pull request #835 from ludovic111/perf/decoration-order-grid
🤖 Agent: merged: decoration_order::vanilla_order on flat arrays (RD32 8.14→1.06 ms per call; ticket order unchanged, checked against the old version). Combined on current main with #834/#835: clippy, client + world tests PASS.
2026-10-18 10:21:16 +02:00
Ludovic Marie 9a71700d51 Merge pull request #834 from ludovic111/perf/section-upload-belt
🤖 Agent: merged: section uploads through one reused staging buffer and one encoder, as vanilla's SectionRenderDispatcher (RD32 exploring: FPS 43.5→56.0, CPU 23.7→18.6 ms/frame, p99 122→106 ms; sections drawn identical in 12 views). Combined on current main with #834/#835: clippy, client + world tests PASS.
2026-10-18 10:21:11 +02:00
Ludovic Marie 6fff54b95d Merge pull request #837 from ludovic111/fix/tooltip-display-harness-bools
🤖 Agent: merged the tooltip_display harness fix (lowercase case flags; harness PASS).
2026-10-18 10:11:18 +02:00
Ludovic MarieandClaude Opus 5.5 9dd896bfc3 tooltip_display harness: write the case flags in lowercase
Rust's bool parser takes only true/false, so the fixture failed on Python's
True/False; Java's Boolean.parseBoolean ignores case.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 10:10:57 +02:00
LookImEcho-Rick 7ddb4ddae5 Fix falling block collisions with partial blocks 2026-10-18 03:02:55 -05:00
Ludovic MarieandClaude Opus 5.5 4676ea2819 Carry: YUNG's jigsaws and pieces with their rules
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 09:54:51 +02:00
Ludovic MarieandClaude Opus 5.5 3c08f74d79 Structures: YUNG's API's jigsaw assembler for carried structures
A jigsaw structure with "horizon:yungsapi" (min_y/max_y) assembles the way
YUNG's API's JigsawManager does: the start piece by priority then weight
from the shuffled raw entries, children breadth-first, pool or fallback per
depth, and per-element rules from the element's "horizon:yungsapi": a name
capped by max_count, a depth range, a priority, ignore_bounds and a
condition (true, false, not, all_of, any_of, depth, altitude,
random_chance, rotation, piece_in_horizontal_direction).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 09:54:51 +02:00
Ludovic MarieandClaude Opus 5.5 571066158e Structure templates read without a node cap, as vanilla does
Vanilla reads structure templates with NbtAccounter.unlimitedHeap()
(TemplateSource.readStructure). Horizon used the default 1,000,000-node
cap, so a template past it loaded as empty: YUNG's Better Ocean
Monuments' start piece (115x64x126 blocks) never started. Limits::templates
keeps the 256 MiB byte cap and drops the node cap, for worldgen and /place.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 09:54:31 +02:00
Ludovic Marie c1b4ccd571 Merge pull request #824 from ludovic111/perf/chunk-light-sync
🤖 Agent: merged: LAN guests and the server-thread game keep each chunk's light and get vanilla light updates (travel CPU −17%, light_chunk −35%; lan_e2e light PASS with and without --server-thread).
2026-10-18 09:46:11 +02:00
Ludovic Marie f845e26050 Merge pull request #829 from ludovic111/perf/anim-tick-hash
🤖 Agent: merged: block-entity animation list rebuilt only when drawn block entities change (anim tick −82% with 32,768 chests). perf-ticks-7's four PRs together on main d85a3603: clippy, 861 client + world tests, 19 harnesses (light, lan, dedicated_server, save_roundtrip, world_reopen, disk, datafix ×6, world, server_world_gen, block_entity ×4, chunk_stage) PASS.
2026-10-18 09:45:28 +02:00
Ludovic Marie 5177fb3601 Merge pull request #828 from ludovic111/perf/brewing-ticker
🤖 Agent: merged: brewing stands tick in the level's single ticker list, in vanilla's order. perf-ticks-7's four PRs together on main d85a3603: clippy, 861 client + world tests, 19 harnesses (light, lan, dedicated_server, save_roundtrip, world_reopen, disk, datafix ×6, world, server_world_gen, block_entity ×4, chunk_stage) PASS.
2026-10-18 09:45:24 +02:00
Ludovic Marie c717628169 Merge pull request #827 from ludovic111/perf/region-in-place
🤖 Agent: merged: chunks saved in place in their region file, as vanilla (save into a full region 80.3→7.1 ms). perf-ticks-7's four PRs together on main d85a3603: clippy, 861 client + world tests, 19 harnesses (light, lan, dedicated_server, save_roundtrip, world_reopen, disk, datafix ×6, world, server_world_gen, block_entity ×4, chunk_stage) PASS.
2026-10-18 09:45:19 +02:00
Ludovic Marie e93c99a4c7 Merge remote-tracking branch 'origin/main' into fix/pr751-merge
# Conflicts:
#	crates/horizon-client/src/options_horizon.rs
2026-10-18 09:44:42 +02:00
Ludovic MarieandClaude Opus 5.5 8901a49d0b Upload section meshes through a reused staging belt
Queue::write_buffer creates a staging buffer for every call: on Metal a
kernel call to make the buffer, then page faults while the mesh is copied
into fresh memory. While exploring at render distance 32 the render thread
made two such writes per uploaded section, plus one for its flat-draw ids.

Section meshes and flat ids now go through their own StagingBelt (4 MiB
mapped buffers, reused once the GPU has read them) into one encoder that
the frame's submission runs ahead of its own commands. A page of the
section arena that grows records its copy into that encoder too, so copies
recorded before the grow reach the grown buffer. A frame that is not drawn
still submits the copies, and more than 64 MiB of them are submitted early.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 09:40:55 +02:00
Ludovic MarieandClaude Opus 5.5 817a71ffa5 Clients keep the light the server sends; the server sends light updates
A remote client (a LAN guest, or the game on its server thread) threw away
the light in each LevelChunkWithLight and relit the column and its
neighbours itself. It now applies the sky and block layers as
ClientPacketListener.applyLightData does, and relights only block changes,
as vanilla's client does.

The hub now sends ClientboundLightUpdatePacket for the light sections that
changed in a column to the players for whom it is on the tracked border
(ChunkHolder.broadcastChanges), so light from blocks a guest doesn't track
reaches it.

horizon-ctl gains 'light X1 Y1 Z1 X2 Y2 Z2', and lan_e2e a 'light' check
comparing the guest's light with the host's.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 09:39:50 +02:00
Ludovic Marie d85a3603bd Merge pull request #761 from rdbtCVS/t3code/9960489f
🤖 Agent: merged rdbt's paletted container access (BlockSection::set 1.12x; identical palette, counts and NBT; CI parity green).
2026-10-18 09:33:45 +02:00
Ludovic MarieandClaude Opus 5.5 d8f81f7a98 Carry: lithostitched set and pool additions, and CTOV's villages
Lithostitched's add_structure_set_entries and add_template_pool_elements
modifiers become horizon:add_to files under worldgen/<kind>/horizon_added/,
upgraded as the mod's own pools are. Their load conditions (lithostitched's
predicate, NeoForge's, else Fabric's) are checked against the mods ported
along; other modifier types and tag targets are counted, not carried.

ChoiceTheorem's Overhauled Village adds its villages and outposts to
vanilla's sets in Java code; carry writes them with its default config
(small, medium and large villages weighted 10, 4 and 1, outposts 1).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 08:56:01 +02:00
Ludovic MarieandClaude Opus 5.5 1ccc41c8b2 Content mods: add to other structure sets and pools (horizon:add_to)
A content mod's structure set or template pool file holding
"horizon:add_to" appends its entries to the sets or pools it names
(vanilla's, another mod's) instead of being one of its own, as
lithostitched's add_structure_set_entries and add_template_pool_elements
modifiers do on NeoForge and Fabric. The content root merges them after
every mod's own files, in mod and file order; a name nobody defines is
skipped. Vanilla data never has the key.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 08:56:01 +02:00
Ludovic Marie 1d937524a9 Merge pull request #826 from ludovic111/docs/modpacks-structures
🤖 Agent: merged: docs/modpacks.md. Combined check of main + modpacks-5's six: clippy, tests, worldgen structure_density block_properties registry_fluid chunk_stage PASS.
2026-10-18 08:53:03 +02:00
Ludovic Marie 6da2d8ebcc Merge pull request #825 from ludovic111/perf/facts-registry-stream
🤖 Agent: merged: facts.json streamed into the block registry (registry peak 816→153 MB at ATM10 size). Combined check of main + modpacks-5's six: clippy, tests, worldgen structure_density block_properties registry_fluid chunk_stage PASS.
2026-10-18 08:52:58 +02:00
Ludovic Marie 3415246998 Merge pull request #820 from ludovic111/feat/structure-start-checks
🤖 Agent: merged: Repurposed and Moog jigsaw start rules (Repurposed 1→13 starts, MVS 829→90, failing pools 65→0, vanilla oracle 35/35). Combined check of main + modpacks-5's six: clippy, tests, worldgen structure_density block_properties registry_fluid chunk_stage PASS.
2026-10-18 08:52:53 +02:00
Ludovic Marie c741bde15f Merge pull request #822 from ludovic111/feat/carry-drop-java-processors
🤖 Agent: merged: Java-only processors dropped and counted (1,427). Combined check of main + modpacks-5's six: clippy, tests, worldgen structure_density block_properties registry_fluid chunk_stage PASS.
2026-10-18 08:52:48 +02:00
Ludovic Marie 166b7f8d05 Merge pull request #821 from ludovic111/feat/carry-structure-rules
🤖 Agent: merged: carry writes the mods' structure start rules and Moog's spacing. Combined check of main + modpacks-5's six: clippy, tests, worldgen structure_density block_properties registry_fluid chunk_stage PASS.
2026-10-18 08:52:44 +02:00
Ludovic Marie 73c726759e Merge pull request #816 from ludovic111/fix/carry-worldgen-block-states
🤖 Agent: merged: worldgen block states rewritten to 26.3's format (1,005 files in 17 mods). Combined check of main + modpacks-5's six: clippy, tests, worldgen structure_density block_properties registry_fluid chunk_stage PASS.
2026-10-18 08:52:39 +02:00
Ludovic Marie eb3f2b94ca Merge pull request #831 from ludovic111/feat/secure-chat
🤖 Agent: merged secure chat as vanilla 26.3's server (chat sessions, signed chat and commands, enforce-secure-profile; secure_chat harness identical to vanilla offline and online; 7 server harnesses pass).
2026-10-18 08:40:05 +02:00
Ludovic Marie dc206d6132 Merge pull request #830 from ludovic111/fix/server-thread-view-distance
🤖 Agent: merged: the integrated server follows the window's render distance past 32 (fixes #804), and the lan_e2e hazards check now finds the guest by name (the server-thread hunger gap was a test bug).
2026-10-18 08:39:19 +02:00
Ludovic MarieandClaude Opus 5.5 84d3d2a9a0 Rerun CI (flaky options_ui test, #802)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 08:14:28 +02:00
Ludovic MarieandClaude Opus 5.5 a2363a3bf6 fix(server-thread): the integrated server follows the render distance, past 32 too
IntegratedServer.tickServer: the server takes the window's render and
simulation distances when they change ("Changing view distance to N, from
M"), tells every player (SetChunkCacheRadius, SetSimulationDistance), and
each player's chunks follow, up to what they asked for. The integrated
server no longer clamps the view distance to 32, and a remote client sends
its render distance again when it changes (Options.broadcastOptions),
past 32 too. Fixes #804.

lan_e2e: a distance check (--server-thread), and the guest is found by
name in lan status, where the server thread lists the owner too: the
hazards check read the owner's food (the "no hunger from moving" gap).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 08:09:49 +02:00
Ludovic MarieandClaude Opus 5.5 8f3d35ab6d Block entity animations: read the drawn block entities only when they change
tick_block_entity_anims rebuilt a map of every drawn block entity, sorted
it and swept the animation states against it every tick. The renderer now
keeps a revision that changes when its block entities do, and the sorted
ticker list is rebuilt only then. The animation states use FxHashMap.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 07:57:10 +02:00
Ludovic MarieandClaude Opus 5.5 454b3842bb feat(server): secure chat, as vanilla 26.3's server
Chat session updates with their profile key checked against the services'
keys, signed chat and the message chain, last-seen acknowledgements, the
signature cache, signed say/me/msg, enforce-secure-profile, and vanilla's
log lines. Offline servers and players without a chat session chat
unsigned, as on vanilla. tools/test_secure_chat_parity.py compares both
servers with test keys and a mock services server.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 07:52:46 +02:00
Ludovic MarieandClaude Opus 5.5 05517a167f docs/modpacks.md: structure carry-over now decodes and starts where the mod would; facts.json streams
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 07:40:41 +02:00
Ludovic MarieandClaude Opus 5.5 d0d5e79f1a Structures: honour a carried mod's start and placement rules
Repurposed Structures' and Moog's jigsaws check biomes and terrain height in
a radius and refuse to start in liquid; their spreads keep a minimum
distance from the world origin and away from several other sets. Carry maps
them to vanilla's jigsaw and random spread and writes those rules under
horizon: keys; the game now runs them (start_checks.rs, the spread's
min distance and exclusion sets). Vanilla data has none of the keys.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 07:38:59 +02:00
Ludovic MarieandClaude Opus 5.5 65732406fc Carry: take processors written in Java out of their lists
The game, like vanilla, refuses a processor list holding a type it does not
know, and with it every pool naming the list, so those structures did not
generate at all. Carry now takes the library types out of processor lists
(files and inline ones in pool elements) and counts them by type in the
carry report: the structures generate, undecorated. On ATM10's carried
mods: 1,427 processors of 146 types; pool decode failures 31 -> 0.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 07:38:10 +02:00
Ludovic MarieandClaude Opus 5.5 f72c8674ad Carry: the start and placement rules of mapped jigsaws
Repurposed Structures' and Moog's jigsaws and spreads add rules on top of
vanilla's; mapped to vanilla they were lost, and Moog's spreads structures
1.65 times as far apart as its files say, so MVS alone started 9x as often
as on NeoForge. Carry now writes the start rules under
horizon:start_checks, the minimum distance and the super exclusion zone's
sets (from the mod's structure set tags) under horizon: placement keys,
and scales Moog's spacing. Layout rules (piece clipping, burying) are still
lost and counted in the carry report's rules_lost.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 07:38:10 +02:00
Ludovic MarieandClaude Opus 5.5 7d27950e31 Carry: write worldgen block states in 26.3's shape
1.21.1 writes a block or fluid state in worldgen JSON as {"Name", "Properties"};
26.3's codec reads {"id", "properties"} and refuses the old shape, so every
processor list, feature or pool holding one failed to decode. Carry now
rewrites them. On ATM10's carried mods: 1,005 files in 17 mods.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 07:38:09 +02:00
Ludovic MarieandClaude Opus 5.5 9190d14b7f Block registry: read facts.json as it streams
The game and the assets step each parsed facts.json into a JSON tree, then
copied each state into an NBT tree, before building the registry. With a
pack's 214,000 states each step peaked at 816 MB for a 139 MB registry.
The registry now deserializes the rows it needs straight from the file
(each row's state and its air and fluid columns, and the defaults):
registry peak 816 -> 153 MB, assets peak 816 -> 235 MB, RSS after the
assets step 1,344 -> 442 MB. Vanilla: 138 -> 27 MB.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 07:35:28 +02:00
Ludovic Marie 5b62d771bb Merge pull request #823 from ludovic111/integ/mp-sh
🤖 Agent: merged modpacks-4's #753–#765 and server-host-5's #759–#790, combined on main and checked locally (clippy, workspace tests, 9 harnesses).
2026-10-18 07:25:41 +02:00
Ludovic MarieandClaude Opus 5.5 2932704156 Region files: write each saved chunk in place, as vanilla's RegionFile
Saving a chunk read every chunk of its region file and wrote them all to a
new file. The writer now writes only the saved chunk, into the first free
run of sectors (RegionBitmap), points the header at it and then frees its
old sectors (RegionFile.write / clear), with one flush per region and batch.
Readers and the writer share one open file per region. A chunk queued again
before the writer reached it is written once, by its newest batch
(IOWorker.pendingWrites).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 07:19:55 +02:00
Ludovic MarieandClaude Opus 5.5 b540ccb642 Brewing stands tick in the level's block entity ticker list
Brewing stands were the last server ticker with a pass of its own over the
whole block entity map every tick. They now tick in their place in
Level.blockEntityTickers, as BrewingStandBlock.getTicker makes them: in
list order, only in block-ticking range and inside the world border, with
their bottle states written as each one ticks.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 07:17:44 +02:00
Ludovic Marie c2f3d01ee4 Merge remote-tracking branch 'pr/779' into integ/mp-sh
# Conflicts:
#	mods/horizon/src/test/java/dev/horizonlab/parity/LocatorBarClient.java
2026-10-18 06:35:13 +02:00
Ludovic Marie 6e7b34d5b8 Merge remote-tracking branch 'pr/790' into integ/mp-sh 2026-10-18 06:34:57 +02:00
Ludovic Marie 1541086b21 Merge remote-tracking branch 'pr/767' into integ/mp-sh 2026-10-18 06:34:56 +02:00
Ludovic Marie 6829da90f2 Merge remote-tracking branch 'pr/769' into integ/mp-sh
# Conflicts:
#	tools/lan_e2e.py
2026-10-18 06:34:56 +02:00
Ludovic Marie 4029475397 Merge remote-tracking branch 'pr/766' into integ/mp-sh 2026-10-18 06:34:38 +02:00
Ludovic Marie daa8b2f528 Merge remote-tracking branch 'pr/760' into integ/mp-sh
# Conflicts:
#	tools/lan_e2e.py
2026-10-18 06:34:26 +02:00
Ludovic Marie e6f0384dda Merge remote-tracking branch 'pr/759' into integ/mp-sh 2026-10-18 06:34:07 +02:00
Ludovic Marie 8a1515b64f Merge remote-tracking branch 'pr/765' into integ/mp-sh 2026-10-18 06:34:07 +02:00
Ludovic Marie 85fa29ee7a Merge remote-tracking branch 'pr/764' into integ/mp-sh 2026-10-18 06:34:06 +02:00
Ludovic Marie 319343992e Merge remote-tracking branch 'pr/763' into integ/mp-sh 2026-10-18 06:34:05 +02:00
Ludovic Marie f683d359a2 Merge remote-tracking branch 'pr/762' into integ/mp-sh 2026-10-18 06:34:04 +02:00
Ludovic Marie 0fbb7bdd0b Merge remote-tracking branch 'pr/756' into integ/mp-sh 2026-10-18 06:34:04 +02:00
Ludovic Marie 8d0ec947a3 Merge remote-tracking branch 'pr/755' into integ/mp-sh 2026-10-18 06:34:03 +02:00
Ludovic Marie 092dc7c641 Merge remote-tracking branch 'pr/754' into integ/mp-sh 2026-10-18 06:34:02 +02:00
Ludovic Marie 14f58d85e0 Merge remote-tracking branch 'pr/753' into integ/mp-sh 2026-10-18 06:34:02 +02:00
Ludovic Marie 2ac362f1d3 Merge pull request #813 from ludovic111/perf/dedicated-no-mesh
🤖 Agent: merged: no meshing on a dedicated server. Combined check of main + perf-ticks-6's six: clippy, tests, 8 harnesses PASS.
2026-10-18 06:33:51 +02:00
Ludovic Marie b63debb376 Merge pull request #793 from ludovic111/perf/creaking-heart-scan
🤖 Agent: merged: creaking heart scan only in sections whose palette holds a heart (240→3 ms per 15 s). Combined check of main + perf-ticks-6's six: clippy, tests, 8 harnesses PASS.
2026-10-18 06:33:46 +02:00
Ludovic Marie 2e2f4a6581 Merge pull request #809 from ludovic111/fix/ask-server-suggestions
🤖 Agent: merged: ask_server suggestions. Combined check of main + perf-ticks-6's six: clippy, tests, 8 harnesses PASS.
2026-10-18 06:33:41 +02:00
Ludovic Marie 0436c9470d Merge pull request #808 from ludovic111/fix/server-titles
🤖 Agent: merged: /title reaches guests and the server thread. Combined check of main + perf-ticks-6's six: clippy, tests, 8 harnesses PASS.
2026-10-18 06:33:36 +02:00
Ludovic Marie 4fb261fd30 Merge pull request #807 from ludovic111/fix/guest-death-health
🤖 Agent: merged: dead guest gets health 0. Combined check of main + perf-ticks-6's six: clippy, tests, 8 harnesses PASS.
2026-10-18 06:33:32 +02:00
Ludovic Marie e19803aa62 Merge pull request #806 from ludovic111/perf/chunk-ticket-levels
🤖 Agent: merged: vanilla FULL ticket square per server player (RD32 finished chunks 8809→4761, spawn CPU 153→98 s). Combined check of main + perf-ticks-6's six: clippy, tests, 8 harnesses PASS.
2026-10-18 06:33:27 +02:00
Ludovic Marie 5f49a85a4a Merge pull request #817 from ludovic111/perf/surface-rules
🤖 Agent: merged the shared ore-vein richness sample (surface −8% alone, −14% with #801; byte-identical, 19 worldgen harnesses pass).
2026-10-18 06:22:08 +02:00
Ludovic Marie 999c532a57 Merge pull request #801 from rdbtCVS/t3/optimize-surface-generation-upstream
🤖 Agent: merged rdbt's constant surface material-rule branches (surface −6%; byte-identical on four dimension/seed hashes, 19 worldgen harnesses pass; review in the thread).
2026-10-18 06:18:08 +02:00
Ludovic MarieandClaude Opus 5.5 cf97a5e1dc Decoration order: dense grids instead of hash maps
vanilla_order replays vanilla's ticket scheduler over every chunk in view
each time the player crosses into another chunk, on the render thread. At
render distance 32 that is about 200,000 level updates in SipHash maps:
8 ms per call. Over dense grids covering the square it can touch, the
same replay takes 1 ms. A test checks the order against the hash map
version for several centres and radii.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 05:44:08 +02:00
Ludovic MarieandClaude Opus 5.5 65fc3b9f94 Content mods: structure mods' worldgen joins the worldgen bundles
A content mod's data/<ns>/worldgen/ files were copied into the content root,
where nothing reads them: the worldgen loader reads bundles. The build now
adds a mod's structures, structure sets, template pools, processor lists and
configured and placed features to their bundles, its data/<ns>/structure/
templates to worldgen/structure/<ns>/, and its biome tags to the biome tag
bundle. The loader refuses a biome tag naming what it lacks (another mod's
biomes, a loader's convention tags), so those entries are dropped, as they are
from block and item tags. Biomes, noise and density functions stay out: they
would change vanilla's terrain.

Structure and placement types Horizon does not have stay unsupported, as
before; carry-over maps the common mod ones next.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 05:24:48 +02:00
Ludovic MarieandClaude Opus 5.5 6f7096b396 Ore veins: sample shared richness once, index the volume without division
26.3's copper and iron vein rules both bind minecraft:overworld/ore_vein/richness
eagerly, so the material stage sampled the same root over the same chunk volume
twice. Bindings of one compiled root now share its buffer; sampling is
deterministic, so the values are identical.

The two vein density reads per solid block went through
DensityVolume::index_of_block, which divides and takes the remainder by the
step on each axis. The stage's volumes are unit-step, so index them directly
(falling back for strided volumes).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 05:00:16 +02:00
Ludovic MarieandClaude Opus 5.5 ff6e01de03 Command suggestions: ask the server for nodes the tree marks ask_server
A remote client's chat now sends ServerboundCommandSuggestionPacket for an
argument node whose provider is minecraft:ask_server (bossbar ids, data
storage, datapacks, team names, functions...), as ClientSuggestionProvider.customSuggestion
does. The suggestions are done when ClientboundCommandSuggestionsPacket
answers that id; its completions join the local ones (Suggestions.merge).
Replies to older requests are dropped.

The remote tree takes each node's provider from the packet only: a local
provider the server didn't send stood in for the server's before.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 04:57:29 +02:00
rdbtCVS 0b5f0a3edf Document 1 2 4 and 8 worker scaling measurements 2026-10-18 03:56:31 +01:00
Ludovic MarieandClaude Opus 5.5 0f482a45c8 Dedicated server: never mesh sections
A dedicated server draws nothing, like the integrated server, yet its
streaming still meshed every section it lit (Streaming::mesh followed only
options.integrated). It now skips meshing too.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 04:49:06 +02:00
rdbtCVS c47b30fcf0 Optimize binary density, Beardifier and Perlin sampling 2026-10-18 03:49:02 +01:00
Ludovic Marie 9f7bd91109 Merge pull request #789 from ludovic111/perf/entity-culling
🤖 Agent: merged vanilla shouldRender culling for boats, paintings, item frames, armor stands, TNT, tridents and fireworks (~525 entities: FPS 107→141; only entities beyond vanilla's range disappear).
2026-10-18 04:41:28 +02:00
Ludovic MarieandClaude Opus 5.5 e8eaaf59d7 Rerun CI (flaky options_ui test, #802)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 04:35:23 +02:00
Ludovic MarieandClaude Opus 5.5 20fada25bd Rerun CI now that the PR is ready for review
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 04:27:55 +02:00
Ludovic Marie e627842ede Merge pull request #788 from ludovic111/perf/occlusion-async
🤖 Agent: merged cave culling on a worker thread (RD32 prepare 10.3→1.0 ms, p99 275→208 ms, FPS 116→135; drawn sections identical in all 18 views; 89/89 harnesses).
2026-10-18 04:12:15 +02:00
rdbtCVS 4cfa6a507c Document incremental surface optimization measurements 2026-10-18 03:11:34 +01:00
rdbtCVS d370364f12 Specialize constant material rule branches after binding 2026-10-18 03:11:34 +01:00
rdbtCVS 594b502780 Remove generated noise benchmark artifacts from PR 2026-10-18 03:10:26 +01:00
LookImEcho-Rick c1211b2245 Fix execute source dimension for player teleports 2026-10-17 21:08:53 -05:00
Ludovic MarieandClaude Opus 5.5 c12f5cf41a Server titles: /title reaches the guest, and its HUD shows and clears them
TitleCommand sends the targeted player ClientboundSetTitleText,
SetSubtitleText, SetActionBarText, SetTitlesAnimation and ClearTitles. A
guest's /title (the window's own player on the server thread too) only
changed the server's HUD; it is now sent to the guest. The remote client
showed titles and action bars as a bare line and ignored subtitles, times
and clearing; it now feeds the HUD's titles as ClientPacketListener does
(the action bar of a SystemChat overlay too) and ticks them (Gui.tick).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 04:00:48 +02:00
Ludovic MarieandClaude Opus 5.5 65d5a35cd2 Server: a dead guest is sent its health of 0, as ServerPlayer.doTick does
The hub held SetHealth back once a guest died, so the guest's health
stayed at its last positive value under the death screen. ServerPlayer
sends it every tick it changes, dead or not, and only when the health or
the food level changes or the saturation reaches or leaves zero: the hub
sent it on every change of saturation too.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 04:00:24 +02:00
Ludovic MarieandClaude Opus 5.5 c6531e4bde Server players finish vanilla's FULL square, not a 1.5x circle
DistanceManager.PlayerTicketTracker gives every chunk within the server's
view distance (Chebyshev) of a player a PLAYER_LOADING ticket at level 31,
so FULL (33) reaches two chunks past it and FEATURES and TERRAIN one and
two more. A server player's streaming area was a circle of radius
(view distance + 2) * 3 / 2 + 1, finished one further: 2.1x the chunks at
view distance 12. It is now that square, and lan status reports the
server world's chunk counts.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 04:00:23 +02:00
Ludovic Marie d223b383ee Merge pull request #798 from ludovic111/perf/block-entity-tickers
🤖 Agent: merged vanilla-order block entity ticking (new harness: 21,590/21,590 ticks match vanilla; block-entity share of the tick 19.5%→9.6% with 50 bots; 90/90 harnesses locally).
2026-10-18 03:59:14 +02:00
Ludovic Marie b651ca5f93 Merge pull request #795 from ludovic111/perf/sculk-fx-maps
🤖 Agent: merged sculk per-mob maps on the fast hasher (small win; keys are server-assigned ids).
2026-10-18 03:54:57 +02:00
Ludovic Marie 5ca2021223 Merge pull request #791 from ludovic111/perf/zlib-rs
🤖 Agent: merged zlib-rs compression (chunk deflate 1.98× faster, writer-thread deflate −60%; 222/222 harnesses locally, verify green).
2026-10-18 03:54:54 +02:00
Ludovic Marie 50daf6ecfc Merge pull request #792 from LookImEcho-Rick/codex/tooltip-display-parity
🤖 Agent: merged LookImEcho-Rick's tooltip_display parity, with its Java oracle and Rust probe.
2026-10-18 03:33:38 +02:00
Ludovic Marie f32e696eb9 Merge pull request #794 from lazy-importer/fix/creative-flight-save-persistence
🤖 Agent: merged lazy-importer's creative flight persistence across save and reload (fixes #777).
2026-10-18 03:33:33 +02:00
rdbtCVS eb3789038e Record repository PR self-check 2026-10-18 02:28:17 +01:00
rdbtCVS ce77a4189d Specialize unary density evaluation per bulk buffer 2026-10-18 02:26:03 +01:00
Ludovic MarieandClaude Opus 5.5 118bfa2be2 fix(game): finish every consumable as vanilla's Consumable.onConsume
Follow-up to #773 (consume effects on food):

- ApplyStatusEffectsConsumeEffect always draws its float and skips the
  effects when the draw is >= probability (#773 skipped the draw at 1.0
  and applied on <=).
- on_consume_effects run for every consumable, not only food: milk can be
  drunk (clear_all_effects, then the bucket), honey removes poison and
  leaves its glass bottle, stews their bowl (UseRemainder).
- The consumable's own timing, sound, animation and particles
  (consume_seconds, sound, has_consume_particles) through horizon-world's
  Consumable.emit, in vanilla's draw order; FoodProperties.onConsume's
  sound and burp; the used statistic; the eat or drink game event after
  the effects.
- teleport_randomly (chorus fruit) through horizon-world's verified
  TeleportConsume port, with LivingEntity.randomTeleport for the player
  (border clamp, entities_can_teleport_to ground, collision and liquid
  checks), the teleport game event and sound, level event 2017's
  directional particles and entity event 46's portal particles.
- tools/agent_control_e2e.py: a consume scenario (golden apple at full
  hunger, milk, honey, chorus fruit).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 03:14:29 +02:00
Ludovic MarieandClaude Opus 5.5 209b898c02 The console's and players' command feedback reaches the operators
CommandSourceStack.broadcastToAdmins beyond a guest on a dedicated server:

- The dedicated server's console tells every operator what its commands
  did, as `[Server: ...]`, when `broadcast-console-to-ops` is on
  (DedicatedServer.shouldInformAdmins). This covers the game's commands
  and the dedicated-only ones vanilla broadcasts (op, deop, ban, pardon,
  kick, whitelist, save-all, save-on/off, stop).
- In a world open to LAN, the host, an operator when commands are
  allowed, hears of its guests' commands, and op guests hear of the
  host's.
- A player's name in the line is their display name, with the `/tell`
  suggestion, the entity hover and the insertion (Player.getDisplayName).

New harness test_server_ops_feedback_parity: two vanilla-codec
operators' system chat while the console and one of them run commands,
deop and op, toggle send_command_feedback and save, with
broadcast-console-to-ops on and off. LocatorBarClient can record system
chat (`chat on`). lan_e2e's feedback check also asserts the host is told.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 03:02:11 +02:00
Ludovic MarieandClaude Opus 5.5 4ac29bf1c1 docs(hosting): players travel between dimensions on their own
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 03:01:56 +02:00
Ludovic MarieandClaude Opus 5.5 9a2d87ec97 fix(server): move players to another dimension with execute in ... run tp
On a dedicated server, `execute in <dimension> run tp @s ...` from a player
moved them inside the level they were in. It now does what vanilla's
TeleportCommand does: ServerPlayer.teleport into the other level, with the
Respawn packet keeping all data and no portal sound, then the feedback. The
players there are tracked to them and they are removed from the ones they
left.

tools/test_server_dimension_players_parity.py joins two players to a vanilla
and a Horizon server and compares, step by step, the players each is sent and
loses and the dimensions it respawns into, while they go to the Nether and
the End by command and through an End portal. LocatorBarClient gains
`players on` and `still NAME` (the position reminder a still client sends).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 03:01:56 +02:00
Ludovic MarieandClaude Opus 5.5 a1af7fdad5 Worlds without structures generate none
`generate-structures=false` (server.properties, stored by #733) and the
world screen's Generate Structures were written to the world's settings
but the generator ignored them. VanillaTerrain now builds its generator
from the world's `generate_structures`
(OverworldGenerator::for_world): without structures there are no starts,
terrain adaptation or structure pieces, as in
ChunkStatusTasks.generateStructureStarts, and `locate structure` and eyes
of ender find nothing (ChunkGenerator.findNearestMapStructure).

The server world gen harness's world without structures now also
compares `locate structure` for a village and a mineshaft and the saved
chunks' structure starts and references.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 03:01:55 +02:00
Ludovic MarieandClaude Opus 5.5 5e1abf2b4e Clients of a server show its primed TNT, falling blocks, projectiles and crystals
A Horizon client of a server places what PR #766 sends: primed TNT with
its fuse, falling blocks with their block state and start position,
arrows (critical, in the ground), snowballs, eggs, pearls, fireballs,
wither skulls, wind charges and llama spit, tridents, firework rockets
and end crystals with their beam and base. They interpolate like mobs,
turn with the move packets, and are cleared with the connection, a
respawn or a dimension change.

`status` lists them (`projectiles`), and lan_e2e has a `projectiles`
check: the guest sees the host's primed TNT, end crystal and landed
arrow where the host has them, and a falling block lands on both sides.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 03:00:42 +02:00
Ludovic MarieandClaude Opus 5.5 946bf84818 Load summoned and saved primed TNT as live TNT; step the entity harness
`/summon minecraft:tnt` and primed TNT in a saved chunk were kept as an
unsimulated entity. They now become primed TNT with the fuse (80 when
absent) and motion they were saved with (PrimedTnt.readAdditionalSaveData).

The entity harness runs its scene for a fixed 200 ticks (`tick step`),
so what still changes when the client stops recording (a fuse) ends the
same on both servers. Its scene has two primed TNT (one falling), a
floating falling block, an arrow that lands and an end crystal. Unit
tests cover arrows' position and rotation, fireballs' bundled power and
precise position syncs.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 02:57:12 +02:00
Ludovic MarieandClaude Opus 5.5 866e772227 Server: track primed TNT, falling blocks, projectiles, tridents, fireworks and crystals
These entities had no id in Horizon, so a server could not send them to
its clients. Each now has one, given from the entity counter before it is
first tracked, and goes to guests as vanilla's ServerEntity sends it:

- primed TNT with its fuse (sent each tick it changes) and falling blocks
  with their block state and start position; both sync their exact
  position while they touch a block (requiresPrecisePosition);
- arrows and tridents send their position and rotation every update
  (AbstractArrow), with their flags, pierce level, in-ground state,
  loyalty and foil; their owner is the spawn data;
- fireballs, wither skulls and wind charges bundle their velocity with
  ClientboundProjectilePowerPacket (new in horizon-protocol);
- snowballs, eggs, pearls, llama spit, shulker bullets and splash potions;
- firework rockets with their item, the player they boost and whether
  they were shot at an angle; end crystals with their beam and base.

The entity harness's scene gains primed TNT, a falling block, an arrow, a
snowball and a trident that land, a fireball, a firework and a crystal.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 02:57:12 +02:00
Ludovic MarieandClaude Opus 5.5 583502f0f4 Clients of a server show its boats, minecarts, armor stands, frames and paintings
ClientPacketListener.handleAddEntity for the objects a server tracks: a
Horizon client of a server (a LAN host, a dedicated server, single
player's server thread) now places the boats, minecarts, armor stands,
item frames and paintings it is sent, where the server says.

- Spawn: carts by kind, boats and rafts by wood (with a chest), armor
  stands, frames and paintings at their block with the direction from the
  spawn data.
- Entity data: frame items and rotation, painting variants (a registry
  holder), armor stand flags and health, boat paddles, vehicle hurt.
- Equipment: what an armor stand wears and holds.
- Moves interpolate like mobs'; rotation and ground state follow.
- They are cleared with the connection, a respawn or a dimension change.

`status` lists them (`objects`), and lan_e2e has an `objects` check: the
host summons them, the guest sees each as the host has it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 02:54:25 +02:00
Ludovic MarieandClaude Opus 5.5 43e8b54918 Server: track boats, minecarts, armor stands, frames and paintings to clients
A server sent its clients only mobs and dropped items. Boats, every kind of
minecart, armor stands, item frames, paintings and the entities Horizon keeps
without simulating are now tracked as ServerEntity does: the AddEntity data
(hanging entities at their block, with their direction), the synched data that
differs from the defaults, equipment, passengers (ClientboundSetPassengersPacket
at pairing and on change; a passenger syncs only its rotation and resyncs its
position when it dismounts), and the type's update interval and range.

Also vanilla-exact on the way, as the new harness found:
- mobs pair with their health (always non-default) and Mob flags (no AI, left-handed);
- /summon without NBT summons any type from an empty compound;
- armor stands move before gravity (LivingEntity.travelInAir), so a stand at rest
  keeps vanilla's -0.0784 vertical motion.

tools/test_server_entities_parity.py compares, from a vanilla-codec client, what
vanilla's dedicated server and Horizon's track for a scene of 20 entities.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 02:50:14 +02:00
Ludovic MarieandClaude Opus 5.5 2c244f1888 Creaking hearts: scan only sections whose palette holds a heart
scan_for_hearts read all 4,096 blocks of every non-air section of each new
chunk on the server thread to find creaking hearts. A section whose palette
holds no heart state (PalettedContainer.maybeHas) cannot have one, so it is
skipped. Same block entities, same order.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 02:48:45 +02:00
Ludovic Marie ce571f789c Merge pull request #786 from LookImEcho-Rick/codex/issue-780-open-mod-folder
🤖 Agent: merged LookImEcho-Rick's Open Mod Folder buttons (fixes #780).
2026-10-18 02:33:20 +02:00
LookImEcho-Rick 2faa99e5f1 Fix item tooltip display parity 2026-10-17 19:30:23 -05:00
Ludovic Marie 43d43df71d Merge pull request #787 from ludovic111/bm2/combined
🤖 Agent: merged the six LAN guest fixes for #696 (#722–#727), combined on main and checked locally (clippy, tests, parity lan/dedicated_server/mob_ai, lan_e2e clicks/sleep/push/eat/xp).
2026-10-18 02:09:22 +02:00
Ludovic MarieandClaude Opus 5.5 01f4c56dc1 Cull boats, paintings, item frames, armor stands, TNT and tridents like vanilla
These entities were drawn every frame wherever they were in the loaded
world: every armor stand, item frame and painting of every loaded village
or base went through its model, item and per-vertex occlusion test even
far behind the camera. EntityRenderer.shouldRender now decides, as for
mobs and minecarts:

- Entity.shouldRenderAtSqrDistance from the current position: 64 times the
  bounding box's mean edge times the entity distance option (boats about
  71 blocks, primed TNT 63, paintings by size); item frames 1024 blocks;
  armor stands 256 times the box's mean edge whatever the option (4 for a
  marker's empty box); tridents ten times the default (AbstractArrow);
  firework rockets 64 blocks.
- the interpolated culling box grown by 0.5 in the view; tridents grow it
  by 1.5 more (ThrownTridentRenderer), armor stands with a dragon head by
  0.5 (LivingEntityRenderer), item frames holding a map use the block-wide
  box.

The fishing bobber is unchanged: vanilla never frustum-culls it
(FishingHookRenderer.affectedByCulling) and Horizon only draws the
player's own, always within its 64 blocks.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 02:04:30 +02:00
Ludovic MarieandClaude Opus 5.5 c78bf74744 Cave culling: search on a worker thread, frames never wait for it
The section occlusion search (SectionOcclusionGraph) ran on the render
thread whenever a section was meshed or the camera entered another
section: at render distance 32 that is a breadth-first walk over about
124,000 cells, and while exploring it ran nearly every frame.

As in vanilla 26.3, the full search now runs off the render thread. A
worker keeps its own copy of every section's face connectivity, fed the
changes with each job, and searches over dense arrays around the camera.
Frames use the last finished graph and never wait for the next. The search
starts at the top of the frame, as soon as the camera is known, so it
usually lands before the frame builds its visible list. A section meshed in
between propagates into the graph the frame has (vanilla's
runPartialUpdate), so a wall that opens shows what is behind it at once; a
section that closes goes at the next full search. A graph that never
reached the camera's section (after a teleport, say) is not used: until the
next one lands every section in the frustum is drawn.

The search itself is unchanged: the same box, steps and first-entry rule,
checked cell for cell against the previous code in a test.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 02:04:21 +02:00
lazy-importer 3bb973efae fix(game): persist creative flight status across world save and reload (#777) 2026-10-18 01:57:22 +02:00
LookImEcho-Rick 1ffdb5f3f5 Add open mod folder buttons
Expose the Horizon mods directory from both the Classic and Horizon mods screens, creating it if needed before opening it.

Closes #780
2026-10-17 18:55:26 -05:00
Ludovic Marie 86d745853a Merge remote-tracking branch 'origin/main' into bm2/combined 2026-10-18 01:51:50 +02:00
Ludovic Marie e4f8154cb4 Merge pull request #783 from LookImEcho-Rick/codex/issue-730-horizon-scrollbar
🤖 Agent: merged LookImEcho-Rick's Horizon options scrollbar dragging (fixes #730).
2026-10-18 01:40:18 +02:00
rdbtCVS 68475af653 Startup stage 3d: make the menu interactive before world core loads (#712) 2026-10-18 00:35:17 +01:00
Ludovic Marie 730c43e7b0 Merge pull request #773 from lazy-importer/fix/always-edible-food-effects
🤖 Agent: merged lazy-importer's always-edible food and consume effects (fixes #750). Vanilla gaps listed in the review are followed up by the agent.
2026-10-18 01:32:49 +02:00
LookImEcho-Rick d33929c8fa Fix Horizon options scrollbar dragging 2026-10-17 18:22:40 -05:00
rdbtCVS 33c6578f5a Log whether the tail is ready when the live backdrop fades in 2026-10-18 00:14:51 +01:00
rdbtCVS 08532d6b1f Report the active tail phase while world entry waits 2026-10-18 00:14:51 +01:00
rdbtCVS a5edc14b97 Startup stage 3c: keep world entry responsive while joining resources (#712) 2026-10-18 00:14:50 +01:00
rdbtCVS 9004019800 light: pack the flood fill and precompute neighbour blocking
light_chunk's sky and block spreads read LightTables::cells for every
neighbour, and the fill and both spreads dominated lighting. Pack each
cell into two bytes (occlusion type and dampening) with a per-type
blocking mask, step neighbours by linear offset with an axis bounds
check, and precompute the per-column present table.

1156-column probe: 1.81 -> 1.31 ms/col (~28%). Vanilla light parity is
unchanged (1,179,648 sky/block values, 0 differences).
2026-10-18 00:12:51 +01:00
rdbtCVS 4e854f8ee3 Format launcher assertions for the shared telemetry checks 2026-10-18 00:10:59 +01:00
lazy-importer ecdff7ad2d fix(game): apply absorption effect attribute modifiers and health (#750) 2026-10-18 00:57:04 +02:00
lazy-importer 9779eb7271 fix(game): allow always-edible food when full and apply consume effects (#750) 2026-10-18 00:57:04 +02:00
rdbtCVS a3cf97f2fa Build the core renderer before starting audio 2026-10-17 23:50:46 +01:00
rdbtCVS d9686faf35 Startup stage 3b: publish world core before the simulation tail (#712) 2026-10-17 23:47:53 +01:00
Ludovic MarieandClaude Opus 5.5 0eec25d14b Sculk: Fx hashing for the per-mob step and damage maps
entity_motion_events looked up each moving mob in a SipHash map every
tick. The keys are mob ids the server gives out, so the fast hasher is
safe there.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 00:45:49 +02:00
Ludovic Marie 437d5272b5 Merge pull request #772 from lazy-importer/fix/hopper-minecart-display-offset
🤖 Agent: merged lazy-importer's hopper minecart display offset fix (#741).
2026-10-18 00:41:12 +02:00
rdbtCVS 42d65465a4 Startup stage 3a: split shared resources into core and rest (#712) 2026-10-17 23:39:11 +01:00
Ludovic MarieandClaude Opus 5.5 5253e94262 Dedicated server: skip the client ticks of spawners, trial spawners and vaults
Their particles, ambient sounds and the turning display mob are for a
window. A dedicated or integrated server has none, yet made a display
mob for every spawner it ticked.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 00:29:46 +02:00
Ludovic MarieandClaude Opus 5.5 7de8c51f4e Block entity tickers: sweep the list only after a slot is freed
The list kept its slots for the whole tick; sweeping it every tick cost
two hash lookups per ticker. Now it is swept only when a slot was freed
or a block entity was taken out and not brought back.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 00:29:46 +02:00
rdbtCVS 05ecd873d4 Startup: show the title before the world bundle finishes loading (#712) 2026-10-17 23:24:05 +01:00
rdbtCVS 5c55e7d855 Paletted container access: skip the storage revalidation a section already did
BlockSection::get and set check the section index and the state id once,
then read and write through new PackedStorage::read/write, which skip the
storage-level bounds and value checks and the Result that get/set keep for
external callers. A section index is below 4,096 and a palette id always
fits the width bits_for_palette chose; both methods debug_assert that
contract. Used for the hot read, the hot write and the growth re-encode.

Benchmark (ignored tests, best of alternating-order rounds, fast profile):
BlockSection::set 13.15 -> 9.12 ns/write (1.44x), get 2.33 -> 1.45 ns/read
(1.61x). See docs/performance/paletted-container-access-results.md.
2026-10-17 23:23:15 +01:00
Cosmic 594929b795 Validate LOD loading, shader depth and current terrain draw paths 2026-10-17 18:13:20 -04:00
lazy-importer e92f74f01d fix(minecarts): use per-kind display offset in cart geometry (#741) 2026-10-18 00:12:39 +02:00
Ludovic MarieandClaude Opus 5.5 ff94490992 Block entities: tick only those with a ticker, in vanilla's order
World::block_entities keeps vanilla's Level.blockEntityTickers beside the
map: a slot when a block entity whose state has a ticker is added, kept
when it is replaced, freed when it is removed, unloaded or its state loses
the ticker; a loaded chunk's in its block entity map's order. The tick
visits that list once, in block-ticking chunks within the world border,
instead of scanning the whole map once per kind.

New harness block_entity_tickers: real LevelChunks of a server level
against BlockEntities.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 00:08:44 +02:00
Cosmic 9ce739796e Merge remote-tracking branch 'origin/main' into feat/terrain-lod
# Conflicts:
#	crates/horizon-client/src/gfx.rs
#	crates/horizon-client/src/graphics.rs
2026-10-17 18:06:36 -04:00
Cosmic bae7975757 Add bounded hierarchical surface terrain LODs for distant views (#718) 2026-10-17 18:05:39 -04:00
Ludovic Marie cf63dcd39f Merge remote-tracking branch 'pr/724' into bm2/combined
# Conflicts:
#	crates/horizon-client/src/game/lan_actions.rs
#	tools/lan_e2e.py
2026-10-17 23:57:19 +02:00
Ludovic Marie 40e9e9660b Merge remote-tracking branch 'pr/727' into bm2/combined
# Conflicts:
#	crates/horizon-client/src/game/agent.rs
#	tools/lan_e2e.py
2026-10-17 23:55:19 +02:00
Ludovic Marie e74d5207dc Merge remote-tracking branch 'pr/726' into bm2/combined
# Conflicts:
#	crates/horizon-client/src/game/remote.rs
#	tools/lan_e2e.py
2026-10-17 23:54:59 +02:00
Ludovic Marie 9bb22caa22 Merge remote-tracking branch 'pr/725' into bm2/combined
# Conflicts:
#	tools/lan_e2e.py
2026-10-17 23:54:22 +02:00
Ludovic Marie 6f5a3319ee Merge commit '123e8286ac55894a0e9f61c78b3844fd46241e2a' into bm2/combined
# Conflicts:
#	crates/horizon-client/src/game/remote.rs
#	tools/lan_e2e.py
2026-10-17 23:54:21 +02:00
Ludovic Marie a72e2a2f0e Merge commit '69dd4feb316616eef6255a4f41dc3b680a6a43e6' into bm2/combined 2026-10-17 23:52:39 +02:00
Ludovic Marie af3e75fece Merge pull request #740 from rdbtCVS/t3code/08bad3fd
🤖 Agent: merged by Ludovic's AI agent. CI parity passed (harness paths stay synchronous). Smoke test of the new path on current main (macOS, --offscreen, normal launch): resources loaded on the worker in 9.3 s, renderer swapped, title screen up and running for 60 s, no panic. Thanks @rdbtCVS!
2026-10-17 23:52:27 +02:00
Ludovic MarieandClaude Opus 5.5 90053d15aa docs/modpacks.md: memory, NeoForge comparison, worldgen and Chipped status
The NBT tables stream, the fluid tables reuse the game's facts and the content
root is kept between starts (#753, #754, #756). Horizon with ATM10's 40 data-tier
mods against the same mods on NeoForge 21.1.259. Worldgen carry-over and what it
does not cover yet (#763, #764). Chipped's harness row with its self-drop tables
(#762). The overlay cache line said it was cached; it is now.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 23:45:43 +02:00
Ludovic Marie 5f5781b17e Merge pull request #745 from rdbtCVS/perf/worker-headroom
🤖 Agent: merged by Ludovic's AI agent. Measured by perf-ticks-6 (comment above): ThinkPad 4 threads RD12 1% low 17→29 FPS (31→40 with --server-thread), FPS up everywhere; in-frame spawn load ~15% slower (32.5→37.2 s); Mac 14 threads within noise. Thanks @rdbtCVS!
2026-10-17 23:44:17 +02:00
rdbtCVS 56a251eb13 Paletted container access: one palette probe per write, dispatched unpack
BlockSection::set resolved the old cell through get and then probed the
section palette three times per non-trivial write: keying the old state by
id, testing the incoming state and finding it. The slot at the written index
already is the old state's id, so read it straight from storage and resolve
only the incoming state, with a single map probe. Palette reuse, free slots
and growth are unchanged, so palette, counts, free list and NBT are identical.

PalettedContainer::unpack and unpack_u16 matched the palette variant inside
the per-cell loop; dispatch Single/List/Global once before it.

Measured against copies of the pre-optimization code (ignored tests):
BlockSection::set 12.73 -> 11.37 ns/write (1.12x), unpack list 1.956 ->
1.860 us/call (1.05x), global 2.255 -> 2.172 (1.04x). See
docs/performance/paletted-container-access-results.md.

set_matches_baseline_under_random_edits drives both implementations over the
same 50k-edit streams for 1/2/16/17/24/64 distinct states, with periodic
whole-section collapses, and asserts equal results, bits, palette, counts,
free list and NBT.
2026-10-17 22:38:35 +01:00
Ludovic MarieandClaude Opus 5.5 9afb6d76de Carry: structure mods' worldgen in vanilla's types, with the loader's biome tags
Structure mods build on libraries whose structure, placement and pool element
types are vanilla's with extra rules: Lithostitched's, Moog's Structure Lib's
and Repurposed Structures' jigsaw structures, their advanced random spreads,
versioned and ocean-bottom single pool elements. Carry rewrites them as
vanilla's types; the extra rules (no start in liquid, a minimum distance from
the origin) are lost, so a structure may start where the mod would not. Types
with no vanilla counterpart (YUNG's, Formations', the nether variants) are left
as they are and counted in the carry report.

The structures' biomes name the loader's convention tags (#c:is_plains). fetch
now also gets the pack's NeoForge jar from NeoForge's Maven repository, checked
against its SHA-1, and carry copies the convention biome tags each mod's
structures use, with those they include, into the mod.

ATM10's 42 carried mods: 1,061 files rewritten, 103 convention biome tags.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 23:32:48 +02:00
Ludovic MarieandClaude Opus 5.5 d3673b1509 Carry: write the self-drop loot table a mod registers in code
Chipped registers its blocks' loot in code: its jar ships 7 loot tables for
about 7,000 blocks, so carried over they dropped nothing, while in the NeoForge
oracle every one drops itself. When a declared block has no loot table in the
jar and the oracle shows vanilla's dropSelf (one of itself with every tool,
nothing only where the state needs a tool it did not get), carry writes that
table in 26.3's shape. When only the states with one property value drop it
and the rest drop nothing (a door's lower half), the entry gets vanilla's
match_block condition on that value, as createDoorTable writes. Chipped:
6,843 tables added.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 23:32:35 +02:00
rdbtCVS c4cc7954ce Portal scans: keep per-chunk section masks across ticks
The nether-portal section precheck from #661 answered "does this section's
palette hold a portal?" per section, per tick. Keep the answer per resident
chunk in `portal_sections`, keyed by a weak handle: an edit or a chunk
replacement rebuilds it, and it is dropped when a chunk unloads and cleared
when the block facts change, so a portal block always forces the sweep. The
entity sweep now finds portal blocks without the per-tick map.

This is a scan microbenchmark, not a whole-game claim: steady improves 2.4x
to 17x over the fixtures and the run parity-checks the cached and per-tick
answers. Rescanning every chunk every tick (cold, reload, edit_all) is slower
because a miss reads all 24 sections of a chunk; see
docs/portal-scan-benchmark.md.
2026-10-17 22:29:59 +01:00
Ludovic MarieandClaude Opus 5.5 a2f8952d05 Compression: flate2's zlib-rs backend instead of miniz_oxide
Halves the CPU of deflating a chunk column and makes small packets about
20 % cheaper. The compressed bytes change; what they inflate to doesn't.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 23:18:12 +02:00
rdbtCVS e25672ed4f Startup: split block assets into block data and render assets (#712) 2026-10-17 22:16:45 +01:00
rdbtCVS f0215473e3 Biome zoom: mix the shared corner prefixes once in quart_at_block
getFiddledDistance restarts a six-step LinearCongruentialGenerator mix for
each of the eight quart corners. Its first two steps depend only on the X
and Y corner offsets, so quart_at_block now mixes the two X states and the
four XY states once, then runs the remaining steps across the eight corners.
next(m, salt + offset) == next(m, salt) + offset, so a coordinate folds into
its step with one addition.

Bit-identical to BiomeManager.getBiome and getFiddledDistance; a reference
oracle test pins the equivalence over a dense grid and wrapping extremes.
2026-10-17 22:08:17 +01:00
LookImEcho-Rick b9d61e5701 Make pr_ready portable to Windows 2026-10-17 15:48:49 -05:00
Ludovic Marie 9b97ba09de Merge pull request #737 from ludovic111/perf/lan-post-thread
🤖 Agent: merged by Ludovic's AI agent: perf-server-5 (combined check passed).
2026-10-17 22:48:42 +02:00
Ludovic Marie d65881a2f0 Merge pull request #739 from ludovic111/perf/chunk-packet-direct
🤖 Agent: merged by Ludovic's AI agent: perf-server-5. Before/after in the PR body; agent parity 87/87 stacked; orchestrator combined check with the perf-ticks-5 stack on current main: clippy clean, client+protocol tests pass (one flaky world-delete test, #749), dedicated_server/lan/chunk_stage/block_entity_storage PASS.
2026-10-17 22:47:47 +02:00
Ludovic Marie f548f5ce6c Merge pull request #748 from ludovic111/perf/shared-compressed-frames
🤖 Agent: merged by Ludovic's AI agent: perf-server-5 (combined check passed).
2026-10-17 22:47:42 +02:00
Ludovic Marie cb35d7e661 Merge pull request #738 from ludovic111/perf/hopper-scan-five-slots
🤖 Agent: merged by Ludovic's AI agent: perf-server-5. Before/after in the PR body; agent parity 87/87 stacked; orchestrator combined check with the perf-ticks-5 stack on current main: clippy clean, client+protocol tests pass (one flaky world-delete test, #749), dedicated_server/lan/chunk_stage/block_entity_storage PASS.
2026-10-17 22:47:27 +02:00
Ludovic Marie da0f954d9b Merge pull request #714 from ludovic111/feat/remote-recipe-book
🤖 Agent: merged by Ludovic's AI agent: perf-ticks-5 stack. Local on current main: clippy clean, horizon-client tests pass, parity 10/10 (dedicated_server lan tab_list chunk_stage locator_bar command_exec online_mode user_cache server_world_gen mob_ai); agent integration 88/88.
2026-10-17 22:40:31 +02:00
Ludovic Marie b807990c8e Merge pull request #708 from ludovic111/feat/server-command-tree
🤖 Agent: merged by Ludovic's AI agent: perf-ticks-5 stack. Local on current main: clippy clean, horizon-client tests pass, parity 10/10 (dedicated_server lan tab_list chunk_stage locator_bar command_exec online_mode user_cache server_world_gen mob_ai); agent integration 88/88.
2026-10-17 22:40:22 +02:00
LookImEcho-Rick 782924fcb7 Add settings search to video and controls 2026-10-17 15:36:13 -05:00
Ludovic Marie 8bfe19245b Merge commit 'bf1cd958686ffc95a6ff30cdbedc9a8de59315ad' into HEAD
# Conflicts:
#	tools/lan_e2e.py
2026-10-17 22:33:41 +02:00
Ludovic MarieandClaude Opus 5.5 bf1cd95868 Merge main into feat/server-command-tree
tools/lan_e2e.py: the check lists keep #700's effects and portals and this PR's commands.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 22:33:18 +02:00
Ludovic Marie bd60381883 Merge pull request #700 from ludovic111/fix/effects-every-player
🤖 Agent: merged by Ludovic's AI agent: perf-ticks-5 stack. Local on current main: clippy clean, horizon-client tests pass, parity 10/10 (dedicated_server lan tab_list chunk_stage locator_bar command_exec online_mode user_cache server_world_gen mob_ai); agent integration 88/88.
2026-10-17 22:11:26 +02:00
Ludovic Marie 2025408979 Merge commit 'b5abc95f' into HEAD 2026-10-17 22:05:28 +02:00
Ludovic MarieandClaude Opus 5.5 1c79addec1 Input capture test: on a server the chat is ChatScreen (#708)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 22:05:28 +02:00
Ludovic Marie a244200097 Merge commit '95bdb96f' into HEAD 2026-10-17 22:05:16 +02:00
Ludovic Marie 3a8ba56c61 Merge commit '7733984e' into HEAD 2026-10-17 22:05:16 +02:00
Ludovic MarieandClaude Opus 5.5 9240aeb9ff Input capture test: the mock Game has lan_acting (#687)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 22:05:16 +02:00
Ludovic MarieandClaude Opus 5.5 24fd51b186 Content mods: keep the built content root for later starts
resource_root built the content root again on every start. With ATM10's 40
carried data-tier mods that is about 25 s on a quiet machine (50 s on a busy
one) and the biggest memory peak of the start, above 3 GB of footprint. The
root now records a stamp of what it was built from (this build of the game,
the base's manifest and rewritten tables, every mod file by size and time) and
later starts with the same stamp use it as it is.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 21:51:33 +02:00
Ludovic MarieandClaude Opus 5.5 580c2b1dd7 Motion: reuse the game's block facts for the fluid tables
FluidResources decoded gameplay-facts.json a second time for three per-state
columns the game's BlockFacts already has. With ATM10's carried mods that file
is 211 MB: the second decode took a few seconds and peaked about 500 MB above
what the game keeps. MotionResources::load_with_facts takes the game's facts;
load still decodes them itself for tests and tools.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 21:46:38 +02:00
Ludovic Marie ce38c8d74f Merge pull request #743 from ludovic111/perf/terrain-draws
🤖 Agent: merged by Ludovic's AI agent: RD32 terrain draws 9,309 → 708 calls on Metal, avg 128 → 162 FPS, CPU 6.76 → 5.73 ms; screenshots identical (6/7 views 0 px, 7th = fading chat); parity 88/88. RD16 and below unchanged.
2026-10-17 21:42:50 +02:00
Ludovic Marie 4d04a997ce Merge pull request #735 from ludovic111/fix/minecart-render-culling
🤖 Agent: merged by Ludovic's AI agent: verify passed, parity proven (CI or perf-frame-2's local run), before/after numbers and screenshots in the PR body.
2026-10-17 21:42:45 +02:00
Ludovic Marie cd1725565a Merge pull request #742 from ludovic111/perf/metal-gpu-timer
🤖 Agent: merged by Ludovic's AI agent: verify passed, parity proven (CI or perf-frame-2's local run), before/after numbers and screenshots in the PR body.
2026-10-17 21:42:40 +02:00
Ludovic Marie f227d04f79 Merge pull request #715 from ludovic111/fix/vsync-windowed
🤖 Agent: merged by Ludovic's AI agent: verify passed, parity proven (CI or perf-frame-2's local run), before/after numbers and screenshots in the PR body.
2026-10-17 21:42:36 +02:00
Ludovic Marie b5abc95fe4 Merge commit 'a4feb6516102eaffb9e82437f808e32f4d05bde0' into HEAD 2026-10-17 21:41:03 +02:00
Ludovic MarieandClaude Opus 5.5 7c5bd06dfe lan_bot: the command tree arrives parsed (#708)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 21:41:03 +02:00
Ludovic Marie 95bdb96f16 Merge commit '805d5532e541deb1888216c8ebfc15775d8b2ea5' into HEAD 2026-10-17 21:40:51 +02:00
Ludovic MarieandClaude Opus 5.5 7733984e78 levels.rs: per-level block events carry u32 states (#674)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 21:40:51 +02:00
rdbtCVS 238278aa41 Startup: keep the title's atlas and panorama in one UI bundle (#712)
The title's GUI atlas and its panorama both come from the resource root and
are only needed to draw the title and menus, but they lived apart: the atlas
on Game and the panorama inside PackResources. Gather them into UiResources so
Stage 2 can load the title bundle before the world bundle. No behaviour change.

Part of #712 (stage 2a).
2026-10-17 20:40:10 +01:00
Ludovic MarieandClaude Opus 5.5 4ae51c1d5e perf_bench --warm: measure a later start, after a first one built the caches
A Horizon game with content mods builds its content root and block behaviour
snapshot on its first start, in its game folder, and reuses them afterwards.
perf_bench starts every game from an empty folder, so it only measured first
starts. --warm starts the game once to the title screen first, unmeasured.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 21:39:48 +02:00
Ludovic Marie ba7451cd31 Merge pull request #728 from Online5277/perf/block-entity-fields
🤖 Agent: merged by Ludovic's AI agent once CI parity passed; screenshots identical to main (perf-frame-2), CPU frame time about 40% lower on a chest field.
2026-10-17 21:38:00 +02:00
Ludovic Marie 1fe38223ac Merge pull request #736 from Online5277/fix/death-screen-save
🤖 Agent: merged by Ludovic's AI agent once CI parity passed.
2026-10-17 21:37:34 +02:00
rdbtCVS fb450342bc Worker threads: leave a core for the render and server threads 2026-10-17 20:37:20 +01:00
Ludovic Marie 8c01418247 Merge pull request #682 from Online5277/fix/blocked-hit-animation
🤖 Agent: merged by Ludovic's AI agent: CI parity and verify passed. Thanks @Online5277!
2026-10-17 21:37:03 +02:00
Ludovic MarieandClaude Opus 5.5 2e531f35e5 Per-state tables: check state identities while reading, never as a tree
occlusion.nbt, cached-collision.nbt, cached-support.nbt, block-properties.nbt and
movement-properties.nbt each carry the identity of every block state, and every
loader parsed them into an NBT tree first: 240 MB per table for ATM10's 153,511
states, about 1.4 GB allocated and freed while the game starts.

nbt::read_named_streaming hands a named list to a visitor element by element.
registry::BlockTableFile reads a table that way, checking each state against the
registry as it comes, and loaders build from a checked registry::BlockTable.
from_nbt stays for trees built in memory (tests, fixtures).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 21:25:11 +02:00
Ludovic MarieandClaude Opus 5.5 0605ee689f LAN: compress a large packet once for every connection
Each player's writer thread compressed every packet it sent, so a chunk
column sent to fifty players was deflated fifty times; compression was
most of the writer threads' time and about half of the server's CPU.
Bodies of 8 KiB or more (chunk columns, mostly) now go through a shared
cache of compressed frame contents keyed by their length, two
independently keyed 64-bit hashes and the threshold, holding the newest
32 MiB. The bytes on the wire are the same: frame::encode is split into
frame::contents and frame::prefixed, and ConnWriter::send_contents
sends a frame from its contents.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 21:06:22 +02:00
Ludovic MarieandClaude Opus 5.5 d746be2ca3 Merge main into feat/server-levels
game/lan.rs: #586's movement-check rules are set before the per-level passes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 20:56:26 +02:00
rdbtCVS 4be192a4b8 Startup: load resources on a worker behind a loading screen (#712)
The synchronous startup loaded every resource before the first window
existed, so the game hung on a blank screen for seconds. An ordinary
launch now enters the event loop at once, opens the window, and draws a
loading screen with the current stage and a progress bar while a
resource-loader thread runs the existing load sequence. When it finishes,
audio is created on the render thread and the real renderer, game and
title screen replace the loading screen on the same window.

Harness, dedicated and scripted runs (--frames, --world-frames,
--benchmark, --control, --dedicated, HORIZON_MENU_STYLE=classic and the
parity fixtures) keep the fully-synchronous path unchanged, so their
output stays byte-identical. title_shown still fires on the real title,
not on the loading screen.
2026-10-17 19:54:10 +01:00
Ludovic Marie dc1a8a4f39 Merge pull request #734 from ludovic111/fix/guest-command-feedback
🤖 Agent: merged by Ludovic's AI agent. Combined with #733 on current main: workspace clippy clean, client 827 tests pass, server_world_gen + dedicated_server PASS (agent: parity --changed 89/89).
2026-10-17 20:43:37 +02:00
Ludovic Marie e2e0752489 Merge pull request #733 from ludovic111/fix/server-level-type
🤖 Agent: merged by Ludovic's AI agent. Combined with #734 on current main: workspace clippy clean, client 827 tests pass, server_world_gen + dedicated_server PASS (agent: parity --changed 89/89).
2026-10-17 20:43:34 +02:00
Cosmic 95b4b7422d Restore saved death and health on world entry (#729) 2026-10-17 14:25:35 -04:00
Ludovic Marie 502222d224 Merge pull request #683 from ludovic111/bench/server-2026-10-17
🤖 Agent: merged by Ludovic's AI agent: server benchmark data on 7bbbf6e9 (data and README only).
2026-10-17 20:25:32 +02:00
Ludovic Marie 4a56e505aa Merge pull request #671 from ludovic111/docs/host-a-server
🤖 Agent: merged by Ludovic's AI agent: hosting guide with the new benchmark numbers; docs only, verify passed.
2026-10-17 20:23:11 +02:00
Ludovic Marie 16d731ebda Merge pull request #710 from ludovic111/fix/release-zip-appledouble
🤖 Agent: merged by Ludovic's AI agent: release zips built without AppleDouble/extended attributes, with a check that fails the build if any slip in.
2026-10-17 20:23:07 +02:00
Ludovic MarieandClaude Opus 5.5 7da32100a8 Server benchmark 2026-10-17: rerun on main 7bbbf6e9 on a quiet machine, equal work, profile
Replaces the overloaded Mac mini data. Keeps the 002f14ce run as a before/after.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 20:18:40 +02:00
Ludovic Marie 3dde7d9955 Merge remote-tracking branch 'origin/main' into sh4/bench-2026-10-17 2026-10-17 20:13:36 +02:00
Ludovic MarieandClaude Opus 5.5 68b0ad8199 Hosting guide: movement checks are in (#586), benchmark on main 7bbbf6e9
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 20:13:26 +02:00
Ludovic Marie 1e655afcab Merge pull request #721 from rdbtCVS/fix/launcher-prism-instance-folder
🤖 Agent: merged by Ludovic's AI agent once verify passed (the earlier failure was a flaky client test #721 doesn't touch; main passes it). Launcher tests run locally on main + #719 + #721: all pass. Thanks @rdbtCVS!
2026-10-17 19:42:04 +02:00
Ludovic Marie ddc7e466e5 Merge pull request #719 from rdbtCVS/feat/launcher-game-arguments
🤖 Agent: merged by Ludovic's AI agent. CI doesn't build the launcher on PRs, so launcher tests were run locally on main + #719 + #721 (cargo test --locked, macOS): all pass. Thanks @rdbtCVS!
2026-10-17 19:41:50 +02:00
Ludovic Marie 09e179804c Merge pull request #680 from ludovic111/perf/mod-content-memory
🤖 Agent: merged by Ludovic's AI agent. Parity 291/291 on the head; combined with main locally: workspace clippy clean, client/world tests pass, mob_ai/save_roundtrip/spawner PASS. ATM10 registry 249 → 125 MB, vanilla 46 → 24 MB.
2026-10-17 19:41:47 +02:00
Ludovic Marie 9403347d13 Merge pull request #732 from ludovic111/docs/website-screenshots
🤖 Agent: merged by Ludovic's AI agent: images, captions and LEGAL.md only; verify passed; screenshots reviewed at full size.
2026-10-17 19:40:35 +02:00
Ludovic MarieandClaude Opus 5.5 c12e1dcd91 Website and README: fresh in-game and menu screenshots from today's Horizon
Six scenes from one fixed-seed world (seed 8675309): a plains village among
savanna hills, sunset over it and the river, moonrise over its lit square, a
crimson forest with a zombified piglin, tropical fish over a warm-ocean coral
reef, and survival at dusk with a zombie and the full HUD. Plus Horizon's own
title, Singleplayer, Video options and pause screens.

Captured with horizon-ctl screenshot from the offscreen client (fullscreen
2560x1286 on the Mac mini, default graphics settings, GUI scale Auto), scenes
cropped to 16:9 and menus to 16:10 (left-aligned) and scaled to the old sizes
(1600x900, 1280x800). The scene sources are JPEGs in docs/media again, the
names scripts/assets.mjs already reads; the README's WebP copies are the
rebuilt website media. Alt texts and LEGAL.md follow the new pictures.

The launcher screenshots are unchanged: the Mac mini's session is locked, so
no window capture works.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 19:38:37 +02:00
Cosmic d35f892b7f Cache closed chest geometry for block-entity fields (#717) 2026-10-17 13:36:09 -04:00
Ludovic Marie cafe59b72e Merge pull request #408 from Online5277/fix/sulfur-cube-inner-layer
🤖 Agent: merged by Ludovic's AI agent. CI parity passed on the head; combined with current main (and #680) locally: workspace clippy clean, horizon-client 826 + horizon-world tests pass, mob_ai/save_roundtrip/spawner PASS. Thanks @Online5277!
2026-10-17 19:30:36 +02:00
Cosmic 41a0b3a9e8 Use the exact standard shield yaw constant 2026-10-17 13:14:16 -04:00
Cosmic 1a6fd4e17d Merge remote-tracking branch 'origin/main' into fix/blocked-hit-animation
# Conflicts:
#	crates/horizon-client/src/mobs/anim.rs
2026-10-17 13:09:02 -04:00
Cosmic cfed77c227 Pose shields in the active hand and preserve local held item data 2026-10-17 13:08:42 -04:00
rdbtCVSandClaude Opus 5.5 4b7812296e launcher: reach Prism's and MultiMC's shared libraries from an instance folder (#693)
A Prism or MultiMC instance keeps no jar of its own: the client sits in the
launcher root's shared libraries, two or three folders above `instances/`,
and the game is downloaded only once the instance is launched. Choosing
`%APPDATA%\PrismLauncher\instances` (or an instance's `minecraft`/`.minecraft`
folder) as the Minecraft folder failed Prepare when the jar was not there yet,
and the error only listed every launcher without saying what to do.

resolve now also looks in the launcher root above an `instances` folder, so a
folder deeper than the four walked above the chosen one still finds the
libraries. When nothing is found and the chosen folder is inside a launcher
Horizon knows, the error names it and says to launch the instance once so it
downloads the game.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 17:57:26 +01:00
Ludovic Marie 39ddc8db3d Merge pull request #674 from ludovic111/feat/wide-block-states
🤖 Agent: merged by Ludovic's AI agent after local checks on a6592eb1: workspace clippy clean; horizon-client/world/protocol tests pass; lan, chunk_stage, dedicated_server, block_entity_storage, chunk_generation PASS.
2026-10-17 18:41:58 +02:00
rdbtCVSandClaude Opus 5.5 dd77fb8016 Launcher: custom arguments for the game's command line
Settings gains a one-line Game arguments field, kept as `game_args` in
settings.json and appended to the client's command line after the
launcher's own, so a player can pass things like `--server-thread` or
`--render-distance 40`. Quotes group a word with spaces in it.

`horizon-launcher game-args [<line>]` shows or sets the line,
`play --arg <ARG>` passes one for a single run, and the MCP launch_client
tool takes an args array. The same splitter feeds all three.

Closes #716.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 17:36:21 +01:00
Ludovic MarieandClaude Opus 5.5 2f82becb9c Hoppers: look up the block only under five-slot containers
tick_hoppers read the block under every plain container block entity
each tick (chests, barrels, shulker boxes, dispensers, droppers) to find
the hoppers. A hopper's block entity is the only five-slot one
(BlockEntity::for_block, and BlockEntity::id already names it the
hopper), so only those are looked up now; a position still needs a
hopper block to tick.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 18:35:42 +02:00
Ludovic MarieandClaude Opus 5.5 d1d36f4121 Guests: experience orbs fly to them and give them their points
On a LAN world or a server, guests never got experience: orbs followed and
were taken only by the host's own player (none on a dedicated server), and
guests weren't sent orbs at all.

- Each orb follows the nearest living, non-spectator player within 8
  blocks, the host or a guest (ExperienceOrb.followNearbyPlayer).
- A guest takes an orb as Player.aiStep does: one of those in its box grown
  by 1, 0.5, 1, at random, at most every 2 ticks (takeXpDelay), repairing
  with Mending first; the rest goes to the guest's experience (sent with
  SetExperience as before), the level-up sound every fifth level.
  TakeItemEntity goes to the players around, the pickup sound plays at the
  orb.
- Orbs have entity ids and are tracked to guests (AddEntity, DATA_VALUE).
  The client shows a server's orbs, moves them with the server's packets and
  flies them to their collector.

tools/lan_e2e.py xp: three 7-point orbs near the guest: it sees three and
ends at level 2 (21 points); the host gets none.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 18:13:55 +02:00
Ludovic MarieandClaude Opus 5.5 22dc3e5954 Guests: touching mobs push them on their own client, as in vanilla
A mob's push of a guest went to the guest as SetEntityMotion carrying the
server copy's velocity, which nothing damps: every tick of contact added
to it and replaced what the guest's client was doing. Vanilla pushes
players on their own client only (EntitySelector.pushableBy): the server
no longer sends those pushes, and a server client now pushes its player
away from every living entity it touches (Entity.push, team rules, no
spectators, bats or passengers of the same vehicle).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 18:13:55 +02:00
Ludovic MarieandClaude Opus 5.5 1c2f7664c3 Cull minecarts by distance and view like vanilla
Minecarts were drawn every frame wherever they were: at render distance 32
every chest minecart of every loaded mineshaft went through the rail
lookups, display block and model, and the per-vertex terrain occlusion
test. Vanilla draws a cart only within 64 times its bounding box's mean
edge (about 57 blocks at entity distance 100%), measured from its current
position, and only when its interpolated box, grown by 0.5 and stretched
up by the display block's offset, is in the view
(EntityRenderer.shouldRender, AbstractMinecartRenderer
.getBoundingBoxForCulling). The cart the player rides always draws.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 18:10:34 +02:00
Ludovic MarieandClaude Opus 5.5 69dd4feb31 Server clients: mobs' and players' heads turn smoothly, not flicker
On a LAN world or a server, other entities' heads flickered between two
directions. The client wrote ClientboundRotateHeadPacket's head yaw into
the entity's yRot and the move packets' yRot into the body yaw, never moved
the head's previous yaw and pitch on, and never ran the body rotation, so
each frame interpolated the head from stale values.

As vanilla: move packets interpolate the position, yRot and xRot over three
ticks (LinearInterpolationHandler), RotateHead the head yaw over three
(lerpHeadTo), each tick keeps the previous head rotation for the frames, and
the body follows (a mob's BodyRotationControl, a player's walk direction
within 50 degrees of the look). AddEntity sets yRot and xRot and puts the
body and head at the packet's head yaw (recreateFromPacket).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 17:52:01 +02:00
Ludovic MarieandClaude Opus 5.5 123e8286ac Guests: container clicks reach the server (chests, furnaces, crafting drags)
On a LAN world or a server, a guest's clicks that went through the menu
engine (placing a carried stack, quick craft drags, double clicks, number
keys, Q) ran only on the guest's own copy and were never sent: the item
showed in the chest or furnace, the server still had it in the inventory,
and the next menu brought it back.

menu_ops now sends every click of the server's open menu as vanilla's
MultiPlayerGameMode.handleContainerInput does: the engine predicts it on
the client's copy, then a ContainerClick goes out with the slots it changed
and the carried stack after it (a drag is its three kinds of clicks). The
plain inventory and crafting table are predicted too, the Shift double click
sends each QUICK_MOVE, and a prediction no longer drops, crafts or makes maps
on the client (the server does).

tools/lan_e2e.py clicks: real mouse presses put a stack into a chest and a
furnace, drag planks over the crafting grid and craft; the server has each.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 17:51:33 +02:00
Ludovic MarieandClaude Opus 5.5 d99cf24400 LAN: wake the writer threads from one post thread, once per tick
At the end of each tick the game thread handed every player's held
packets to that player's writer thread, waking each one: a system call
per player, and on a busy machine often the rest of the core's time
slice to the woken writer. Now the batches go to one post thread in a
single send, and it wakes the writers. A packet sent to a player while
its batch is still with the post thread goes the same way, so each
player's packets keep their order.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 17:51:00 +02:00
Ludovic MarieandClaude Opus 5.5 43f598fd4e LAN: write chunk packets straight into one buffer
ChunkEncoder::body_in writes the LevelChunkWithLight body that
Clientbound::encode would give, into one buffer sized up front: the
cached section bytes and light layers are copied once, uniform light
layers are filled in place, and the heightmaps are shared rather than
cloned. Before, each chunk sent built a packet (growing its buffer,
cloning every 2048-byte layer), then encode copied all of it again.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 17:50:32 +02:00
Ludovic MarieandClaude Opus 5.5 ce62f9c57f Server clients: eating plays on the player's own screen
A server client sent UseItem and never started the use itself, so food
showed no eating animation or crumbs, and holding right click sent a new
UseItem every 4 ticks. Like MultiPlayerGameMode.useItem, the client now
starts eating (Consumable.startConsuming), counts the use down for the
animation and its particles, and starts no other use until it ends; the
server still eats the item. Looking at another player no longer stops
the item from being used (Player.interactOn passes).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 17:42:19 +02:00
Ludovic MarieandClaude Opus 5.5 28d218b3a2 Guests: a bed puts the guest to sleep, not the host
The bed's sleeping state, timer and respawn point were the host's fields,
so a guest clicking a bed put the host to sleep and moved the host's
spawn. as_guest now swaps them with the guest's own; the guest gets its
sleeping pose and bed (entity data 14), wakes on STOP_SLEEPING with the
WAKE_UP animation, and the night skips only once enough players sleep
(players_sleeping_percentage). Guests respawn at their own bed, else the
world spawn. Server clients lie in the bed from the synced position.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 17:40:40 +02:00
Ludovic MarieandClaude Opus 5.5 51b58f3897 Metal: draw terrain sections as one index list per page run
wgpu replays an indirect multi-draw on Metal as one draw call per entry, so
every frame encoded a draw per visible face range and translucent section
(9,300 at render distance 32). Where multi-draw is emulated (Metal) and a
frame has at least 4,096 section draws, a compute pass now copies the
frame's section draws into one index list, in draw order, and each run of
sections in the same arena page is one draw call: 9,309 draw calls become
about 750 at RD32. Smaller frames keep the per-section draws, where the
list's GPU cost (about 0.4 ms on an M4 Pro at RD16) outweighs the CPU saved.

The picture is unchanged by construction: the same triangles from the same
vertices in the same order through the same shader code. Only where a
vertex finds its section's origin changes, from the instance attribute to
a table: a section's vertices fill whole 256-vertex blocks of its page, each
block names the section, and the section's camera-relative origin is
computed on the CPU exactly as before.

Vulkan keeps the native multi-draw. HORIZON_FLAT_DRAWS=1 draws every frame
flat (any backend that supports it), 0 never; the control command
`section-draws flat|indirect` switches in a running game for A/B checks.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 17:34:52 +02:00
Ludovic MarieandClaude Opus 5.5 a6592eb188 Merge main into feat/wide-block-states
lan/chunks.rs: #706's per-section block entity lists carry u32 states.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 17:31:24 +02:00
Ludovic Marie 7bbbf6e97f Merge pull request #670 from ludovic111/feat/server-bench-equal
Server benchmark against vanilla 26.3 on equal work
2026-10-17 17:27:43 +02:00
Ludovic Marie 0354c43f62 Merge pull request #586 from ludovic111/feat/server-move-checks
Dedicated server: vanilla's chunk pacing and movement checks
2026-10-17 17:27:33 +02:00
Ludovic MarieandClaude Opus 5.5 fc6db37b6a GPU frame time on Metal, render spans and section draw counts
Apple GPUs time only pass boundaries (no TIMESTAMP_QUERY_INSIDE_ENCODERS),
so the GPU timer was off on every Mac and benchmarks reported no GPU time.
Where only pass boundaries can be timed, the frame is now bracketed by two
empty compute passes that write the timestamps.

HORIZON_SPIKES also names the renderer's parts (occlusion, visibility,
encode, submit), and the status reply counts the frame's section draws and
the draw calls that submit them.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 17:17:20 +02:00
Ludovic MarieandClaude Opus 5.5 897cd7a1b7 Hosting guide: names you add are accounts (#589, #694), the locator bar is in, HORIZON_ONLINE
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 16:57:47 +02:00
Ludovic Marie 900e0af776 Merge commit 'HEAD@{1}' into HEAD 2026-10-17 16:51:55 +02:00
Ludovic MarieandClaude Opus 5.5 bbde190f06 Merge main into feat/server-move-checks
TickCtx gained `collision` here; main's two newer initializers
(lan_actions respawn, server_travel's server_ctx) now pass it too, as
every other one does.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 16:51:53 +02:00
Ludovic Marie 17dcb6f793 Merge remote-tracking branch 'origin/main' into HEAD 2026-10-17 16:50:12 +02:00
Ludovic MarieandClaude Opus 5.5 eda6e4b510 Merge feat/wide-block-states (with origin/main) into perf/mod-content-memory
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 16:32:46 +02:00
Ludovic MarieandClaude Opus 5.5 a79baaa031 server_world_gen: an unknown biome logs vanilla's 'Unknown biome, defaulting to plains'
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 16:32:37 +02:00
Ludovic Marie 3b8cc9fcc8 Merge pull request #713 from ludovic111/perf/advancement-tree-prefetch
Advancements: read the tree on another thread while the world loads
2026-10-17 16:31:31 +02:00
Ludovic Marie bdfa466bd9 Merge pull request #707 from ludovic111/perf/hub-fx-player-sets
Server: Fx hashing for each player's sent chunks and tracked entities
2026-10-17 16:31:26 +02:00
Ludovic Marie 9198e36c7c Merge pull request #706 from ludovic111/perf/chunk-block-entity-cache
Chunk packets: keep each section's block entity blocks with its bytes
2026-10-17 16:31:21 +02:00
Ludovic Marie fb536df24d Merge pull request #705 from ludovic111/perf/chunk-occupied-lookups
Chunk packets: one cache lock per column for its non-air sections
2026-10-17 16:31:17 +02:00
Ludovic Marie 689c262b37 Merge pull request #704 from ludovic111/perf/entity-tracking-ranges
Entity tracking: each entity type's range once per tick, not per player
2026-10-17 16:31:13 +02:00
Ludovic Marie 83afec2d52 Merge pull request #703 from ludovic111/perf/generation-scan-union
Chunk streaming: look at each column once when players' areas overlap
2026-10-17 16:31:07 +02:00
Ludovic Marie 655e171d9a Merge pull request #702 from ludovic111/perf/hopper-tick-prefilter
Hoppers: look up the block only under container block entities
2026-10-17 16:31:02 +02:00
Ludovic Marie c32c00aeb2 Merge pull request #701 from ludovic111/perf/advancements-flush-idle
Server: skip the advancement flush for players whose progress didn't change
2026-10-17 16:30:57 +02:00
Ludovic MarieandClaude Opus 5.5 3cae8d1d73 Dedicated server: level-type, generator-settings and generate-structures for a new world
DedicatedServerProperties.WorldDimensionData: a world that doesn't exist
yet gets level-type's preset (flat or normal; an unknown name warns and
falls back to minecraft:normal), a flat world's generator-settings
(FlatLevelGeneratorSettings: layers, biome, lakes, features; a layer that
doesn't parse is logged and left out, and settings without layers use the
preset's classic ones) and generate-structures, written to
world_gen_settings.dat before the world opens, with vanilla's log lines.
Horizon ignored all three and always made a normal world.

New harness server_world_gen compares world_gen_settings.dat and the lines
logged with vanilla's for five server.properties.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 16:15:30 +02:00
Ludovic Marie 4fffbaa1c7 Merge origin/main into docs/host-a-server 2026-10-17 16:06:56 +02:00
Ludovic MarieandClaude Opus 5.5 59fe284c7d Commands name the guest who runs them, not "Player"
/give, /clear, /gamemode, /experience, /advancement, /playsound and the
whisper-to-yourself lines printed the host's placeholder name for the
acting player. They now use the acting player's name (a guest's own while
their command runs), as target_name and the scoreboard do.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 16:06:26 +02:00
Ludovic MarieandClaude Opus 5.5 8912d5d269 dedicated_server harness: compare the op's command feedback on the console, and a score on @s
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 15:41:19 +02:00
Ludovic MarieandClaude Opus 5.5 e2f2177b60 Scoreboard: a guest's @s is the guest, not "Player"
While a guest's command or action runs, the score holder, team, display
name and death and kill criteria of the acting player use the guest's own
name (ScoreHolder.getScoreboardName), as on vanilla's server.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 15:41:19 +02:00
Ludovic MarieandClaude Opus 5.5 91a4290f50 Guests' command feedback reaches the console and the other operators
CommandSourceStack.broadcastToAdmins: on a dedicated server, a guest's
command success (sendSuccess with broadcast) is logged on the console as
"[Name: message]" when log_admin_commands is on, and sent gray and italic
to every other operator when send_command_feedback is on. The queries and
lists vanilla sends with broadcast false (commands/admin.rs) stay with the
player who asked. Lines are queued while the guest acts and sent once the
command returns.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 15:41:19 +02:00
Ludovic MarieandClaude Opus 5.5 7d50e879da Release zip: no macOS ._ metadata files
ditto without --norsrc --noextattr --noqtn stores an AppleDouble ._ entry
beside every file that has extended attributes, so the macOS release zip
held hundreds of them (horizon-macos-arm64/._horizon-ctl, tools/._*.py).
Package it as packaging/macos/package.sh already does, and fail the job if
any ._ or __MACOSX entry is left.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 15:40:07 +02:00
Ludovic Marie 1fc1b75007 Merge pull request #699 from ludovic111/ci/launcher-release-paths
Launcher releases: count every crate the launcher builds in
2026-10-17 15:32:58 +02:00
Ludovic MarieandClaude Opus 5.5 805d5532e5 Give mob, beacon, sculk and splash effects to every player
On a server (the integrated server thread and the dedicated server)
the host has no player of its own, so effects that only touched the
host's player reached nobody; on a LAN world they reached only the
host. Beacons, the warden's and shriekers' darkness, the elder
guardian's mining fatigue, splash and lingering potions' splash and
the raid victory's Hero of the Village now run for each player in
the level, guests through `as_guest`.

The elder guardian's curse sends the guest the GUARDIAN_ELDER_EFFECT
game event, which the remote client now shows (the apparition and
the curse sound).

lan_e2e.py gains an `effects` check: a speed beacon next to both
players must give both of them speed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 15:04:04 +02:00
Ludovic MarieandClaude Opus 5.5 f537c84ebb VSync on waits for the display whatever the present mode (#684)
With an explicit Present Mode (Mailbox, Immediate), the VSync option did
nothing: the setting replaced vanilla's choice even with VSync on, so a
window kept running uncapped. Vanilla's VSync means Fifo, in a window and in
fullscreen alike. Now VSync on always presents with Fifo, and the Present
Mode setting chooses only while VSync is off (Auto keeps vanilla's choice:
Immediate, else Mailbox, else Fifo). The choice is one tested function,
window::chosen_present_mode.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 15:04:00 +02:00
Ludovic MarieandClaude Opus 5.5 a60519eb63 Merge origin/main into feat/wide-block-states
The LAN chunk encoder's section cache (#623) and dimension range (#668)
are kept; a cached section's wire bytes use the registry's palette width,
set once per process before any encoder exists, so the cache stays keyed
on the section alone. The sculk scan's palette check takes the u32 state.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 15:02:53 +02:00
Ludovic Marie f90f33b6a9 Merge pull request #698 from ludovic111/launcher-win-fix
Launcher: Windows tests pass again (releases blocked since launcher-v0.4.0)
2026-10-17 15:02:37 +02:00
Ludovic MarieandClaude Opus 5.5 f49af8d995 Launcher releases: count every crate the launcher builds in
LAUNCHER_PATHS and launcher.yml's push paths now list horizon-bench,
horizon-friends, horizon-mods and horizon-port-pack too, the launcher's
path packages per cargo metadata. A change there (like the horizon-mods
dependency that left launcher/Cargo.lock stale) then builds and releases
the launcher instead of going unnoticed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 14:59:01 +02:00
Ludovic MarieandClaude Opus 5.5 b2784363a1 Mobs see only the guests in their own level
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 14:49:29 +02:00
Ludovic MarieandClaude Opus 5.5 bff5891073 Launcher: Cargo.lock has horizon-port-pack's horizon-mods dependency
horizon-port-pack now depends on horizon-mods (1f1d5a5a), which the
launcher's own lock file didn't have, so every --locked build of the launcher
failed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 14:46:16 +02:00
Ludovic MarieandClaude Opus 5.5 a67a323959 Server: one level per dimension, so players can be in different dimensions
A server (the dedicated server and single player's server thread) now
holds a level per dimension, like vanilla's `MinecraftServer.levels`. Each
server tick runs every level with players in it, one at a time, by swapping
its fields into place (game/levels.rs), so the world tick code runs
unchanged. Before this, a portal did nothing while two or more players were
online.

- Levels: the parked levels keep the world, streaming, save, block ticks,
  entities, block entity state, raids, sculk and creaking scans. The
  Overworld stays loaded; another level is saved and closed after 300 ticks
  with nobody in it. Saving saves every level.
- Hub: `tick` is split into `poll` (the network, once per tick),
  `tick_level` (the packets, chunks, block changes, containers and entities
  of the players in the level in place) and `end_tick` (latency). Each guest
  has a dimension; time, weather, sounds and events reach only the players
  in their level, and the clock and weather last sent are kept per level.
- Travel: every player has their own portal state; a portal moves only that
  player into the destination's level (`ServerPlayer.teleport` packets as
  before). Respawning brings a player back to the Overworld's level.
- Player data saves and loads each player's dimension, and a returning
  player is placed in their own level.
- tools/lan_e2e.py --server-thread --check portals: the guest goes to the
  Nether while the host stays, then the host goes to the End while the guest
  is in the Nether, and both come back.

The in-frame game keeps its one level that follows its player.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 14:46:03 +02:00
Ludovic MarieandClaude Opus 5.5 05821d4e62 Launcher: Windows tests check behaviour, not symlink paths
On Windows the launcher links another launcher's client jar with a hard link
(a copy across volumes) and its libraries and assets with a junction, since
symlinks need developer mode. canonicalize() of a hard link is its own path,
so the sources tests, which compared canonical paths, failed on Windows and
blocked every launcher release since launcher-v0.4.0. They now check that the
linked jar is the launcher's file (a byte written through one shows in the
other), which holds for a symlink and a hard link alike.

The modpack profile test compares paths as paths rather than strings ending
in "a/carry".

mklink /J's paths are always quoted: Rust only quotes arguments with spaces,
and cmd reads an unquoted & or ^ in a user folder as its own syntax.

CI runs every launcher test target, not just the library's.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 14:45:52 +02:00
Ludovic MarieandClaude Opus 5.5 d989d4033f Advancements: read the tree on another thread while the world loads
The first criterion trigger of a world's first tick loaded the
advancement tree: about 1,400 JSON files read and parsed on the game
thread, about 60 ms of a 125 ms first tick. open_world now starts that
read on a thread, and the first advancement_tree() takes its result,
the same tree as before.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 14:16:54 +02:00
Ludovic MarieandClaude Opus 5.5 45cbb4f7d6 LAN: a guest's chest, crafting table or sign no longer takes the host's mouse (#687)
A guest's action runs on the host's game with the guest's state swapped in
(`as_guest`). The swap restored the open screen but not what opening one
does to the window: `capture(false)` released the host's pointer, so the
cursor showed and the camera stopped whenever a guest opened a chest, a
crafting table or any other menu. `keys.clear()` dropped the keys the host
was holding, and a sign, book, command block or mount menu the guest opened
was drawn on the host's screen.

- `capture` leaves the window alone while a guest acts.
- `as_guest` keeps the host's held keys, sign editor, book, command block
  screen and mount menu, as it already kept the open screen.
- `state` reports `pointer_grabbed` (mouse movement turns the camera).
- tools/lan_e2e.py: `chest` and `crafting` check that the host keeps its
  screen and its mouse look; a new `sign` check places a sign as the guest.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 14:13:46 +02:00
Ludovic MarieandClaude Opus 5.5 042f634c7a Hosting guide: link the locator bar PR
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 13:32:45 +02:00
Ludovic MarieandClaude Opus 5.5 1ac9c72ea3 Server: Fx hashing for each player's sent chunks and tracked entities
The hub tests every player's sent-chunk set for every entity and every
column of the view each tick, and each player's tracked-entity map for
every entity: tens of thousands of SipHash lookups a tick with 50
players. Both now use the Fx hash. Their iteration order was random
before and carries no meaning.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 13:15:36 +02:00
Ludovic MarieandClaude Opus 5.5 a7af5b76cf Chunk packets: keep each section's block entity blocks with its bytes
Every chunk packet scanned all 4,096 blocks of each section holding a
block entity block to list them, though the section's wire bytes were
already cached. The cache now keeps those blocks (index and state, in
scan order) beside the bytes, made in the same pass that encodes them.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 13:03:47 +02:00
Ludovic MarieandClaude Opus 5.5 4cdad5aae6 Chunk packets: one cache lock per column for its non-air sections
encode_in asks whether each of the nine surrounding columns' 24 sections
holds anything, to decide which light layers exist: 216 cache lookups,
each locking the cache and SipHashing a section's address. The column's
sections now share one lock, and the cache uses the Fx hash.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 13:00:27 +02:00
Ludovic MarieandClaude Opus 5.5 66a8fa1181 Entity tracking: each entity type's range once per tick, not per player
The hub looked up an entity's tracking range (a binary search over type
names) for every player and every entity each tick: about 87,000 string
comparisons a tick with 50 players. The ranges are now worked out once
per entity per tick; the visibility test is the same.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 12:56:30 +02:00
Ludovic MarieandClaude Opus 5.5 690e656048 Chunk streaming: look at each column once when players' areas overlap
The generation scan walked every area's square of columns, so with 50
players standing near each other it tested about 100,000 columns a tick
(each against every player's range) to find the few hundred around them.
The squares are now merged into one run per row first. The candidates
are the same: they were sorted and deduplicated before.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 12:56:30 +02:00
Ludovic MarieandClaude Opus 5.5 47a00d283d Hoppers: look up the block only under container block entities
tick_hoppers read the block under every block entity in the loaded world
each tick to find the hoppers. A hopper's block entity is always a plain
container (BlockEntity::Chest), so only those need the lookup.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 12:56:30 +02:00
Ludovic MarieandClaude Opus 5.5 c98a4f7a58 Server advancements: flushDirty skips players whose progress didn't change
PlayerAdvancements.flushDirty only works when it is the first packet or
progress changed. Horizon walked the whole advancement tree for every
network player every tick to find nothing new: a fifth of the server
tick with 50 players.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 12:56:29 +02:00
Ludovic MarieandClaude Opus 5.5 74da721249 Hosting guide: online mode is merged, the 10-17 benchmark, working links
Online mode checks accounts since v2026.10.17.07; until #589, players added
before they join get offline UUIDs, with a workaround. Portals follow a
lone player since v2026.10.17.09. Copied game files need v2026.10.17.07 or
newer. Section 10 links the 2026-10-17 benchmark and says plainly that 25
moving players are too many for now. Links to other docs are absolute, so
they work both on GitHub and on the site.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 11:37:16 +02:00
Ludovic MarieandClaude Opus 5.5 b6603122d7 Publish the 2026-10-17 server benchmark against vanilla, with the machine's load
horizon-bots and tools/server_bench.py at 1, 10, 25, 50 and 100 bots,
walk and explore, Horizon (main at 123987c4) and vanilla 26.3 taking turns.
The Mac mini was overloaded by other work (load 55-141), so only the 1-
and 10-bot rows are a fair comparison; the README says so and gives the
load of every run. It also records where Horizon's server thread goes at
25 walking bots: 96 % in ChunkEncoder::encode_in.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 11:24:03 +02:00
Cosmic e0364da4eb Avoid hurt animation for fully blocked shield hits 2026-10-17 05:11:42 -04:00
Ludovic MarieandClaude Opus 5.5 ed9a25ad49 Measure where content mods' memory goes; drop the registry's second copy
- perf_bench.py samples the physical footprint on macOS as well as
  RSS. RSS counts the pages the allocator freed and marked reusable,
  so the six mods' "+1.1 GB" is about 0.5 GB of footprint.
- examples/resource_memory.rs: live heap, peak, RSS and footprint after
  each resource loader, for a prepared or a content-mod root.
- BlockRegistry indexes ids by a hash of the state, checked against
  the states, instead of keeping every state twice: -22 MB live for
  vanilla, -124 MB for ATM10's 42 mods (193,762 states).
- docs/modpacks.md: the per-loader breakdown and the next cut (the
  per-state tables parsed whole at startup).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 10:44:05 +02:00
Ludovic MarieandClaude Opus 5.5 4d1596fe3b Load ATM10's 42 carried mods: room past 8,192 textures and tags
With the wide state ids, ATM10's 42 data-tier mods (193,762 states)
reach the world once these loaders stop refusing them:

- Block sprites: eight texture arrays of 2048 layers instead of four
  (9,790 block textures for the 42).
- Per-state tables read as NBT: the table limits cover 2^20 states, and
  the collision table in level data uses them.
- Worldgen block tags: 2^20 blocks instead of 8,192 (mineable/pickaxe
  holds every stone block of a pack).
- As vanilla's loaders do, a blockstate definition that does not bind,
  a missing or broken block model of a mod, and a recipe that does not
  parse are logged and skipped instead of dropping every mod (or
  stopping the game).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 09:12:41 +02:00
Ludovic MarieandClaude Opus 5.5 f48a54da09 Block state ids past 16 bits
Chunk sections keep block states as u32 and size the global palette
to the registry: 16 bits for vanilla's 35,723 states, wider when
content mods take the registry past 65,536. The content-mod limit
goes from 2^16 to 2^20 states.

The LAN host writes chunks at the registry's width, so with vanilla
(or mods within 16 bits) the wire is unchanged. Remote chunks are read
at the server's width, 16 from a vanilla server, and widened to ours.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 09:12:41 +02:00
Ludovic MarieandClaude Opus 5.5 57ee531c63 Host a server: a guide in docs/hosting.md, and the site's /server page built from it
What works on main today (install, game files, first run, server.toml, the commands, the web panel,
backups, services, Docker built by hand) and, plainly, what doesn't yet: offline mode only, no
published image, no secure chat or locator bar, the open PRs that are coming. The site renders the
guide into /server at build time, so it has one source. The CSP hashes now cover every page's inline
script, so the privacy page's theme script is allowed too.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 08:45:48 +02:00
Cosmic 78e05bdc95 Merge latest main into fix/classic-menu-default 2026-10-17 02:41:52 -04:00
Cosmic a195b4849e Merge main into fix/classic-menu-default 2026-10-17 02:39:42 -04:00
Ludovic MarieandClaude Opus 5.5 6b0b34098e Benchmark: Horizon's server against vanilla 26.3 on equal work
Walk and explore passes at 1 to 100 bots on the ThinkPad (bots on the Mac
mini), vanilla with the locator bar off, plus an explore pass at a rate both
servers keep up with. The README has the method, the tables, the profile of
what remains at 25 walking bots and the earlier runs; docs/server.md section
9 and the Scale row summarise it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 08:09:50 +02:00
Ludovic MarieandClaude Opus 5.5 a3bf174ddb Collision scans: a counting cursor, and fewer lookups and allocations per block
Block collision scans (vanilla's BlockCollisions) are most of every mob's
move, and moves were the largest share of the world tick with 600 mobs.
Per cell and per hit, the scan did work vanilla's JIT makes cheap:

- the cursor divided its index by the box width and height for every cell;
  it now counts x, then y, then z, as Cursor3D does (same cells, same order,
  same budget);
- the unit cube test cloned the shared unit cube to compare identities
  (`VoxelShape::is_block` compares with it in place), and built a
  normalised box to test it against the entity box (`intersects_corners`);
- every non-empty block looked its state up in the contextual rule map
  (scaffolding, liquids, powder snow); a flag in the scan table now says
  which states have one;
- a moved shape decomposed its occupancy into boxes again; the moved
  shape's boxes are its own boxes moved, the same values.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 05:55:21 +02:00
Ludovic MarieandClaude Opus 5.5 c5851e77aa horizon-bots: --explore-speed, for an explore pass with equal work
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 05:42:27 +02:00
Ludovic MarieandClaude Opus 5.5 3cd26c6d0b server_bench: --gamerule, more game rules for both servers
Horizon has no locator bar yet, so equal-work runs turn vanilla's off
(--gamerule locator_bar=false): its waypoint updates cost every move
a pass over every other player.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 05:17:33 +02:00
Ludovic MarieandClaude Opus 5.5 c8003301bc horizon-bots: vanilla's chunk batch sizing, chunk-gated movement and chunk accounting
The bots acknowledge chunk batches with ChunkBatchSizeCalculator's
desired chunks per tick, from how long each batch takes to arrive,
instead of a fixed 25. A bot moves only while it holds the chunk it
stands in, as vanilla's client stops ticking its player in a chunk it
hasn't received, so no bot outruns a server's chunk sending. Each bot
keeps the chunks it holds, so the report separates chunks that entered
its view from chunks sent again while it still held them, and counts
the moves the server refused and the ticks it waited for its chunk.

server_bench.py runs one pass per behaviour (walk, explore), spawns
every bot on the world spawn itself (respawn_radius 0) so both servers
see the same paths, can run the bots on another machine (--bots-via,
--bots-bin, --connect) and the servers under nice, and reports new,
dropped and re-sent chunks per second, pull-backs and the server's
"moved too quickly" lines.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 05:17:33 +02:00
Ludovic MarieandClaude Opus 5.5 b347ceb195 Dedicated server: vanilla's chunk pacing and movement checks
PlayerChunkSender: one unacknowledged batch until the client's first
acknowledgement and ten after it, batches capped at the client's
desired chunks per tick (not twice that), and the quota reset to one
chunk whenever every batch is acknowledged.

ServerGamePacketListenerImpl.handleMovePlayer: a move further from
where the tick began (firstGood) than 100 blocks squared per move
packet (300 while gliding; the count falls back to 1 past 5 packets)
logs "<name> moved too quickly!" and pulls the player back, as the
player_movement_check and elytra_movement_check game rules allow.
NaN coordinates or non-finite angles and two position moves in one
client tick disconnect with invalid_player_movement, positions are
clamped to the world's limits, an unanswered teleport is sent again
after 20 ticks, and accepting a teleport runs the move it carries.

The move is then replayed against the blocks: a survival or adventure
player who ends more than 0.25 blocks short of the target (horizontally;
vanilla drops the vertical part) logs "<name> moved wrongly!" and is
sent back unless they were already stuck, and any move into a block
the old box did not touch (isEntityCollidingWithAnythingNew) is sent
back silently. Spectators and sleepers skip both. Angles wrap as
Mth.wrapDegrees does.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 05:16:08 +02:00
Cosmic 6285a15cf3 Default to Classic menus while preserving explicit preferences 2026-10-16 19:15:02 -04:00
Ludovic MarieandClaude Opus 5.5 65a6ab7fa4 Merge main into fix/sulfur-cube-inner-layer
Resolve minecarts.rs against the resources split (#537): the block model
catalog now lives in self.pack. Fill MobData's new villager_hats and pois
fields in the sulfur cube render test.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 00:21:04 +02:00
Cosmic 92236010fd Document the sulfur body component limitation 2026-10-15 22:22:07 -04:00
Cosmic 938f2ec884 Merge remote-tracking branch 'origin/main' into fix/sulfur-cube-inner-layer
# Conflicts:
#	crates/horizon-client/src/game/creatures.rs
2026-10-15 22:19:52 -04:00
Cosmic e43e8ae82b Match sulfur cube block placement, lighting and fuse flashes 2026-10-15 22:19:39 -04:00
Cosmic 18e9c14642 Render swallowed sulfur cube blocks and award Uh Oh 2026-10-15 22:12:08 -04:00
Cosmic f5a795988b Draw sulfur cube cores before translucent shells 2026-10-15 15:56:59 -04:00
Ludovic MarieandClaude Opus 5.5 f0aa88b0f6 Centre the cursor when a screen opens from gameplay
Releasing the pointer only ungrabbed it, so the cursor showed up wherever
the system had it: on Wayland, where the locked pointer last stopped (often
where the previous screen was closed), elsewhere wherever the hidden pointer
had drifted. Opening the inventory, a chest, the pause menu, chat, a sign or
a book could put it anywhere.

Game::capture now follows MouseHandler.grabMouse/releaseMouse: both move the
pointer to the window centre, and releasing also sets Horizon's own cursor
(what every screen hit-tests and draws hover with) to that point. On Wayland
the position can only be set as the locked pointer's hint, applied by the
compositor as the lock goes, so it is set before ungrabbing; X11, Windows
and macOS warp, so it is set again once the pointer is free. Releasing an
already released pointer does nothing, so screen-to-screen changes keep the
cursor where it is, as in vanilla, and nothing warps while the window is
unfocused (GLFW's glfwSetCursorPos does the same), so losing focus never
pulls the pointer back into the game.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-15 01:12:18 +02:00
2218 changed files with 310039 additions and 51088 deletions

No files matched your search

-1
View File
@@ -2,7 +2,6 @@
target/
.local/
**/node_modules/
website/
benchmarks/
packs/
mods/
+6
View File
@@ -0,0 +1,6 @@
# 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
crates/horizon-theme/tokens.css text eol=lf
docs/api/*-reference.md text eol=lf
+1 -1
View File
@@ -1,2 +1,2 @@
#!/bin/sh
exec python3 tools/check_public_tree.py --history
exec python3 tools/check_public_tree.py --pre-push
+16
View File
@@ -0,0 +1,16 @@
# Changes to these files wait for Ludovic's review once branch protection requires code owners' reviews
# (tools/ci/protect_pipeline.sh, docs/security.md). They are what builds, signs and ships releases, the code that
# checks downloads before installing them (backend.rs downloads, installs and updates the launcher itself; links.json
# says where from; the Cargo.toml files pick the crates that do it), and this file. Everything else merges as soon as
# `verify` passes.
/.github/ @ludovic111
/tools/ci/ @ludovic111
/tools/release_train.py @ludovic111
/crates/horizon-release-sign/ @ludovic111
/crates/horizon-server/src/update.rs @ludovic111
/crates/horizon-server/Cargo.toml @ludovic111
/launcher/src/signed.rs @ludovic111
/launcher/src/backend.rs @ludovic111
/launcher/links.json @ludovic111
/launcher/Cargo.toml @ludovic111
/packaging/ @ludovic111
+1
View File
@@ -0,0 +1 @@
0.2
+27
View File
@@ -0,0 +1,27 @@
name: cancel-on-close
# A closed or merged pull request's queued and running checks free the runners at once.
# pull_request_target so the token can cancel runs of pull requests from forks too;
# it checks out nothing and runs no code from the pull request.
on:
pull_request_target:
types: [closed]
permissions:
actions: write
jobs:
cancel:
runs-on: ubuntu-latest
steps:
- name: Cancel the pull request's unfinished runs
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
BRANCH: ${{ github.event.pull_request.head.ref }}
SHA: ${{ github.event.pull_request.head.sha }}
run: |
for status in queued in_progress waiting pending requested; do
gh run list -R "$REPO" --branch "$BRANCH" --status "$status" --limit 100 \
--json databaseId,event,headSha \
-q ".[] | select(.event == \"pull_request\" and .headSha == \"$SHA\") | .databaseId"
done | sort -u | while read -r id; do
gh run cancel -R "$REPO" "$id" || true
done
+88 -129
View File
@@ -1,23 +1,30 @@
name: checks
# verify: every pull request and every push to main, on GitHub's runners (the repository is public).
# verify: every pull request and every push to main, on GitHub's runners (the repository is public). It is the
# check main requires, so it never has a condition: a skipped job counts as passed, and every run of this workflow
# must report a real verify (a pull request that changes no Rust finishes it in seconds instead).
# parity: the vanilla harnesses a pull request's changes can affect, on the Mac mini's runner, which holds the
# maintainer's own Minecraft files (Horizon never downloads the game). A fork's pull request runs it only when
# a maintainer adds the run-parity label, once per label. See docs/release-flow.md.
# maintainer's own Minecraft files (Horizon never downloads the game); .github/workflows/parity.yml. Only for a pull
# request that changes a harness, oracle, probe or the runner: the rest are checked on main by parity-main. A fork's pull
# request runs it only when a maintainer adds the run-parity label (.github/workflows/run-parity.yml), so labels
# never start this workflow. See docs/release-flow.md.
on:
push:
branches: [main]
pull_request:
types: [opened, synchronize, reopened, labeled]
types: [opened, synchronize, reopened]
workflow_dispatch:
permissions:
contents: read
concurrency:
# A new push to a pull request replaces its running checks; main's runs all finish (releases follow them).
group: checks-${{ github.event.pull_request.number || github.sha }}-${{ github.event.action == 'labeled' && 'label' || 'run' }}
group: checks-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
# Test runs never reach the telemetry server: no live-count ping, no uploads (docs/telemetry.md).
HORIZON_TELEMETRY_URL: "off"
jobs:
verify:
if: github.event.action != 'labeled'
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
@@ -55,43 +62,89 @@ 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
parity:
if: >-
github.event_name == 'pull_request' && (
(github.event.pull_request.head.repo.full_name == github.repository && github.event.action != 'labeled') ||
(github.event.action == 'labeled' && github.event.label.name == 'run-parity'))
runs-on: [self-hosted, horizon-game]
timeout-minutes: 120 # a change to core crates reruns ~300 harnesses: ~60-90 min on the Mac mini
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
# The launcher is its own workspace (launcher/Cargo.toml), so verify's cargo runs never build it. A pull request that
# changes it or a crate it builds is checked here, and so is every push to main: two pull requests can each pass
# alone and break it together (#2059 and the registry ratings did, 2026-10-26).
launcher:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Does this pull request change the launcher or a crate it builds?
if: github.event_name == 'pull_request'
id: launcher
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: python3 tools/ci/rust_changes.py --launcher "$BASE_SHA" "$HEAD_SHA" >> "$GITHUB_OUTPUT"
- name: Install Rust and the headers GPUI links against
if: steps.launcher.outputs.required != 'false'
run: |
sudo apt-get update && sudo apt-get install -y --no-install-recommends libxkbcommon-x11-dev libwayland-dev \
libx11-xcb-dev libfontconfig-dev libvulkan-dev libasound2-dev libudev-dev
rustup toolchain install stable --profile minimal --component rustfmt --component clippy
- uses: Swatinem/rust-cache@v2
if: steps.launcher.outputs.required != 'false'
with:
workspaces: launcher
shared-key: launcher
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Check the launcher's formatting, lint and tests
if: steps.launcher.outputs.required != 'false'
working-directory: launcher
run: |
cargo +stable fmt --check
cargo +stable clippy --all-targets --locked -- -D warnings
cargo +stable test --locked --lib
# Most pull requests leave the harnesses to parity-main, which runs them on main a batch of merges at a time
# (.github/workflows/parity-main.yml; releases wait for it). A pull request still runs them itself when it changes
# a harness, an oracle, a probe or the runner, or carries the run-parity label (run-parity.yml, when it is added).
parity-scope:
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
outputs:
run: ${{ steps.scope.outputs.run }}
steps:
- uses: actions/checkout@v4
with:
# The commit the event names: a fork's later pushes need another run-parity label.
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 0
persist-credentials: false
# Keeps .local/ (parity state, probe builds, oracle results) and target/ between runs.
clean: false
- uses: dtolnay/rust-toolchain@stable
- name: Tools from the runner
run: bash tools/ci/runner-env.sh
- name: Vanilla data, and resources prepared by this preparer
run: bash tools/ci/vanilla-data.sh
- name: The harnesses this change can affect
- id: scope
env:
PARITY_HOSTS: ''
run: python3 tools/parity.py --changed ${{ github.event.pull_request.base.sha }} --timeout 2400 # world_spawn takes ~1480 s on a busy runner
- name: Logs of the failed harnesses
if: failure()
uses: actions/upload-artifact@v4
with:
name: parity-logs
path: .local/parity-runner/logs/
if-no-files-found: ignore
retention-days: 3
BASE_SHA: ${{ github.event.pull_request.base.sha }}
LABELS: ${{ join(github.event.pull_request.labels.*.name, ' ') }}
run: |
changed=$(git diff --name-only "$(git merge-base "$BASE_SHA" HEAD)" HEAD)
harness=$(grep -E '^(tools/test_[a-z0-9_]+_parity\.py|tools/(parity[a-z_]*|lab)\.py|tools/parity_hosts\.json|mods/horizon/src/test/|crates/[^/]+/examples/|crates/horizon-client/src/game/[a-z_]*(fixture|harness)\.rs|\.github/workflows/parity[a-z-]*\.yml)' <<< "$changed" || true)
if [ -n "$harness" ] || [[ " $LABELS " == *" run-parity "* ]]; then
echo "run=true" >> "$GITHUB_OUTPUT"
echo "The pull request runs its harnesses: ${harness:-label run-parity}"
else
echo "run=false" >> "$GITHUB_OUTPUT"
echo "No harness, oracle, probe or runner change: parity-main checks this on main"
fi
parity:
needs: parity-scope
if: needs.parity-scope.outputs.run == 'true'
uses: ./.github/workflows/parity.yml
with:
base: ${{ github.event.pull_request.base.sha }}
head: ${{ github.event.pull_request.head.sha }}
full: ${{ contains(github.event.pull_request.labels.*.name, 'full-parity') }}
# Horizon never downloads the game. These jobs need a self-hosted Linux runner
# (label horizon-game) holding the maintainer's own copy: the official launcher's
# game directory in HORIZON_MINECRAFT_DIR and the pinned server jar in
@@ -444,7 +497,7 @@ jobs:
env:
RUSTUP_TOOLCHAIN: stable
run: python tools/test_float_log_parity.py
portable-mod:
jfr-analysis:
if: github.event_name == 'workflow_dispatch'
runs-on: [self-hosted, linux, horizon-game]
steps:
@@ -456,101 +509,7 @@ jobs:
with:
distribution: temurin
java-version: '25'
- name: Compile original mod and test metrics
run: python tools/build_mod.py
- name: Verify streaming JFR analysis against the JDK exporter
run: python tools/test_jfr.py
- name: Verify ZGC pause and allocation event interpretation
run: python tools/test_jfr.py --gc zgc
- name: Build pinned portable packs
run: python tools/build_pack.py
- name: Measure isolated worker pacing on this platform
run: python -c "from tools.build_mod import benchmark_workers; benchmark_workers()"
- name: Measure isolated debounce queue polling
run: python -c "from tools.build_mod import benchmark_debounce; benchmark_debounce()"
- name: Measure wakeable idle queue latency
run: python -c "from tools.build_mod import benchmark_idle; benchmark_idle()"
- name: Measure object allocations with compact headers off and on
run: python -c "from tools.build_mod import benchmark_headers; benchmark_headers()"
- name: Test pack integrity and comparisons
run: python -m unittest discover -s tests -v
- name: Start recommended OpenGL macOS clients and verify framebuffer readback
if: runner.os == 'macOS'
run: |
python tools/test_client_graphics.py --native-display
cp dist/client-graphics-smoke.json dist/client-graphics-macos-title.json
python tools/test_client_graphics.py --native-display --world
cp dist/client-graphics-smoke.json dist/client-graphics-macos-world.json
- name: Compile the private dispatch fixture
run: python -c "from tools.build_mod import build_dispatch_fixture; build_dispatch_fixture()"
- name: Compile the private integrated-server graphics fixture
run: python -c "from tools.build_mod import build_integration_fixture; build_integration_fixture('server-graphics', 'dev.horizonlab.metrics.ServerGraphicsIntegration', fixture_dependencies={})"
- name: Compile the private tick-manager state fixture
run: python -c "from tools.build_mod import build_integration_fixture; build_integration_fixture('tick-state', 'dev.horizonlab.metrics.TickStateIntegration')"
- name: Compile the private terrain tick membership fixture
run: python -c "from tools.build_mod import build_tick_dedup_fixture; build_tick_dedup_fixture()"
- name: Compile the private culling whitelist fixture
run: python -c "from tools.build_mod import build_culling_empty_fixture; build_culling_empty_fixture()"
- name: Compile the private culling ray geometry fixture
run: python -c "from tools.build_mod import build_integration_fixture; build_integration_fixture('culling-rays', 'dev.horizonlab.dispatch.CullingRayIntegration', ['entityculling'], ['CullingRayAccess'])"
- name: Compile the private terrain position lookup fixture
run: python -c "from tools.build_mod import build_integration_fixture; build_integration_fixture('terrain-position', 'dev.horizonlab.dispatch.PositionIntegration')"
- name: Compile the private terrain quad reuse fixture
run: python -c "from tools.build_mod import build_integration_fixture; build_integration_fixture('quad-reuse', 'dev.horizonlab.dispatch.QuadReuseIntegration')"
- name: Compile the private object-size agent and terrain quad fixture
run: python -c "from tools.test_quad_sizes import build_agent; from tools.build_mod import build_integration_fixture; build_agent(); build_integration_fixture('quad-size', 'dev.horizonlab.dispatch.QuadSizeIntegration')"
- name: Compile the private terrain read shutdown fixture
run: python -c "from tools.build_mod import build_integration_fixture; build_integration_fixture('terrain-read', 'dev.horizonlab.dispatch.TerrainReadIntegration')"
- name: Install headless graphics libraries for applied-hook checks
if: runner.os == 'Linux'
run: sudo apt-get update && sudo apt-get install -y xvfb libgl1 libegl1 libvulkan1 mesa-vulkan-drivers libasound2t64 libxkbcommon0
- name: Verify integrated-server graphics report against the actual device
run: python tools/test_server_graphics.py ${{ runner.os == 'macOS' && '--native-display' || '' }}
- name: Exercise the applied dispatcher hooks and upstream rejection path
if: runner.os == 'Linux'
run: python tools/test_dispatch.py
- name: Verify applied tick membership transitions and allocation
if: runner.os == 'Linux'
run: python tools/test_tick_dedup.py
- name: Verify live culling whitelist registration and empty iteration
if: runner.os == 'Linux'
run: python tools/test_culling_empty.py
- name: Compare applied culling ray geometry and provider queries
if: runner.os == 'Linux'
run: python tools/test_culling_rays.py
- name: Verify actual full-data position lookup equivalence
if: runner.os == 'Linux'
run: python tools/test_terrain_position.py
- name: Verify actual terrain quad geometry and allocation
if: runner.os == 'Linux'
run: python tools/test_quad_reuse.py
- name: Measure actual builder and quad shallow sizes in both header modes
if: runner.os == 'Linux'
run: python tools/test_quad_sizes.py
- name: Verify interrupted DH reads and preserved shared futures
if: runner.os == 'Linux'
run: python tools/test_terrain_read.py
- name: Upload original mod and pack manifests only
uses: actions/upload-artifact@v4
with:
name: horizon-linux
path: |
dist/*.mrpack
dist/horizon-*.jar
dist/worker-pacing-benchmark.txt
dist/debounce-queue-benchmark.txt
dist/idle-queue-benchmark.txt
dist/object-header-benchmark.txt
dist/dispatch-integration.json
dist/tick-dedup-integration.json
dist/culling-empty-integration.json
dist/terrain-read-integration.json
dist/culling-rays-integration.json
dist/terrain-position-integration.json
dist/quad-reuse-integration.json
dist/quad-size-integration.json
dist/server-graphics-smoke.json
dist/client-graphics-smoke.json
dist/client-graphics-macos-title.json
dist/client-graphics-macos-world.json
retention-days: 3
+95 -18
View File
@@ -1,12 +1,16 @@
name: launcher
# Builds the launcher on Linux, macOS and Windows, and releases it by itself (docs/release-flow.md): after a
# push to main that changes the launcher or what it builds in (crates/horizon-prepare, crates/horizon-theme,
# crates/horizon-telemetry),
# push to main that changes the launcher or a crate it builds in (the paths below),
# every 10 minutes (a release waiting for merges to settle goes out) and by hand, tools/release_train.py decides as
# for the game.
# A release is launcher-vX.Y.Z: the patch after the last launcher tag, or the next minor when a merged pull
# request is labelled feature. The version is given to the build (HORIZON_LAUNCHER_VERSION): no bump commits.
# A release is launcher@X.Y.Z on the release line in .github/release-line.txt (docs/versioning.md); without it
# launcher-vX.Y.Z, the patch after the last launcher tag or the next minor when a merged pull request is labelled
# feature. The version and the tag are given to the build (HORIZON_LAUNCHER_VERSION, HORIZON_LAUNCHER_TAG): no bump
# commits.
# Never marked latest. Pushes to launcher branches only build. RELEASES_PAUSED=1 stops releases.
# macOS: with the repository variable MACOS_SIGN_IDENTITY set, Horizon.app and the .dmg are signed with
# Developer ID, notarized and stapled, from the runner's signing keychain (packaging/macos/README.md);
# unset (forks), they are signed ad hoc.
# gh workflow run launcher --ref <branch> # dry run: builds everything, publishes nothing
# gh workflow run launcher --ref main -f dry_run=false -f force=true
@@ -14,10 +18,16 @@ on:
push:
branches: [main, launcher, 'launcher-*']
paths:
# Every crate the launcher builds in, as tools/release_train.py's LAUNCHER_PATHS.
- 'launcher/**'
- 'crates/horizon-bench/**'
- 'crates/horizon-friends/**'
- 'crates/horizon-mods/**'
- 'crates/horizon-port-pack/**'
- 'crates/horizon-prepare/**'
- 'crates/horizon-theme/**'
- 'crates/horizon-release-sign/**'
- 'crates/horizon-telemetry/**'
- 'crates/horizon-theme/**'
- '.github/workflows/launcher.yml'
schedule:
- cron: '7-59/10 * * * *'
@@ -39,6 +49,10 @@ concurrency:
group: launcher-${{ github.ref == 'refs/heads/main' && 'release' || github.ref }}
cancel-in-progress: false
env:
# Test runs never reach the telemetry server: no live-count ping, no uploads (docs/telemetry.md).
HORIZON_TELEMETRY_URL: "off"
jobs:
decide:
runs-on: ubuntu-latest
@@ -67,7 +81,8 @@ jobs:
python3 tools/release_train.py decide --kind launcher ${HEAD_SHA:+--head "$HEAD_SHA"} ${FORCE:+--force} | tee decision.json
echo "### $(jq -r '.reason' decision.json)" >> "$GITHUB_STEP_SUMMARY"
tag=$(jq -r '.tag' decision.json)
echo "version=${tag#launcher-v}" >> "$GITHUB_OUTPUT"
version=${tag#launcher-v}
echo "version=${version#launcher@}" >> "$GITHUB_OUTPUT"
# A dry run (and a launcher branch's push) builds whatever the decision.
if [[ "${{ steps.mode.outputs.dry_run }}" == true ]]; then
echo "release=true" >> "$GITHUB_OUTPUT"
@@ -98,15 +113,20 @@ jobs:
# which only the Visual C++ redistributable installs.
rustflags: -C target-feature=+crt-static
runs-on: ${{ matrix.mac && (vars.MAC_RUNNER || 'ludovics-mac-mini') || matrix.os }}
timeout-minutes: 40
# Notarizing the app and then the .dmg can take a few minutes each.
timeout-minutes: ${{ matrix.mac && 60 || 40 }}
env:
RUSTFLAGS: ${{ matrix.rustflags }}
# Horizon's Azure app for Microsoft sign-in (docs/microsoft-account.md): a repository
# variable, not a secret. Unset, the launcher says sign-in is not configured.
HORIZON_MSA_CLIENT_ID: ${{ vars.HORIZON_MSA_CLIENT_ID }}
# The release's version, built in (launcher/src/backend.rs VERSION); a launcher branch's build keeps
# Cargo.toml's.
# The mod browser's CurseForge source (docs/mod-browser.md#curseforge): a repository secret. Unset, the
# launcher searches Modrinth only.
HORIZON_CURSEFORGE_API_KEY: ${{ secrets.HORIZON_CURSEFORGE_API_KEY }}
# The release's version and tag, built in (launcher/src/backend.rs VERSION and own_tag: the tag says which
# line it is on, docs/versioning.md); a launcher branch's build keeps Cargo.toml's.
HORIZON_LAUNCHER_VERSION: ${{ (github.event_name != 'push' || github.ref == 'refs/heads/main') && needs.decide.outputs.version || '' }}
HORIZON_LAUNCHER_TAG: ${{ (github.event_name != 'push' || github.ref == 'refs/heads/main') && needs.decide.outputs.tag || '' }}
defaults:
run:
working-directory: launcher
@@ -116,8 +136,9 @@ jobs:
with:
ref: ${{ needs.decide.outputs.head }}
persist-credentials: false
# The Mac mini's runner keeps its target/ between runs.
clean: ${{ runner.environment == 'github-hosted' }}
# Always from a clean checkout, the Mac mini's runner too: its work folder is shared with pull requests'
# parity jobs, and nothing they leave may end up in a launcher release (target/ is rebuilt from scratch).
clean: true
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
if: runner.environment == 'github-hosted'
@@ -138,7 +159,8 @@ jobs:
echo "::notice::No Metal toolchain on this runner: GPUI compiles its shaders at run time"
echo "LAUNCHER_FEATURES=gpui/runtime_shaders" >> "$GITHUB_ENV"
fi
- run: cargo test --release --locked --lib ${LAUNCHER_FEATURES:+--features "$LAUNCHER_FEATURES"}
# Every test target (the library, the binary, tests/), on each system the launcher ships for.
- run: cargo test --release --locked ${LAUNCHER_FEATURES:+--features "$LAUNCHER_FEATURES"}
- run: cargo build --release --locked ${LAUNCHER_FEATURES:+--features "$LAUNCHER_FEATURES"}
- name: The version is the release's
run: |
@@ -170,9 +192,6 @@ jobs:
retry "$launcher" install latest
"$launcher" versions --installed
"$launcher" links --json
"$launcher" menu-style classic
grep -qx 'menuStyle:classic' "$HORIZON_HOME/horizon.txt"
"$launcher" menu-style horizon
clients=("$HORIZON_LAUNCHER_HOME"/versions/v*/horizon-client*)
client="${clients[0]}"
echo "installed client: $client"
@@ -188,11 +207,19 @@ jobs:
help() { "$client" --help > "$RUNNER_TEMP/help.txt" 2>&1 && head -3 "$RUNNER_TEMP/help.txt" && grep -q -- '--resources' "$RUNNER_TEMP/help.txt"; }
soft help
rm -rf "$HORIZON_LAUNCHER_HOME"
- name: The signing keychain, unlocked
if: runner.os == 'macOS' && vars.MACOS_SIGN_IDENTITY != ''
env:
MACOS_SIGN_IDENTITY: ${{ vars.MACOS_SIGN_IDENTITY }}
run: ../packaging/macos/keychain.sh unlock
- name: Package
env:
MACOS_SIGN_IDENTITY: ${{ vars.MACOS_SIGN_IDENTITY }}
run: |
rm -rf dist out && mkdir -p out
if [[ "$RUNNER_OS" == "macOS" ]]; then
# Horizon.app, zipped (self-update) and in a .dmg (installer): packaging/macos.
# Horizon.app, zipped (self-update) and in a .dmg (installer), signed (and notarized with
# Developer ID): packaging/macos.
python3 -m venv "$RUNNER_TEMP/dmg" && "$RUNNER_TEMP/dmg/bin/pip" install --quiet dmgbuild==1.6.5
version=${HORIZON_LAUNCHER_VERSION:-$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -1)}
DMGBUILD="$RUNNER_TEMP/dmg/bin/dmgbuild" ../packaging/macos/package.sh target/release/${{ matrix.binary }} "$version" out
@@ -212,11 +239,53 @@ jobs:
name: ${{ matrix.asset }}
path: launcher/out/*
retention-days: 3
- name: The signing keychain, locked again
if: always() && runner.os == 'macOS' && vars.MACOS_SIGN_IDENTITY != ''
run: ../packaging/macos/keychain.sh lock
release:
sign:
needs: [decide, build]
runs-on: ubuntu-latest
timeout-minutes: 10
# The only job that holds the release signing key, a secret of the `release` environment, which only main's real
# releases enter (dry runs sign with a throwaway key). It runs no code from the repository but
# tools/ci/sign_release.sh, which calls only the runner's sha256sum and OpenSSL (docs/release-flow.md).
environment: ${{ needs.decide.outputs.dry_run != 'true' && 'release' || '' }}
steps:
- uses: actions/checkout@v4
with:
sparse-checkout: tools/ci/sign_release.sh
sparse-checkout-cone-mode: false
persist-credentials: false
- uses: actions/download-artifact@v4
with:
path: dist
pattern: horizon-launcher-*
merge-multiple: true
- name: Sign the checksums (SHA256SUMS, SHA256SUMS.sig)
env:
RELEASE_SIGNING_KEY: ${{ secrets.RELEASE_SIGNING_KEY }}
RELEASE_SIGNING_KEY_NEXT: ${{ secrets.RELEASE_SIGNING_KEY_NEXT }}
EPHEMERAL: ${{ needs.decide.outputs.dry_run == 'true' && 'yes' || '' }}
TAG: ${{ needs.decide.outputs.tag }}
run: bash tools/ci/sign_release.sh "$TAG" dist/horizon-launcher-*
- uses: actions/upload-artifact@v4
with:
name: signature
path: dist/SHA256SUMS*
if-no-files-found: error
retention-days: 3
- uses: actions/upload-artifact@v4
if: needs.decide.outputs.dry_run == 'true'
with:
name: ephemeral.pub
path: dist/ephemeral.pub
retention-days: 3
release:
needs: [decide, build, sign]
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: write
steps:
@@ -234,6 +303,13 @@ jobs:
with:
path: dist
merge-multiple: true
- name: The signature, checked as launchers check it
env:
TAG: ${{ needs.decide.outputs.tag }}
EPHEMERAL: ${{ needs.decide.outputs.dry_run == 'true' && 'yes' || '' }}
run: |
key=(); [[ -n "$EPHEMERAL" ]] && key=(--key "$(cat dist/ephemeral.pub)")
cargo run --locked --quiet -p horizon-release-sign -- verify --tag "$TAG" --publishing "${key[@]}" dist
# Never marked latest: Horizon's own releases stay the repository's latest.
- name: Publish ${{ needs.decide.outputs.tag }}
if: needs.decide.outputs.dry_run != 'true'
@@ -241,8 +317,9 @@ jobs:
GH_TOKEN: ${{ github.token }}
TAG: ${{ needs.decide.outputs.tag }}
run: |
signature=(); [[ -f dist/SHA256SUMS.sig ]] && signature=(dist/SHA256SUMS.sig)
gh release create "$TAG" --target "${{ needs.decide.outputs.head }}" --title "$(cat notes/title.txt)" \
--notes-file notes/body.md --latest=false dist/horizon-launcher-*
--notes-file notes/body.md --latest=false dist/horizon-launcher-* dist/SHA256SUMS "${signature[@]}"
- name: Dry run, the release that would be published
if: needs.decide.outputs.dry_run == 'true'
run: |
+34
View File
@@ -0,0 +1,34 @@
name: mod-registry
# The submission service is its own workspace (crates/horizon-mod-registry), so the checks job doesn't build
# it: this one does, when it or horizon-mods (the checks it shares with the launcher and the game) changes.
on:
push:
branches: [main]
paths:
- 'crates/horizon-mods/**'
- 'crates/horizon-mod-registry/**'
- '.github/workflows/mod-registry.yml'
pull_request:
paths:
- 'crates/horizon-mods/**'
- 'crates/horizon-mod-registry/**'
- '.github/workflows/mod-registry.yml'
permissions:
contents: read
jobs:
check:
runs-on: ubuntu-latest
timeout-minutes: 20
defaults:
run:
working-directory: crates/horizon-mod-registry
steps:
- uses: actions/checkout@v4
- run: rustup toolchain install stable --profile minimal --component rustfmt --component clippy
- uses: Swatinem/rust-cache@v2
with:
workspaces: crates/horizon-mod-registry
save-if: ${{ github.ref == 'refs/heads/main' }}
- run: cargo +stable fmt --check
- run: cargo +stable clippy --all-targets --locked -- -D warnings
- run: cargo +stable test --locked
+19 -8
View File
@@ -8,7 +8,7 @@ name: nightly audit
# gh workflow run "nightly audit" -f dry_run=true # the audit, without touching issues
on:
schedule:
# GitHub's schedules are in UTC: both, and the first step keeps the one that is 03:00 in Paris.
# GitHub's schedules are in UTC: both, and the first step keeps the one that is 03:07 in Paris today.
- cron: '7 1 * * *'
- cron: '7 2 * * *'
workflow_dispatch:
@@ -23,17 +23,27 @@ permissions:
concurrency:
group: nightly-audit
cancel-in-progress: false
env:
# Test runs never reach the telemetry server: no live-count ping, no uploads (docs/telemetry.md).
HORIZON_TELEMETRY_URL: "off"
jobs:
audit:
runs-on: [self-hosted, horizon-game]
timeout-minutes: 150
# The Mac mini's runners only: the audit compares with the previous one, so it stays on one platform.
runs-on: [self-hosted, horizon-game, macOS]
timeout-minutes: 240 # every harness (~80 min) plus lan_integration (~40 min an attempt under load, retried)
steps:
- name: 03:00 in Paris
- name: 03:07 in Paris
id: when
env:
SCHEDULE: ${{ github.event.schedule }}
run: |
hour=$(TZ=Europe/Paris date +%H)
if [[ "${{ github.event_name }}" == schedule && "$hour" != 03 ]]; then
echo "It is ${hour}:00 in Paris: the other schedule runs the audit."
# Which cron is 03:07 in Paris, from Paris's offset today: GitHub can start a schedule hours late, so the
# clock at start time can't tell (on 2026-10-26 both started after 04:00 Paris and both skipped).
offset=$(TZ=Europe/Paris date +%z)
ours=$([ "$offset" = +0200 ] && echo '7 1 * * *' || echo '7 2 * * *')
if [[ "${{ github.event_name }}" == schedule && "$SCHEDULE" != "$ours" ]]; then
echo "Paris is at UTC${offset} today: the '$ours' schedule runs the audit."
echo "run=false" >> "$GITHUB_OUTPUT"
else
echo "run=true" >> "$GITHUB_OUTPUT"
@@ -61,7 +71,8 @@ jobs:
run: |
echo "since=$(date +%Y-%m-%dT%H:%M:%S)" >> "$GITHUB_OUTPUT"
set +e
python3 tools/parity.py --all --timeout 1800 2>&1 | tee "$RUNNER_TEMP/audit.log"
# lan_integration (the whole lan_e2e suite) takes ~2,400 s under load, past the usual 1,800.
python3 tools/parity.py --all --timeout 3000 2>&1 | tee "$RUNNER_TEMP/audit.log"
echo "exit=${PIPESTATUS[0]}" >> "$GITHUB_OUTPUT"
{ echo '```'; tail -40 "$RUNNER_TEMP/audit.log"; echo '```'; } >> "$GITHUB_STEP_SUMMARY"
- name: Issues for the harnesses that regressed
+77
View File
@@ -0,0 +1,77 @@
name: parity-main
# The vanilla harnesses on main, a batch of merges at a time (docs/release-flow.md#parity-on-main), on the Mac mini's
# runner. One run at a time: merges that land while a run goes on wait in one pending run (GitHub keeps only the
# newest), which checks every merge since main's last green parity-main (tools/parity_main.py base). On a failure it
# bisects the failing harnesses over those merges and comments on the pull request that broke them. Releases ship
# only a commit whose parity-main passed (tools/release_train.py), so nothing a harness catches reaches players.
on:
push:
branches: [main]
workflow_dispatch:
permissions:
contents: read
# The comment on the merge that broke a harness.
pull-requests: write
# tools/parity_main.py base reads earlier runs.
actions: read
concurrency:
group: parity-main
cancel-in-progress: false
env:
# Test runs never reach the telemetry server: no live-count ping, no uploads (docs/telemetry.md).
HORIZON_TELEMETRY_URL: "off"
jobs:
parity-main:
# A Mac mini runner of its own (ludovics-mac-mini-2 holds only this label since 2026-10-26): behind PR parity jobs
# this run waited 2 h while every merge replaced the pending one, and on the release runner a release then waited
# behind the next run. PR parity jobs have ludovics-mac-mini and playhorizon. Not playhorizon: its 4 CPUs
# make a core-crate run several times longer, and a failure here bisects and comments on merges.
runs-on: [self-hosted, macOS, horizon-parity-main]
timeout-minutes: 240 # a core-crate batch reruns ~300 harnesses (~60-90 min), a bisect a few rounds of the failures
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.sha }}
fetch-depth: 0
persist-credentials: false
# Keeps .local/ (parity state, probe builds, oracle results) and target/ between runs.
clean: false
- uses: dtolnay/rust-toolchain@stable
- name: Tools from the runner
run: bash tools/ci/runner-env.sh
- name: Vanilla data, and resources prepared by this preparer
run: bash tools/ci/vanilla-data.sh
- name: Main's last green parity-main
id: base
env:
GH_TOKEN: ${{ github.token }}
run: echo "sha=$(python3 tools/parity_main.py base)" >> "$GITHUB_OUTPUT"
- name: The harnesses the merges since then can affect
id: harnesses
# A failure here is judged by the next step: the run passes when every failed harness passes again at HEAD.
continue-on-error: true
env:
PARITY_HOSTS: ''
BASE_SHA: ${{ steps.base.outputs.sha }}
run: |
echo "Merges since ${BASE_SHA}:"
git log --first-parent --format=' %h %s' "$BASE_SHA..HEAD"
python3 tools/parity.py --changed "$BASE_SHA" --timeout 2400
- name: A flake, or the merge that broke them
if: steps.harnesses.outcome == 'failure'
env:
GH_TOKEN: ${{ github.token }}
PARITY_HOSTS: ''
BASE_SHA: ${{ steps.base.outputs.sha }}
run: |
rm -rf .local/parity-main-logs && cp -R .local/parity-runner/logs .local/parity-main-logs 2>/dev/null || true
python3 tools/parity_main.py bisect "$BASE_SHA" --comment
- name: Logs of the failed harnesses
if: steps.harnesses.outcome == 'failure'
uses: actions/upload-artifact@v4
with:
name: parity-main-logs
path: .local/parity-main-logs/
if-no-files-found: ignore
retention-days: 7
+97
View File
@@ -0,0 +1,97 @@
name: parity
# The vanilla harnesses a pull request's changes can affect, on the Mac mini's runner or playhorizon's, which hold the
# maintainer's own Minecraft files (Horizon never downloads the game). Called by ci.yml for pull requests from this repository
# and by run-parity.yml when a maintainer labels a fork's pull request run-parity. See docs/release-flow.md.
on:
workflow_call:
inputs:
base:
description: The pull request's base commit (the harnesses its changes since then can affect)
type: string
required: true
head:
description: The commit to check out and test
type: string
required: true
full:
description: >-
The pull request carries the full-parity label (a change every harness reaches, such as a crate split): it
runs on playhorizon's runner only, so the Mac mini's runners stay free for the others. While that runner is
busy the job waits for it.
type: boolean
default: false
permissions:
contents: read
env:
# Test runs never reach the telemetry server: no live-count ping, no uploads (docs/telemetry.md).
HORIZON_TELEMETRY_URL: "off"
jobs:
parity:
runs-on: ${{ inputs.full && fromJSON('["self-hosted", "horizon-game", "playhorizon"]') || fromJSON('["self-hosted", "horizon-game"]') }}
# A change to core crates reruns ~350 harnesses: ~60-90 min on the Mac mini; on playhorizon (4 of its 8 vCPUs, 4
# harnesses at a time) up to a few hours, and any pull request's job may land there.
timeout-minutes: ${{ inputs.full && 360 || 180 }}
steps:
- uses: actions/checkout@v4
with:
# The commit the event names: a fork's later pushes need another run-parity label. The next step merges
# main into it.
ref: ${{ inputs.head }}
fetch-depth: 0
persist-credentials: false
# Keeps .local/ (parity state, probe builds, oracle results) and target/ between runs.
clean: false
- name: The head merged into main (what merging it would land)
id: merge
env:
BASE_SHA: ${{ inputs.base }}
HEAD_SHA: ${{ inputs.head }}
run: |
# Harness fixes and game fixes that reached main since the branch was made then apply here too: a head
# tested alone failed harnesses already fixed on main (server_knockback before #2114, mob_ai's heap before
# #2270, save_roundtrip before #2212). Selection then compares with this main, so it sees only the pull
# request's own changes. A head that conflicts with main is tested alone.
set -euo pipefail
git merge --abort 2>/dev/null || true
git fetch -q --no-tags origin +refs/heads/main:refs/remotes/origin/main
main=$(git rev-parse origin/main)
if git -c user.name=parity -c user.email=parity@localhost merge -q --no-edit --no-ff "$main" >/dev/null 2>&1; then
echo "base=$main" >> "$GITHUB_OUTPUT"
echo "Tested: head ${HEAD_SHA:0:9} merged into main ${main:0:9}" | tee -a "$GITHUB_STEP_SUMMARY"
else
git merge --abort
echo "base=$BASE_SHA" >> "$GITHUB_OUTPUT"
echo "::warning::head ${HEAD_SHA:0:9} conflicts with main ${main:0:9}: tested alone, without main's fixes"
echo "Tested: head ${HEAD_SHA:0:9} alone (it conflicts with main ${main:0:9})" | tee -a "$GITHUB_STEP_SUMMARY"
fi
- uses: dtolnay/rust-toolchain@stable
- name: Tools from the runner
run: bash tools/ci/runner-env.sh
- name: Vanilla data, and resources prepared by this preparer
run: bash tools/ci/vanilla-data.sh
- name: The prepared output is the one the preparer's OUTPUT_VERSION says
env:
BASE_SHA: ${{ steps.merge.outputs.base }}
run: |
# A head made before OUTPUT_VERSION (#1947) that conflicts with main has neither the tool nor the number.
if [ ! -f tools/prepare_output.py ] || ! grep -q OUTPUT_VERSION crates/horizon-prepare/src/lib.rs; then
echo "This head's preparer has no OUTPUT_VERSION: nothing to check"
exit 0
fi
python3 tools/prepare_output.py check --cache --base "$BASE_SHA"
- name: The version constants are the pinned jar's
run: python3 tools/gen_version_consts.py --check
- name: The harnesses this change can affect
env:
PARITY_HOSTS: ''
BASE_SHA: ${{ steps.merge.outputs.base }}
run: python3 tools/parity.py --changed "$BASE_SHA" --timeout 2400 # world_spawn takes ~1480 s on a busy runner
- name: Logs of the failed harnesses
if: failure()
uses: actions/upload-artifact@v4
with:
name: parity-logs
path: .local/parity-runner/logs/
if-no-files-found: ignore
retention-days: 3
+230 -20
View File
@@ -1,17 +1,26 @@
name: release
# Automatic game releases (docs/release-flow.md). After every green `checks` run on main, every 10 minutes (so a
# release waiting for merges to settle goes out without another merge) and by hand, tools/release_train.py decides:
# a release when pull requests merged since the last one changed what players run and the newest merged 15 minutes
# ago (the oldest an hour ago, if merges never pause), or at once for a pull request labelled release-now.
# RELEASES_PAUSED=1 (repository variable) stops them.
# release waiting for merges to settle goes out without another merge) and by hand, tools/release_train.py decides,
# for a commit whose checks and parity-main (the vanilla harnesses on main) both passed:
# a release when 5 pull requests merged since the last one changed what players run and the newest merged 5 minutes
# ago, or when the oldest has waited 6 hours, or at once for a pull request labelled release-now.
# RELEASES_PAUSED=1 (repository variable) stops them; so does an open issue labelled release-blocker, forced runs too.
# Builds Linux (an older Ubuntu, so the binary runs on any recent distribution and on NixOS through nix-ld) and
# Windows on GitHub, macOS on the Mac mini's runner (MAC_RUNNER=macos-latest sends it to GitHub), then
# publishes vYYYY.MM.DD[.N] with notes from the merged pull requests.
# publishes vX.Y.Z on the release line in .github/release-line.txt (docs/versioning.md; without it, the dated
# vYYYY.MM.DD[.N] from before 0.2) with notes from the merged pull requests, as the release and GitHub's latest: one
# channel, which launchers, `horizon-server update` and install.sh follow at once. So it is published only after the
# checks that used to gate a promotion pass in this run: the Mac mini's smoke test drew a world, and no release-blocker
# is open (tools/release_train.py gate); otherwise nothing is published. macOS: with the repository variable
# MACOS_SIGN_IDENTITY set, the programs are signed with
# Developer ID and the hardened runtime and the zip is notarized, from the runner's signing keychain
# (packaging/macos/README.md); unset (forks), ad hoc.
# gh workflow run release --ref <branch> # dry run: builds and notes, publishes nothing
# gh workflow run release -f dry_run=false -f force=true # on main: release now, whatever the rules say
on:
workflow_run:
workflows: [checks]
# A game release ships a commit both passed on (parity-main runs on batches: the newest commit it passed on).
workflows: [checks, parity-main]
types: [completed]
branches: [main]
schedule:
@@ -28,6 +37,8 @@ on:
default: false
permissions:
contents: read
# The open release-blocker issues (decide).
issues: read
concurrency:
group: game-release
cancel-in-progress: false
@@ -41,6 +52,7 @@ jobs:
outputs:
release: ${{ steps.decide.outputs.release }}
tag: ${{ steps.decide.outputs.tag }}
version: ${{ steps.decide.outputs.version }}
head: ${{ steps.decide.outputs.head }}
dry_run: ${{ steps.mode.outputs.dry_run }}
steps:
@@ -54,19 +66,63 @@ jobs:
env:
GH_TOKEN: ${{ github.token }}
RELEASES_PAUSED: ${{ vars.RELEASES_PAUSED }}
# The commit whose checks just passed; a dry run builds the branch it was started on; otherwise
# main's newest commit with green checks.
# The commit whose checks or parity-main just passed; a dry run builds the branch it was started on;
# otherwise main's newest commit with green checks and parity-main.
HEAD_SHA: ${{ github.event.workflow_run.head_sha || (steps.mode.outputs.dry_run == 'true' && github.sha) || '' }}
FORCE: ${{ inputs.force && 'yes' || '' }}
run: |
python3 tools/release_train.py decide --kind game ${HEAD_SHA:+--head "$HEAD_SHA"} ${FORCE:+--force} | tee decision.json
echo "### $(jq -r '.reason' decision.json)" >> "$GITHUB_STEP_SUMMARY"
# The version the programs are built with (docs/versioning.md): the tag without its v.
tag=$(jq -r '.tag' decision.json)
echo "version=${tag#v}" >> "$GITHUB_OUTPUT"
# A dry run builds whatever the decision, to test the builds.
if [[ "${{ steps.mode.outputs.dry_run }}" == true ]]; then echo "release=true" >> "$GITHUB_OUTPUT"; fi
build:
exporters:
needs: decide
if: needs.decide.outputs.release == 'true'
# This runner holds the maintainer's own pinned Minecraft jars. Only our classes leave it.
runs-on: ${{ vars.MAC_RUNNER || 'ludovics-mac-mini' }}
# Three full preparations (two isolated, one batched) besides the compile.
timeout-minutes: 60
steps:
- uses: actions/checkout@v4
with:
ref: ${{ needs.decide.outputs.head }}
persist-credentials: false
clean: true
- uses: dtolnay/rust-toolchain@stable
- run: bash tools/ci/runner-env.sh
- run: bash tools/ci/vanilla-data.sh
- name: Compile and validate original exporters against the pinned jars
shell: bash
run: |
set -euo pipefail
# target/ is gone after the clean checkout, and vanilla-data.sh builds the preparer only when it
# prepares afresh.
cargo build --release --locked -p horizon-prepare
# Compiled here, never taken from the runner's shared preparations: pull requests' parity jobs
# write those (vanilla-data.sh links .local/exporters to them).
rm -rf .local/exporters
bundle=$(target/release/horizon-prepare compile-exporters --root "$PWD" | tail -1)
mkdir -p exporters
cp -R "$bundle/server" "$bundle/client" "$bundle/manifest.json" exporters/
# Only our classes and their manifest leave this runner.
stray=$(find exporters -type f ! -path exporters/manifest.json \
! \( -name '*.class' \( -path 'exporters/server/dev/horizonlab/parity/*' -o -path 'exporters/client/dev/horizonlab/parity/*' \) \))
[ -z "$stray" ] || { echo "::error::not one of our classes: $stray"; exit 1; }
python3 tools/check_prepare_exporters.py --binary target/release/horizon-prepare
- uses: actions/upload-artifact@v4
with:
name: original-exporters
path: exporters/
if-no-files-found: error
retention-days: 3
build:
needs: [decide, exporters]
if: needs.decide.outputs.release == 'true'
name: build ${{ matrix.name }}
strategy:
fail-fast: false
@@ -76,6 +132,9 @@ jobs:
os: ubuntu-22.04
dir: horizon-linux-x86_64
asset: horizon-linux-x86_64.tar.gz
# The server build too: packaging/server/install.sh installs it on Linux.
server_dir: horizon-server-linux-x86_64
server_asset: horizon-server-linux-x86_64.tar.gz
- name: windows
os: windows-latest
dir: horizon-windows-x64
@@ -88,9 +147,13 @@ jobs:
dir: horizon-macos-arm64
asset: horizon-macos-arm64.zip
runs-on: ${{ matrix.mac && (vars.MAC_RUNNER || 'ludovics-mac-mini') || matrix.os }}
timeout-minutes: 40
# Notarizing the zip can take a few minutes.
timeout-minutes: ${{ matrix.mac && 60 || 40 }}
env:
RUSTFLAGS: ${{ matrix.rustflags }}
# The release's version, built in (horizon_core::release): --version, user agents, crash reports and
# telemetry say it. No version-bump commits.
HORIZON_VERSION: ${{ needs.decide.outputs.version }}
defaults:
run:
shell: bash
@@ -99,8 +162,11 @@ jobs:
with:
ref: ${{ needs.decide.outputs.head }}
persist-credentials: false
# On the Mac mini's runner, keep target/ (incremental) and .local/ (the prepared resources).
clean: ${{ runner.environment == 'github-hosted' }}
# Always from a clean checkout, the Mac mini's runner too: its work folder is shared with pull requests'
# parity jobs, and nothing they leave (target/, an untracked .cargo/config.toml, a build script's output)
# may end up in a release. target/ is rebuilt from scratch; .local/ is relinked by vanilla-data.sh, which
# keeps its preparations outside the checkout.
clean: true
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
if: runner.environment == 'github-hosted'
@@ -110,12 +176,29 @@ 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
test -f "target/release/$binary${{ matrix.exe }}" || { echo "::error::target/release/$binary${{ matrix.exe }} is missing"; exit 1; }
done
- name: The signing keychain, unlocked
if: runner.os == 'macOS' && vars.MACOS_SIGN_IDENTITY != ''
env:
MACOS_SIGN_IDENTITY: ${{ vars.MACOS_SIGN_IDENTITY }}
run: packaging/macos/keychain.sh unlock
- name: Signed with Developer ID and the hardened runtime
# Before the checks below, so they run the programs as players will.
if: runner.os == 'macOS' && vars.MACOS_SIGN_IDENTITY != ''
env:
MACOS_SIGN_IDENTITY: ${{ vars.MACOS_SIGN_IDENTITY }}
run: |
for binary in horizon-client horizon-ctl horizon-prepare horizon-server; do
codesign --force --options runtime --timestamp --keychain "$MACOS_SIGN_KEYCHAIN" \
--identifier "dev.horizon.${binary#horizon-}" --sign "$MACOS_SIGN_IDENTITY" "target/release/$binary"
codesign --verify --strict --verbose=2 "target/release/$binary"
done
- name: Only system libraries
# Windows: no non-system DLL (a "DLL not found" dialog on some players' machines). Linux: the standard
# loader and libraries every desktop distribution has. macOS: only the system's own libraries.
@@ -135,9 +218,13 @@ jobs:
bin=target/release
"$bin/horizon-client${{ matrix.exe }}" --help | grep -q -- '--resources'
"$bin/horizon-ctl${{ matrix.exe }}" --help > /dev/null
"$bin/horizon-prepare${{ matrix.exe }}" --version
"$bin/horizon-prepare${{ matrix.exe }}" --help | grep -q -- '--minecraft-dir'
"$bin/horizon-server${{ matrix.exe }}" --version
# The version is the release's.
for binary in horizon-prepare horizon-server; do
out=$("$bin/$binary${{ matrix.exe }}" --version | tr -d '\r')
echo "$out"
[[ "$out" == "$binary $HORIZON_VERSION" ]] || { echo "::error::expected $binary $HORIZON_VERSION"; exit 1; }
done
- name: Tools from the runner
if: runner.environment == 'self-hosted'
run: bash tools/ci/runner-env.sh
@@ -165,6 +252,19 @@ 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; 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'
with:
name: smoke-test.json
path: ${{ runner.temp }}/smoke-test.json
if-no-files-found: error
retention-days: 3
- uses: actions/download-artifact@v4
with:
name: original-exporters
path: exporters
- name: Package
run: |
set -euo pipefail
@@ -174,22 +274,106 @@ jobs:
case "${{ matrix.asset }}" in
*.tar.gz) tar czf "${{ matrix.asset }}" "${{ matrix.dir }}" ;;
*) if [[ "$RUNNER_OS" == Windows ]]; then 7z a -tzip "${{ matrix.asset }}" "${{ matrix.dir }}" > /dev/null
else ditto -c -k --keepParent "${{ matrix.dir }}" "${{ matrix.asset }}"; fi ;;
else
# Without resource forks and extended attributes: plain ditto adds an
# AppleDouble `._` file beside every file that has any.
ditto -c -k --norsrc --noextattr --noqtn --keepParent "${{ matrix.dir }}" "${{ matrix.asset }}"
if unzip -Z1 "${{ matrix.asset }}" | grep -E '(^|/)(\._|__MACOSX/)'; then
echo "::error::macOS metadata files in ${{ matrix.asset }}"; exit 1
fi
fi ;;
esac
ls -l "${{ matrix.asset }}"
- name: Notarized
# A zip of programs can't be stapled: Gatekeeper asks Apple the first time each one runs.
if: runner.os == 'macOS' && vars.MACOS_SIGN_IDENTITY != ''
run: |
for binary in horizon-client horizon-ctl horizon-prepare horizon-server; do
codesign --verify --strict "dist/${{ matrix.dir }}/$binary"
done
packaging/macos/notarize.sh "dist/${{ matrix.asset }}"
- uses: actions/upload-artifact@v4
with:
name: ${{ matrix.asset }}
path: dist/${{ matrix.asset }}
if-no-files-found: error
retention-days: 3
- name: The server build (the game without sound or controllers, as horizon-engine)
if: matrix.server_asset
run: |
set -euo pipefail
# After the client is packaged: target/release/horizon-client is rebuilt without the audio and
# controllers features, so it needs neither libasound nor libudev (minimal server images lack them),
# and with code mods like the client.
cargo build --release --locked -p horizon-client --no-default-features --features mods --bin horizon-client
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"
"$folder/horizon-engine" --help | grep -q -- '--resources'
[[ "$("$folder/horizon-server" --version)" == "horizon-server $HORIZON_VERSION" ]]
cd dist
rm -f "${{ matrix.server_asset }}"
tar czf "${{ matrix.server_asset }}" "${{ matrix.server_dir }}"
ls -l "${{ matrix.server_asset }}"
- uses: actions/upload-artifact@v4
if: matrix.server_asset
with:
name: ${{ matrix.server_asset }}
path: dist/${{ matrix.server_asset }}
if-no-files-found: error
retention-days: 3
- name: The signing keychain, locked again
if: always() && runner.os == 'macOS' && vars.MACOS_SIGN_IDENTITY != ''
run: packaging/macos/keychain.sh lock
publish:
sign:
needs: [decide, build]
runs-on: ubuntu-latest
timeout-minutes: 10
# The only job that holds the release signing key, a secret of the `release` environment, which only main's real
# releases enter (dry runs sign with a throwaway key). It runs no code from the repository but
# tools/ci/sign_release.sh, which calls only the runner's sha256sum and OpenSSL (docs/release-flow.md).
environment: ${{ needs.decide.outputs.dry_run != 'true' && 'release' || '' }}
steps:
- uses: actions/checkout@v4
with:
sparse-checkout: tools/ci/sign_release.sh
sparse-checkout-cone-mode: false
persist-credentials: false
- uses: actions/download-artifact@v4
with:
path: dist
pattern: horizon-*
merge-multiple: true
- name: Sign the checksums (SHA256SUMS, SHA256SUMS.sig)
env:
RELEASE_SIGNING_KEY: ${{ secrets.RELEASE_SIGNING_KEY }}
RELEASE_SIGNING_KEY_NEXT: ${{ secrets.RELEASE_SIGNING_KEY_NEXT }}
EPHEMERAL: ${{ needs.decide.outputs.dry_run == 'true' && 'yes' || '' }}
TAG: ${{ needs.decide.outputs.tag }}
run: |
bash tools/ci/sign_release.sh "$TAG" dist/horizon-linux-x86_64.tar.gz dist/horizon-windows-x64.zip \
dist/horizon-macos-arm64.zip dist/horizon-server-linux-x86_64.tar.gz
- uses: actions/upload-artifact@v4
with:
name: signature
path: dist/SHA256SUMS*
if-no-files-found: error
retention-days: 3
- uses: actions/upload-artifact@v4
if: needs.decide.outputs.dry_run == 'true'
with:
name: ephemeral.pub
path: dist/ephemeral.pub
retention-days: 3
publish:
needs: [decide, build, sign]
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: write
issues: read
steps:
- uses: actions/checkout@v4
with:
@@ -205,19 +389,45 @@ jobs:
with:
path: dist
merge-multiple: true
- name: The signature, checked as launchers check it
env:
TAG: ${{ needs.decide.outputs.tag }}
EPHEMERAL: ${{ needs.decide.outputs.dry_run == 'true' && 'yes' || '' }}
run: |
key=(); [[ -n "$EPHEMERAL" ]] && key=(--key "$(cat dist/ephemeral.pub)")
cargo run --locked --quiet -p horizon-release-sign -- verify --tag "$TAG" --publishing "${key[@]}" dist
- name: Ready to publish (the smoke test passed, no release-blocker is open)
# Right before publishing: an issue labelled while the builds ran still holds this release. A dry run only
# reports what a real run would do.
env:
GH_TOKEN: ${{ github.token }}
DRY_RUN: ${{ needs.decide.outputs.dry_run }}
run: |
if ! python3 tools/release_train.py gate --kind game --smoke dist/smoke-test.json --blockers; then
[[ "$DRY_RUN" == true ]] || { echo "::error::Nothing published"; exit 1; }
echo "::warning::A real run would publish nothing"
fi
- name: Publish ${{ needs.decide.outputs.tag }}
if: needs.decide.outputs.dry_run != 'true'
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ needs.decide.outputs.tag }}
run: |
signature=(); [[ -f dist/SHA256SUMS.sig ]] && signature=(dist/SHA256SUMS.sig)
# The release and the latest at once: there is one channel (docs/release-flow.md). Each archive gets a
# label (file#label), which GitHub's page shows instead of the file name, so nobody takes the game's
# files for the download (#1647). Launchers, servers and install.sh match the name, which stays.
gh release create "$TAG" --target "${{ needs.decide.outputs.head }}" --title "$(cat notes/title.txt)" \
--notes-file notes/body.md --latest dist/horizon-linux-x86_64.tar.gz dist/horizon-windows-x64.zip \
dist/horizon-macos-arm64.zip
--notes-file notes/body.md --latest \
"dist/horizon-linux-x86_64.tar.gz#Game files the launcher installs: Linux (horizon-linux-x86_64.tar.gz)" \
"dist/horizon-windows-x64.zip#Game files the launcher installs: Windows (horizon-windows-x64.zip)" \
"dist/horizon-macos-arm64.zip#Game files the launcher installs: macOS (horizon-macos-arm64.zip)" \
"dist/horizon-server-linux-x86_64.tar.gz#Dedicated server for Linux (horizon-server-linux-x86_64.tar.gz)" \
dist/SHA256SUMS "${signature[@]}" dist/smoke-test.json
- name: Dry run, the release that would be published
if: needs.decide.outputs.dry_run == 'true'
run: |
echo "Would publish ${{ needs.decide.outputs.tag }} at ${{ needs.decide.outputs.head }}: $(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:
+25
View File
@@ -0,0 +1,25 @@
name: run-parity
# A fork's pull request runs the parity job (parity.yml) on the Mac mini only when a maintainer who has read the
# change adds the run-parity label: once, on the commit the pull request has then. Remove the label and add it
# again after new pushes. A separate workflow, so that labels never start `checks`: there a label's run reported a
# skipped `verify`, and GitHub counts a skipped required check as passed.
on:
pull_request:
types: [labeled]
permissions:
contents: read
concurrency:
group: run-parity-${{ github.event.pull_request.number }}
cancel-in-progress: true
env:
# Test runs never reach the telemetry server: no live-count ping, no uploads (docs/telemetry.md).
HORIZON_TELEMETRY_URL: "off"
jobs:
parity:
if: github.event.label.name == 'run-parity'
uses: ./.github/workflows/parity.yml
with:
base: ${{ github.event.pull_request.base.sha }}
head: ${{ github.event.pull_request.head.sha }}
full: ${{ contains(github.event.pull_request.labels.*.name, 'full-parity') }}
+3
View File
@@ -31,3 +31,6 @@
/.horizon-cache/
/crash-reports/
/debug/
# Release-only compiled original exporter bundle.
/exporters/
+182
View File
@@ -0,0 +1,182 @@
# 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`, and, for the
launcher, `HORIZON_LAUNCHER_TRASH`, `HORIZON_LAUNCHER_CLIPBOARD` and `HORIZON_LAUNCHER_DESKTOP` pointing into the scratch home. Never write to
`~/.local/share/{applications,horizon,horizon-launcher}` or `~/.local/bin`.
- **Mark what an agent posts.** A PR body starts with a line naming whose agent opened it and with what.
Ludovic's agents write `> 🤖 Opened by Ludovic's AI agent (Claude), not by Ludovic himself.`, and a
contributor's agent names its own person and tool (issues: "Filed by", reviews: "Review by"). Every
comment starts with `🤖 Agent:`. Never write as Ludovic or as anyone else.
- **Attribution.** Commits end with a `Co-Authored-By:` line for the agent that wrote them, and PR bodies
with that agent's own "Generated with" line. Ludovic's agents use
`Co-Authored-By: Claude <model> <[email protected]>` and
`🤖 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. Merges ship in batches of 5, or after
6 hours ([docs/release-flow.md](docs/release-flow.md)).
- Contributors first: `gh pr list --state open --search <area>` before you start; never duplicate theirs.
## 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`, ~180 flat modules) and the bin (`src/main.rs` → `game.rs` + `game/`), plus `horizon-ctl` (`src/bin/horizon-ctl/`). Also the dedicated server (`--dedicated`) |
| `horizon-command` | Command parsing (Brigadier): the vanilla tree, argument types, selectors, suggestions, the tree's network form. Re-exported as `horizon_client::command` |
| `horizon-client-core` | The client lib's bottom layer: `text.rs`, `lang.rs`, `snbt.rs`, `nbt_path.rs`, `component.rs`, `scoreboard.rs`, `bossbar.rs`, `gamerules.rs`, `block_entity_nbt.rs`, registry/particle/sound tables. Re-exported under the old paths (`horizon_client::text`...) |
| `horizon-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-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-shader-api` | Horizon's own WGSL shader packs ([docs/api/shaders.md](docs/api/shaders.md)): `horizon-shaders.toml`, the `.hshader` package; shared by the game, the launcher and the registry |
| `horizon-shaders` | Shader packs (the `shaders/` folder format), rebuilt clean-room from a written spec of the format; no wgpu |
| `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`, `xtask` | DualSense output, thread priorities, signed release checksums, load-test bots, 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),
`docs/`, `.github/workflows/`. The website (playhorizon.dev) is the private ludovic111/horizon-site: its build
reads `README.md`, `launcher/links.json`, `docs/api/`, `roadmap/roadmap.json` and `crates/horizon-theme/tokens.css` from main.
## 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 (server-thread host); --in-frame compares the fallback
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 predator-missile --exclude horizon-mods-test-guest --exclude horizon-mods-test-ui-guest`.
Their real check is `cargo test -p horizon-mods` (builds them for `wasm32-unknown-unknown`).
- Linux needs `libasound2-dev libudev-dev`; `--no-default-features` drops audio and controllers (the server build).
- Run and drive the game (starts the `horizon-client` beside `horizon-ctl`; other arguments go to the game):
`target/fast/horizon-ctl --socket $PWD/ctl.sock launch --world W --creative --offscreen --wait --resources $PWD/.local/horizon-client/26.3`,
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: crate `horizon-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`; `horizon-client-core`: `nbt_path.rs`, `scoreboard.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`, `options_file.rs`; `horizon-client-core/src/block_entity_nbt.rs`; G: `saving.rs`, `persist.rs` (entities); `crates/horizon-world/src/{region.rs,nbt.rs,datafix/}` |
| Input | game.rs `fn window_event`, `fn key_pressed`, `fn play_mouse`; 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`; `horizon-client-core/src/lang.rs`; crate `horizon-assets`; G: `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. **Server thread (default since #1514):** `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. This is what players run. Open
`[server-thread]` issues list the gaps left.
2. **In-frame (`--in-frame`, `--no-server-thread` or `HORIZON_SERVER_THREAD=0`):** the window's `Game` ticks
the world itself, one level that follows the player (`fn enter_dimension`). Deterministic rule fixtures still use this path; player-facing integration uses the default server thread.
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 # server thread
cargo run --profile fast -p horizon-client -- --resources .local/horizon-client/26.3 --world T --in-frame # in-frame
python3 tools/lan_e2e.py --check <name> # host on its server thread, guest is a server client
python3 tools/lan_e2e.py --in-frame --check <name> # comparison (portals, levels, distance, lava need server-thread mode)
python3 tools/parity.py dedicated_server server_entities server_player_damage
```
## 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
+24 -2
View File
@@ -4,8 +4,13 @@ Thanks for helping. Horizon is Minecraft: Java Edition 26.3 rewritten in Rust, a
everything is that **it plays exactly like vanilla**. This page is the short version of how we work; questions are
welcome on [Discord](https://discord.gg/HcTeMZxe5E).
This repository holds the game, the launcher and the website, plus the tools, tests and docs that build, check and
release them. Anything else (videos, Discord and other community material) lives elsewhere.
This repository holds the game and the launcher, plus the tools, tests and docs that build, check and release them.
The website (playhorizon.dev) is built in a separate repository from this one's README, `docs/api/` and roadmap. Anything else (videos, Discord and other community material) lives elsewhere.
**Where to start: [playhorizon.dev/hub](https://playhorizon.dev/hub).** It shows what is being done now and next
([ROADMAP.md](ROADMAP.md) in the repository), the work board, good first tasks, and the forms to report a bug or
suggest something. A pull request for a roadmap item carries its `roadmap:<id>` label; ideas go to the suggestion
queue (the hub, or `/suggest` on Discord) rather than to issues, which are for bugs.
## Build it
@@ -39,6 +44,17 @@ probe against Horizon, and compares their outputs. There are about 290 of them.
Never commit game files, decompiled source or prepared resources (`.local/`): `tools/check_public_tree.py` rejects
them (`git config core.hooksPath .githooks` runs it before every commit).
The pre-push hook checks the full history reachable from the branches and tags being pushed. Run
`python3 tools/check_public_tree.py --history` to audit the history of all local refs.
## Only original code
Horizon's code is written from Minecraft 26.3 itself and from public documentation, never from another mod, loader
or renderer. Don't read, copy, translate or paraphrase the source of Iris, OptiFine, Sodium or any other project,
and don't send code an AI tool wrote from it. Their user documentation and file formats are fine to read, and their
packs and mods are fine to run and observe. Shader packs were removed on 24 October 2026 (#1561) because their code
had been written from another project's source; they come back only as a clean-room rewrite from a written
description of the pack format.
## Before asking for review
@@ -53,6 +69,11 @@ crates you touched (and the launcher's), generated files, `Cargo.lock`, the Pyth
your change can affect. Without the vanilla data on your machine it lists those harnesses instead; CI runs them.
`--quick` skips the harnesses. It exits non-zero when something fails.
For the game workspace, Clippy checks all targets of the touched crates and their transitive workspace dependents,
including examples. The dependency scope includes optional, platform, build and development dependencies.
Tests run library, binary, integration and doc tests without building unrelated example executables. Shared Cargo
configuration and toolchain changes select the whole workspace; CI also keeps its workspace-wide checks.
## Pull requests
- **Small.** One change per pull request, under about 500 lines of code where you can; split bigger work into
@@ -66,6 +87,7 @@ your change can affect. Without the vanilla data on your machine it lists those
`release-now` is for crash fixes that shouldn't wait. pr_ready suggests one.
Every merge to main is released automatically ([release flow](docs/release-flow.md)), so main always stays playable.
How agents, checks, releases and signed updates keep this safe: [security](docs/security.md).
## Parity checks on forks
Generated
+517 -671
View File
File diff suppressed because it is too large. Load diff
+49 -2
View File
@@ -1,6 +1,45 @@
[workspace]
exclude = ["launcher", "crates/horizon-telemetry-server"]
members = ["crates/horizon-core", "crates/horizon-dualsense", "crates/horizon-world", "crates/voxel-kernels", "crates/horizon-client", "crates/horizon-server-files", "crates/horizon-server", "crates/horizon-bots", "crates/horizon-assets", "crates/horizon-protocol", "crates/horizon-theme", "crates/horizon-shaderpack", "crates/horizon-prepare", "crates/horizon-telemetry", "crates/horizon-bench", "crates/horizon-friends", "crates/horizon-relay", "crates/horizon-mod-sdk", "crates/horizon-mods", "crates/horizon-mods/tests/guest", "crates/cargo-horizon-mod", "crates/horizon-port-pack", "crates/horizon-mod-sdk/examples/coords-hud", "xtask"]
exclude = ["launcher", "crates/horizon-telemetry-server", "crates/horizon-mod-registry", "third_party/wgpu-hal"]
members = [
"crates/horizon-core",
"crates/horizon-dualsense",
"crates/horizon-threads",
"crates/horizon-hdr",
"crates/horizon-world",
"crates/horizon-client",
"crates/horizon-client-core",
"crates/horizon-command",
"crates/horizon-server-files",
"crates/horizon-server",
"crates/horizon-bots",
"crates/horizon-assets",
"crates/horizon-protocol",
"crates/horizon-theme",
"crates/horizon-prepare",
"crates/horizon-telemetry",
"crates/horizon-bench",
"crates/horizon-friends",
"crates/horizon-relay",
"crates/horizon-mod-sdk",
"crates/horizon-mods",
"crates/horizon-mods/tests/guest",
"crates/horizon-mods/tests/ui_guest",
"crates/cargo-horizon-mod",
"crates/horizon-port-pack",
"crates/horizon-shaders",
"crates/horizon-shader-api",
"crates/horizon-release-sign",
"crates/horizon-mod-sdk/examples/coords-hud",
"crates/horizon-mod-sdk/examples/predator-missile",
"crates/horizon-mod-sdk/examples/welcome",
"crates/horizon-mod-sdk/examples/beacons",
"mods/official/fps-overlay",
"mods/official/keystrokes",
"mods/official/minimap",
"mods/official/zoom",
"xtask",
"mods/official/recipes",
]
resolver = "2"
[workspace.package]
@@ -16,6 +55,14 @@ type_complexity = "allow"
needless_range_loop = "allow"
large_enum_variant = "allow"
# wgpu-hal 30.0.1 with gfx-rs/wgpu#10124: wgpu 30 creates a Vulkan descriptor
# pool with no pool sizes while it creates the device, which Vulkan allows only
# since 1.3.215, and Intel's Windows driver for 6th-10th gen iGPUs (HD 520/530,
# UHD 620/630) crashes on it with an access violation (#746). Drop this and the
# folder once a wgpu release has the fix.
[patch.crates-io]
wgpu-hal = { path = "third_party/wgpu-hal" }
# Line tables keep file:line in backtraces; full debug info made every
# example and test binary hundreds of megabytes and slow to link.
[profile.dev]
+42 -77
View File
@@ -14,104 +14,65 @@
<p align="center">
<a href="https://playhorizon.dev"><b>Website</b></a> ·
<a href="#download"><b>Download</b></a> ·
<a href="#what-changed-in-v202610131"><b>What changed</b></a> ·
<a href="#screenshots"><b>Screenshots</b></a> ·
<a href="#project-status"><b>Parity status</b></a> ·
<a href="docs/README.md"><b>Documentation</b></a>
<a href="https://playhorizon.dev/docs/"><b>Documentation</b></a> ·
<a href="https://github.com/ludovic111/horizon/releases"><b>Release notes</b></a> ·
<a href="ROADMAP.md"><b>Roadmap</b></a> ·
<a href="#project-status"><b>Project status</b></a> ·
<a href="#contributing"><b>Contributing</b></a>
</p>
# Horizon
A Rust rewrite of Minecraft: Java Edition 26.3 built for one-to-one vanilla parity, checked against the game itself with differential tests.
Every subsystem matches vanilla in all 281 parity harnesses, on Linux and macOS: 100% of what the harnesses compare (see [Project status](#project-status)). The native client is playable on Linux, macOS (Apple Silicon) and Windows.
Horizon is Minecraft: Java Edition 26.3 rewritten in Rust: a native game that reads the same worlds, follows the same rules and generates the same terrain as the Java game, and is checked against it by 281 differential harnesses. You play it through a small launcher, on the files from your own copy of Minecraft. It is free, open source and unofficial.
> Not an official Minecraft product. Not approved by or associated with Mojang or Microsoft. Horizon contains no Minecraft code, assets or data: you need your own legally obtained copy of Minecraft: Java Edition 26.3.
## What changed in v2026.10.13.1
The latest release ([notes](https://github.com/ludovic111/horizon/releases/tag/v2026.10.13.1)):
- **Horizon's own menus:** a new title screen, world list, options and pause menu in the launcher's style, over a live frozen landscape. The vanilla look is still available: Options → Interface → Menu style → Classic.
- **Graphics settings, Simple and Advanced:** presets (Performance, Balanced, Quality, Ultra), or every rendering, quality, display and performance setting, each with a short description and its cost, plus a performance overlay.
- **Much faster rendering:** for example, at render distance 8 over a village, 363 → 2,409 FPS on an RTX 4080. Render distance 32 still stutters in places.
- **Fixed:** no sound on Linux; Linux builds that only ran on the machine they were built on; a missing DLL on Windows (the launcher no longer needs the Visual C++ runtime); memory growing and the frame rate dropping near villages (mobs were duplicated when chunks reloaded); resource preparation failing on Linux with "Missing … java-objc-bridge" ([#41](https://github.com/ludovic111/horizon/issues/41)); the hand and held item at high FOV; missing swing animations.
## Download
Horizon is downloaded only through **the Horizon launcher**: get it from [playhorizon.dev](https://playhorizon.dev) or from [the launcher's releases](https://github.com/ludovic111/horizon/releases?q=launcher-v&expanded=true) (the newest `launcher-v*` release, currently [launcher-v0.1.3](https://github.com/ludovic111/horizon/releases/tag/launcher-v0.1.3)), for Linux x86-64, macOS Apple Silicon or Windows x64. The launcher installs and updates the game, prepares its resources from your own copy of Minecraft, starts the client or a dedicated server, and updates itself.
Horizon is installed through **the Horizon launcher**. Get it from [playhorizon.dev](https://playhorizon.dev) (the Download button picks your system) or from [the launcher releases](https://github.com/ludovic111/horizon/releases?q=launcher-v&expanded=true), for Linux x86-64, macOS on Apple Silicon or Windows x64. The launcher installs and updates the game, prepares its files from your own copy of Minecraft, and updates itself.
**You need** Minecraft: Java Edition installed with the official launcher, with 26.3 played once; Python 3.10 or newer and Java 25 (a JDK, e.g. Eclipse Temurin 25) for the one-time resource preparation; and a GPU with Vulkan on Linux and Windows, or any Apple Silicon Mac.
### The launcher
**You need** Minecraft: Java Edition 26.3, played once (the official launcher, Prism, MultiMC and other launchers all work); a Java 25 runtime, for the one-time preparation; and a GPU with Vulkan on Linux and Windows, or any Apple Silicon Mac. Step by step, with macOS's first-open prompt and the first launch: [Install and first launch](https://playhorizon.dev/docs/install).
<table>
<tr>
<td width="50%"><img src="docs/media/launcher-play.webp" alt="The launcher's Play page: v2026.10.14.1 ready to play, its summary, Client and Server, the version picker and Play, the resources and the Minecraft folder as a numbered list"></td>
<td width="50%"><img src="docs/media/launcher-patch-notes.webp" alt="The launcher's Patch notes page, showing the notes of Horizon 2026.10.14.1"></td>
<td width="50%"><img src="docs/media/launcher-play.webp" alt="The launcher's Play page: a version ready to play, its summary, Client and Server, the version picker and Play"></td>
<td width="50%"><img src="docs/media/launcher-patch-notes.webp" alt="The launcher's Patch notes page"></td>
</tr>
</table>
It lists Horizon's releases and installs the build for your platform, shows each release's patch notes, updates Horizon and itself, and doubles as a command-line tool and an MCP server. See [the launcher's README](launcher/README.md).
## What works today
## Screenshots
The [documentation](https://playhorizon.dev/docs/) covers each of these.
Captured in the Rust client itself, with vanilla assets and no mods or shaders.
- **The game:** singleplayer with vanilla's world generation, mobs, villagers, raids, redstone, every command, the HUD and the recipe book, on saves in vanilla's format. Worlds from 1.8 to 26.2 are upgraded as vanilla does. [Status table](#project-status).
- **Horizon's own extras:** new menus (vanilla's are one setting away), graphics presets, distant terrain up to 1024 chunks, controller support, optional zoom, minimap and more, all off by default where they would change the vanilla look. See [Settings](https://playhorizon.dev/docs/settings).
- **Resource packs and data packs:** resource packs work, and data packs load functions, predicates and item modifiers.
- **Playing with others:** invite friends from the pause menu's Friends button with a code, Open to LAN, and joining offline-mode servers. Horizon cannot sign in to a Microsoft account yet, so it cannot join servers that check accounts. See [Multiplayer](https://playhorizon.dev/docs/multiplayer).
- **Mods:** Horizon has its own mod system (Rust compiled to WebAssembly). Java mod jars do not run. See [Mods and data packs](https://playhorizon.dev/docs/mods).
### Menus and settings
<table>
<tr>
<td colspan="2"><img src="docs/media/menu-title.webp" alt="Horizon's title screen: the mark and the pages on the left, the Horizon title, Singleplayer and Multiplayer, and the last world played and the faint wordmark, over a veiled snowy landscape"></td>
</tr>
<tr>
<td width="50%"><img src="docs/media/menu-worlds.webp" alt="The Singleplayer page: three worlds with their dates and game modes, and Create world and Play buttons"></td>
<td width="50%"><img src="docs/media/settings.webp" alt="The Video settings in their Simple view: a preset, render distance, field of view, framerate, VSync and GUI scale, each with what it costs"></td>
</tr>
</table>
The vanilla menus are one setting away: Options → Interface → Menu style → Classic. See [Graphics settings](docs/graphics.md).
### In game
Screenshots, captured in the Rust client itself with vanilla assets and no mods or shaders:
<table>
<tr>
<td colspan="2"><img src="docs/media/sunset.webp" alt="Sunset over a savanna river valley and a plains village"></td>
</tr>
<tr>
<td width="50%"><img src="docs/media/village.webp" alt="A plains village with farms, an iron golem and animals"></td>
<td width="50%"><img src="docs/media/survival.webp" alt="Survival at dusk: a zombie attacks near the village"></td>
<td width="50%"><img src="docs/media/survival.webp" alt="Survival at dusk: a zombie walks up, with the hotbar, health, hunger and experience"></td>
<td width="50%"><img src="docs/media/settings.webp" alt="Vanilla's Video Settings screen with Horizon's own buttons at the top: Horizon Graphics and Advanced Graphics"></td>
</tr>
<tr>
<td width="50%"><img src="docs/media/village.webp" alt="A plains village among savanna hills, with farms, villagers and an iron golem"></td>
<td width="50%"><img src="docs/media/nether.webp" alt="A crimson forest in the Nether with a zombified piglin"></td>
<td width="50%"><img src="docs/media/night.webp" alt="Moonrise over a torch-lit village and river"></td>
</tr>
<tr>
<td width="50%"><img src="docs/media/underwater.webp" alt="Salmon swimming in a river"></td>
<td width="50%" valign="middle">
Vanilla world generation ported to Rust (terrain, aquifers, carvers, features, villages and the Nether), every block model with smooth lighting and biome tints, the vanilla sky and fog, mobs, the HUD, chat and advancements: everything in these frames is drawn by Horizon.
</td>
</tr>
</table>
## What it does
## Host a server
Horizon reimplements the Minecraft Java client and its game logic in Rust, one subsystem at a time, and compares each piece with the vanilla game. Each `tools/test_*_parity.py` harness drives the same inputs through Horizon and through the vanilla client and server bytecode from your own copy of the game, then checks that the outputs are identical. It is for players and developers interested in game reimplementation, and for anyone who wants to see how far a behaviour-exact native rewrite can go.
The launcher's **Servers** page creates, starts, backs up and updates a Horizon server with buttons, and invites friends over Tailscale. On a VPS, `horizon-server` installs with one command and runs without Java. Guide: [Host a server](https://playhorizon.dev/docs/hosting) ([reference](docs/hosting.md)).
The [native client](docs/native-client.md) opens on a vanilla-style title screen with world creation and selection, and generates worlds with the Rust port of the vanilla generators: noise terrain, aquifers, surface rules, carvers and placed features, all checked against vanilla. It streams and lights chunks on worker threads and renders every vanilla block model with smooth lighting, biome tints, the vanilla sky, sun, moon, stars and clouds. Gameplay includes:
## Modpacks
- validated player physics, health and hunger;
- mining with vanilla tool rules and loot tables;
- placing, crafting, chests and furnaces;
- dropped items, particles and sounds;
- passive and hostile mobs with combat and vanilla AI, including guardians, shulkers, illagers, ravagers, the wither, the ender dragon fight, the breeze, the creaking, the sniffer, the happy ghast, llamas and the copper golem;
- villagers that take jobs, trade, restock, meet at the bell and sleep in beds, on a port of vanilla's brain;
- raids and pillager patrols;
- dispensers, crafters, the loom and the recipe book;
- every command, scoreboards and teams, the player list, subtitles and the HUD overlays;
- boats, minecarts, item frames, paintings and armor stands;
- shields, maps, books, fireworks, beacons, advancements and statistics;
- Anvil saves, joining vanilla servers, and opening a world to LAN.
The launcher's **Modpacks** page imports a Modrinth or CurseForge pack, shows which of its mods Horizon can carry over, and plays the ones it can as their own profile. Today that means content mods made of data and assets; mods with Java code are not ported yet. See [Modpacks](https://playhorizon.dev/docs/modpacks).
## Agent control
@@ -147,7 +108,22 @@ cargo run --release -p horizon-client
Everything prepared lands in the gitignored `.local/`. It is derived from the game: never commit it or share it.
The client starts on the title screen. Command-line options (`--world`, `--seed`, `--creative`, `--render-distance`, `--dedicated`), controls, playing and scripting through `horizon-ctl` and its MCP server, and the architecture are described in [the native client guide](docs/native-client.md). The rest of the documentation is indexed in [docs/README.md](docs/README.md), including [the parity workflow](docs/parity-workflow.md).
The client starts on the title screen. Command-line options (`--world`, `--seed`, `--creative`, `--render-distance`, `--dedicated`), controls, scripting through `horizon-ctl` and the architecture are described in [the native client guide](docs/native-client.md). The rest of the developer documentation is indexed in [docs/README.md](docs/README.md), including [the parity workflow](docs/parity-workflow.md).
## Roadmap
What is being done now, what comes next and where you can help, in plain words and in technical detail: [ROADMAP.md](ROADMAP.md). [playhorizon.dev/hub](https://playhorizon.dev/hub) shows it with the work board, the journal of what shipped and how to help.
## Contributing
Issues and pull requests are welcome: see [CONTRIBUTING.md](CONTRIBUTING.md) for how we work (vanilla first, small pull requests, labels), or [the contributing guide](https://playhorizon.dev/docs/contributing) on the site. Questions and ideas: [Discord](https://discord.gg/HcTeMZxe5E). Before opening a pull request, run the self-check and paste the block it prints:
```sh
git config core.hooksPath .githooks
python3 tools/pr_ready.py
```
It runs rustfmt, clippy with warnings denied, the tests of the crates you touched, the Python tests, the public-tree check, and the vanilla harnesses your change can affect. CI runs the same checks on GitHub's Linux runners; the vanilla harnesses need the game, which Horizon never downloads, so they run on a maintainer's computer holding their own copy. Never commit game files, decompiled source or prepared resources: `tools/check_public_tree.py` rejects them, and tracked Java is allowed only under the project's own `dev/horizonlab` namespace.
## Project status
@@ -182,18 +158,7 @@ them too (Rust uses Java's own float maths on each platform).
### Known issues
Online-mode servers (Mojang's session servers) are not supported; skins served over https show the default skin unless already cached; pack icons show the unknown-pack texture in game; the creative hotbar keys (C/X) aren't wired to the hotbar loader; item tooltips lack attribute lines.
## Contributing
Issues and pull requests are welcome: see [CONTRIBUTING.md](CONTRIBUTING.md) for how we work (vanilla first, small pull requests, labels). Before opening a pull request, run the self-check and paste the block it prints:
```sh
git config core.hooksPath .githooks
python3 tools/pr_ready.py
```
It runs rustfmt, clippy with warnings denied, the tests of the crates you touched, the Python tests, the public-tree check, and the vanilla harnesses your change can affect. CI runs the same checks on GitHub's Linux runners; the vanilla harnesses need the game, which Horizon never downloads, so they run on a self-hosted runner holding the maintainer's own copy. Never commit game files, decompiled source or prepared resources: `tools/check_public_tree.py` rejects them, and tracked Java is allowed only under the project's own `dev/horizonlab` namespace.
Horizon can't sign in to a Microsoft account yet, so it can't join online-mode servers (it joins offline-mode ones); skins served over https show the default skin unless already cached; pack icons show the unknown-pack texture in game; the creative hotbar keys (C/X) aren't wired to the hotbar loader; item tooltips lack attribute lines.
## Licence
+980
View File
@@ -0,0 +1,980 @@
# Horizon roadmap
<!-- Generated by tools/roadmap.py from roadmap/roadmap.toml: edit that file, then run `python3 tools/roadmap.py build`. -->
What is being done on Horizon, what comes next and where you can help. There are no dates: things are Now, Next or Later. Each item is said in plain words, with the technical picture folded under it. The same roadmap, with a Simple / Technical switch, the board, the journal and how to help, is on [playhorizon.dev/hub](https://playhorizon.dev/hub). Pull requests carry the `roadmap:<id>` label of their item.
## At a glance
Horizon is Minecraft: Java Edition 26.3 rewritten from scratch in Rust, so it runs faster on the same computer. Single player, LAN, servers, resource packs and Horizon's own mods work today, on your own copy of the game.
**Being done now**
- **Steady frames at long render distances**: Making Horizon faster than vanilla on every computer, with smooth camera movement even at 32 chunks, by spreading the work better over the processor's cores.
- **Modpacks' world generation, natively**: Making the world-generation mods of popular modpacks (new structures, YUNG's dungeons and mineshafts, packs built on Lithostitched) generate their worlds in Horizon, natively and fast.
- **Playing on servers: everything shows as it should**: When you play on a server, other players, mobs, items and menus look and behave as in vanilla: skins, poses, item names, inventories and server networks.
- **Single player as a server: the last differences**: Single player now runs a small server in the background, as vanilla does. We are fixing what still behaves differently there; every fix also works for LAN and server players.
- **Horizon's mod registry**: One place for mods, modpacks and resource packs made for Horizon: browse them on the website and install them from the launcher in one click.
**Next**: Microsoft sign-in (coming soon); CurseForge in the mod browser (coming soon); Fresh benchmarks against vanilla; Pistons push players and mobs; Horizon on Intel Macs; Faster world creation and entry on small CPUs; Servers that hold 20 TPS with 50 to 100 players; Low-end computers: Steam Deck, old Macs, 8 GB; Native upscaling; A batch of targeted optimisations; A Convert to Rust button for what isn't rewritten yet; Mods with code: machines, magic, mobs; Data packs in the mod browser; Prepare a version without Java; Server console, bans and a full admin API; Hosting polish: Windows service, safe updates; Guided hosting with Tailscale and an invite button; An official Docker image for servers; One single-player path: remove the in-frame world; Villages and combat, exactly as in vanilla; Choose the audio output device; Mods that read and change the world; Machines and blocks with inventories (API 0.4); Lua and other languages for mods; Well-known shader looks, rewritten for Horizon; A visible beta label; Wayland and Hyprland polish; A versioned agent-control protocol; Port mods with your own AI agent; Horizon on X (coming soon); Pipeline protection and a GitHub App for agents; Signed updates everywhere, installers included; A server you install with one command.
**How to help**: Play it and tell us what differs from vanilla (Discord or GitHub), run the benchmark on your computer, vote for what you want next, or pick one of the Help wanted items.
**Developers**: Read CONTRIBUTING.md, pick a Help wanted item or a To do card, check vanilla's code for the behaviour, and paste the block `python3 tools/pr_ready.py` prints in your pull request.
## Now
### Performance
#### Steady frames at long render distances
Making Horizon faster than vanilla on every computer, with smooth camera movement even at 32 chunks, by spreading the work better over the processor's cores.
<details><summary>Technical</summary>
Frame pacing at render distance 32 ([#678](https://github.com/ludovic111/horizon/issues/678)): long frames come from work competing with the render thread. In review: world generation workers build free structure starts instead of queueing them ([#1760](https://github.com/ludovic111/horizon/issues/1760)), the worker pool leaves one core as vanilla does ([#1778](https://github.com/ludovic111/horizon/issues/1778)), and the terrain vertex arena is read as storage where flat draws run ([#1742](https://github.com/ludovic111/horizon/issues/1742)). Still open: far terrain at 48 to 64 chunks ([#718](https://github.com/ludovic111/horizon/issues/718)) and CPU-bound fields of block entities ([#717](https://github.com/ludovic111/horizon/issues/717)). Every change shows before and after numbers from `tools/perf_bench.py` against vanilla 26.3 ([docs/benchmark.md](docs/benchmark.md)).
</details>
#### Faster block lookups
Speeding up the code that reads and writes the blocks of the world, which almost everything in the game goes through: generation, lighting, meshing and ticking.
<details><summary>Technical</summary>
`PalettedContainer` and `PackedStorage` find an entry's word by multiply and shift instead of an integer division ([#1790](https://github.com/ludovic111/horizon/issues/1790), [#1794](https://github.com/ludovic111/horizon/issues/1794)); meshing looks up baked models by index, not by name ([#1747](https://github.com/ludovic111/horizon/issues/1747)). Each change keeps the paletted storage harnesses identical and shows its microbenchmark ([docs/performance/paletted-container-access-results.md](docs/performance/paletted-container-access-results.md)).
</details>
### Modpacks
#### Modpacks' world generation, natively
Making the world-generation mods of popular modpacks (new structures, YUNG's dungeons and mineshafts, packs built on Lithostitched) generate their worlds in Horizon, natively and fast.
<details><summary>Technical</summary>
`horizon-port-pack` carries a pack's data-driven content over; structure mods that need code get a native port: YUNG's worldgen as written ([#1746](https://github.com/ludovic111/horizon/issues/1746)), Lithostitched's limited and guaranteed elements with pool and processor modifiers ([#1777](https://github.com/ludovic111/horizon/issues/1777)), its processors as the game's own types ([#1765](https://github.com/ludovic111/horizon/issues/1765)). Proved by the structure blocks and layout harnesses ([#1753](https://github.com/ludovic111/horizon/issues/1753), [#1757](https://github.com/ludovic111/horizon/issues/1757)). Mods whose Java code does more than world generation don't run ([docs/modpacks.md](docs/modpacks.md)).
</details>
### Servers and multiplayer
#### Playing on servers: everything shows as it should
When you play on a server, other players, mobs, items and menus look and behave as in vanilla: skins, poses, item names, inventories and server networks.
<details><summary>Technical</summary>
Client-of-a-server fixes in `game/remote*.rs` and `horizon-protocol`: mobs' aggressive and item-use flags ([#1802](https://github.com/ludovic111/horizon/issues/1802), [#1798](https://github.com/ludovic111/horizon/issues/1798)), ghost items after a remote menu ([#1795](https://github.com/ludovic111/horizon/issues/1795)), skins ([#1652](https://github.com/ludovic111/horizon/issues/1652)), cookies and transfer packets for proxy networks ([#1650](https://github.com/ludovic111/horizon/issues/1650)), firework boosts ([#1671](https://github.com/ludovic111/horizon/issues/1671)), living-entity attributes and simulation distance ([#1731](https://github.com/ludovic111/horizon/issues/1731)), styled item names ([#1702](https://github.com/ludovic111/horizon/issues/1702)), primed TNT between updates ([#1710](https://github.com/ludovic111/horizon/issues/1710)).
</details>
#### Server dialogs and login screens
Servers that show dialogs, such as the register and login screens of AuthMe, will work in Horizon.
<details><summary>Technical</summary>
26.3's dialog packets and model ([#1720](https://github.com/ludovic111/horizon/issues/1720)), the dialog screen with its buttons and actions ([#1734](https://github.com/ludovic111/horizon/issues/1734)), then text and boolean inputs ([#1735](https://github.com/ludovic111/horizon/issues/1735)).
</details>
#### Pre-generate an area with /pregen
A /pregen command that builds an area of the world ahead of time, so players don't wait for new terrain when they explore.
<details><summary>Technical</summary>
Generates, lights and saves an area before anyone walks there ([#1805](https://github.com/ludovic111/horizon/issues/1805), a contributor's pull request, in review). Useful on servers, and it moves the generation cost away from the first visit; the generation speed-ups are under 'Steady frames at long render distances'.
</details>
### Mods and API
#### Horizon's mod registry
One place for mods, modpacks and resource packs made for Horizon: browse them on the website and install them from the launcher in one click.
<details><summary>Technical</summary>
Merged: the registry index (schema 1, `.hpack`, checks before install, [#1634](https://github.com/ludovic111/horizon/issues/1634)), `cargo horizon-mod check` ([#1642](https://github.com/ludovic111/horizon/issues/1642)) and the submission service (`crates/horizon-mod-registry`, [#1659](https://github.com/ludovic111/horizon/issues/1659)). In review: the website's /mods ([#1596](https://github.com/ludovic111/horizon/issues/1596)) and /mods/submit ([#1635](https://github.com/ludovic111/horizon/issues/1635)), and in the launcher the registry as a source ([#1636](https://github.com/ludovic111/horizon/issues/1636)), install, enable and takedowns ([#1637](https://github.com/ludovic111/horizon/issues/1637)), the Mods page ([#1638](https://github.com/ludovic111/horizon/issues/1638)) and modpacks as their own profiles ([#1640](https://github.com/ludovic111/horizon/issues/1640)).
More: [docs/mods.md](docs/mods.md)
</details>
#### Mods that run on the server
Mods that run on the server side of a world, so a dedicated server or a LAN world can have mods that react to players joining, chat and blocks being broken or placed, and that add commands.
<details><summary>Technical</summary>
API 0.3, experimental first ([#1833](https://github.com/ludovic111/horizon/issues/1833), in review): the same sandboxed component runs on the server thread and the dedicated server, with world events (block broken or placed, entity spawned or killed, player joined or left), game rules and server commands with typed arguments. The world, item and entity interfaces come after (see 'Mods that read and change the world'). [docs/api/mods.md](docs/api/mods.md), [docs/mods.md](docs/mods.md).
</details>
#### Screens, input and drawing for mods
Mods will be able to open their own screens and settings pages and react to keys and item use, so bigger mods, such as the Orbital Laser, can run on the normal game.
<details><summary>Technical</summary>
Capability `ui` is in review ([#1864](https://github.com/ludovic111/horizon/issues/1864)): mod-owned screens and a settings page under Options > Mods, built from vanilla's widgets. Still to design: `input` (an item's use, key binds, the crosshair target), `render` (a budgeted draw list on the native shader API) and `world` (changing blocks). Orbital Laser is the acceptance test: it needs all four today through its author's fork (see 'Orbital Laser on the normal game').
More: [docs/mods.md](docs/mods.md)
</details>
#### Tools for people who make mods
Better tools for mod makers: a test runner that needs no game window, templates for each kind of mod, and clear promises about what keeps working from one version to the next.
<details><summary>Technical</summary>
`cargo horizon-mod test` runs a mod's test scripts against a fake game snapshot ([#1816](https://github.com/ludovic111/horizon/issues/1816)) and, for server mods, scripted server events ([#1837](https://github.com/ludovic111/horizon/issues/1837)); the content template and the guides are merged ([#1859](https://github.com/ludovic111/horizon/issues/1859), [#1810](https://github.com/ludovic111/horizon/issues/1810), [docs/api/stability.md](docs/api/stability.md)). Left: templates for hud, server and machine mods, typed wrappers in the SDK, a frozen 0.1 compatibility mod that every runtime must load, a CI check that fails when a stable reference entry is removed without a new major version, and the SDK on crates.io once the API is stable.
More: [https://playhorizon.dev/docs/test-a-mod](https://playhorizon.dev/docs/test-a-mod)
</details>
### Gameplay parity
#### Single player as a server: the last differences
Single player now runs a small server in the background, as vanilla does. We are fixing what still behaves differently there; every fix also works for LAN and server players.
<details><summary>Technical</summary>
Since [#1514](https://github.com/ludovic111/horizon/issues/1514) single player runs on the server thread (`game/integrated.rs`) and the window joins it as a client; `--in-frame` keeps the old path. The open `[server-thread]` issues list the gaps. In review: a tool that changes a block ends the click ([#1797](https://github.com/ludovic111/horizon/issues/1797)), potions keep their potion over the connection ([#1796](https://github.com/ludovic111/horizon/issues/1796)), mobs' aggression and arm swings ([#1802](https://github.com/ludovic111/horizon/issues/1802)). [docs/server-levels.md](docs/server-levels.md).
</details>
#### Play in your language
Item names, container titles and server messages follow the language you pick, as in vanilla. Today parts of the game stay in English.
<details><summary>Technical</summary>
Translatable components resolved through the selected language for items, containers and server text ([#1806](https://github.com/ludovic111/horizon/issues/1806)), on top of the bundled languages the prepared resources carry ([#1594](https://github.com/ludovic111/horizon/issues/1594)). Left: the client's own screens ([#1672](https://github.com/ludovic111/horizon/issues/1672)) and the main menu with zh_cn ([#1774](https://github.com/ludovic111/horizon/issues/1774)).
</details>
#### Cooldowns drawn on items
After you throw an ender pearl, its slot should show the cooldown sweeping away, as in vanilla.
<details><summary>Technical</summary>
Vanilla draws `ItemCooldowns`' percent as a white overlay over the slot (`GuiGraphicsExtractor.itemDecorations` and `itemCooldown`) in the hotbar and in inventories. Issue [#1763](https://github.com/ludovic111/horizon/issues/1763).
More: [#1933](https://github.com/ludovic111/horizon/issues/1933)
</details>
#### Chat settings and team prefixes
The Hidden and Commands Only chat settings should hide chat, and team prefixes should appear before names in chat.
<details><summary>Technical</summary>
Vanilla checks `ChatVisiblity` before it delivers chat and system messages ([#1376](https://github.com/ludovic111/horizon/issues/1376)), and chat uses the team-formatted display name ([#1378](https://github.com/ludovic111/horizon/issues/1378)). Both issues have exact steps to reproduce.
More: [#1932](https://github.com/ludovic111/horizon/issues/1932) · [#1841](https://github.com/ludovic111/horizon/issues/1841)
</details>
#### A pointing hand over buttons
The mouse pointer should turn into a pointing hand over menu buttons, as in vanilla 26.3.
<details><summary>Technical</summary>
26.3's `AbstractWidget` asks for a cursor from `CursorTypes` when it is hovered; winit's `CursorIcon::Pointer` gives the same shape. Issue [#1347](https://github.com/ludovic111/horizon/issues/1347).
More: [#1897](https://github.com/ludovic111/horizon/issues/1897)
</details>
#### Ctrl+Backspace and scroll bars in menus
Ctrl+Backspace should delete a whole word in the creative search, and every list's scroll bar should be draggable.
<details><summary>Technical</summary>
`EditBox` deletes by word with Ctrl ([#1764](https://github.com/ludovic111/horizon/issues/1764)); the Game Rules and Statistics lists' scroll bars can't be dragged ([#1782](https://github.com/ludovic111/horizon/issues/1782), after [#978](https://github.com/ludovic111/horizon/issues/978)).
More: [#1884](https://github.com/ludovic111/horizon/issues/1884) · [#1931](https://github.com/ludovic111/horizon/issues/1931)
</details>
#### Chickens flap while they fall
Chickens should flap their wings while they fall, not stand still in the air.
<details><summary>Technical</summary>
Vanilla's `Chicken.aiStep` updates `flap` and `flapSpeed` when the chicken isn't on the ground, and the model's wings follow them. Issue [#1706](https://github.com/ludovic111/horizon/issues/1706).
More: [#1892](https://github.com/ludovic111/horizon/issues/1892)
</details>
### Rendering
#### Shader packs, written again from scratch
Shader packs were removed and are being rebuilt from scratch. They come back, off by default, once the best-known packs draw properly.
<details><summary>Technical</summary>
A clean-room rewrite in `horizon-shaderpack`, from the packs' public format documentation only: reading packs, the preprocessor and `.properties` files are merged ([#1584](https://github.com/ludovic111/horizon/issues/1584), [#1585](https://github.com/ludovic111/horizon/issues/1585), [#1586](https://github.com/ludovic111/horizon/issues/1586)); translating programs to WGSL through naga, buffers, uniforms and the terrain, entity, sky, weather and hand stages are in review ([#1698](https://github.com/ludovic111/horizon/issues/1698) to [#1801](https://github.com/ludovic111/horizon/issues/1801)).
More: [#1561](https://github.com/ludovic111/horizon/issues/1561) · [#1771](https://github.com/ludovic111/horizon/issues/1771) · [#1786](https://github.com/ludovic111/horizon/issues/1786)
</details>
#### Horizon's own shader format and kit
Horizon's own way to make shader packs, with tools to write and test them. Every pack is checked before it runs, and the registry will list them.
<details><summary>Technical</summary>
`horizon-shader-api` (API 0.1, experimental): WGSL stages with typed inputs inside the renderer's passes, options as WGSL override constants, and the `horizon-shader new|check|pack` tool with templates and the example pack Golden Hour. Merged: the check every pack passes ([#1820](https://github.com/ludovic111/horizon/issues/1820)) and the tool, templates and guide ([#1822](https://github.com/ludovic111/horizon/issues/1822)). In review: the post stage running in the game with hot reload ([#1861](https://github.com/ludovic111/horizon/issues/1861)), the sky stage ([#1863](https://github.com/ludovic111/horizon/issues/1863)) and registry checks that use the game's own check ([#1855](https://github.com/ludovic111/horizon/issues/1855)). Left: the material, lighting and shadow stages, a last-good fallback on reload, a GPU-time overlay per stage and budgets ([docs/shaders.md](docs/shaders.md), [docs/api/shaders.md](docs/api/shaders.md)). The Iris/OptiFine loader is a separate item, 'Shader packs, written again from scratch'.
</details>
### Horizon extras
#### Discord Rich Presence
Your Discord profile can show that you are playing Horizon, with a switch to turn it off.
<details><summary>Technical</summary>
A Discord IPC client on its own thread ([#1787](https://github.com/ludovic111/horizon/issues/1787)) and the presence with its setting ([#1788](https://github.com/ludovic111/horizon/issues/1788)). Merged: it ships with the next release.
</details>
### Code and tools
#### A leaner codebase that builds faster
Cleaning up Horizon's code: removing what nothing uses and making it quicker to build, so fixes and new features land faster.
<details><summary>Technical</summary>
A review of the whole tree: dead code and stubs removed ([#1432](https://github.com/ludovic111/horizon/issues/1432), [#1658](https://github.com/ludovic111/horizon/issues/1658)), one probe binary per crate ([#1453](https://github.com/ludovic111/horizon/issues/1453), [#1454](https://github.com/ludovic111/horizon/issues/1454)), shared harness helpers ([#1448](https://github.com/ludovic111/horizon/issues/1448)), clippy clean on the whole tree ([#1707](https://github.com/ludovic111/horizon/issues/1707)). [docs/architecture.md](docs/architecture.md) maps each feature to its files and harnesses.
</details>
## Next
### Performance
#### Fresh benchmarks against vanilla
New public numbers comparing Horizon with vanilla on many kinds of computers, measured the same way every time, with a history that shows what each release changed.
<details><summary>Technical</summary>
Redo the comparison after the current performance work ([#1073](https://github.com/ludovic111/horizon/issues/1073)); reliability, coverage and presentation ([#1346](https://github.com/ludovic111/horizon/issues/1346)); a scene must count as loaded only once its first chunk arrives ([#1749](https://github.com/ludovic111/horizon/issues/1749)). Next: a performance history across releases (an automatic comparison of each release with the last, on a timeline), more report fields (server TPS and MSPT, power, battery drain, temperature, throttling) and more hardware (a MacBook on battery, low-end laptops, community reports). Players' reports come in through `--benchmark` and `horizon-launcher data benchmark` ([docs/benchmark.md](docs/benchmark.md)).
</details>
#### Faster world creation and entry on small CPUs
Creating a world and joining it should be quick even on a four-thread laptop, where a fresh world at 32 chunks can make you wait minutes for the first terrain.
<details><summary>Technical</summary>
Cut the per-column generation cost (noise sampling, surface rules, aquifers) and the lock convoy on structure starts, and leave one core free for the rest of the game as vanilla does ([#1760](https://github.com/ludovic111/horizon/issues/1760) and [#1778](https://github.com/ludovic111/horizon/issues/1778), in review, are listed under 'Steady frames at long render distances'). Still to profile on small CPUs: the first seconds of a fresh world deliver no chunk, and fluids remesh for about a minute after loading. Measured with `tools/perf_bench.py` ([docs/benchmark.md](docs/benchmark.md), [docs/profiling.md](docs/profiling.md)).
</details>
#### Servers that hold 20 TPS with 50 to 100 players
Servers that keep 20 ticks per second with 50 to 100 players. Today Horizon holds it up to 50 bots and drops to about 10 at 100.
<details><summary>Technical</summary>
Measured against vanilla on equal work ([docs/server.md](docs/server.md), Scale): Horizon is ahead with few players and when exploring, but vanilla's tick is shorter with 25 to 50 players near spawn. Ideas on the list: send entity changes instead of rebuilding every entity each tick (about 15 percent), fewer memory reads in random ticks and collision scans, cheaper item collision checks, mob AI and natural spawning. The bot benchmark (`horizon-bots`) is the proof.
</details>
#### Low-end computers: Steam Deck, old Macs, 8 GB
Horizon should run well on small machines: integrated graphics, the Steam Deck, older Macs and 8 GB of memory, with a battery mode that saves power on laptops.
<details><summary>Technical</summary>
Partly done: an opt-in 60 FPS cap with fewer workers on battery, Far Detail Reduced for integrated GPUs and animated textures on the GPU (both shipped). Left: measured targets and fixes per class of machine (memory use, worker counts, startup) on a Steam Deck, old Intel and Apple Silicon Macs and 8 GB computers, reported on the Benchmarks page.
More: [docs/benchmark.md](docs/benchmark.md) · [https://playhorizon.dev/docs/performance](https://playhorizon.dev/docs/performance)
</details>
#### Native upscaling
Draw the game at a lower resolution and sharpen it to your screen's size, in the way of FSR or DLSS, to gain frames on weaker graphics cards. An optional graphics setting.
<details><summary>Technical</summary>
A render-scale setting below the window size with an upscaler of our own that runs on every wgpu backend. Proof: frame time against native resolution on integrated GPUs, and screenshots for image quality. Issue [#1403](https://github.com/ludovic111/horizon/issues/1403).
</details>
#### A batch of targeted optimisations
Several smaller speed-ups, each measured: idle block entities that sleep, cheaper particles, faster structure placement, high-rate mice and very large texture packs.
<details><summary>Technical</summary>
Block entities and hoppers that skip work when idle (the hopper item scan is already skipped when no item is near); a particle frustum test first (also a parity gap), then particle quads built on the GPU; jigsaw shortcuts that skip blocked connectors and pool elements known not to fit, with layouts unchanged (trial chambers and ancient cities first); batched raw mouse input on Windows for 8000 Hz mice; a better atlas packer for huge resource packs (a 64x pack builds a 704 MiB atlas for 452 MiB of textures); sign-heavy scenes ([#1461](https://github.com/ludovic111/horizon/issues/1461)). Profile first, keep what pays, show before and after numbers.
More: [docs/profiling.md](docs/profiling.md)
</details>
### Modpacks
#### CurseForge in the mod browser (coming soon)
Coming soon: search and download CurseForge mods and modpacks from the launcher, as you can with Modrinth today.
<details><summary>Technical</summary>
The mod browser lists CurseForge as coming soon ([docs/mod-browser.md](docs/mod-browser.md)): it needs CurseForge API access. Meanwhile packs import from a CurseForge zip or instance folder, and jars only CurseForge hosts come from the player's own instance ([docs/modpacks.md](docs/modpacks.md)).
</details>
#### A Convert to Rust button for what isn't rewritten yet
When a mod, shader pack or modpack hasn't been rewritten for Horizon, one button converts what can be converted, so you don't have to wait for the rewrite.
<details><summary>Technical</summary>
Generalise the mod browser's Port step (`horizon-port-pack`: the data, assets, worldgen and block and item definitions of Java mods, .mrpack and CurseForge zips) to every kind of content: code mods where the API allows it (see 'Mods with code') and shader packs, converting an Iris/OptiFine pack into Horizon's format. The browser says what works before the install. Java code itself does not run ([docs/modpacks.md](docs/modpacks.md), [docs/mod-browser.md](docs/mod-browser.md)).
</details>
#### Mods with code: machines, magic, mobs
Making mods that add machines, magic and mobs work in Horizon, in steps, starting with machines. They are most of what a big modpack is made of.
<details><summary>Technical</summary>
333 of the 496 mods in a big pack (ATM10) contain code. Issue [#1854](https://github.com/ludovic111/horizon/issues/1854) ranks the API gaps (ticking block entities, networking, rendering, menus, inventories) that unlock them in tiers, with machines first as the recommended path and a furnace ported tick for tick as proof. The first API step is a draft ([#1189](https://github.com/ludovic111/horizon/issues/1189), see 'Machines and blocks with inventories'). The path isn't started yet ([docs/modpacks.md](docs/modpacks.md)).
</details>
#### Data packs in the mod browser
Search and install data packs from the launcher's mod browser, for the world you choose, as you do mods and resource packs.
<details><summary>Technical</summary>
A Datapacks tab beside Mods and Resource packs: install into a chosen world's datapacks folder, with the same checks and 'works in Horizon' notes as the other kinds ([docs/mod-browser.md](docs/mod-browser.md)).
</details>
### Servers and multiplayer
#### Prepare a version without Java
Set up Horizon, a server first, without Java on your computer: published tables are combined with your own copy of the game.
<details><summary>Technical</summary>
Today `horizon-prepare` runs Mojang's exporters with Java. Issue [#1750](https://github.com/ludovic111/horizon/issues/1750) proposes publishing the computed version tables once from CI and assembling the rest locally from the player's own jar and assets, provided the tables hold no Mojang content; a contributor offered a pull request, and it would help phones too. A server-only prepare from `server.jar` alone is the first step ([docs/server.md](docs/server.md)).
</details>
#### Server console, bans and a full admin API
Manage a server from a console, ban and pardon players, and drive it from other tools through a full admin API and an MCP server for AI agents.
<details><summary>Technical</summary>
`horizon-server console` (a text interface), `ban`, `pardon`, `ban-ip` and `mcp` over stdio. Admin API additions: the player list, kick, ban, op and whitelist, an SSE stream of logs and events, a text metrics endpoint, config GET and PATCH, backup delete and download. The panel's later features (bans, worlds, server.properties import, logs and crash reports, modpacks) and vanilla's management protocol (the JSON-RPC over WebSocket that the `management-server-*` keys configure), so vanilla's tools can manage a Horizon server. [docs/server.md](docs/server.md), [docs/api/server.md](docs/api/server.md).
</details>
#### Hosting polish: Windows service, safe updates
Smoother hosting: a real Windows service, a backup before every update, backups that keep the world and each player's state, and a changed view distance or message without a restart.
<details><summary>Technical</summary>
A Windows service wrapper instead of the logon scheduled task; `backups.before_update`; hot reload of config for view distance and the message of the day (needs engine support); backups that keep world and player state ([#1808](https://github.com/ludovic111/horizon/issues/1808), a contributor's draft, in review). [docs/hosting.md](docs/hosting.md), docs/server.md.
</details>
#### Guided hosting with Tailscale and an invite button
In the launcher, a guide and a button to host a server and invite friends over Tailscale, so nobody has to open ports on a router.
<details><summary>Technical</summary>
On the Servers page (create, start, back up, update and invite are shipped) add a Tailscale setup that explains and checks each step, and an invite button that hands friends what they need to join over the tailnet. Buttons first, no docs needed, with the details in an Advanced section. Remote access over Tailscale is already described in [docs/server.md](docs/server.md).
More: [docs/hosting.md](docs/hosting.md)
</details>
#### An official Docker image for servers
An official image to run a Horizon dedicated server in Docker with one command, your own copy of the game files mounted in.
<details><summary>Technical</summary>
The repository has the Dockerfile (`packaging/server/Dockerfile`: a multi-stage build on Debian bookworm-slim, user `horizon`, volume `/data`, port 25565, game files mounted read-only, `HORIZON_EULA` and friends answering the first-start questions), and anyone can build it ([docs/hosting.md](docs/hosting.md), section 9). Left: publish the image to a public registry from the release workflow, tagged per release, so it follows the same checks, and update the docs and the launcher. The image never holds Mojang's files.
More: [docs/server.md](docs/server.md)
</details>
#### A server you install with one command
Install and run a Horizon dedicated server from a terminal with a single command, no launcher needed, for VPSes and headless machines.
<details><summary>Technical</summary>
A player's idea (#horizon-ideas). `horizon-server` already runs headless; left: an install script (Linux/macOS shell, Windows PowerShell) that fetches the signed release, checks it, asks for or points at the player's own game files and starts the server, documented in [docs/hosting.md](docs/hosting.md).
</details>
### Mods and API
#### Mods that read and change the world
Mods that can see and change blocks, read inventories and items, and find and affect entities, all through the server so every player sees the same thing.
<details><summary>Technical</summary>
After server mods: read blocks, states and biomes in loaded chunks and change them through the server's block log, so clients and the parity harnesses see vanilla's updates; the player's inventory, held item, open container and item stacks with components; entity queries near a position, a safe subset of entity data and data-driven mod entity types. Snapshots and batched calls per tick, never per-block host calls in hot paths. [docs/api/mods.md](docs/api/mods.md) ('Not there yet').
More: [docs/modpacks.md](docs/modpacks.md)
</details>
#### Machines and blocks with inventories (API 0.4)
Mods that add machines: blocks that tick, hold items and have their own menus. This unlocks the technology mods of big modpacks.
<details><summary>Technical</summary>
Block entities and menus: ticking machines with slots, server menu logic and the menu protocol to clients, energy, item and fluid transfer, data attachments, accessory slots and custom recipe types, with a typed client and server channel and typed config beside them. A draft ([#1189](https://github.com/ludovic111/horizon/issues/1189)) proves a furnace ported tick for tick and holds the ATM10 code-mods plan; the design is redone through the API policy before it merges ([docs/modpacks.md](docs/modpacks.md), docs/api/stability.md).
</details>
#### Lua and other languages for mods
Write mods as Lua scripts, in the style of KubeJS, with no compiler, and later in other languages that pass the tests. Every language runs in the same sandbox.
<details><summary>Technical</summary>
Lua first: Horizon ships a Lua interpreter that is itself a WebAssembly mod, and a script mod's manifest names it and the capabilities it uses, so a script can do exactly what a compiled mod with the same manifest can, with hot reload of `main.lua`. Then C, C++, Zig, Go (TinyGo), AssemblyScript and JavaScript or TypeScript are measured against Rust on the same tick-heavy example, and each is supported only with a template, an example and a CI test. [docs/api/mods.md](docs/api/mods.md) ('Ways in').
</details>
### Gameplay parity
#### Pistons push players and mobs
Pistons and slime blocks will push players, mobs and items as in vanilla. Today moving blocks pass through them.
<details><summary>Technical</summary>
Port `PistonMovingBlockEntity.moveCollidedEntities` and `moveStuckEntities` (honey), the slime bounce and `Entity.limitPistonMovement` for `MoverType.PISTON`, in single player and for a server's players. Issues [#1496](https://github.com/ludovic111/horizon/issues/1496), [#1609](https://github.com/ludovic111/horizon/issues/1609), [#1608](https://github.com/ludovic111/horizon/issues/1608), [#1704](https://github.com/ludovic111/horizon/issues/1704), [#1758](https://github.com/ludovic111/horizon/issues/1758).
</details>
#### One single-player path: remove the in-frame world
Single player will have one way to run, the server thread, so every fix works everywhere and there are fewer bugs. The older in-frame mode goes once everything passes without it.
<details><summary>Technical</summary>
`--in-frame` (also `--no-server-thread` and `HORIZON_SERVER_THREAD=0`) still ticks the world inside the window's `Game`, and most harnesses test that path. Delete it once `lan_e2e --server-thread` and the parity harnesses pass on the server thread, which also halves the bug surface. See also 'Single player as a server' and [docs/server-levels.md](docs/server-levels.md).
</details>
#### Villages and combat, exactly as in vanilla
Villages and fights behaving as in vanilla: iron golems defending villages, villagers remembering their homes, mace smashes and knockback.
<details><summary>Technical</summary>
Villages: iron golem village goals, villager memories across saves, villager age, job-site claiming on dedicated servers, flower offers ([#1639](https://github.com/ludovic111/horizon/issues/1639) is one report). Combat: the mobs' `attack_knockback` attribute, mace smash logic, the hit knockback source on the host, the fire burnout biome tag. Each is checked against vanilla by a harness ([docs/rust-parity.md](docs/rust-parity.md)).
</details>
#### Choose the audio output device
Pick the speakers or headphones Horizon plays through, in Music & Sounds.
<details><summary>Technical</summary>
An output device list in Music & Sounds, falling back to the system default. [#1035](https://github.com/ludovic111/horizon/issues/1035) started it: a player on an older Windows computer had no sound at all, and a device choice is how such cases are found and fixed.
</details>
### Rendering
#### Well-known shader looks, rewritten for Horizon
Popular shader packs rewritten in Horizon's format, so good packs exist from the start. Only packs whose licence is open source and compatible with ours are rewritten, never anything else.
<details><summary>Technical</summary>
The first candidates are Photon, Chocapic13 and MakeUp Ultra Fast, each only after its licence is read and recorded: a pack is rewritten only if it is open source and compatible with Horizon's licence, and the rewrite is written from the shader format's documentation and the look on screen, not from anyone's code. Today only the Golden Hour example exists (`[crates/horizon-shader-api/packs/golden_hour](crates/horizon-shader-api/packs/golden_hour)`).
More: [docs/shaders.md](docs/shaders.md)
</details>
### Launcher
#### Microsoft sign-in (coming soon)
Coming soon: sign in with your Microsoft account to play on online-mode servers. The code is ready; it waits for Microsoft's approval.
<details><summary>Technical</summary>
Device-code sign-in in the launcher and online-mode login in the client ([docs/microsoft-account.md](docs/microsoft-account.md)). It is turned on by setting the app's client id once Minecraft API access is approved; until then the launcher shows "Coming soon".
More: [#1270](https://github.com/ludovic111/horizon/issues/1270)
</details>
#### A visible beta label
A clear 'beta' label in the launcher and the game, so nobody is surprised that Horizon is still being built.
<details><summary>Technical</summary>
Show Beta in the launcher's header and about page, on the game's title screen and on the website, beside the version number ([docs/versioning.md](docs/versioning.md)).
</details>
### Platforms
#### Horizon on Intel Macs
Builds of the game and the launcher for Macs with an Intel processor.
<details><summary>Technical</summary>
x86_64 game and launcher builds, picked per processor ([#1171](https://github.com/ludovic111/horizon/issues/1171)). Before it ships it needs a run on a real Intel Mac: Rosetta on Apple Silicon only proves the CPU code. Requests: [#1161](https://github.com/ludovic111/horizon/issues/1161), [#1792](https://github.com/ludovic111/horizon/issues/1792).
</details>
#### Wayland and Hyprland polish
Fixes for Linux desktops that use Wayland and Hyprland: fullscreen, the clipboard, the GUI scale after a restart and the like.
<details><summary>Technical</summary>
Open: the GUI Scale is clamped to 2 after a restart under Hyprland, while XWayland keeps it ([#1866](https://github.com/ludovic111/horizon/issues/1866)). Earlier reports covered fullscreen dropping on menu clicks, the clipboard and a white overlay ([#1007](https://github.com/ludovic111/horizon/issues/1007), closed). Checked on a real Wayland session.
</details>
### AI and agents
#### A versioned agent-control protocol
A stable, documented way for AI agents and scripts to drive the game and the launcher, with versions and an event stream so tools keep working as Horizon changes.
<details><summary>Technical</summary>
`hello` answers with the API version, the release and the features; `horizon-ctl --api`; MCP serverInfo; reply schemas generated into the reference; a `test.` namespace for hooks; transcript compatibility tests; `subscribe` as a stream of events on one connection; Horizon option files by key and saved-server commands. Agent control itself (`horizon-ctl`, both MCP servers, the No AI switch) is done: [docs/agent-control.md](docs/agent-control.md), [docs/api/agent-control.md](docs/api/agent-control.md).
More: [docs/api/stability.md](docs/api/stability.md)
</details>
#### Port mods with your own AI agent
You can port a mod to Horizon with your own AI agent on your own computer. Horizon documents how and gives you the tools, but provides no hosted agents.
<details><summary>Technical</summary>
A player runs their own agent locally with `horizon-ctl` and the MCP servers ([docs/agent-control.md](docs/agent-control.md)), `cargo horizon-mod`, `horizon-port-pack` and the parity harnesses as the check. Horizon writes the guide (playhorizon.dev/docs/agents, [docs/modpacks.md](docs/modpacks.md)) and runs no agents, accounts or keys for players. The No AI switch turns all of it off.
More: [https://playhorizon.dev/docs/agents](https://playhorizon.dev/docs/agents)
</details>
### Community
#### Horizon on X (coming soon)
Coming soon: an official Horizon account on X, used to announce releases and the weekly summary.
<details><summary>Technical</summary>
Kira already posts the roadmap items marked `announce = true` when they ship, and the weekly summary, as soon as the account's credentials exist ([roadmap/README.md](roadmap/README.md), 'The journal'). Waiting for the account itself.
</details>
### Code and tools
#### Pipeline protection and a GitHub App for agents
Safer releases: changes to the release machinery need the owner's review, and AI agents use a smaller identity of their own instead of a personal account.
<details><summary>Technical</summary>
`.github/CODEOWNERS` lists the workflows, the release and signing tools, the update-checking code and the installers ([#1015](https://github.com/ludovic111/horizon/issues/1015)); the branch protection that makes it count waits for the `horizon-agent` GitHub App, which reads and writes code, pull requests and issues but can't change settings, workflows, secrets or members, and whose tokens last an hour. It needs the owner to create the App and switch the protection on ([docs/security.md](docs/security.md)).
</details>
#### Signed updates everywhere, installers included
Every download Horizon makes is checked against the project's signature, including the first install with install.sh and install.ps1.
<details><summary>Technical</summary>
Releases carry signed checksums, and the launcher and `horizon-server update` refuse anything unsigned ([#1011](https://github.com/ludovic111/horizon/issues/1011), [#1014](https://github.com/ludovic111/horizon/issues/1014)). Left: the first install through `install.sh` and `install.ps1` still trusts only the checksum GitHub publishes, because verifying Ed25519 signatures needs OpenSSL 3, which macOS doesn't ship ([docs/security.md](docs/security.md)).
</details>
## Later
### Performance
#### Exact far terrain from real chunks
Far terrain drawn from the world you really built and explored, caves and builds included, not only a preview from the seed.
<details><summary>Technical</summary>
Keep a 3D mip store on disk built from real chunks, drawn by the GPU beside the seed preview for areas nobody has visited. Written from scratch and measured before it is kept. Builds on Far Detail and the far-terrain levels of detail already shipped; the open work on them is [#718](https://github.com/ludovic111/horizon/issues/718).
</details>
### Modpacks
#### Older Fabric and Forge mods
Recognise mods built for versions before 26.1 in the mod browser, so they can be checked and carried over like newer ones.
<details><summary>Technical</summary>
A mapping table from intermediary and SRG names so pre-26.1 jars can be classified, and a rename for mod ids with dashes (`fabric-api`); the port step then treats them like newer jars ([docs/mod-browser.md](docs/mod-browser.md)).
</details>
#### A modpack's own resource and shader packs
Importing a modpack also brings the resource packs and shader packs it ships with, where Horizon can use them.
<details><summary>Technical</summary>
Porting a pack carries data, assets and worldgen today; the resource packs and shader packs bundled with it are left out. Resource packs can be carried as they are; shader packs go through the converter ([docs/modpacks.md](docs/modpacks.md)).
</details>
#### Whole modpacks, such as ATM10, rewritten
Rewriting a complete big modpack for Horizon so it runs far better than the original. Not a priority now: the work above comes first.
<details><summary>Technical</summary>
The project's first goal for modpacks: support them ten times better than vanilla and port any of them. It was set aside in favour of compatibility through the browser. What exists: ATM10's world generates and 42 of its 496 mods are carried at least in part ([docs/modpacks.md](docs/modpacks.md)). It depends on 'Mods with code' and the native mod API.
</details>
#### Running Java mods that aren't rewritten
Run Java mods that nobody has rewritten for Horizon yet, through a bridge or a converter. Hard and not started: the Convert button and native rewrites come first.
<details><summary>Technical</summary>
Earlier research ([#527](https://github.com/ludovic111/horizon/issues/527), closed) found that Java mods can't simply run, because they call Minecraft's own classes. Options to look at again: a jar-to-WebAssembly backend, or a bridge for simple Fabric mods. Whatever is chosen has to run inside the same sandbox and capability checks as native mods.
</details>
### Servers and multiplayer
#### Ely.by and other sign-in providers
Sign in with other account providers, such as Ely.by or any Yggdrasil-compatible one, beside Microsoft.
<details><summary>Technical</summary>
Issue [#1270](https://github.com/ludovic111/horizon/issues/1270) asks for Ely.by and Yggdrasil-compatible servers with an account selector in the launcher. It builds on the Microsoft sign-in ([docs/microsoft-account.md](docs/microsoft-account.md)).
</details>
#### Online-mode checks for Play with Friends
Friends who join through an invite code could prove who they are with their Microsoft account, as on online-mode servers.
<details><summary>Technical</summary>
Play with Friends joins over an encrypted tunnel with an invite code ([docs/play-with-friends.md](docs/play-with-friends.md), 'Identity'). Online-mode checks against Mojang's session servers can be added on top of the same tunnel; they need Microsoft sign-in first, which is coming soon. Play with Friends stays self-hosted between players, with no relay run by the project.
</details>
### Mods and API
#### Orbital Laser on the normal game
The community mod Orbital Laser, made for a fork of Horizon, running on the normal game and listed in the registry as the showcase of the mod API.
<details><summary>Technical</summary>
A pull request on [https://github.com/Stink-O/orbital-laser](https://github.com/Stink-O/orbital-laser) (MIT) once the input, ui, render and world capabilities exist (see 'Screens, input and drawing for mods'): today the mod needs them through its author's fork. Tested in the game, and listed in the registry.
</details>
#### Native versions of popular client mods
Built-in versions of the client mods players ask for: block info on hover, a recipe viewer like JEI, a world map and schematic overlays.
<details><summary>Technical</summary>
Done as extras: a mod list and menu, a minimap and a zoom ([docs/extras.md](docs/extras.md)). Left: block info tooltips, a recipe viewer (a contributor offered to port JEI if it becomes a native feature), a world map and a schematic overlay. They need the read-only interfaces `inventory-read`, `recipes`, `world-read` and `render` in the mod API, or they go into the game directly.
More: [docs/mods.md](docs/mods.md)
</details>
#### Create-style physics as a native mod
Contraptions and physics like the Create and Aeronautics mods, as a mod for Horizon rather than part of the base game.
<details><summary>Technical</summary>
Issue [#805](https://github.com/ludovic111/horizon/issues/805) (porting QOL mods) closed with this answer: it would come as a native Horizon mod on the mod API. It needs server mods, entities and rendering first.
</details>
#### Proximity voice chat as a native mod
Hear nearby players talk, as with the voice chat mods of other Minecraft versions, as a Horizon mod.
<details><summary>Technical</summary>
Needs server mods, mod networking and an audio interface in the mod API before anything can be built ([docs/api/mods.md](docs/api/mods.md)).
</details>
### Gameplay parity
#### Every Minecraft version, prepared automatically
Every past and future Minecraft version prepared for Horizon automatically. When Mojang releases one, Horizon follows it, and every system is checked against it again.
<details><summary>Technical</summary>
The port pipeline ([docs/version-ports.md](docs/version-ports.md), `tools/version_port.py`) detects a release, prepares it, diffs its classes against the pinned 26.3, regenerates the data-driven tables, runs every parity harness on it and writes a port report that becomes issues. The goal is as little human work as possible for any version, past or future. Detection and the first slices are merged; the scheduled agent that runs it every day isn't installed yet. Two versions in one build are out of scope.
</details>
#### Controllers: tested on real pads, and in the launcher
Make controller support solid: tested with real PlayStation and Xbox pads and a Steam Deck, and usable in the launcher too.
<details><summary>Technical</summary>
The game side is merged ([docs/controllers.md](docs/controllers.md)): pads read with gilrs, menus, rumble, glyphs, remapping and DualSense adaptive triggers, lightbar and haptics, but not yet confirmed on real pads or a Steam Deck (DualSense haptics stay behind a switch until then). Left: a test pass on real hardware, and the same focus navigation in the GPUI launcher.
</details>
#### The full debug screen
Vanilla's complete F3 screen: debug options, a profiler chart, ten-second captures and an atlas dump.
<details><summary>Technical</summary>
Today F3 shows position, chunk, light, biome and FPS ([docs/native-client.md](docs/native-client.md)). Vanilla also has F3+F6 debug options with profiles, F3+1 the profiler pie, F3+L a ten-second capture and F3+S the atlas dump ([#562](https://github.com/ludovic111/horizon/issues/562) asked for the F3 editor).
</details>
### Rendering
#### A light default shader pack
A small built-in look (tone mapping, colour grade, a cheap vignette) that costs almost nothing, for players who want a nicer picture without a heavy pack.
<details><summary>Technical</summary>
An opt-in, composite-only pack on the native shader API, under about 0.3 ms at 1080p, shown on the Shader Packs page. Pack option sliders and profiles, and a Failed row with the full error, belong with it.
More: [docs/shaders.md](docs/shaders.md)
</details>
#### Optional ray tracing
An optional ray-tracing mode for graphics cards that support it (RTX and recent AMD): shadows, water reflections and global lighting on top of the vanilla look.
<details><summary>Technical</summary>
A spike with ray-traced sun shadows, off by default, is a draft ([#1180](https://github.com/ludovic111/horizon/issues/1180)). Reflections, global illumination, Apple M3 and newer if it is cheap, and a performance report per card come after.
</details>
#### HDR monitors
Output for HDR monitors: brighter highlights and richer colour where your screen supports it.
<details><summary>Technical</summary>
Issue [#1441](https://github.com/ludovic111/horizon/issues/1441). Needs a surface format and a tone-mapping path per backend, and tests on real HDR screens on Windows, macOS and Linux.
</details>
#### MSAA, anisotropic filtering, exclusive fullscreen
More graphics options: multisample anti-aliasing, anisotropic filtering and an exclusive fullscreen mode. Today Horizon has FXAA and borderless fullscreen.
<details><summary>Technical</summary>
Not implemented ([docs/graphics.md](docs/graphics.md)). Anisotropic filtering needs linear magnification in wgpu, which blurs close blocks, so close-up sharpness has to be kept first; MSAA would sit beside FXAA; exclusive fullscreen where the backend supports it.
</details>
### Launcher
#### Lean installs that use less disk
Smaller installs: Horizon keeps one shared copy of the prepared game files and removes what only the preparation needed.
<details><summary>Technical</summary>
A shared resource store keyed by the preparer's digest, no prepare intermediates left behind, the current and the previous version kept, and an automatic clean-up. Not started.
</details>
### Platforms
#### Horizon on Linux ARM64
Game and launcher builds for 64-bit ARM Linux computers.
<details><summary>Technical</summary>
Issue [#1709](https://github.com/ludovic111/horizon/issues/1709): aarch64 builds of the game and the launcher in the release workflow, tested on a real ARM machine with its Vulkan driver. The server build already drops audio and controllers (`--no-default-features`).
</details>
#### Android, iOS and the browser
Versions of Horizon for Android, iOS and web browsers. Nothing official yet; contributors have started the first pieces.
<details><summary>Technical</summary>
No official build. Android was first closed as not planned, with touch controls, a mobile interface, packaging and loading the player's own assets all missing; the roadmap now includes it, iOS and a WebAssembly build for the browser as long-term goals. First pieces from contributors: `horizon-dualsense` compiling on iOS ([#1813](https://github.com/ludovic111/horizon/issues/1813)) and helper-thread limits on Android ([#1751](https://github.com/ludovic111/horizon/issues/1751)). Preparing without Java ([#1750](https://github.com/ludovic111/horizon/issues/1750)) matters here too. Horizon ships none of Mojang's files on any platform.
</details>
#### AppImage and a Nix flake
Install Horizon on any Linux distribution as an AppImage, and on NixOS through a Nix flake.
<details><summary>Technical</summary>
A player's idea (#horizon-ideas). An AppImage of the launcher and game from the release workflow, and a flake.nix packaging the release binaries with their runtime libraries (Vulkan loader, X11/Wayland, ALSA), the way horizon-parity's NixOS setup runs them today.
</details>
### Horizon extras
#### A complete video studio inside the game
Record, replay and edit videos without leaving the game: record in a keypress, replay a session, cut and finish the video. One of Horizon's original goals.
<details><summary>Technical</summary>
Three stages. Recording: capture gameplay in a keypress with an encoder thread and little frame-time cost. Replay: a replay mode for recorded sessions. Editing: cuts, camera paths, titles and export. None of it exists yet; the agent-control socket ([docs/agent-control.md](docs/agent-control.md)) already lets tools drive the game for scripted shots.
</details>
#### Game menus in the launcher's style
An optional look for the game's own menus and options that matches the Horizon launcher. Off by default: the vanilla menus stay as they are.
<details><summary>Technical</summary>
A player's idea (#horizon-ideas). A built-in resource pack or a Horizon setting that restyles the title, pause and options screens with the launcher's design tokens (crates/horizon-theme), following [docs/design-system.md](docs/design-system.md); off by default because it changes the vanilla look.
</details>
### AI and agents
#### An optional AI teammate
An optional AI teammate you can talk to by voice. It plays beside you, helps or finishes the game with you, runs on your computer first, and exists only if you turn it on. The No AI switch hides it.
<details><summary>Technical</summary>
Draft pull requests: the companion ([#1068](https://github.com/ludovic111/horizon/issues/1068)), its safety modes and their evaluation ([#1069](https://github.com/ludovic111/horizon/issues/1069)) and a route that finishes the game ([#1070](https://github.com/ludovic111/horizon/issues/1070)). In the drafts: a companion that follows, fights, mines, crafts, sleeps and respawns; chat and push-to-talk voice, with speech recognition and a choice of voices; presets (very strong, helper, beat the game); a guard model with Clean, Kid and Salty modes; perception levels from Fair to All-knowing; hardware detection with a simple mode and an Advanced tab; local models first, cloud keys optional. The No AI switch is already in the game. Left: the speedrun route doesn't finish the game reliably yet, voices for more languages, and the safety evaluation, which must pass before it ships.
</details>
### Community
#### A video series about Horizon
A series of videos about how Horizon is made and what is new, with each upload announced on Discord.
<details><summary>Technical</summary>
Kira already shows new uploads in a read-only Discord channel (done). The series follows each stage of the project: a script, edits, titles and a thumbnail per video.
</details>
## Help wanted
### Performance
#### Benchmark your computer
Run Horizon's built-in benchmark and share the report: every new computer makes the performance work more accurate. No coding needed.
<details><summary>Technical</summary>
`horizon-launcher play --arg=--benchmark --arg=standard` writes a JSON report; `horizon-launcher data benchmark <report>` shows what would be sent and uploads it (playhorizon.dev/docs/performance). Results from three installs with the same hardware appear on the Benchmarks page. Example: [#1711](https://github.com/ludovic111/horizon/issues/1711).
More: [https://playhorizon.dev/docs/performance](https://playhorizon.dev/docs/performance)
</details>
### Gameplay parity
#### End city banners keep their pattern
Banners in End cities show as plain magenta instead of with their pattern.
<details><summary>Technical</summary>
Compare the banner block entities the End city templates place with vanilla's (`EndCityPieces`, the templates' block entity NBT, `BannerBlockEntity`'s patterns), in `crates/horizon-world/src/worldgen/structure/`. Issue [#1438](https://github.com/ludovic111/horizon/issues/1438); possibly related: [#526](https://github.com/ludovic111/horizon/issues/526).
</details>
#### Enchanted loot in End cities
Armour and tools in End city chests should come enchanted, as in vanilla.
<details><summary>Technical</summary>
`loot.rs` implements `enchant_with_levels`: find why the End city chest table's rolls come out bare, and compare them with vanilla's for a few seeds. Issues [#1437](https://github.com/ludovic111/horizon/issues/1437), [#1660](https://github.com/ludovic111/horizon/issues/1660).
</details>
## Recently shipped
- **OpenGL for old computers** (v0.2.63): Horizon now starts on old PCs whose graphics card has no Vulkan or DirectX 12 driver: it switches to OpenGL by itself, and Options > Graphics can pick OpenGL by hand. [#1917](https://github.com/ludovic111/horizon/issues/1917) · [#1920](https://github.com/ludovic111/horizon/issues/1920) · [#1936](https://github.com/ludovic111/horizon/issues/1936)
- **Server resource packs** (v0.2.48): Servers can send you their resource pack: Horizon asks, downloads it and applies it, like vanilla. [#1562](https://github.com/ludovic111/horizon/issues/1562) · [#1569](https://github.com/ludovic111/horizon/issues/1569) · [#1570](https://github.com/ludovic111/horizon/issues/1570)
- **Single player on its own server thread** (v0.2.39): Single player runs on a server thread, as vanilla does, so the world keeps ticking smoothly while the window draws. [#1514](https://github.com/ludovic111/horizon/issues/1514) · [docs/server-levels.md](docs/server-levels.md)
- **GPU occlusion culling on Macs** (v0.2.17): On Macs the GPU now skips terrain hidden behind hills and caves: 8 to 48% more frames per second at 32 chunks on an M4 Pro. [#1434](https://github.com/ludovic111/horizon/issues/1434)
- **Animated textures on the GPU** (v0.2.5): Water, lava and other animated blocks are now animated by the graphics card: 1.7 to 2.4 times better 1% lows on Intel graphics. [#1239](https://github.com/ludovic111/horizon/issues/1239)
- **Far Detail for integrated graphics** (v0.2.5): An optional Reduced setting for far terrain, suggested on integrated graphics, that saves 2 to 4 ms of GPU time per frame. [#1199](https://github.com/ludovic111/horizon/issues/1199) · [#1029](https://github.com/ludovic111/horizon/issues/1029)
- **The launcher's mod browser** (launcher@0.2.0): Search Modrinth for mods, modpacks and resource packs from the launcher; it says before installing what works in Horizon. [#1154](https://github.com/ludovic111/horizon/issues/1154) · [#1242](https://github.com/ludovic111/horizon/issues/1242) · [#1243](https://github.com/ludovic111/horizon/issues/1243) · [#1246](https://github.com/ludovic111/horizon/issues/1246) · [docs/mod-browser.md](docs/mod-browser.md)
- **A blocky, Minecraft-like launcher** (launcher@0.2.0): The launcher got a new blocky look in Horizon's purple, in dark and light. [#1269](https://github.com/ludovic111/horizon/issues/1269) · [docs/design-system.md](docs/design-system.md)
- **Dynamic lights** (v2026.10.22): An optional extra: torches you hold, drop or set on fire light up what is around them. [#1162](https://github.com/ludovic111/horizon/issues/1162)
- **Host a server from the launcher** (launcher-v0.15.0): The launcher's Servers page creates, starts, backs up and updates a Horizon server with buttons, and invites friends. [#1026](https://github.com/ludovic111/horizon/issues/1026) · [docs/hosting.md](docs/hosting.md)
+14 -24
View File
@@ -1,31 +1,21 @@
# Third-party notices
## CullLeaves
## wgpu-hal
The optional leaf and mangrove-root face-culling behavior is adapted from
[CullLeaves](https://github.com/TeamMidnightDust/CullLeaves) by MidnightDust.
Its MIT license is reproduced below.
`third_party/wgpu-hal` is [wgpu-hal](https://github.com/gfx-rs/wgpu) 30.0.1 by the gfx-rs
developers, with one upstream fix applied
([gfx-rs/wgpu#10124](https://github.com/gfx-rs/wgpu/pull/10124)). It is licensed under MIT or
Apache-2.0; both licenses are in that folder (`LICENSE.MIT`, `LICENSE.APACHE`).
```text
MIT License
## Pixelify Sans
Copyright (c) 2020 MidnightDust
`crates/horizon-theme/fonts/PixelifySans-*.ttf` is [Pixelify Sans](https://github.com/eifetx/Pixelify-Sans)
by The Pixelify Sans Project Authors, licensed under the SIL Open Font License 1.1
(`crates/horizon-theme/fonts/OFL-PixelifySans.txt`).
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
## Pixelarticons
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```
`crates/horizon-theme/icons/pixel/*.svg` are icons from
[Pixelarticons](https://github.com/halfmage/pixelarticons) by Gerrit Halfmann, under the MIT
license (`crates/horizon-theme/icons/pixel/LICENSE-pixelarticons.txt`). `star.svg` is Horizon's own, drawn on
the same grid.
+174
View File
@@ -0,0 +1,174 @@
# Dedicated server against vanilla 26.3, with bots (2026-10-17)
`horizon-client --dedicated` (run by `horizon-server`) against the pinned vanilla 26.3 `server.jar`, on `main`
at `7bbbf6e9` (in release v2026.10.17.17). That build has the movement checks and chunk pacing of #586, the
equal-work method of #670, and the server performance PRs #701–#707 and #713.
This replaces the data first published here, which came from a Mac mini overloaded by other work (load 55–141 on
14 threads). An earlier run of this method, on `main` at `002f14ce`, is kept in `earlier-002f14ce/` and compared
at the end.
## Setup
- **Server machine:** ThinkPad, Intel i7-7500U (2 cores, 4 threads), 32 GB, NixOS. Nothing else heavy ran on it:
its load average was 0.4 when idle. Both servers ran under `nice -n 19`.
- **Bots:** `horizon-bots` on another machine (Mac mini M4 Pro), over the tailnet. That machine was shared with
other work (load 36–170 on 14 threads, mean 78). The bots need little CPU, but a busy bot host can answer later.
Both servers saw the same conditions, since they took turns at each bot count.
- **Horizon:** release build of `7bbbf6e9` (`horizon-server`, `horizon-client`).
- **Vanilla:** the pinned 26.3 `server.jar` on OpenJDK 25.0.4.1, with `-Xms4g -Xmx4g`.
- **Properties:**
- `level-seed=12345`, view and simulation distance 10, creative (forced);
- `online-mode=false`, `difficulty=easy`, `max-players=200`;
- default game rules on both (the locator bar is on).
- **Each run:** a fresh server, bots joining one per second, a 60 s warm-up, then 180 s measured.
- **Commands** (on the server machine):
```
python3 tools/server_bench.py --servers horizon,vanilla --nice 19 --warmup 60 --seconds 180 \
--bots-via "ssh <bot host>" --bots-bin <horizon-bots> --connect <server address> \
--bots 1,10,25,50,100 --behaviour walk,explore
python3 tools/server_bench.py ...same... --bots 1,10,25,50 --behaviour explore --explore-speed 0.8
```
- **Columns:**
- **MSPT** (mean) and **P95** come from `/tick query`, in ms. **TPS** is the tick rate.
- **RSS** is the mean over the run. **CPU** is the whole process tree, as a percentage of one core.
- **Chunks/s** is what the bots received. **New chunks/s** counts each chunk once per bot.
- **Pulled back** counts the times the server moved a bot back (vanilla's "moved too quickly" and similar).
- **Load** is the mean load average during that run: the server machine (which includes the server's own
threads) and the bot host.
## Results
### Equal work: bots explore at the same pace (`--explore-speed 0.8`)
Each bot flies outwards at 0.8 blocks a second, which both servers keep up with. They send the same chunks, so
the tick times compare the same work.
| Bots | Server | MSPT | P95 | TPS | RSS | CPU | Chunks/s | New chunks/s | Pulled back | Load (server / bots) |
|---:|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|
| 1 | Horizon | 3.16 | 5.78 | 20.0 | 0.76 GB | 22 % | 2 | 1.27 | 0 | 2.8 / 63 |
| 1 | Vanilla | 16.82 | 27.3 | 20.0 | 3.37 GB | 69 % | 2 | 1.35 | 0 | 1.8 / 94 |
| 10 | Horizon | 9.83 | 19.01 | 20.0 | 0.86 GB | 77 % | 14 | 12.31 | 1051 | 2.2 / 74 |
| 10 | Vanilla | 24.38 | 39.58 | 20.0 | 3.57 GB | 132 % | 14 | 12.32 | 1027 | 2.5 / 64 |
| 25 | Horizon | 16.87 | 30.88 | 20.0 | 0.89 GB | 111 % | 34 | 29.92 | 3770 | 5.1 / 51 |
| 25 | Vanilla | 30.29 | 43.47 | 20.0 | 3.61 GB | 176 % | 33 | 29.29 | 5139 | 5.2 / 55 |
| 50 | Horizon | 23.57 | 47.53 | 20.0 | 0.92 GB | 127 % | 67 | 59.42 | 7434 | 8.5 / 64 |
| 50 | Vanilla | 42.44 | 54.84 | 20.0 | 3.67 GB | 201 % | 65 | 58.11 | 10604 | 5.1 / 59 |
### `walk`: bots walk around the spawn
| Bots | Server | MSPT | P95 | TPS | RSS | CPU | Chunks/s | New chunks/s | Pulled back | Load (server / bots) |
|---:|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|
| 1 | Horizon | 4.01 | 8.19 | 20.0 | 0.78 GB | 59 % | 10 | 0.43 | 0 | 1.4 / 70 |
| 1 | Vanilla | 18.22 | 29.34 | 20.0 | 3.43 GB | 69 % | 10 | 0.47 | 0 | 1.9 / 63 |
| 10 | Horizon | 8.79 | 17.35 | 20.0 | 0.78 GB | 119 % | 95 | 3.58 | 0 | 2.9 / 69 |
| 10 | Vanilla | 18.67 | 38.26 | 20.0 | 3.50 GB | 101 % | 101 | 4.38 | 5 | 3.2 / 59 |
| 25 | Horizon | 15.01 | 31.12 | 20.0 | 0.80 GB | 161 % | 234 | 8.49 | 0 | 6.3 / 43 |
| 25 | Vanilla | 18.78 | 28.16 | 20.0 | 3.52 GB | 155 % | 252 | 11.89 | 10 | 6.7 / 57 |
| 50 | Horizon | 22.72 | 60.19 | 20.0 | 0.81 GB | 180 % | 465 | 16.94 | 0 | 14.7 / 50 |
| 50 | Vanilla | 34.3 | 55.55 | 20.0 | 3.58 GB | 267 % | 497 | 26.74 | 39 | 9.5 / 56 |
| 100 | Horizon | 63.33 | 125.04 | 15.79 | 0.87 GB | 246 % | 678 | 31.8 | 0 | 32.9 / 95 |
| 100 | Vanilla | stopped by its watchdog (a 60 s tick) | | | | | | | | 15.1 / 120 |
Both servers sent each walking bot the same number of distinct chunks over the whole run (680–690). Horizon's
"new chunks/s" inside the measured window is lower, so compare the totals, not that column.
### `explore` at full speed: unequal work
Bots fly outwards as fast as chunks arrive (`horizon-bots`' default, up to 22 blocks a second). Both servers now pull them back ("moved too quickly"), but Horizon sends
more new terrain, so this pass doesn't compare equal work. The server-machine load of Horizon's 1-bot run is
estimated from the time between the runs around it: its log was overwritten before it was read.
| Bots | Server | MSPT | P95 | TPS | RSS | CPU | Chunks/s | New chunks/s | Pulled back | Load (server / bots) |
|---:|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|
| 1 | Horizon | 9.73 | 20.99 | 20.0 | 0.84 GB | 330 % | 41 | 35.96 | 0 | 4.7 / 129 |
| 1 | Vanilla | 9.71 | 20.37 | 20.0 | 3.61 GB | 247 % | 35 | 33.61 | 0 | 4.3 / 156 |
| 10 | Horizon | 21.27 | 39.15 | 20.0 | 1.18 GB | 322 % | 46 | 46.11 | 9856 | 5.0 / 114 |
| 10 | Vanilla | 16.94 | 30.28 | 20.0 | 3.95 GB | 254 % | 25 | 25.01 | 11123 | 5.5 / 89 |
| 25 | Horizon | 25.76 | 45.01 | 20.0 | 1.17 GB | 314 % | 61 | 60.7 | 31264 | 6.6 / 82 |
| 25 | Vanilla | 24.93 | 37.09 | 20.0 | 4.04 GB | 305 % | 38 | 37.84 | 33467 | 6.1 / 91 |
| 50 | Horizon | 39.31 | 65.33 | 20.0 | 1.25 GB | 325 % | 115 | 112.25 | 64163 | 6.9 / 95 |
| 50 | Vanilla | stopped by its watchdog (a 60 s tick) | | | | | | | | 7.7 / 86 |
| 100 | Horizon | 52.7 | 85.73 | 18.97 | 1.36 GB | 328 % | 154 | 151.83 | 116161 | 13.4 / 79 |
| 100 | Vanilla | stopped by its watchdog (a 60 s tick) | | | | | | | | 10.0 / 72 |
Vanilla's watchdog stopped it after a 60-second tick in three runs (walk 100, explore 50 and 100). Each time its
server thread was in entity tracking (`ChunkMap$TrackedEntity.updatePlayer`).
## What it shows
- **Same work, 1 to 50 players:** Horizon's mean tick is 44–81 % shorter than vanilla's, its 95th percentile is
lower, and its process uses 32–63 % of vanilla's CPU.
- **Walking, 1 to 50 players:** Horizon's mean tick is shorter at every count (4.0 against 18.2 ms with 1 bot,
22.7 against 34.3 ms with 50).
- **Where vanilla wins:**
- With 25 and 50 walking bots, vanilla's 95th percentile is lower (28.2 against 31.1 ms, 55.6 against
60.2 ms). Horizon's slow ticks are spikier.
- In the full-speed explore pass, vanilla's mean tick is shorter with 10 and 25 bots (16.9 against 21.3 ms,
24.9 against 25.8 ms), while Horizon sends 1.6–1.8 times as many new chunks.
- With 50 walking bots, Horizon makes the server machine busier (load 14.7 against 9.5). Its connection
writer threads compress every packet (below).
- **100 players:** Horizon keeps 15.8 TPS walking and 19.0 TPS exploring at full speed. Vanilla's watchdog
stopped it in both.
- **Memory:** Horizon used 0.76–0.92 GB in every walk and equal-work run, and 1.36 GB at most when 100 bots
explored at full speed. Vanilla used 3.4–4.0 GB with its 4 GB heap.
- **Start:** Horizon's fresh world was ready in 9.2–9.9 s, vanilla's in 14.9–17.2 s.
## Where Horizon's time goes (50 walking bots)
`perf record -e cycles:u --call-graph lbr` on the engine for 30 s, during a separate 50-bot `walk` run on the same
build and machine (MSPT 21.7 ms, 178 % CPU). Shares of all the engine's CPU samples:
| Thread | Share | Mostly |
|---|---:|---|
| Connection writers (`horizon-lan-out`) | 55 % | 53 % in `miniz_oxide` deflate, from `horizon_protocol::frame::encode` |
| World generation workers | 33 % | noise, surface rules, aquifers |
| Server thread (the tick) | 12 % | see below |
Inside the tick:
- the locator bar (`Waypoints::tick`): a third of the tick;
- `Hub::tick`: a quarter;
- block entities, creatures, chunk encoding (`ChunkEncoder::encode_in`) and `Streaming::update`: 7–10 % each;
- SipHash of `u128` keys (the default `HashMap` hasher): about 10 % of the tick.
**The next biggest cost is compressing packets.** Every chunk packet is compressed again for each player who gets
it, with `flate2`'s default `miniz_oxide` backend at level 6. On this 4-thread CPU that competes with the tick
and with world generation. Two fixes, which can be combined:
- the `zlib-rs` backend for `flate2`: 2.2–10 times faster than `miniz_oxide` at level 6 in a quick test, with
output about 3 % larger;
- compressing a chunk packet once and sending the same bytes to every player who needs it.
## Before and after the performance PRs
The earlier run, on `main` at `002f14ce` with #670's bench changes (build `a9ee56a8`), before #586, #701–#707 and
#713. Same machines and settings. MSPT mean / P95, in ms:
Walk:
| Bots | Horizon before | Horizon now | Vanilla before | Vanilla now |
|---:|---|---|---|---|
| 1 | 5.77 / 11.04 | 4.01 / 8.19 | 18.84 / 29.55 | 18.22 / 29.34 |
| 10 | 16.23 / 29.32 | 8.79 / 17.35 | 18.02 / 37.62 | 18.67 / 38.26 |
| 25 | 23.98 / 49.18 | 15.01 / 31.12 | 19.19 / 33.84 | 18.78 / 28.16 |
| 50 | 35.07 / 73.24 | 22.72 / 60.19 | 35.23 / 60.87 | 34.3 / 55.55 |
| 100 | 104.8 / 158.8 | 63.33 / 125.04 | watchdog | watchdog |
Equal work:
| Bots | Horizon before | Horizon now | Vanilla before | Vanilla now |
|---:|---|---|---|---|
| 1 | 4.78 / 7.3 | 3.16 / 5.78 | 16.63 / 26.23 | 16.82 / 27.3 |
| 10 | 15.19 / 25.66 | 9.83 / 19.01 | 24.65 / 40.81 | 24.38 / 39.58 |
| 25 | 23.37 / 42.59 | 16.87 / 30.88 | 30.28 / 44.81 | 30.29 / 43.47 |
| 50 | 30.59 / 67.94 | 23.57 / 47.53 | 43.52 / 56.18 | 42.44 / 54.84 |
Vanilla's numbers repeat within a few percent, so the runs are comparable. In the earlier run, 25 walking bots
were faster on vanilla (19.2 against 24.0 ms). A profile then put a third of the tick in advancement updates,
which #701 fixed.
## Files
- `walk-explore.json`: the `walk` and full-speed `explore` passes. `explore-equal-rate.json`: the equal-work pass.
- `load.txt`: both machines' load averages every 30 s during the runs.
- `earlier-002f14ce/`: the same files for the earlier run.
- The server's tailnet address is replaced by `<server address>` in the JSON.
@@ -0,0 +1,341 @@
{
"kind": "horizon-server-benchmark",
"date": "2026-10-17T15:37:03+00:00",
"machine": {
"system": "Linux",
"machine": "x86_64",
"cpus": 4
},
"commit": "a9ee56a8b143999c9bc7056850ffcd622a9214aa",
"java": [
"openjdk 25.0.4.1 2026-08-18"
],
"settings": {
"servers": "horizon,vanilla",
"bots": "1,10,25,50",
"behaviour": "explore",
"join_every": 1.0,
"warmup": 60,
"seconds": 180,
"seed": 12345,
"explore_speed": 0.8,
"gamerule": "",
"heap": "4g",
"out": "/home/ludovic/code/horizon/.claude/worktrees/server-host-4/.local/bench-out/explore-equal-rate.json",
"nice": 19,
"bots_via": "ssh macmini",
"bots_bin": "/Users/ludovicmarie/code/horizon/.claude/worktrees/server-host-4/target/release/horizon-bots",
"connect": "<server address>",
"properties": {
"level-seed": "12345",
"view-distance": "10",
"simulation-distance": "10",
"online-mode": "false",
"white-list": "false",
"gamemode": "creative",
"force-gamemode": "true",
"spawn-protection": "0",
"max-players": "200",
"difficulty": "easy",
"sync-chunk-writes": "true"
}
},
"runs": [
{
"server": "horizon",
"bots": 1,
"behaviour": "explore",
"start_seconds": 9.21,
"mspt": {
"average": 4.78,
"p50": 4.59,
"p95": 7.3,
"p99": 13.53
},
"mspt_worst_p99": 36.5,
"tps": 20.0,
"rss_mb": {
"mean": 759.91,
"max": 771.3
},
"cpu_percent": 24.49,
"chunks_per_second": 1.64,
"unique_per_second": 1.27,
"forgotten_per_second": 1.54,
"waited_ticks_per_second": 0.0,
"chunks_total": 841,
"chunks_per_bot": {
"explore": 841.0
},
"unique_chunks_per_bot": {
"explore": 781.0
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 1,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "vanilla",
"bots": 1,
"behaviour": "explore",
"start_seconds": 14.35,
"mspt": {
"average": 16.63,
"p50": 16.73,
"p95": 26.23,
"p99": 35.39
},
"mspt_worst_p99": 68.5,
"tps": 20.0,
"rss_mb": {
"mean": 3431.12,
"max": 3459.2
},
"cpu_percent": 69.76,
"chunks_per_second": 1.66,
"unique_per_second": 1.37,
"forgotten_per_second": 1.67,
"waited_ticks_per_second": 0.0,
"chunks_total": 840,
"chunks_per_bot": {
"explore": 840.0
},
"unique_chunks_per_bot": {
"explore": 780.0
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 1,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "horizon",
"bots": 10,
"behaviour": "explore",
"start_seconds": 9.31,
"mspt": {
"average": 15.19,
"p50": 14.45,
"p95": 25.66,
"p99": 38.89
},
"mspt_worst_p99": 57.6,
"tps": 20.0,
"rss_mb": {
"mean": 859.86,
"max": 920.2
},
"cpu_percent": 84.95,
"chunks_per_second": 14.22,
"unique_per_second": 12.7,
"forgotten_per_second": 14.11,
"waited_ticks_per_second": 0.0,
"chunks_total": 7962,
"chunks_per_bot": {
"explore": 796.2
},
"unique_chunks_per_bot": {
"explore": 768.2
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 10,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "vanilla",
"bots": 10,
"behaviour": "explore",
"start_seconds": 14.74,
"mspt": {
"average": 24.65,
"p50": 21.82,
"p95": 40.81,
"p99": 70.23
},
"mspt_worst_p99": 119.1,
"tps": 20.0,
"rss_mb": {
"mean": 3528.77,
"max": 3643.2
},
"cpu_percent": 133.1,
"chunks_per_second": 13.62,
"unique_per_second": 12.32,
"forgotten_per_second": 13.62,
"waited_ticks_per_second": 0.0,
"chunks_total": 7746,
"chunks_per_bot": {
"explore": 774.6
},
"unique_chunks_per_bot": {
"explore": 748.7
},
"resent": 0,
"pulled_back": 1150,
"moved_too_quickly": 5,
"moved_wrongly": 0,
"bots_joined": 10,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "horizon",
"bots": 25,
"behaviour": "explore",
"start_seconds": 9.42,
"mspt": {
"average": 23.37,
"p50": 21.61,
"p95": 42.59,
"p99": 60.21
},
"mspt_worst_p99": 102.1,
"tps": 20.0,
"rss_mb": {
"mean": 889.21,
"max": 950.7
},
"cpu_percent": 117.59,
"chunks_per_second": 35.34,
"unique_per_second": 31.62,
"forgotten_per_second": 35.12,
"waited_ticks_per_second": 0.0,
"chunks_total": 20137,
"chunks_per_bot": {
"explore": 805.48
},
"unique_chunks_per_bot": {
"explore": 774.52
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 25,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "vanilla",
"bots": 25,
"behaviour": "explore",
"start_seconds": 15.14,
"mspt": {
"average": 30.28,
"p50": 28.17,
"p95": 44.81,
"p99": 71.77
},
"mspt_worst_p99": 158.1,
"tps": 20.0,
"rss_mb": {
"mean": 3574.32,
"max": 3692.7
},
"cpu_percent": 173.43,
"chunks_per_second": 32.76,
"unique_per_second": 29.38,
"forgotten_per_second": 33.02,
"waited_ticks_per_second": 0.0,
"chunks_total": 19417,
"chunks_per_bot": {
"explore": 776.68
},
"unique_chunks_per_bot": {
"explore": 747.36
},
"resent": 0,
"pulled_back": 4728,
"moved_too_quickly": 15,
"moved_wrongly": 0,
"bots_joined": 25,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "horizon",
"bots": 50,
"behaviour": "explore",
"start_seconds": 9.11,
"mspt": {
"average": 30.59,
"p50": 25.19,
"p95": 67.94,
"p99": 90.77
},
"mspt_worst_p99": 122.1,
"tps": 20.0,
"rss_mb": {
"mean": 915.93,
"max": 978.0
},
"cpu_percent": 132.13,
"chunks_per_second": 71.31,
"unique_per_second": 63.56,
"forgotten_per_second": 71.19,
"waited_ticks_per_second": 0.0,
"chunks_total": 41365,
"chunks_per_bot": {
"explore": 827.3
},
"unique_chunks_per_bot": {
"explore": 793.14
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 50,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "vanilla",
"bots": 50,
"behaviour": "explore",
"start_seconds": 15.35,
"mspt": {
"average": 43.52,
"p50": 41.69,
"p95": 56.18,
"p99": 84.55
},
"mspt_worst_p99": 155.1,
"tps": 20.0,
"rss_mb": {
"mean": 3801.29,
"max": 3920.7
},
"cpu_percent": 198.97,
"chunks_per_second": 65.86,
"unique_per_second": 58.63,
"forgotten_per_second": 66.12,
"waited_ticks_per_second": 0.0,
"chunks_total": 39790,
"chunks_per_bot": {
"explore": 795.8
},
"unique_chunks_per_bot": {
"explore": 764.48
},
"resent": 0,
"pulled_back": 8392,
"moved_too_quickly": 38,
"moved_wrongly": 0,
"bots_joined": 50,
"bots_failed": [],
"tick_queries": 35
}
]
}
@@ -0,0 +1,278 @@
2026-10-17T13:17:13Z thinkpad: 2.76 2.26 2.04 | macmini (bots): { 73.08 70.73 75.98 }
2026-10-17T13:17:43Z thinkpad: 2.90 2.33 2.07 | macmini (bots): { 75.66 71.62 76.12 }
2026-10-17T13:18:13Z thinkpad: 3.02 2.43 2.11 | macmini (bots): { 65.77 69.61 75.24 }
2026-10-17T13:18:44Z thinkpad: 2.15 2.27 2.07 | macmini (bots): { 59.85 67.75 74.37 }
2026-10-17T13:19:14Z thinkpad: 1.45 2.09 2.02 | macmini (bots): { 63.86 67.87 74.17 }
2026-10-17T13:19:44Z thinkpad: 1.28 1.98 1.98 | macmini (bots): { 63.51 67.54 73.84 }
2026-10-17T13:20:14Z thinkpad: 1.14 1.89 1.95 | macmini (bots): { 65.82 67.85 73.71 }
2026-10-17T13:20:45Z thinkpad: 0.90 1.77 1.91 | macmini (bots): { 63.62 67.08 73.22 }
2026-10-17T13:21:15Z thinkpad: 0.55 1.60 1.85 | macmini (bots): { 62.45 66.37 72.75 }
2026-10-17T13:21:45Z thinkpad: 0.79 1.54 1.82 | macmini (bots): { 67.04 67.04 72.76 }
2026-10-17T13:22:15Z thinkpad: 1.96 1.75 1.88 | macmini (bots): { 65.53 66.60 72.40 }
2026-10-17T13:22:46Z thinkpad: 1.82 1.75 1.88 | macmini (bots): { 61.52 65.55 71.82 }
2026-10-17T13:23:16Z thinkpad: 1.29 1.63 1.83 | macmini (bots): { 62.36 65.36 71.53 }
2026-10-17T13:23:46Z thinkpad: 0.86 1.49 1.78 | macmini (bots): { 68.76 66.51 71.73 }
2026-10-17T13:24:16Z thinkpad: 1.29 1.52 1.78 | macmini (bots): { 77.68 68.77 72.36 }
2026-10-17T13:24:46Z thinkpad: 1.82 1.62 1.80 | macmini (bots): { 74.03 68.88 72.28 }
2026-10-17T13:25:17Z thinkpad: 1.50 1.56 1.78 | macmini (bots): { 71.53 68.76 72.12 }
2026-10-17T13:25:47Z thinkpad: 0.96 1.42 1.72 | macmini (bots): { 74.46 69.85 72.40 }
2026-10-17T13:26:17Z thinkpad: 0.87 1.35 1.69 | macmini (bots): { 80.85 71.79 73.01 }
2026-10-17T13:26:47Z thinkpad: 3.95 2.02 1.90 | macmini (bots): { 74.77 71.31 72.81 }
2026-10-17T13:27:18Z thinkpad: 2.98 1.97 1.89 | macmini (bots): { 70.52 70.52 72.46 }
2026-10-17T13:27:48Z thinkpad: 3.78 2.26 1.99 | macmini (bots): { 67.62 69.91 72.18 }
2026-10-17T13:28:18Z thinkpad: 3.05 2.23 1.99 | macmini (bots): { 72.51 70.83 72.43 }
2026-10-17T13:28:48Z thinkpad: 2.05 2.07 1.94 | macmini (bots): { 79.07 72.64 73.03 }
2026-10-17T13:29:19Z thinkpad: 2.09 2.11 1.96 | macmini (bots): { 90.42 75.94 74.21 }
2026-10-17T13:29:49Z thinkpad: 1.61 1.99 1.93 | macmini (bots): { 102.68 80.15 75.78 }
2026-10-17T13:30:19Z thinkpad: 2.31 2.12 1.97 | macmini (bots): { 111.15 84.19 77.39 }
2026-10-17T13:30:49Z thinkpad: 4.14 2.61 2.14 | macmini (bots): { 115.67 87.82 78.94 }
2026-10-17T13:31:20Z thinkpad: 4.84 2.92 2.26 | macmini (bots): { 122.95 92.31 80.88 }
2026-10-17T13:31:50Z thinkpad: 6.84 3.60 2.51 | macmini (bots): { 136.25 98.41 83.48 }
2026-10-17T13:32:20Z thinkpad: 4.85 3.43 2.49 | macmini (bots): { 128.59 100.39 84.73 }
2026-10-17T13:32:50Z thinkpad: 3.48 3.25 2.46 | macmini (bots): { 111.05 98.73 84.66 }
2026-10-17T13:33:20Z thinkpad: 3.09 3.15 2.45 | macmini (bots): { 102.40 98.03 84.98 }
2026-10-17T13:33:51Z thinkpad: 2.47 2.99 2.42 | macmini (bots): { 94.57 96.54 84.89 }
2026-10-17T13:34:21Z thinkpad: 2.42 2.94 2.42 | macmini (bots): { 86.26 94.36 84.50 }
2026-10-17T13:34:51Z thinkpad: 2.95 3.04 2.47 | macmini (bots): { 76.90 91.56 83.84 }
2026-10-17T13:35:21Z thinkpad: 2.66 2.96 2.46 | macmini (bots): { 65.57 87.42 82.61 }
2026-10-17T13:35:52Z thinkpad: 2.47 2.88 2.45 | macmini (bots): { 64.13 85.05 81.92 }
2026-10-17T13:36:22Z thinkpad: 3.89 3.17 2.56 | macmini (bots): { 58.65 81.74 80.83 }
2026-10-17T13:36:52Z thinkpad: 6.43 3.83 2.80 | macmini (bots): { 66.32 81.32 80.71 }
2026-10-17T13:37:22Z thinkpad: 8.73 4.60 3.09 | macmini (bots): { 67.30 80.14 80.30 }
2026-10-17T13:37:53Z thinkpad: 9.03 5.10 3.30 | macmini (bots): { 63.66 78.24 79.62 }
2026-10-17T13:38:23Z thinkpad: 9.49 5.57 3.52 | macmini (bots): { 61.25 76.23 78.84 }
2026-10-17T13:38:53Z thinkpad: 7.55 5.50 3.56 | macmini (bots): { 62.30 74.88 78.25 }
2026-10-17T13:39:23Z thinkpad: 8.62 5.99 3.79 | macmini (bots): { 57.34 72.56 77.29 }
2026-10-17T13:39:53Z thinkpad: 6.65 5.81 3.80 | macmini (bots): { 51.24 69.62 76.06 }
2026-10-17T13:40:24Z thinkpad: 5.55 5.62 3.80 | macmini (bots): { 48.87 67.26 74.98 }
2026-10-17T13:40:54Z thinkpad: 6.11 5.71 3.89 | macmini (bots): { 51.16 66.08 74.29 }
2026-10-17T13:41:24Z thinkpad: 8.93 6.43 4.19 | macmini (bots): { 48.67 64.00 73.24 }
2026-10-17T13:41:54Z thinkpad: 9.05 6.74 4.36 | macmini (bots): { 46.47 61.96 72.18 }
2026-10-17T13:42:25Z thinkpad: 7.70 6.65 4.41 | macmini (bots): { 44.24 60.05 71.14 }
2026-10-17T13:42:55Z thinkpad: 5.64 6.24 4.34 | macmini (bots): { 49.30 59.83 70.68 }
2026-10-17T13:43:25Z thinkpad: 4.83 6.02 4.33 | macmini (bots): { 47.39 58.24 69.72 }
2026-10-17T13:43:55Z thinkpad: 3.57 5.61 4.25 | macmini (bots): { 47.32 57.23 68.96 }
2026-10-17T13:44:25Z thinkpad: 3.41 5.40 4.23 | macmini (bots): { 47.86 56.49 68.28 }
2026-10-17T13:44:55Z thinkpad: 3.37 5.21 4.20 | macmini (bots): { 50.10 56.19 67.77 }
2026-10-17T13:45:26Z thinkpad: 3.78 5.16 4.22 | macmini (bots): { 51.91 55.98 67.28 }
2026-10-17T13:45:56Z thinkpad: 4.34 5.12 4.23 | macmini (bots): { 49.16 54.85 66.42 }
2026-10-17T13:46:26Z thinkpad: 10.04 6.46 4.71 | macmini (bots): { 45.01 53.33 65.47 }
2026-10-17T13:46:56Z thinkpad: 18.10 8.73 5.52 | macmini (bots): { 46.41 52.87 64.88 }
2026-10-17T13:47:27Z thinkpad: 14.99 8.91 5.68 | macmini (bots): { 51.12 53.21 64.57 }
2026-10-17T13:47:57Z thinkpad: 13.46 9.27 5.92 | macmini (bots): { 53.84 53.81 64.40 }
2026-10-17T13:48:27Z thinkpad: 12.76 9.41 6.07 | macmini (bots): { 51.94 53.27 63.84 }
2026-10-17T13:48:58Z thinkpad: 10.20 9.11 6.08 | macmini (bots): { 50.70 52.83 63.31 }
2026-10-17T13:49:28Z thinkpad: 9.95 9.25 6.23 | macmini (bots): { 51.51 52.89 62.97 }
2026-10-17T13:49:58Z thinkpad: 11.98 9.87 6.53 | macmini (bots): { 50.98 52.73 62.57 }
2026-10-17T13:50:29Z thinkpad: 14.96 10.76 6.94 | macmini (bots): { 49.68 52.31 62.08 }
2026-10-17T13:50:59Z thinkpad: 10.54 10.09 6.83 | macmini (bots): { 53.34 53.01 62.00 }
2026-10-17T13:51:29Z thinkpad: 10.21 10.01 6.91 | macmini (bots): { 51.61 52.69 61.57 }
2026-10-17T13:52:00Z thinkpad: 11.86 10.42 7.14 | macmini (bots): { 48.94 51.89 60.97 }
2026-10-17T13:52:30Z thinkpad: 10.88 10.33 7.22 | macmini (bots): { 48.46 51.44 60.49 }
2026-10-17T13:53:01Z thinkpad: 10.94 10.41 7.35 | macmini (bots): { 54.27 52.41 60.47 }
2026-10-17T13:53:31Z thinkpad: 10.77 10.42 7.45 | macmini (bots): { 57.13 53.30 60.51 }
2026-10-17T13:54:02Z thinkpad: 10.62 10.40 7.53 | macmini (bots): { 53.78 52.84 60.09 }
2026-10-17T13:54:32Z thinkpad: 10.85 10.48 7.66 | macmini (bots): { 52.01 52.51 59.73 }
2026-10-17T13:55:02Z thinkpad: 10.36 10.39 7.73 | macmini (bots): { 53.44 52.83 59.59 }
2026-10-17T13:55:32Z thinkpad: 8.58 9.95 7.67 | macmini (bots): { 58.66 54.12 59.82 }
2026-10-17T13:56:03Z thinkpad: 7.29 9.52 7.60 | macmini (bots): { 60.98 55.05 59.96 }
2026-10-17T13:56:33Z thinkpad: 5.02 8.75 7.40 | macmini (bots): { 65.14 56.71 60.39 }
2026-10-17T13:57:03Z thinkpad: 5.03 8.38 7.32 | macmini (bots): { 57.51 55.73 59.91 }
2026-10-17T13:57:34Z thinkpad: 7.39 8.66 7.45 | macmini (bots): { 52.57 54.73 59.41 }
2026-10-17T13:58:04Z thinkpad: 10.56 9.45 7.76 | macmini (bots): { 53.11 54.60 59.20 }
2026-10-17T13:58:34Z thinkpad: 28.87 14.20 9.41 | macmini (bots): { 54.53 54.93 59.16 }
2026-10-17T13:59:05Z thinkpad: 39.33 17.79 10.73 | macmini (bots): { 62.67 56.86 59.71 }
2026-10-17T13:59:35Z thinkpad: 34.07 18.33 11.12 | macmini (bots): { 61.78 57.22 59.75 }
2026-10-17T14:00:06Z thinkpad: 34.42 19.72 11.80 | macmini (bots): { 53.10 55.55 59.03 }
2026-10-17T14:00:36Z thinkpad: 33.60 20.94 12.46 | macmini (bots): { 48.70 54.23 58.43 }
2026-10-17T14:01:07Z thinkpad: 29.29 20.99 12.74 | macmini (bots): { 50.25 54.00 58.20 }
2026-10-17T14:01:37Z thinkpad: 20.83 19.79 12.61 | macmini (bots): { 50.48 53.74 57.96 }
2026-10-17T14:02:08Z thinkpad: 21.74 20.44 13.11 | macmini (bots): { 48.65 53.04 57.56 }
2026-10-17T14:02:38Z thinkpad: 17.68 19.67 13.09 | macmini (bots): { 54.17 53.85 57.69 }
2026-10-17T14:03:08Z thinkpad: 13.97 18.55 12.93 | macmini (bots): { 53.39 53.84 57.56 }
2026-10-17T14:03:39Z thinkpad: 14.10 18.17 12.98 | macmini (bots): { 50.71 53.09 57.16 }
2026-10-17T14:04:09Z thinkpad: 13.60 17.65 12.97 | macmini (bots): { 52.30 53.28 57.08 }
2026-10-17T14:04:39Z thinkpad: 12.09 16.91 12.88 | macmini (bots): { 51.52 53.03 56.87 }
2026-10-17T14:05:10Z thinkpad: 10.15 15.98 12.69 | macmini (bots): { 54.53 53.57 56.91 }
2026-10-17T14:05:42Z thinkpad: 7.77 14.86 12.42 | macmini (bots): { 63.58 55.55 57.49 }
2026-10-17T14:06:12Z thinkpad: 6.58 13.89 12.18 | macmini (bots): { 75.29 59.41 58.84 }
2026-10-17T14:06:42Z thinkpad: 5.96 13.02 11.94 | macmini (bots): { 78.06 61.64 59.67 }
2026-10-17T14:07:12Z thinkpad: 4.86 12.08 11.66 | macmini (bots): { 72.26 61.85 59.81 }
2026-10-17T14:07:42Z thinkpad: 4.49 11.18 11.38 | macmini (bots): { 67.84 61.73 59.84 }
2026-10-17T14:08:13Z thinkpad: 4.37 10.51 11.15 | macmini (bots): { 64.25 61.45 59.80 }
2026-10-17T14:08:43Z thinkpad: 4.57 9.97 10.94 | macmini (bots): { 57.86 60.14 59.38 }
2026-10-17T14:09:13Z thinkpad: 4.60 9.46 10.74 | macmini (bots): { 60.21 60.39 59.49 }
2026-10-17T14:09:43Z thinkpad: 4.20 8.89 10.51 | macmini (bots): { 57.11 59.57 59.22 }
2026-10-17T14:10:14Z thinkpad: 3.80 8.36 10.28 | macmini (bots): { 66.05 61.48 59.92 }
2026-10-17T14:10:44Z thinkpad: 4.19 8.00 10.10 | macmini (bots): { 78.51 64.91 61.22 }
2026-10-17T14:11:14Z thinkpad: 4.27 7.65 9.92 | macmini (bots): { 74.15 65.33 61.51 }
2026-10-17T14:11:44Z thinkpad: 4.35 7.35 9.74 | macmini (bots): { 62.47 63.48 60.98 }
2026-10-17T14:12:14Z thinkpad: 4.98 7.22 9.62 | macmini (bots): { 56.73 61.86 60.46 }
2026-10-17T14:12:45Z thinkpad: 4.60 6.91 9.44 | macmini (bots): { 57.78 61.49 60.37 }
2026-10-17T14:13:15Z thinkpad: 4.15 6.57 9.25 | macmini (bots): { 56.21 60.71 60.12 }
2026-10-17T14:13:45Z thinkpad: 3.87 6.25 9.05 | macmini (bots): { 54.75 60.00 59.89 }
2026-10-17T14:14:15Z thinkpad: 3.86 6.02 8.89 | macmini (bots): { 52.21 58.88 59.49 }
2026-10-17T14:14:46Z thinkpad: 3.14 5.64 8.67 | macmini (bots): { 58.81 59.59 59.71 }
2026-10-17T14:15:16Z thinkpad: 3.76 5.54 8.54 | macmini (bots): { 64.13 60.86 60.16 }
2026-10-17T14:15:46Z thinkpad: 3.91 5.41 8.40 | macmini (bots): { 59.02 59.97 59.87 }
2026-10-17T14:16:16Z thinkpad: 4.33 5.35 8.28 | macmini (bots): { 64.48 60.97 60.22 }
2026-10-17T14:16:46Z thinkpad: 5.20 5.44 8.21 | macmini (bots): { 79.56 64.98 61.70 }
2026-10-17T14:17:17Z thinkpad: 4.88 5.36 8.10 | macmini (bots): { 94.21 69.99 63.63 }
2026-10-17T14:17:47Z thinkpad: 4.80 5.30 8.00 | macmini (bots): { 85.75 70.51 64.08 }
2026-10-17T14:18:17Z thinkpad: 4.82 5.25 7.89 | macmini (bots): { 82.33 71.25 64.57 }
2026-10-17T14:18:48Z thinkpad: 4.50 5.12 7.75 | macmini (bots): { 77.67 71.11 64.75 }
2026-10-17T14:19:18Z thinkpad: 3.76 4.88 7.58 | macmini (bots): { 77.29 71.67 65.17 }
2026-10-17T14:19:48Z thinkpad: 3.68 4.75 7.45 | macmini (bots): { 71.16 70.71 65.05 }
2026-10-17T14:20:18Z thinkpad: 4.25 4.82 7.39 | macmini (bots): { 66.97 69.63 64.85 }
2026-10-17T14:20:49Z thinkpad: 4.72 4.89 7.33 | macmini (bots): { 59.23 67.50 64.24 }
2026-10-17T14:21:19Z thinkpad: 5.12 4.98 7.28 | macmini (bots): { 52.38 65.10 63.48 }
2026-10-17T14:21:49Z thinkpad: 5.69 5.13 7.26 | macmini (bots): { 59.43 65.53 63.69 }
2026-10-17T14:22:19Z thinkpad: 5.78 5.19 7.21 | macmini (bots): { 62.28 65.65 63.80 }
2026-10-17T14:22:50Z thinkpad: 5.34 5.14 7.13 | macmini (bots): { 57.40 64.12 63.30 }
2026-10-17T14:23:20Z thinkpad: 4.89 5.04 7.03 | macmini (bots): { 58.39 63.61 63.14 }
2026-10-17T14:23:50Z thinkpad: 5.00 5.07 6.98 | macmini (bots): { 64.88 64.63 63.52 }
2026-10-17T14:24:20Z thinkpad: 3.88 4.80 6.83 | macmini (bots): { 80.70 68.30 64.88 }
2026-10-17T14:24:51Z thinkpad: 2.90 4.47 6.65 | macmini (bots): { 94.09 72.75 66.61 }
2026-10-17T14:25:21Z thinkpad: 2.63 4.25 6.51 | macmini (bots): { 93.13 74.91 67.63 }
2026-10-17T14:25:51Z thinkpad: 6.66 5.07 6.71 | macmini (bots): { 72.87 71.72 66.72 }
2026-10-17T14:26:21Z thinkpad: 7.15 5.34 6.74 | macmini (bots): { 67.03 70.37 66.40 }
2026-10-17T14:26:51Z thinkpad: 6.20 5.29 6.68 | macmini (bots): { 58.68 68.08 65.71 }
2026-10-17T14:27:22Z thinkpad: 6.11 5.35 6.66 | macmini (bots): { 55.26 66.28 65.14 }
2026-10-17T14:27:52Z thinkpad: 5.45 5.26 6.59 | macmini (bots): { 56.51 65.74 64.99 }
2026-10-17T14:28:22Z thinkpad: 6.11 5.45 6.61 | macmini (bots): { 52.24 63.77 64.30 }
2026-10-17T14:28:52Z thinkpad: 6.06 5.50 6.59 | macmini (bots): { 55.56 63.38 64.13 }
2026-10-17T14:29:23Z thinkpad: 6.11 5.58 6.58 | macmini (bots): { 59.66 63.63 64.20 }
2026-10-17T14:29:53Z thinkpad: 5.52 5.54 6.53 | macmini (bots): { 63.71 64.30 64.42 }
2026-10-17T14:30:23Z thinkpad: 4.79 5.35 6.43 | macmini (bots): { 69.84 65.61 64.89 }
2026-10-17T14:30:53Z thinkpad: 6.21 5.63 6.49 | macmini (bots): { 62.76 64.46 64.50 }
2026-10-17T14:31:23Z thinkpad: 8.36 6.21 6.66 | macmini (bots): { 53.51 62.09 63.64 }
2026-10-17T14:31:54Z thinkpad: 8.49 6.45 6.72 | macmini (bots): { 52.80 60.98 63.18 }
2026-10-17T14:32:24Z thinkpad: 8.28 6.59 6.77 | macmini (bots): { 48.98 59.20 62.45 }
2026-10-17T14:32:54Z thinkpad: 8.02 6.70 6.79 | macmini (bots): { 48.95 58.30 62.02 }
2026-10-17T14:33:25Z thinkpad: 7.02 6.59 6.76 | macmini (bots): { 51.06 57.70 61.66 }
2026-10-17T14:33:55Z thinkpad: 7.09 6.62 6.76 | macmini (bots): { 70.89 61.81 63.00 }
2026-10-17T14:34:25Z thinkpad: 6.18 6.44 6.69 | macmini (bots): { 69.38 62.46 63.21 }
2026-10-17T14:34:55Z thinkpad: 6.67 6.52 6.72 | macmini (bots): { 76.87 64.84 64.04 }
2026-10-17T14:35:25Z thinkpad: 4.76 6.08 6.56 | macmini (bots): { 86.19 68.04 65.21 }
2026-10-17T14:35:56Z thinkpad: 3.47 5.64 6.40 | macmini (bots): { 101.03 73.28 67.21 }
2026-10-17T14:36:26Z thinkpad: 2.68 5.24 6.24 | macmini (bots): { 106.05 77.09 68.79 }
2026-10-17T14:36:56Z thinkpad: 2.41 4.91 6.10 | macmini (bots): { 107.57 80.20 70.20 }
2026-10-17T14:37:26Z thinkpad: 3.74 4.99 6.08 | macmini (bots): { 101.16 81.38 70.98 }
2026-10-17T14:37:57Z thinkpad: 5.96 5.39 6.18 | macmini (bots): { 86.51 79.61 70.70 }
2026-10-17T14:38:27Z thinkpad: 7.91 5.98 6.35 | macmini (bots): { 81.13 79.06 70.81 }
2026-10-17T14:38:57Z thinkpad: 8.45 6.25 6.43 | macmini (bots): { 79.56 79.01 71.08 }
2026-10-17T14:39:27Z thinkpad: 8.75 6.52 6.52 | macmini (bots): { 71.92 77.22 70.71 }
2026-10-17T14:39:58Z thinkpad: 8.86 6.76 6.60 | macmini (bots): { 71.38 76.58 70.70 }
2026-10-17T14:40:28Z thinkpad: 8.74 6.98 6.68 | macmini (bots): { 71.25 76.12 70.74 }
2026-10-17T14:40:58Z thinkpad: 7.99 6.96 6.68 | macmini (bots): { 70.49 75.51 70.71 }
2026-10-17T14:41:28Z thinkpad: 7.64 7.01 6.70 | macmini (bots): { 68.93 74.58 70.56 }
2026-10-17T14:41:59Z thinkpad: 6.57 6.84 6.66 | macmini (bots): { 68.35 73.75 70.39 }
2026-10-17T14:42:29Z thinkpad: 5.83 6.62 6.59 | macmini (bots): { 63.57 72.20 69.95 }
2026-10-17T14:42:59Z thinkpad: 7.40 6.91 6.69 | macmini (bots): { 56.77 69.69 69.12 }
2026-10-17T14:43:29Z thinkpad: 9.07 7.37 6.85 | macmini (bots): { 49.53 66.71 68.06 }
2026-10-17T14:44:00Z thinkpad: 9.39 7.63 6.96 | macmini (bots): { 43.53 63.68 66.91 }
2026-10-17T14:44:30Z thinkpad: 9.16 7.73 7.01 | macmini (bots): { 41.12 61.23 65.92 }
2026-10-17T14:45:00Z thinkpad: 7.96 7.57 6.98 | macmini (bots): { 40.62 59.23 65.03 }
2026-10-17T14:45:30Z thinkpad: 6.34 7.24 6.89 | macmini (bots): { 38.17 56.80 63.94 }
2026-10-17T14:46:01Z thinkpad: 6.39 7.18 6.88 | macmini (bots): { 40.35 55.54 63.24 }
2026-10-17T14:46:31Z thinkpad: 5.63 6.95 6.82 | macmini (bots): { 48.66 56.25 63.24 }
2026-10-17T14:47:01Z thinkpad: 4.73 6.60 6.70 | macmini (bots): { 46.68 55.20 62.62 }
2026-10-17T14:47:31Z thinkpad: 9.18 7.55 7.02 | macmini (bots): { 41.45 53.19 61.64 }
2026-10-17T14:48:01Z thinkpad: 10.40 8.02 7.19 | macmini (bots): { 40.76 51.87 60.87 }
2026-10-17T14:48:32Z thinkpad: 8.24 7.73 7.12 | macmini (bots): { 44.46 51.67 60.48 }
2026-10-17T14:49:02Z thinkpad: 9.60 8.09 7.26 | macmini (bots): { 38.75 49.66 59.45 }
2026-10-17T14:49:32Z thinkpad: 8.90 8.10 7.29 | macmini (bots): { 34.93 47.72 58.41 }
2026-10-17T14:50:02Z thinkpad: 11.17 8.66 7.50 | macmini (bots): { 35.70 46.68 57.66 }
2026-10-17T14:50:33Z thinkpad: 12.06 9.19 7.72 | macmini (bots): { 43.94 47.43 57.54 }
2026-10-17T14:51:03Z thinkpad: 12.50 9.49 7.87 | macmini (bots): { 40.76 46.36 56.80 }
2026-10-17T14:51:33Z thinkpad: 10.35 9.24 7.83 | macmini (bots): { 44.81 46.69 56.55 }
2026-10-17T14:52:03Z thinkpad: 9.32 9.09 7.83 | macmini (bots): { 54.94 48.97 57.04 }
2026-10-17T14:52:34Z thinkpad: 7.07 8.58 7.70 | macmini (bots): { 54.59 49.54 56.92 }
2026-10-17T14:53:04Z thinkpad: 5.33 8.01 7.54 | macmini (bots): { 67.48 53.27 58.02 }
2026-10-17T14:53:34Z thinkpad: 7.22 8.20 7.62 | macmini (bots): { 57.82 52.33 57.52 }
2026-10-17T14:54:04Z thinkpad: 9.69 8.73 7.81 | macmini (bots): { 46.67 50.20 56.57 }
2026-10-17T14:54:35Z thinkpad: 9.14 8.66 7.82 | macmini (bots): { 49.88 50.61 56.49 }
2026-10-17T14:55:05Z thinkpad: 9.75 8.87 7.91 | macmini (bots): { 46.88 49.95 56.06 }
2026-10-17T14:55:35Z thinkpad: 8.51 8.67 7.88 | macmini (bots): { 42.90 48.80 55.43 }
2026-10-17T14:56:06Z thinkpad: 8.46 8.62 7.89 | macmini (bots): { 49.23 49.72 55.53 }
2026-10-17T14:56:36Z thinkpad: 6.19 8.07 7.73 | macmini (bots): { 54.41 50.86 55.74 }
2026-10-17T14:57:06Z thinkpad: 4.69 7.53 7.56 | macmini (bots): { 60.26 52.69 56.24 }
2026-10-17T14:57:36Z thinkpad: 3.21 6.90 7.35 | macmini (bots): { 67.66 55.21 57.03 }
2026-10-17T14:58:06Z thinkpad: 1.94 6.24 7.11 | macmini (bots): { 75.73 58.41 58.13 }
2026-10-17T14:58:37Z thinkpad: 1.18 5.64 6.89 | macmini (bots): { 74.69 59.90 58.68 }
2026-10-17T14:59:07Z thinkpad: 0.78 5.12 6.67 | macmini (bots): { 77.10 61.82 59.42 }
2026-10-17T14:59:37Z thinkpad: 0.88 4.72 6.49 | macmini (bots): { 87.05 65.59 60.86 }
2026-10-17T15:00:07Z thinkpad: 0.53 4.27 6.28 | macmini (bots): { 78.18 65.51 60.99 }
2026-10-17T15:00:38Z thinkpad: 0.38 3.88 6.09 | macmini (bots): { 79.38 67.00 61.69 }
2026-10-17T15:01:08Z thinkpad: 1.50 3.80 5.99 | macmini (bots): { 78.36 67.86 62.18 }
2026-10-17T15:01:38Z thinkpad: 2.97 3.93 5.95 | macmini (bots): { 74.18 67.90 62.39 }
2026-10-17T15:02:08Z thinkpad: 2.61 3.77 5.83 | macmini (bots): { 68.63 67.00 62.25 }
2026-10-17T15:02:39Z thinkpad: 2.10 3.55 5.69 | macmini (bots): { 69.84 67.50 62.60 }
2026-10-17T15:03:09Z thinkpad: 1.44 3.25 5.53 | macmini (bots): { 67.10 67.06 62.61 }
2026-10-17T15:03:39Z thinkpad: 1.39 3.08 5.40 | macmini (bots): { 60.83 65.50 62.19 }
2026-10-17T15:04:09Z thinkpad: 1.28 2.88 5.26 | macmini (bots): { 75.40 68.61 63.46 }
2026-10-17T15:04:40Z thinkpad: 2.30 2.99 5.22 | macmini (bots): { 106.08 76.54 66.50 }
2026-10-17T15:05:10Z thinkpad: 2.65 3.02 5.15 | macmini (bots): { 111.96 81.09 68.52 }
2026-10-17T15:05:40Z thinkpad: 2.41 2.92 5.05 | macmini (bots): { 104.19 82.08 69.31 }
2026-10-17T15:06:10Z thinkpad: 2.71 2.95 4.99 | macmini (bots): { 116.20 87.04 71.54 }
2026-10-17T15:06:40Z thinkpad: 2.51 2.87 4.90 | macmini (bots): { 136.68 94.24 74.66 }
2026-10-17T15:07:11Z thinkpad: 3.68 3.11 4.91 | macmini (bots): { 132.76 97.43 76.50 }
2026-10-17T15:07:41Z thinkpad: 3.77 3.19 4.88 | macmini (bots): { 132.73 100.97 78.51 }
2026-10-17T15:08:11Z thinkpad: 3.00 3.06 4.79 | macmini (bots): { 116.92 100.33 79.07 }
2026-10-17T15:08:41Z thinkpad: 2.40 2.91 4.68 | macmini (bots): { 109.83 100.07 79.70 }
2026-10-17T15:09:12Z thinkpad: 2.18 2.81 4.59 | macmini (bots): { 113.21 101.71 81.00 }
2026-10-17T15:09:42Z thinkpad: 1.67 2.62 4.47 | macmini (bots): { 131.87 107.11 83.66 }
2026-10-17T15:10:12Z thinkpad: 1.90 2.58 4.39 | macmini (bots): { 133.45 109.92 85.49 }
2026-10-17T15:10:42Z thinkpad: 4.00 3.02 4.48 | macmini (bots): { 130.14 111.67 86.99 }
2026-10-17T15:11:13Z thinkpad: 4.77 3.32 4.54 | macmini (bots): { 118.78 110.28 87.32 }
2026-10-17T15:11:43Z thinkpad: 3.86 3.24 4.47 | macmini (bots): { 124.01 112.36 88.87 }
2026-10-17T15:12:13Z thinkpad: 3.61 3.26 4.44 | macmini (bots): { 122.21 112.94 89.88 }
2026-10-17T15:12:43Z thinkpad: 3.12 3.17 4.36 | macmini (bots): { 111.62 111.10 90.00 }
2026-10-17T15:13:14Z thinkpad: 3.07 3.14 4.31 | macmini (bots): { 122.97 113.95 91.77 }
2026-10-17T15:13:44Z thinkpad: 3.33 3.20 4.30 | macmini (bots): { 102.64 109.89 91.07 }
2026-10-17T15:14:14Z thinkpad: 2.85 3.10 4.23 | macmini (bots): { 94.46 107.15 90.72 }
2026-10-17T15:14:44Z thinkpad: 2.66 3.03 4.17 | macmini (bots): { 94.94 106.07 90.90 }
2026-10-17T15:15:14Z thinkpad: 2.54 2.96 4.11 | macmini (bots): { 94.52 104.93 91.01 }
2026-10-17T15:15:45Z thinkpad: 3.20 3.08 4.11 | macmini (bots): { 84.00 101.47 90.25 }
2026-10-17T15:16:15Z thinkpad: 5.56 3.61 4.25 | macmini (bots): { 83.16 99.76 90.02 }
2026-10-17T15:16:45Z thinkpad: 5.66 3.84 4.30 | macmini (bots): { 69.71 94.66 88.56 }
2026-10-17T15:17:15Z thinkpad: 5.04 3.90 4.31 | macmini (bots): { 59.31 89.78 87.00 }
2026-10-17T15:17:46Z thinkpad: 4.29 3.87 4.29 | macmini (bots): { 58.57 86.47 85.89 }
2026-10-17T15:18:16Z thinkpad: 4.16 3.91 4.29 | macmini (bots): { 56.43 83.29 84.75 }
2026-10-17T15:18:46Z thinkpad: 2.80 3.60 4.18 | macmini (bots): { 50.60 79.22 83.22 }
2026-10-17T15:19:16Z thinkpad: 1.85 3.28 4.05 | macmini (bots): { 54.97 77.50 82.46 }
2026-10-17T15:19:47Z thinkpad: 1.82 3.14 3.98 | macmini (bots): { 56.82 75.80 81.67 }
2026-10-17T15:20:17Z thinkpad: 2.19 3.10 3.94 | macmini (bots): { 59.30 74.46 80.97 }
2026-10-17T15:20:47Z thinkpad: 4.44 3.54 4.06 | macmini (bots): { 55.22 72.11 79.90 }
2026-10-17T15:21:17Z thinkpad: 5.99 4.03 4.21 | macmini (bots): { 49.41 69.01 78.50 }
2026-10-17T15:21:48Z thinkpad: 4.85 3.95 4.18 | macmini (bots): { 46.89 66.58 77.29 }
2026-10-17T15:22:18Z thinkpad: 4.31 3.92 4.16 | macmini (bots): { 45.13 64.31 76.10 }
2026-10-17T15:22:48Z thinkpad: 4.28 3.95 4.16 | macmini (bots): { 51.82 63.98 75.56 }
2026-10-17T15:23:18Z thinkpad: 4.75 4.11 4.21 | macmini (bots): { 50.35 62.51 74.63 }
2026-10-17T15:23:48Z thinkpad: 4.08 4.02 4.17 | macmini (bots): { 44.62 60.01 73.31 }
2026-10-17T15:24:19Z thinkpad: 3.93 4.00 4.16 | macmini (bots): { 46.02 58.72 72.37 }
2026-10-17T15:24:49Z thinkpad: 3.67 3.92 4.13 | macmini (bots): { 47.23 57.90 71.60 }
2026-10-17T15:25:19Z thinkpad: 2.79 3.68 4.04 | macmini (bots): { 50.80 57.78 71.09 }
2026-10-17T15:25:49Z thinkpad: 2.79 3.58 3.99 | macmini (bots): { 47.66 56.35 70.11 }
2026-10-17T15:26:19Z thinkpad: 5.04 3.97 4.11 | macmini (bots): { 45.15 54.84 69.08 }
2026-10-17T15:26:50Z thinkpad: 8.46 4.86 4.40 | macmini (bots): { 43.59 53.51 68.10 }
2026-10-17T15:27:20Z thinkpad: 6.84 4.81 4.39 | macmini (bots): { 42.38 52.39 67.19 }
2026-10-17T15:27:50Z thinkpad: 8.24 5.33 4.58 | macmini (bots): { 49.87 53.02 66.82 }
2026-10-17T15:28:21Z thinkpad: 7.11 5.34 4.61 | macmini (bots): { 61.30 55.60 67.28 }
2026-10-17T15:28:51Z thinkpad: 6.11 5.27 4.61 | macmini (bots): { 67.60 57.74 67.66 }
2026-10-17T15:29:21Z thinkpad: 5.33 5.17 4.59 | macmini (bots): { 63.72 57.79 67.33 }
2026-10-17T15:29:51Z thinkpad: 3.57 4.75 4.47 | macmini (bots): { 60.80 57.62 66.94 }
2026-10-17T15:30:22Z thinkpad: 4.74 4.88 4.52 | macmini (bots): { 59.69 57.64 66.62 }
2026-10-17T15:30:52Z thinkpad: 3.44 4.54 4.42 | macmini (bots): { 66.89 59.49 66.97 }
2026-10-17T15:31:22Z thinkpad: 4.57 4.69 4.47 | macmini (bots): { 83.43 64.16 68.40 }
2026-10-17T15:31:52Z thinkpad: 7.17 5.30 4.68 | macmini (bots): { 77.62 64.77 68.49 }
2026-10-17T15:32:23Z thinkpad: 8.55 5.82 4.88 | macmini (bots): { 74.20 65.09 68.47 }
2026-10-17T15:32:53Z thinkpad: 7.49 5.85 4.92 | macmini (bots): { 82.07 67.70 69.29 }
2026-10-17T15:33:23Z thinkpad: 5.65 5.59 4.86 | macmini (bots): { 79.44 68.47 69.51 }
2026-10-17T15:33:54Z thinkpad: 5.73 5.64 4.91 | macmini (bots): { 78.94 69.26 69.76 }
2026-10-17T15:34:24Z thinkpad: 6.84 5.92 5.02 | macmini (bots): { 76.33 69.71 69.91 }
2026-10-17T15:34:54Z thinkpad: 5.11 5.59 4.94 | macmini (bots): { 86.09 72.63 70.95 }
2026-10-17T15:35:24Z thinkpad: 6.68 5.93 5.08 | macmini (bots): { 72.77 70.70 70.31 }
2026-10-17T15:35:55Z thinkpad: 5.75 5.75 5.04 | macmini (bots): { 71.50 70.49 70.24 }
2026-10-17T15:36:25Z thinkpad: 4.31 5.41 4.95 | macmini (bots): { 80.75 72.67 71.03 }
2026-10-17T15:36:55Z thinkpad: 3.15 5.02 4.83 | macmini (bots): { 89.83 75.59 72.14 }
@@ -0,0 +1,695 @@
{
"kind": "horizon-server-benchmark",
"date": "2026-10-17T14:56:24+00:00",
"machine": {
"system": "Linux",
"machine": "x86_64",
"cpus": 4
},
"commit": "a9ee56a8b143999c9bc7056850ffcd622a9214aa",
"java": [
"openjdk 25.0.4.1 2026-08-18"
],
"settings": {
"servers": "horizon,vanilla",
"bots": "1,10,25,50,100",
"behaviour": "walk,explore",
"join_every": 1.0,
"warmup": 60,
"seconds": 180,
"seed": 12345,
"explore_speed": 0,
"gamerule": "",
"heap": "4g",
"out": "/home/ludovic/code/horizon/.claude/worktrees/server-host-4/.local/bench-out/walk-explore.json",
"nice": 19,
"bots_via": "ssh macmini",
"bots_bin": "/Users/ludovicmarie/code/horizon/.claude/worktrees/server-host-4/target/release/horizon-bots",
"connect": "<server address>",
"properties": {
"level-seed": "12345",
"view-distance": "10",
"simulation-distance": "10",
"online-mode": "false",
"white-list": "false",
"gamemode": "creative",
"force-gamemode": "true",
"spawn-protection": "0",
"max-players": "200",
"difficulty": "easy",
"sync-chunk-writes": "true"
}
},
"runs": [
{
"server": "horizon",
"bots": 1,
"behaviour": "walk",
"start_seconds": 9.51,
"mspt": {
"average": 5.77,
"p50": 5.15,
"p95": 11.04,
"p99": 28.99
},
"mspt_worst_p99": 45.9,
"tps": 20.0,
"rss_mb": {
"mean": 777.78,
"max": 779.8
},
"cpu_percent": 59.02,
"chunks_per_second": 9.94,
"unique_per_second": 0.45,
"forgotten_per_second": 9.99,
"waited_ticks_per_second": 0.0,
"chunks_total": 2763,
"chunks_per_bot": {
"walk": 2763.0
},
"unique_chunks_per_bot": {
"walk": 697.0
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 1,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "vanilla",
"bots": 1,
"behaviour": "walk",
"start_seconds": 15.35,
"mspt": {
"average": 18.84,
"p50": 18.87,
"p95": 29.55,
"p99": 51.32
},
"mspt_worst_p99": 456.4,
"tps": 20.0,
"rss_mb": {
"mean": 3442.06,
"max": 3462.1
},
"cpu_percent": 70.63,
"chunks_per_second": 10.47,
"unique_per_second": 0.47,
"forgotten_per_second": 10.53,
"waited_ticks_per_second": 0.0,
"chunks_total": 2763,
"chunks_per_bot": {
"walk": 2763.0
},
"unique_chunks_per_bot": {
"walk": 697.0
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 1,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "horizon",
"bots": 10,
"behaviour": "walk",
"start_seconds": 9.31,
"mspt": {
"average": 16.23,
"p50": 15.6,
"p95": 29.32,
"p99": 44.43
},
"mspt_worst_p99": 100.6,
"tps": 20.0,
"rss_mb": {
"mean": 781.84,
"max": 782.6
},
"cpu_percent": 136.59,
"chunks_per_second": 93.82,
"unique_per_second": 3.33,
"forgotten_per_second": 93.61,
"waited_ticks_per_second": 0.0,
"chunks_total": 26481,
"chunks_per_bot": {
"walk": 2648.1
},
"unique_chunks_per_bot": {
"walk": 679.5
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 10,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "vanilla",
"bots": 10,
"behaviour": "walk",
"start_seconds": 14.65,
"mspt": {
"average": 18.02,
"p50": 15.22,
"p95": 37.62,
"p99": 54.5
},
"mspt_worst_p99": 73.8,
"tps": 20.0,
"rss_mb": {
"mean": 3408.62,
"max": 3423.6
},
"cpu_percent": 100.57,
"chunks_per_second": 101.12,
"unique_per_second": 4.25,
"forgotten_per_second": 101.17,
"waited_ticks_per_second": 0.0,
"chunks_total": 26763,
"chunks_per_bot": {
"walk": 2676.3
},
"unique_chunks_per_bot": {
"walk": 687.8
},
"resent": 0,
"pulled_back": 7,
"moved_too_quickly": 7,
"moved_wrongly": 0,
"bots_joined": 10,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "horizon",
"bots": 25,
"behaviour": "walk",
"start_seconds": 9.31,
"mspt": {
"average": 23.98,
"p50": 20.89,
"p95": 49.18,
"p99": 65.87
},
"mspt_worst_p99": 109.8,
"tps": 20.0,
"rss_mb": {
"mean": 786.18,
"max": 786.5
},
"cpu_percent": 159.81,
"chunks_per_second": 235.75,
"unique_per_second": 7.84,
"forgotten_per_second": 235.72,
"waited_ticks_per_second": 0.0,
"chunks_total": 67251,
"chunks_per_bot": {
"walk": 2690.04
},
"unique_chunks_per_bot": {
"walk": 683.92
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 25,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "vanilla",
"bots": 25,
"behaviour": "walk",
"start_seconds": 14.95,
"mspt": {
"average": 19.19,
"p50": 17.34,
"p95": 33.84,
"p99": 56.21
},
"mspt_worst_p99": 79.2,
"tps": 20.0,
"rss_mb": {
"mean": 3531.46,
"max": 3582.1
},
"cpu_percent": 153.78,
"chunks_per_second": 252.35,
"unique_per_second": 11.73,
"forgotten_per_second": 251.58,
"waited_ticks_per_second": 0.0,
"chunks_total": 66759,
"chunks_per_bot": {
"walk": 2670.36
},
"unique_chunks_per_bot": {
"walk": 689.76
},
"resent": 0,
"pulled_back": 12,
"moved_too_quickly": 12,
"moved_wrongly": 0,
"bots_joined": 25,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "horizon",
"bots": 50,
"behaviour": "walk",
"start_seconds": 9.21,
"mspt": {
"average": 35.07,
"p50": 30.55,
"p95": 73.24,
"p99": 99.38
},
"mspt_worst_p99": 117.2,
"tps": 20.0,
"rss_mb": {
"mean": 816.58,
"max": 818.5
},
"cpu_percent": 187.42,
"chunks_per_second": 469.11,
"unique_per_second": 15.92,
"forgotten_per_second": 468.62,
"waited_ticks_per_second": 0.0,
"chunks_total": 136962,
"chunks_per_bot": {
"walk": 2739.24
},
"unique_chunks_per_bot": {
"walk": 685.68
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 50,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "vanilla",
"bots": 50,
"behaviour": "walk",
"start_seconds": 15.95,
"mspt": {
"average": 35.23,
"p50": 31.39,
"p95": 60.87,
"p99": 104.2
},
"mspt_worst_p99": 443.7,
"tps": 20.0,
"rss_mb": {
"mean": 3562.57,
"max": 3598.9
},
"cpu_percent": 263.27,
"chunks_per_second": 497.51,
"unique_per_second": 30.67,
"forgotten_per_second": 495.7,
"waited_ticks_per_second": 0.0,
"chunks_total": 131116,
"chunks_per_bot": {
"walk": 2622.32
},
"unique_chunks_per_bot": {
"walk": 687.78
},
"resent": 0,
"pulled_back": 96,
"moved_too_quickly": 96,
"moved_wrongly": 0,
"bots_joined": 50,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "horizon",
"bots": 100,
"behaviour": "walk",
"start_seconds": 9.21,
"mspt": {
"average": 104.8,
"p50": 100.6,
"p95": 158.8,
"p99": 198.61
},
"mspt_worst_p99": 259.8,
"tps": 9.54,
"rss_mb": {
"mean": 850.05,
"max": 851.2
},
"cpu_percent": 198.95,
"chunks_per_second": 371.72,
"unique_per_second": 26.27,
"forgotten_per_second": 365.57,
"waited_ticks_per_second": 0.0,
"chunks_total": 144465,
"chunks_per_bot": {
"walk": 1444.65
},
"unique_chunks_per_bot": {
"walk": 571.76
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 100,
"bots_failed": [],
"tick_queries": 34
},
{
"server": "vanilla",
"bots": 100,
"behaviour": "walk",
"start_seconds": 15.25,
"error": "[Errno 32] Broken pipe"
},
{
"server": "horizon",
"bots": 1,
"behaviour": "explore",
"start_seconds": 9.41,
"mspt": {
"average": 11.42,
"p50": 10.15,
"p95": 24.66,
"p99": 37.27
},
"mspt_worst_p99": 60.3,
"tps": 20.0,
"rss_mb": {
"mean": 838.61,
"max": 888.0
},
"cpu_percent": 326.85,
"chunks_per_second": 41.83,
"unique_per_second": 36.39,
"forgotten_per_second": 41.75,
"waited_ticks_per_second": 0.0,
"chunks_total": 9845,
"chunks_per_bot": {
"explore": 9845.0
},
"unique_chunks_per_bot": {
"explore": 8718.0
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 1,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "vanilla",
"bots": 1,
"behaviour": "explore",
"start_seconds": 15.44,
"mspt": {
"average": 9.83,
"p50": 7.72,
"p95": 20.36,
"p99": 84.65
},
"mspt_worst_p99": 385.9,
"tps": 20.0,
"rss_mb": {
"mean": 3598.02,
"max": 3658.9
},
"cpu_percent": 244.86,
"chunks_per_second": 35.28,
"unique_per_second": 33.34,
"forgotten_per_second": 35.13,
"waited_ticks_per_second": 0.0,
"chunks_total": 7881,
"chunks_per_bot": {
"explore": 7881.0
},
"unique_chunks_per_bot": {
"explore": 7540.0
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 1,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "horizon",
"bots": 10,
"behaviour": "explore",
"start_seconds": 9.52,
"mspt": {
"average": 23.54,
"p50": 21.18,
"p95": 43.4,
"p99": 60.85
},
"mspt_worst_p99": 109.1,
"tps": 20.0,
"rss_mb": {
"mean": 1139.51,
"max": 1301.0
},
"cpu_percent": 331.74,
"chunks_per_second": 43.86,
"unique_per_second": 43.09,
"forgotten_per_second": 43.82,
"waited_ticks_per_second": 172.35,
"chunks_total": 14140,
"chunks_per_bot": {
"explore": 1414.0
},
"unique_chunks_per_bot": {
"explore": 1395.9
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 10,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "vanilla",
"bots": 10,
"behaviour": "explore",
"start_seconds": 15.78,
"mspt": {
"average": 16.42,
"p50": 14.01,
"p95": 30.29,
"p99": 96.17
},
"mspt_worst_p99": 1144.3,
"tps": 20.0,
"rss_mb": {
"mean": 3964.49,
"max": 4248.3
},
"cpu_percent": 256.86,
"chunks_per_second": 25.43,
"unique_per_second": 25.33,
"forgotten_per_second": 25.96,
"waited_ticks_per_second": 76.05,
"chunks_total": 7991,
"chunks_per_bot": {
"explore": 799.1
},
"unique_chunks_per_bot": {
"explore": 797.7
},
"resent": 0,
"pulled_back": 11028,
"moved_too_quickly": 17,
"moved_wrongly": 0,
"bots_joined": 10,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "horizon",
"bots": 25,
"behaviour": "explore",
"start_seconds": 9.31,
"mspt": {
"average": 36.87,
"p50": 33.92,
"p95": 63.19,
"p99": 84.17
},
"mspt_worst_p99": 124.2,
"tps": 20.0,
"rss_mb": {
"mean": 1236.2,
"max": 1438.4
},
"cpu_percent": 345.03,
"chunks_per_second": 91.07,
"unique_per_second": 88.7,
"forgotten_per_second": 92.51,
"waited_ticks_per_second": 443.88,
"chunks_total": 30035,
"chunks_per_bot": {
"explore": 1201.4
},
"unique_chunks_per_bot": {
"explore": 1176.84
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 25,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "vanilla",
"bots": 25,
"behaviour": "explore",
"start_seconds": 14.55,
"mspt": {
"average": 24.31,
"p50": 21.76,
"p95": 36.55,
"p99": 155.88
},
"mspt_worst_p99": 1535.8,
"tps": 20.0,
"rss_mb": {
"mean": 4047.76,
"max": 4459.2
},
"cpu_percent": 305.24,
"chunks_per_second": 37.94,
"unique_per_second": 37.82,
"forgotten_per_second": 38.87,
"waited_ticks_per_second": 261.79,
"chunks_total": 16604,
"chunks_per_bot": {
"explore": 664.16
},
"unique_chunks_per_bot": {
"explore": 660.92
},
"resent": 0,
"pulled_back": 33246,
"moved_too_quickly": 20,
"moved_wrongly": 0,
"bots_joined": 25,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "horizon",
"bots": 50,
"behaviour": "explore",
"start_seconds": 9.21,
"mspt": {
"average": 60.06,
"p50": 57.31,
"p95": 88.38,
"p99": 112.89
},
"mspt_worst_p99": 172.4,
"tps": 16.65,
"rss_mb": {
"mean": 1192.47,
"max": 1372.5
},
"cpu_percent": 362.42,
"chunks_per_second": 157.28,
"unique_per_second": 154.2,
"forgotten_per_second": 156.07,
"waited_ticks_per_second": 890.09,
"chunks_total": 54551,
"chunks_per_bot": {
"explore": 1091.02
},
"unique_chunks_per_bot": {
"explore": 1078.5
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 50,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "vanilla",
"bots": 50,
"behaviour": "explore",
"start_seconds": 15.64,
"error": "[Errno 32] Broken pipe"
},
{
"server": "horizon",
"bots": 100,
"behaviour": "explore",
"start_seconds": 9.21,
"mspt": {
"average": 124.25,
"p50": 120.1,
"p95": 168.33,
"p99": 204.64
},
"mspt_worst_p99": 262.2,
"tps": 8.05,
"rss_mb": {
"mean": 1283.26,
"max": 1418.6
},
"cpu_percent": 334.14,
"chunks_per_second": 185.47,
"unique_per_second": 184.54,
"forgotten_per_second": 179.79,
"waited_ticks_per_second": 1864.45,
"chunks_total": 78556,
"chunks_per_bot": {
"explore": 785.56
},
"unique_chunks_per_bot": {
"explore": 784.88
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 100,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "vanilla",
"bots": 100,
"behaviour": "explore",
"start_seconds": 15.44,
"error": "[Errno 32] Broken pipe"
}
]
}
@@ -0,0 +1,341 @@
{
"kind": "horizon-server-benchmark",
"date": "2026-10-17T18:12:19+00:00",
"machine": {
"system": "Linux",
"machine": "x86_64",
"cpus": 4
},
"commit": "7bbbf6e97fb53683eae0a01ce2c134e53cb649a7",
"java": [
"openjdk 25.0.4.1 2026-08-18"
],
"settings": {
"servers": "horizon,vanilla",
"bots": "1,10,25,50",
"behaviour": "explore",
"join_every": 1.0,
"warmup": 60,
"seconds": 180,
"seed": 12345,
"explore_speed": 0.8,
"gamerule": "",
"heap": "4g",
"out": "/home/ludovic/code/horizon/.claude/worktrees/server-host-4/.local/bench-out/explore-equal-rate.json",
"nice": 19,
"bots_via": "ssh macmini",
"bots_bin": "/Users/ludovicmarie/code/horizon/.claude/worktrees/server-host-4/target/release/horizon-bots",
"connect": "<server address>",
"properties": {
"level-seed": "12345",
"view-distance": "10",
"simulation-distance": "10",
"online-mode": "false",
"white-list": "false",
"gamemode": "creative",
"force-gamemode": "true",
"spawn-protection": "0",
"max-players": "200",
"difficulty": "easy",
"sync-chunk-writes": "true"
}
},
"runs": [
{
"server": "horizon",
"bots": 1,
"behaviour": "explore",
"start_seconds": 9.42,
"mspt": {
"average": 3.16,
"p50": 2.98,
"p95": 5.78,
"p99": 12.17
},
"mspt_worst_p99": 38.1,
"tps": 20.0,
"rss_mb": {
"mean": 759.26,
"max": 769.5
},
"cpu_percent": 21.74,
"chunks_per_second": 1.64,
"unique_per_second": 1.27,
"forgotten_per_second": 1.54,
"waited_ticks_per_second": 0.0,
"chunks_total": 841,
"chunks_per_bot": {
"explore": 841.0
},
"unique_chunks_per_bot": {
"explore": 781.0
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 1,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "vanilla",
"bots": 1,
"behaviour": "explore",
"start_seconds": 17.17,
"mspt": {
"average": 16.82,
"p50": 16.67,
"p95": 27.3,
"p99": 52.16
},
"mspt_worst_p99": 453.4,
"tps": 20.0,
"rss_mb": {
"mean": 3373.16,
"max": 3405.9
},
"cpu_percent": 69.22,
"chunks_per_second": 1.66,
"unique_per_second": 1.35,
"forgotten_per_second": 1.54,
"waited_ticks_per_second": 0.0,
"chunks_total": 838,
"chunks_per_bot": {
"explore": 838.0
},
"unique_chunks_per_bot": {
"explore": 779.0
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 1,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "horizon",
"bots": 10,
"behaviour": "explore",
"start_seconds": 9.82,
"mspt": {
"average": 9.83,
"p50": 8.65,
"p95": 19.01,
"p99": 32.9
},
"mspt_worst_p99": 64.0,
"tps": 20.0,
"rss_mb": {
"mean": 861.49,
"max": 922.2
},
"cpu_percent": 77.43,
"chunks_per_second": 13.71,
"unique_per_second": 12.31,
"forgotten_per_second": 13.59,
"waited_ticks_per_second": 0.0,
"chunks_total": 7831,
"chunks_per_bot": {
"explore": 783.1
},
"unique_chunks_per_bot": {
"explore": 757.1
},
"resent": 0,
"pulled_back": 1051,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 10,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "vanilla",
"bots": 10,
"behaviour": "explore",
"start_seconds": 15.06,
"mspt": {
"average": 24.38,
"p50": 21.55,
"p95": 39.58,
"p99": 66.64
},
"mspt_worst_p99": 107.1,
"tps": 20.0,
"rss_mb": {
"mean": 3569.41,
"max": 3685.1
},
"cpu_percent": 132.09,
"chunks_per_second": 13.54,
"unique_per_second": 12.32,
"forgotten_per_second": 13.62,
"waited_ticks_per_second": 0.0,
"chunks_total": 7753,
"chunks_per_bot": {
"explore": 775.3
},
"unique_chunks_per_bot": {
"explore": 749.2
},
"resent": 0,
"pulled_back": 1027,
"moved_too_quickly": 6,
"moved_wrongly": 0,
"bots_joined": 10,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "horizon",
"bots": 25,
"behaviour": "explore",
"start_seconds": 9.31,
"mspt": {
"average": 16.87,
"p50": 15.4,
"p95": 30.88,
"p99": 46.73
},
"mspt_worst_p99": 84.9,
"tps": 20.0,
"rss_mb": {
"mean": 887.11,
"max": 945.5
},
"cpu_percent": 111.2,
"chunks_per_second": 33.51,
"unique_per_second": 29.92,
"forgotten_per_second": 33.33,
"waited_ticks_per_second": 0.0,
"chunks_total": 19718,
"chunks_per_bot": {
"explore": 788.72
},
"unique_chunks_per_bot": {
"explore": 759.08
},
"resent": 0,
"pulled_back": 3770,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 25,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "vanilla",
"bots": 25,
"behaviour": "explore",
"start_seconds": 14.94,
"mspt": {
"average": 30.29,
"p50": 28.25,
"p95": 43.47,
"p99": 68.56
},
"mspt_worst_p99": 165.2,
"tps": 20.0,
"rss_mb": {
"mean": 3608.97,
"max": 3732.0
},
"cpu_percent": 176.28,
"chunks_per_second": 32.98,
"unique_per_second": 29.29,
"forgotten_per_second": 32.84,
"waited_ticks_per_second": 0.0,
"chunks_total": 19442,
"chunks_per_bot": {
"explore": 777.68
},
"unique_chunks_per_bot": {
"explore": 748.24
},
"resent": 0,
"pulled_back": 5139,
"moved_too_quickly": 11,
"moved_wrongly": 0,
"bots_joined": 25,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "horizon",
"bots": 50,
"behaviour": "explore",
"start_seconds": 9.41,
"mspt": {
"average": 23.57,
"p50": 20.43,
"p95": 47.53,
"p99": 67.23
},
"mspt_worst_p99": 106.2,
"tps": 20.0,
"rss_mb": {
"mean": 924.41,
"max": 984.3
},
"cpu_percent": 127.14,
"chunks_per_second": 66.56,
"unique_per_second": 59.42,
"forgotten_per_second": 66.43,
"waited_ticks_per_second": 0.0,
"chunks_total": 39950,
"chunks_per_bot": {
"explore": 799.0
},
"unique_chunks_per_bot": {
"explore": 767.56
},
"resent": 0,
"pulled_back": 7434,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 50,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "vanilla",
"bots": 50,
"behaviour": "explore",
"start_seconds": 15.04,
"mspt": {
"average": 42.44,
"p50": 40.71,
"p95": 54.84,
"p99": 94.23
},
"mspt_worst_p99": 212.3,
"tps": 20.0,
"rss_mb": {
"mean": 3667.85,
"max": 3780.8
},
"cpu_percent": 200.85,
"chunks_per_second": 65.17,
"unique_per_second": 58.11,
"forgotten_per_second": 65.22,
"waited_ticks_per_second": 0.0,
"chunks_total": 39772,
"chunks_per_bot": {
"explore": 795.44
},
"unique_chunks_per_bot": {
"explore": 764.02
},
"resent": 0,
"pulled_back": 10604,
"moved_too_quickly": 38,
"moved_wrongly": 0,
"bots_joined": 50,
"bots_failed": [],
"tick_queries": 35
}
]
}
+279
View File
@@ -0,0 +1,279 @@
2026-10-17T15:51:54Z thinkpad: 1.30 2.47 3.85 | macmini (bots): { 67.44 76.34 77.44 }
2026-10-17T15:52:25Z thinkpad: 2.07 2.54 3.83 | macmini (bots): { 64.78 74.76 76.82 }
2026-10-17T15:52:55Z thinkpad: 2.08 2.51 3.78 | macmini (bots): { 62.50 73.29 76.22 }
2026-10-17T15:53:25Z thinkpad: 1.46 2.31 3.67 | macmini (bots): { 63.70 72.67 75.90 }
2026-10-17T15:53:55Z thinkpad: 1.12 2.15 3.58 | macmini (bots): { 69.06 73.27 76.01 }
2026-10-17T15:54:26Z thinkpad: 1.34 2.09 3.51 | macmini (bots): { 72.55 73.63 76.04 }
2026-10-17T15:54:56Z thinkpad: 1.11 1.97 3.42 | macmini (bots): { 77.53 74.77 76.37 }
2026-10-17T15:55:26Z thinkpad: 1.02 1.87 3.34 | macmini (bots): { 77.61 74.90 76.36 }
2026-10-17T15:55:56Z thinkpad: 1.09 1.81 3.27 | macmini (bots): { 71.56 73.69 75.86 }
2026-10-17T15:56:26Z thinkpad: 2.23 1.99 3.29 | macmini (bots): { 65.21 71.93 75.15 }
2026-10-17T15:56:57Z thinkpad: 2.60 2.12 3.29 | macmini (bots): { 58.16 69.40 74.10 }
2026-10-17T15:57:27Z thinkpad: 2.57 2.18 3.27 | macmini (bots): { 57.54 68.26 73.52 }
2026-10-17T15:57:57Z thinkpad: 2.37 2.17 3.23 | macmini (bots): { 56.15 66.85 72.83 }
2026-10-17T15:58:27Z thinkpad: 1.90 2.08 3.17 | macmini (bots): { 59.28 66.57 72.52 }
2026-10-17T15:58:58Z thinkpad: 1.71 2.02 3.11 | macmini (bots): { 55.64 64.94 71.72 }
2026-10-17T15:59:28Z thinkpad: 1.66 1.97 3.06 | macmini (bots): { 66.06 66.49 72.04 }
2026-10-17T15:59:58Z thinkpad: 1.18 1.83 2.98 | macmini (bots): { 78.15 69.43 72.92 }
2026-10-17T16:00:28Z thinkpad: 0.83 1.68 2.89 | macmini (bots): { 70.77 68.57 72.49 }
2026-10-17T16:00:58Z thinkpad: 1.27 1.70 2.86 | macmini (bots): { 71.95 68.97 72.49 }
2026-10-17T16:01:29Z thinkpad: 1.85 1.79 2.85 | macmini (bots): { 83.43 72.22 73.56 }
2026-10-17T16:01:59Z thinkpad: 2.51 1.95 2.86 | macmini (bots): { 70.49 70.18 72.77 }
2026-10-17T16:02:29Z thinkpad: 1.98 1.87 2.81 | macmini (bots): { 64.00 68.64 72.12 }
2026-10-17T16:02:59Z thinkpad: 1.77 1.84 2.77 | macmini (bots): { 59.65 67.09 71.44 }
2026-10-17T16:03:30Z thinkpad: 4.04 2.35 2.91 | macmini (bots): { 62.67 67.15 71.31 }
2026-10-17T16:04:00Z thinkpad: 4.94 2.72 3.01 | macmini (bots): { 67.63 67.82 71.40 }
2026-10-17T16:04:30Z thinkpad: 4.22 2.78 3.02 | macmini (bots): { 70.41 68.39 71.48 }
2026-10-17T16:05:00Z thinkpad: 3.81 2.80 3.02 | macmini (bots): { 66.59 67.83 71.18 }
2026-10-17T16:05:31Z thinkpad: 3.20 2.75 3.00 | macmini (bots): { 72.53 69.01 71.48 }
2026-10-17T16:06:01Z thinkpad: 4.22 3.04 3.08 | macmini (bots): { 71.30 69.27 71.50 }
2026-10-17T16:06:31Z thinkpad: 5.43 3.47 3.23 | macmini (bots): { 68.37 68.63 71.19 }
2026-10-17T16:07:01Z thinkpad: 4.35 3.42 3.22 | macmini (bots): { 60.20 66.68 70.39 }
2026-10-17T16:07:31Z thinkpad: 3.47 3.31 3.19 | macmini (bots): { 58.03 65.51 69.84 }
2026-10-17T16:08:02Z thinkpad: 2.30 3.04 3.11 | macmini (bots): { 59.05 65.13 69.55 }
2026-10-17T16:08:32Z thinkpad: 1.88 2.86 3.04 | macmini (bots): { 51.77 62.75 68.53 }
2026-10-17T16:09:02Z thinkpad: 1.38 2.65 2.97 | macmini (bots): { 49.05 61.01 67.70 }
2026-10-17T16:09:32Z thinkpad: 3.10 2.95 3.06 | macmini (bots): { 48.91 59.82 67.03 }
2026-10-17T16:10:02Z thinkpad: 2.91 2.91 3.04 | macmini (bots): { 47.56 58.44 66.25 }
2026-10-17T16:10:33Z thinkpad: 2.58 2.83 3.01 | macmini (bots): { 49.23 57.69 65.70 }
2026-10-17T16:11:03Z thinkpad: 8.38 4.15 3.44 | macmini (bots): { 44.16 55.72 64.71 }
2026-10-17T16:11:33Z thinkpad: 9.94 4.92 3.72 | macmini (bots): { 44.50 54.71 64.03 }
2026-10-17T16:12:03Z thinkpad: 10.08 5.42 3.92 | macmini (bots): { 42.15 53.21 63.17 }
2026-10-17T16:12:33Z thinkpad: 6.79 5.06 3.85 | macmini (bots): { 36.13 50.73 61.92 }
2026-10-17T16:13:04Z thinkpad: 4.61 4.72 3.77 | macmini (bots): { 41.28 50.64 61.51 }
2026-10-17T16:13:34Z thinkpad: 3.81 4.50 3.73 | macmini (bots): { 42.92 50.15 60.95 }
2026-10-17T16:14:04Z thinkpad: 3.39 4.35 3.71 | macmini (bots): { 41.56 49.16 60.22 }
2026-10-17T16:14:35Z thinkpad: 7.53 5.17 4.00 | macmini (bots): { 47.48 49.82 60.07 }
2026-10-17T16:15:05Z thinkpad: 9.73 6.01 4.32 | macmini (bots): { 51.43 50.41 59.92 }
2026-10-17T16:15:35Z thinkpad: 8.07 5.94 4.35 | macmini (bots): { 69.31 54.81 61.18 }
2026-10-17T16:16:05Z thinkpad: 9.27 6.45 4.57 | macmini (bots): { 71.85 56.92 61.73 }
2026-10-17T16:16:35Z thinkpad: 8.69 6.59 4.68 | macmini (bots): { 64.77 56.72 61.50 }
2026-10-17T16:17:06Z thinkpad: 6.11 6.17 4.60 | macmini (bots): { 57.92 55.86 61.02 }
2026-10-17T16:17:36Z thinkpad: 6.79 6.33 4.70 | macmini (bots): { 51.44 54.51 60.36 }
2026-10-17T16:18:06Z thinkpad: 5.16 5.99 4.64 | macmini (bots): { 51.33 54.23 60.05 }
2026-10-17T16:18:36Z thinkpad: 5.30 5.93 4.66 | macmini (bots): { 55.22 54.87 60.08 }
2026-10-17T16:19:07Z thinkpad: 4.78 5.72 4.63 | macmini (bots): { 51.62 53.94 59.56 }
2026-10-17T16:19:37Z thinkpad: 5.25 5.78 4.69 | macmini (bots): { 48.77 53.04 59.04 }
2026-10-17T16:20:07Z thinkpad: 4.63 5.60 4.67 | macmini (bots): { 54.59 53.92 59.14 }
2026-10-17T16:20:37Z thinkpad: 5.46 5.67 4.72 | macmini (bots): { 59.07 55.03 59.36 }
2026-10-17T16:21:07Z thinkpad: 8.07 6.19 4.91 | macmini (bots): { 52.53 53.83 58.77 }
2026-10-17T16:21:38Z thinkpad: 15.01 8.05 5.57 | macmini (bots): { 48.85 52.78 58.19 }
2026-10-17T16:22:08Z thinkpad: 19.08 9.69 6.20 | macmini (bots): { 54.73 53.61 58.29 }
2026-10-17T16:22:39Z thinkpad: 27.72 12.63 7.29 | macmini (bots): { 54.23 53.61 58.13 }
2026-10-17T16:23:09Z thinkpad: 23.10 12.88 7.53 | macmini (bots): { 51.63 53.06 57.77 }
2026-10-17T16:23:39Z thinkpad: 20.51 13.53 7.96 | macmini (bots): { 45.29 51.38 57.00 }
2026-10-17T16:24:10Z thinkpad: 12.70 12.30 7.72 | macmini (bots): { 45.61 50.91 56.63 }
2026-10-17T16:24:40Z thinkpad: 8.67 11.35 7.55 | macmini (bots): { 45.18 50.35 56.23 }
2026-10-17T16:25:10Z thinkpad: 6.35 10.53 7.40 | macmini (bots): { 41.10 48.88 55.50 }
2026-10-17T16:25:40Z thinkpad: 6.26 10.16 7.38 | macmini (bots): { 49.86 50.24 55.76 }
2026-10-17T16:26:11Z thinkpad: 7.71 10.11 7.45 | macmini (bots): { 53.93 51.11 55.88 }
2026-10-17T16:26:41Z thinkpad: 9.50 10.32 7.61 | macmini (bots): { 47.61 49.86 55.26 }
2026-10-17T16:27:11Z thinkpad: 10.26 10.39 7.72 | macmini (bots): { 46.43 49.36 54.89 }
2026-10-17T16:27:42Z thinkpad: 10.61 10.48 7.83 | macmini (bots): { 48.88 49.66 54.80 }
2026-10-17T16:28:12Z thinkpad: 10.39 10.45 7.91 | macmini (bots): { 58.42 51.74 55.37 }
2026-10-17T16:28:42Z thinkpad: 10.36 10.44 7.99 | macmini (bots): { 59.59 52.67 55.58 }
2026-10-17T16:29:13Z thinkpad: 9.63 10.25 8.00 | macmini (bots): { 55.30 52.38 55.38 }
2026-10-17T16:29:43Z thinkpad: 10.33 10.37 8.12 | macmini (bots): { 57.79 53.13 55.52 }
2026-10-17T16:30:13Z thinkpad: 9.68 10.18 8.12 | macmini (bots): { 57.83 53.60 55.61 }
2026-10-17T16:30:43Z thinkpad: 9.72 10.17 8.19 | macmini (bots): { 78.94 59.18 57.56 }
2026-10-17T16:31:14Z thinkpad: 6.60 9.36 7.98 | macmini (bots): { 87.29 62.98 58.99 }
2026-10-17T16:31:44Z thinkpad: 6.77 9.08 7.93 | macmini (bots): { 81.82 64.16 59.57 }
2026-10-17T16:32:14Z thinkpad: 13.15 10.41 8.41 | macmini (bots): { 81.20 65.92 60.37 }
2026-10-17T16:32:45Z thinkpad: 27.80 14.12 9.71 | macmini (bots): { 75.41 66.22 60.69 }
2026-10-17T16:33:15Z thinkpad: 44.59 19.25 11.54 | macmini (bots): { 66.92 65.08 60.47 }
2026-10-17T16:33:46Z thinkpad: 44.87 21.89 12.67 | macmini (bots): { 61.47 63.94 60.22 }
2026-10-17T16:34:16Z thinkpad: 38.98 22.67 13.23 | macmini (bots): { 62.13 63.78 60.28 }
2026-10-17T16:34:47Z thinkpad: 47.64 26.08 14.65 | macmini (bots): { 81.23 68.06 61.94 }
2026-10-17T16:35:17Z thinkpad: 47.83 28.02 15.65 | macmini (bots): { 116.14 78.43 65.98 }
2026-10-17T16:35:48Z thinkpad: 40.43 28.18 16.10 | macmini (bots): { 125.61 84.41 68.58 }
2026-10-17T16:36:19Z thinkpad: 41.34 29.71 17.00 | macmini (bots): { 143.61 92.86 72.20 }
2026-10-17T16:36:49Z thinkpad: 34.40 29.19 17.29 | macmini (bots): { 156.05 100.88 75.83 }
2026-10-17T16:37:20Z thinkpad: 25.34 27.65 17.17 | macmini (bots): { 151.90 105.26 78.29 }
2026-10-17T16:37:50Z thinkpad: 18.36 25.71 16.85 | macmini (bots): { 141.10 107.25 79.96 }
2026-10-17T16:38:20Z thinkpad: 16.19 24.47 16.72 | macmini (bots): { 131.21 107.92 81.13 }
2026-10-17T16:38:51Z thinkpad: 13.36 23.00 16.48 | macmini (bots): { 122.08 107.78 82.00 }
2026-10-17T16:39:21Z thinkpad: 11.27 21.57 16.21 | macmini (bots): { 107.07 105.50 82.07 }
2026-10-17T16:39:51Z thinkpad: 11.19 20.56 16.04 | macmini (bots): { 96.38 103.12 82.02 }
2026-10-17T16:40:22Z thinkpad: 10.06 19.37 15.79 | macmini (bots): { 89.06 100.79 81.91 }
2026-10-17T16:40:52Z thinkpad: 7.38 17.86 15.40 | macmini (bots): { 79.35 97.41 81.34 }
2026-10-17T16:41:23Z thinkpad: 6.18 16.57 15.05 | macmini (bots): { 81.70 96.20 81.46 }
2026-10-17T16:41:53Z thinkpad: 5.56 15.44 14.72 | macmini (bots): { 86.51 95.88 81.84 }
2026-10-17T16:42:23Z thinkpad: 4.90 14.33 14.37 | macmini (bots): { 99.20 97.69 83.05 }
2026-10-17T16:42:53Z thinkpad: 5.01 13.47 14.08 | macmini (bots): { 116.29 101.87 85.07 }
2026-10-17T16:43:24Z thinkpad: 4.34 12.50 13.74 | macmini (bots): { 131.32 106.77 87.42 }
2026-10-17T16:43:54Z thinkpad: 4.08 11.65 13.42 | macmini (bots): { 155.44 114.77 90.98 }
2026-10-17T16:44:24Z thinkpad: 4.00 10.90 13.11 | macmini (bots): { 167.86 121.62 94.28 }
2026-10-17T16:44:54Z thinkpad: 3.98 10.13 12.77 | macmini (bots): { 160.36 124.31 96.20 }
2026-10-17T16:45:25Z thinkpad: 4.20 9.59 12.50 | macmini (bots): { 163.23 128.59 98.74 }
2026-10-17T16:45:55Z thinkpad: 4.54 9.15 12.27 | macmini (bots): { 169.93 133.73 101.65 }
2026-10-17T16:46:25Z thinkpad: 4.44 8.69 12.01 | macmini (bots): { 162.40 135.22 103.29 }
2026-10-17T16:46:55Z thinkpad: 4.06 8.20 11.74 | macmini (bots): { 164.16 138.31 105.52 }
2026-10-17T16:47:26Z thinkpad: 3.96 7.78 11.49 | macmini (bots): { 157.92 139.31 107.02 }
2026-10-17T16:47:56Z thinkpad: 3.71 7.35 11.23 | macmini (bots): { 155.72 140.55 108.59 }
2026-10-17T16:48:26Z thinkpad: 4.93 7.32 11.09 | macmini (bots): { 153.39 141.52 110.05 }
2026-10-17T16:48:56Z thinkpad: 4.61 7.03 10.88 | macmini (bots): { 137.54 138.87 110.18 }
2026-10-17T16:49:27Z thinkpad: 4.01 6.65 10.63 | macmini (bots): { 136.53 138.36 110.98 }
2026-10-17T16:49:57Z thinkpad: 3.19 6.19 10.35 | macmini (bots): { 141.12 139.18 112.22 }
2026-10-17T16:50:27Z thinkpad: 5.31 6.43 10.30 | macmini (bots): { 122.76 135.27 111.75 }
2026-10-17T16:50:57Z thinkpad: 5.95 6.48 10.19 | macmini (bots): { 114.26 132.02 111.39 }
2026-10-17T16:51:28Z thinkpad: 5.76 6.41 10.05 | macmini (bots): { 116.04 130.87 111.69 }
2026-10-17T16:51:58Z thinkpad: 5.11 6.18 9.86 | macmini (bots): { 115.06 129.17 111.74 }
2026-10-17T16:52:28Z thinkpad: 5.09 6.07 9.70 | macmini (bots): { 109.91 126.59 111.41 }
2026-10-17T16:52:58Z thinkpad: 4.74 5.90 9.53 | macmini (bots): { 101.90 123.11 110.67 }
2026-10-17T16:53:29Z thinkpad: 4.89 5.84 9.39 | macmini (bots): { 83.29 116.38 108.74 }
2026-10-17T16:53:59Z thinkpad: 5.10 5.82 9.27 | macmini (bots): { 119.16 122.10 111.09 }
2026-10-17T16:54:29Z thinkpad: 4.57 5.67 9.10 | macmini (bots): { 114.87 120.76 110.98 }
2026-10-17T16:55:00Z thinkpad: 4.74 5.58 8.96 | macmini (bots): { 129.84 123.74 112.39 }
2026-10-17T16:55:30Z thinkpad: 6.40 5.90 8.96 | macmini (bots): { 100.10 117.29 110.46 }
2026-10-17T16:56:00Z thinkpad: 6.96 6.08 8.92 | macmini (bots): { 80.18 110.75 108.32 }
2026-10-17T16:56:30Z thinkpad: 6.07 5.95 8.79 | macmini (bots): { 74.41 106.39 106.82 }
2026-10-17T16:57:01Z thinkpad: 6.63 6.13 8.75 | macmini (bots): { 81.71 104.99 106.29 }
2026-10-17T16:57:31Z thinkpad: 6.39 6.14 8.68 | macmini (bots): { 101.10 107.55 107.18 }
2026-10-17T16:58:01Z thinkpad: 6.32 6.16 8.60 | macmini (bots): { 94.45 105.66 106.52 }
2026-10-17T16:58:31Z thinkpad: 5.19 5.90 8.44 | macmini (bots): { 78.76 100.83 104.75 }
2026-10-17T16:59:02Z thinkpad: 5.24 5.83 8.33 | macmini (bots): { 78.19 98.42 103.73 }
2026-10-17T16:59:32Z thinkpad: 3.95 5.48 8.13 | macmini (bots): { 77.57 96.42 102.82 }
2026-10-17T17:00:02Z thinkpad: 3.12 5.13 7.93 | macmini (bots): { 85.23 96.29 102.54 }
2026-10-17T17:00:32Z thinkpad: 3.17 4.93 7.77 | macmini (bots): { 99.84 99.07 103.35 }
2026-10-17T17:01:02Z thinkpad: 6.09 5.50 7.88 | macmini (bots): { 80.56 94.49 101.54 }
2026-10-17T17:01:33Z thinkpad: 9.23 6.29 8.06 | macmini (bots): { 79.70 93.03 100.77 }
2026-10-17T17:02:03Z thinkpad: 9.09 6.54 8.09 | macmini (bots): { 76.63 90.94 99.74 }
2026-10-17T17:02:33Z thinkpad: 7.86 6.52 8.04 | macmini (bots): { 93.08 93.50 100.36 }
2026-10-17T17:03:03Z thinkpad: 7.81 6.67 8.04 | macmini (bots): { 83.14 91.09 99.25 }
2026-10-17T17:03:34Z thinkpad: 6.69 6.51 7.94 | macmini (bots): { 79.37 89.64 98.45 }
2026-10-17T17:04:04Z thinkpad: 5.63 6.27 7.81 | macmini (bots): { 73.68 87.23 97.27 }
2026-10-17T17:04:34Z thinkpad: 5.75 6.21 7.75 | macmini (bots): { 75.89 86.23 96.54 }
2026-10-17T17:05:04Z thinkpad: 5.05 6.01 7.63 | macmini (bots): { 79.29 86.13 96.10 }
2026-10-17T17:05:35Z thinkpad: 3.88 5.62 7.44 | macmini (bots): { 76.07 84.79 95.28 }
2026-10-17T17:06:05Z thinkpad: 6.47 6.07 7.53 | macmini (bots): { 70.59 82.62 94.13 }
2026-10-17T17:06:35Z thinkpad: 8.20 6.53 7.63 | macmini (bots): { 69.09 81.18 93.21 }
2026-10-17T17:07:05Z thinkpad: 7.59 6.54 7.60 | macmini (bots): { 77.55 82.11 93.13 }
2026-10-17T17:07:36Z thinkpad: 6.93 6.49 7.55 | macmini (bots): { 84.23 83.41 93.22 }
2026-10-17T17:08:06Z thinkpad: 7.21 6.61 7.56 | macmini (bots): { 84.31 83.68 92.99 }
2026-10-17T17:08:36Z thinkpad: 6.73 6.57 7.51 | macmini (bots): { 81.04 82.99 92.42 }
2026-10-17T17:09:06Z thinkpad: 6.12 6.43 7.44 | macmini (bots): { 86.55 83.77 92.35 }
2026-10-17T17:09:36Z thinkpad: 6.77 6.58 7.46 | macmini (bots): { 95.19 86.31 92.99 }
2026-10-17T17:10:07Z thinkpad: 7.13 6.68 7.46 | macmini (bots): { 111.30 91.11 94.50 }
2026-10-17T17:10:37Z thinkpad: 5.09 6.23 7.29 | macmini (bots): { 118.51 95.17 95.87 }
2026-10-17T17:11:07Z thinkpad: 4.00 5.86 7.13 | macmini (bots): { 121.12 98.01 96.88 }
2026-10-17T17:11:38Z thinkpad: 3.50 5.57 6.99 | macmini (bots): { 109.57 97.67 96.81 }
2026-10-17T17:12:08Z thinkpad: 2.92 5.22 6.83 | macmini (bots): { 108.43 98.44 97.11 }
2026-10-17T17:12:38Z thinkpad: 3.42 5.12 6.74 | macmini (bots): { 101.61 97.86 96.95 }
2026-10-17T17:13:09Z thinkpad: 5.64 5.51 6.82 | macmini (bots): { 101.66 98.17 97.09 }
2026-10-17T17:13:39Z thinkpad: 7.06 5.89 6.91 | macmini (bots): { 88.43 95.36 96.11 }
2026-10-17T17:14:09Z thinkpad: 7.61 6.13 6.96 | macmini (bots): { 79.03 92.54 95.07 }
2026-10-17T17:14:39Z thinkpad: 9.06 6.68 7.12 | macmini (bots): { 92.55 94.57 95.72 }
2026-10-17T17:15:10Z thinkpad: 9.81 7.06 7.22 | macmini (bots): { 93.71 94.49 95.64 }
2026-10-17T17:15:40Z thinkpad: 9.98 7.41 7.34 | macmini (bots): { 92.26 93.86 95.35 }
2026-10-17T17:16:10Z thinkpad: 7.60 7.08 7.23 | macmini (bots): { 88.61 92.91 94.96 }
2026-10-17T17:16:40Z thinkpad: 6.81 6.93 7.17 | macmini (bots): { 96.96 94.47 95.45 }
2026-10-17T17:17:11Z thinkpad: 6.31 6.81 7.12 | macmini (bots): { 102.92 96.14 96.02 }
2026-10-17T17:17:41Z thinkpad: 4.81 6.38 6.97 | macmini (bots): { 91.20 94.16 95.32 }
2026-10-17T17:18:11Z thinkpad: 7.36 6.81 7.09 | macmini (bots): { 93.60 94.31 95.32 }
2026-10-17T17:18:41Z thinkpad: 9.42 7.38 7.27 | macmini (bots): { 90.86 93.57 95.02 }
2026-10-17T17:19:11Z thinkpad: 9.95 7.72 7.39 | macmini (bots): { 88.94 92.94 94.75 }
2026-10-17T17:19:42Z thinkpad: 9.51 7.85 7.45 | macmini (bots): { 94.97 93.93 95.03 }
2026-10-17T17:20:12Z thinkpad: 8.97 7.90 7.48 | macmini (bots): { 80.46 90.39 93.71 }
2026-10-17T17:20:42Z thinkpad: 7.56 7.65 7.41 | macmini (bots): { 74.38 87.95 92.71 }
2026-10-17T17:21:12Z thinkpad: 6.39 7.36 7.32 | macmini (bots): { 77.48 87.26 92.28 }
2026-10-17T17:21:43Z thinkpad: 5.21 6.98 7.19 | macmini (bots): { 82.05 87.48 92.19 }
2026-10-17T17:22:13Z thinkpad: 3.87 6.49 7.02 | macmini (bots): { 88.15 88.45 92.38 }
2026-10-17T17:22:43Z thinkpad: 3.86 6.22 6.91 | macmini (bots): { 100.23 91.12 93.19 }
2026-10-17T17:23:13Z thinkpad: 8.32 7.05 7.16 | macmini (bots): { 84.55 88.39 92.15 }
2026-10-17T17:23:43Z thinkpad: 13.84 8.37 7.59 | macmini (bots): { 73.74 85.38 90.92 }
2026-10-17T17:24:14Z thinkpad: 21.02 10.71 8.40 | macmini (bots): { 71.36 83.72 90.13 }
2026-10-17T17:24:44Z thinkpad: 20.73 11.82 8.85 | macmini (bots): { 82.27 85.04 90.38 }
2026-10-17T17:25:14Z thinkpad: 16.80 11.77 8.94 | macmini (bots): { 78.63 83.92 89.78 }
2026-10-17T17:25:44Z thinkpad: 14.62 11.68 8.99 | macmini (bots): { 82.33 84.06 89.61 }
2026-10-17T17:26:15Z thinkpad: 14.51 11.94 9.17 | macmini (bots): { 74.13 81.89 88.63 }
2026-10-17T17:26:45Z thinkpad: 12.48 11.74 9.20 | macmini (bots): { 75.65 81.46 88.24 }
2026-10-17T17:27:15Z thinkpad: 15.24 12.56 9.56 | macmini (bots): { 71.35 79.78 87.35 }
2026-10-17T17:27:45Z thinkpad: 15.70 12.87 9.76 | macmini (bots): { 68.21 78.17 86.51 }
2026-10-17T17:28:16Z thinkpad: 12.80 12.47 9.72 | macmini (bots): { 79.74 80.10 86.92 }
2026-10-17T17:28:46Z thinkpad: 9.05 11.58 9.51 | macmini (bots): { 101.01 85.27 88.56 }
2026-10-17T17:29:16Z thinkpad: 9.38 11.41 9.53 | macmini (bots): { 93.76 85.18 88.43 }
2026-10-17T17:29:46Z thinkpad: 11.08 11.62 9.65 | macmini (bots): { 74.91 81.56 87.01 }
2026-10-17T17:30:17Z thinkpad: 10.98 11.53 9.69 | macmini (bots): { 61.10 77.50 85.34 }
2026-10-17T17:30:47Z thinkpad: 9.83 11.21 9.64 | macmini (bots): { 51.19 73.56 83.64 }
2026-10-17T17:31:18Z thinkpad: 9.65 11.03 9.63 | macmini (bots): { 49.64 71.03 82.37 }
2026-10-17T17:31:48Z thinkpad: 7.22 10.33 9.44 | macmini (bots): { 53.62 69.90 81.56 }
2026-10-17T17:32:18Z thinkpad: 5.79 9.68 9.25 | macmini (bots): { 61.07 70.36 81.34 }
2026-10-17T17:32:48Z thinkpad: 3.70 8.80 8.97 | macmini (bots): { 60.39 69.24 80.55 }
2026-10-17T17:33:19Z thinkpad: 2.72 8.08 8.73 | macmini (bots): { 57.06 67.51 79.53 }
2026-10-17T17:33:49Z thinkpad: 1.97 7.37 8.47 | macmini (bots): { 65.72 68.41 79.42 }
2026-10-17T17:34:19Z thinkpad: 1.25 6.68 8.20 | macmini (bots): { 62.60 67.40 78.67 }
2026-10-17T17:34:49Z thinkpad: 0.94 6.09 7.96 | macmini (bots): { 71.83 69.24 78.95 }
2026-10-17T17:35:20Z thinkpad: 0.72 5.54 7.71 | macmini (bots): { 66.16 68.07 78.19 }
2026-10-17T17:35:50Z thinkpad: 0.44 5.01 7.47 | macmini (bots): { 70.96 68.91 78.13 }
2026-10-17T17:36:20Z thinkpad: 1.70 4.79 7.30 | macmini (bots): { 79.74 71.45 78.74 }
2026-10-17T17:36:50Z thinkpad: 3.03 4.80 7.22 | macmini (bots): { 79.71 72.75 78.95 }
2026-10-17T17:37:21Z thinkpad: 3.07 4.67 7.10 | macmini (bots): { 85.13 74.60 79.40 }
2026-10-17T17:37:51Z thinkpad: 2.32 4.33 6.91 | macmini (bots): { 84.85 75.70 79.64 }
2026-10-17T17:38:21Z thinkpad: 1.83 4.03 6.73 | macmini (bots): { 86.47 76.79 79.88 }
2026-10-17T17:38:51Z thinkpad: 1.11 3.64 6.51 | macmini (bots): { 106.96 82.74 81.94 }
2026-10-17T17:39:22Z thinkpad: 1.03 3.37 6.33 | macmini (bots): { 114.52 87.01 83.52 }
2026-10-17T17:39:52Z thinkpad: 1.11 3.16 6.16 | macmini (bots): { 103.99 87.05 83.65 }
2026-10-17T17:40:22Z thinkpad: 1.31 3.02 6.02 | macmini (bots): { 100.27 87.59 83.95 }
2026-10-17T17:40:52Z thinkpad: 1.36 2.87 5.87 | macmini (bots): { 98.17 88.44 84.40 }
2026-10-17T17:41:22Z thinkpad: 2.01 2.88 5.78 | macmini (bots): { 96.27 88.67 84.60 }
2026-10-17T17:41:53Z thinkpad: 1.96 2.77 5.65 | macmini (bots): { 96.31 89.44 85.03 }
2026-10-17T17:42:23Z thinkpad: 3.06 2.97 5.62 | macmini (bots): { 80.49 86.30 84.04 }
2026-10-17T17:42:53Z thinkpad: 2.38 2.81 5.49 | macmini (bots): { 66.40 82.24 82.64 }
2026-10-17T17:43:24Z thinkpad: 2.44 2.81 5.40 | macmini (bots): { 56.70 78.37 81.22 }
2026-10-17T17:43:54Z thinkpad: 1.93 2.65 5.26 | macmini (bots): { 61.56 77.39 80.77 }
2026-10-17T17:44:24Z thinkpad: 1.96 2.59 5.16 | macmini (bots): { 58.25 75.14 79.83 }
2026-10-17T17:44:54Z thinkpad: 2.25 2.60 5.08 | macmini (bots): { 56.32 73.21 78.98 }
2026-10-17T17:45:24Z thinkpad: 1.79 2.46 4.95 | macmini (bots): { 62.87 73.14 78.75 }
2026-10-17T17:45:55Z thinkpad: 2.78 2.64 4.93 | macmini (bots): { 62.58 72.22 78.23 }
2026-10-17T17:46:25Z thinkpad: 4.82 3.14 5.03 | macmini (bots): { 58.89 70.44 77.38 }
2026-10-17T17:46:55Z thinkpad: 3.10 2.90 4.88 | macmini (bots): { 61.68 70.08 77.01 }
2026-10-17T17:47:25Z thinkpad: 2.27 2.71 4.75 | macmini (bots): { 70.43 71.40 77.25 }
2026-10-17T17:47:56Z thinkpad: 1.71 2.53 4.62 | macmini (bots): { 70.51 71.56 77.12 }
2026-10-17T17:48:26Z thinkpad: 1.59 2.42 4.52 | macmini (bots): { 68.98 70.98 76.71 }
2026-10-17T17:48:56Z thinkpad: 2.09 2.47 4.47 | macmini (bots): { 67.75 70.60 76.38 }
2026-10-17T17:49:26Z thinkpad: 2.14 2.44 4.39 | macmini (bots): { 61.53 69.01 75.58 }
2026-10-17T17:49:56Z thinkpad: 2.22 2.43 4.33 | macmini (bots): { 58.49 67.45 74.78 }
2026-10-17T17:50:27Z thinkpad: 1.98 2.36 4.24 | macmini (bots): { 66.15 68.33 74.84 }
2026-10-17T17:50:57Z thinkpad: 2.38 2.42 4.20 | macmini (bots): { 61.65 67.22 74.22 }
2026-10-17T17:51:27Z thinkpad: 3.62 2.72 4.24 | macmini (bots): { 54.05 64.88 73.13 }
2026-10-17T17:51:57Z thinkpad: 3.25 2.70 4.19 | macmini (bots): { 49.14 62.62 72.02 }
2026-10-17T17:52:28Z thinkpad: 2.45 2.55 4.09 | macmini (bots): { 43.72 60.06 70.77 }
2026-10-17T17:52:58Z thinkpad: 2.85 2.62 4.06 | macmini (bots): { 42.53 58.22 69.73 }
2026-10-17T17:53:28Z thinkpad: 8.44 3.97 4.46 | macmini (bots): { 45.21 57.34 69.01 }
2026-10-17T17:53:58Z thinkpad: 10.75 4.91 4.76 | macmini (bots): { 44.86 56.01 68.11 }
2026-10-17T17:54:29Z thinkpad: 8.36 4.90 4.76 | macmini (bots): { 51.73 56.60 67.91 }
2026-10-17T17:54:59Z thinkpad: 7.41 4.98 4.79 | macmini (bots): { 55.80 57.05 67.67 }
2026-10-17T17:55:29Z thinkpad: 5.45 4.73 4.71 | macmini (bots): { 50.40 55.67 66.81 }
2026-10-17T17:55:59Z thinkpad: 6.26 4.97 4.79 | macmini (bots): { 57.22 56.88 66.87 }
2026-10-17T17:56:30Z thinkpad: 8.22 5.52 4.98 | macmini (bots): { 48.94 54.86 65.78 }
2026-10-17T17:57:00Z thinkpad: 6.71 5.42 4.96 | macmini (bots): { 50.54 54.70 65.35 }
2026-10-17T17:57:30Z thinkpad: 4.94 5.14 4.89 | macmini (bots): { 58.02 56.25 65.55 }
2026-10-17T17:58:01Z thinkpad: 4.54 5.04 4.86 | macmini (bots): { 53.98 55.38 64.90 }
2026-10-17T17:58:31Z thinkpad: 4.65 5.04 4.87 | macmini (bots): { 49.96 54.23 64.16 }
2026-10-17T17:59:01Z thinkpad: 5.29 5.16 4.92 | macmini (bots): { 49.63 53.68 63.61 }
2026-10-17T17:59:31Z thinkpad: 4.23 4.94 4.85 | macmini (bots): { 57.15 55.10 63.78 }
2026-10-17T18:00:01Z thinkpad: 3.57 4.72 4.78 | macmini (bots): { 61.70 56.26 63.89 }
2026-10-17T18:00:32Z thinkpad: 3.16 4.52 4.71 | macmini (bots): { 62.10 56.86 63.79 }
2026-10-17T18:01:02Z thinkpad: 2.81 4.29 4.63 | macmini (bots): { 76.29 60.79 64.98 }
2026-10-17T18:01:32Z thinkpad: 7.77 5.26 4.93 | macmini (bots): { 75.36 62.16 65.34 }
2026-10-17T18:02:02Z thinkpad: 11.60 6.45 5.34 | macmini (bots): { 70.83 62.28 65.27 }
2026-10-17T18:02:33Z thinkpad: 11.54 6.97 5.56 | macmini (bots): { 65.66 61.87 65.01 }
2026-10-17T18:03:03Z thinkpad: 10.80 7.33 5.73 | macmini (bots): { 63.09 61.54 64.78 }
2026-10-17T18:03:33Z thinkpad: 10.52 7.56 5.85 | macmini (bots): { 60.31 61.12 64.52 }
2026-10-17T18:04:04Z thinkpad: 10.27 7.76 5.97 | macmini (bots): { 60.99 61.21 64.44 }
2026-10-17T18:04:34Z thinkpad: 8.77 7.69 6.01 | macmini (bots): { 56.72 60.19 63.96 }
2026-10-17T18:05:05Z thinkpad: 5.77 7.06 5.85 | macmini (bots): { 56.14 59.70 63.65 }
2026-10-17T18:05:35Z thinkpad: 5.16 6.79 5.80 | macmini (bots): { 51.25 58.17 62.95 }
2026-10-17T18:06:05Z thinkpad: 4.16 6.36 5.69 | macmini (bots): { 52.03 57.64 62.59 }
2026-10-17T18:06:35Z thinkpad: 4.28 6.18 5.65 | macmini (bots): { 64.66 60.00 63.27 }
2026-10-17T18:07:06Z thinkpad: 7.47 6.76 5.86 | macmini (bots): { 56.20 58.45 62.59 }
2026-10-17T18:07:36Z thinkpad: 6.95 6.74 5.89 | macmini (bots): { 51.92 57.20 62.00 }
2026-10-17T18:08:06Z thinkpad: 7.70 6.99 6.00 | macmini (bots): { 51.14 56.65 61.64 }
2026-10-17T18:08:36Z thinkpad: 5.83 6.60 5.90 | macmini (bots): { 48.08 55.33 60.98 }
2026-10-17T18:09:07Z thinkpad: 4.89 6.30 5.82 | macmini (bots): { 53.80 56.00 61.00 }
2026-10-17T18:09:37Z thinkpad: 4.93 6.19 5.80 | macmini (bots): { 60.49 57.24 61.27 }
2026-10-17T18:10:07Z thinkpad: 4.61 5.98 5.74 | macmini (bots): { 58.95 57.21 61.12 }
2026-10-17T18:10:37Z thinkpad: 4.74 5.91 5.73 | macmini (bots): { 60.84 57.65 61.13 }
2026-10-17T18:11:07Z thinkpad: 3.96 5.61 5.63 | macmini (bots): { 64.23 58.82 61.43 }
2026-10-17T18:11:38Z thinkpad: 4.16 5.53 5.61 | macmini (bots): { 75.59 62.26 62.60 }
2026-10-17T18:12:08Z thinkpad: 2.97 5.11 5.46 | macmini (bots): { 75.25 63.65 63.10 }
@@ -0,0 +1,695 @@
{
"kind": "horizon-server-benchmark",
"date": "2026-10-17T17:31:40+00:00",
"machine": {
"system": "Linux",
"machine": "x86_64",
"cpus": 4
},
"commit": "7bbbf6e97fb53683eae0a01ce2c134e53cb649a7",
"java": [
"openjdk 25.0.4.1 2026-08-18"
],
"settings": {
"servers": "horizon,vanilla",
"bots": "1,10,25,50,100",
"behaviour": "walk,explore",
"join_every": 1.0,
"warmup": 60,
"seconds": 180,
"seed": 12345,
"explore_speed": 0,
"gamerule": "",
"heap": "4g",
"out": "/home/ludovic/code/horizon/.claude/worktrees/server-host-4/.local/bench-out/walk-explore.json",
"nice": 19,
"bots_via": "ssh macmini",
"bots_bin": "/Users/ludovicmarie/code/horizon/.claude/worktrees/server-host-4/target/release/horizon-bots",
"connect": "<server address>",
"properties": {
"level-seed": "12345",
"view-distance": "10",
"simulation-distance": "10",
"online-mode": "false",
"white-list": "false",
"gamemode": "creative",
"force-gamemode": "true",
"spawn-protection": "0",
"max-players": "200",
"difficulty": "easy",
"sync-chunk-writes": "true"
}
},
"runs": [
{
"server": "horizon",
"bots": 1,
"behaviour": "walk",
"start_seconds": 9.62,
"mspt": {
"average": 4.01,
"p50": 3.54,
"p95": 8.19,
"p99": 25.59
},
"mspt_worst_p99": 47.5,
"tps": 20.0,
"rss_mb": {
"mean": 782.52,
"max": 785.3
},
"cpu_percent": 58.87,
"chunks_per_second": 9.6,
"unique_per_second": 0.43,
"forgotten_per_second": 9.65,
"waited_ticks_per_second": 0.0,
"chunks_total": 2697,
"chunks_per_bot": {
"walk": 2697.0
},
"unique_chunks_per_bot": {
"walk": 693.0
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 1,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "vanilla",
"bots": 1,
"behaviour": "walk",
"start_seconds": 14.85,
"mspt": {
"average": 18.22,
"p50": 17.99,
"p95": 29.34,
"p99": 50.06
},
"mspt_worst_p99": 395.5,
"tps": 20.0,
"rss_mb": {
"mean": 3429.06,
"max": 3446.3
},
"cpu_percent": 68.81,
"chunks_per_second": 10.47,
"unique_per_second": 0.47,
"forgotten_per_second": 10.53,
"waited_ticks_per_second": 0.0,
"chunks_total": 2755,
"chunks_per_bot": {
"walk": 2755.0
},
"unique_chunks_per_bot": {
"walk": 697.0
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 1,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "horizon",
"bots": 10,
"behaviour": "walk",
"start_seconds": 9.21,
"mspt": {
"average": 8.79,
"p50": 8.05,
"p95": 17.35,
"p99": 30.47
},
"mspt_worst_p99": 48.8,
"tps": 20.0,
"rss_mb": {
"mean": 781.88,
"max": 782.8
},
"cpu_percent": 119.16,
"chunks_per_second": 94.63,
"unique_per_second": 3.58,
"forgotten_per_second": 94.31,
"waited_ticks_per_second": 0.0,
"chunks_total": 26003,
"chunks_per_bot": {
"walk": 2600.3
},
"unique_chunks_per_bot": {
"walk": 679.9
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 10,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "vanilla",
"bots": 10,
"behaviour": "walk",
"start_seconds": 15.14,
"mspt": {
"average": 18.67,
"p50": 15.8,
"p95": 38.26,
"p99": 55.93
},
"mspt_worst_p99": 101.8,
"tps": 20.0,
"rss_mb": {
"mean": 3497.35,
"max": 3499.4
},
"cpu_percent": 100.8,
"chunks_per_second": 101.1,
"unique_per_second": 4.38,
"forgotten_per_second": 101.15,
"waited_ticks_per_second": 0.0,
"chunks_total": 26537,
"chunks_per_bot": {
"walk": 2653.7
},
"unique_chunks_per_bot": {
"walk": 687.5
},
"resent": 0,
"pulled_back": 5,
"moved_too_quickly": 5,
"moved_wrongly": 0,
"bots_joined": 10,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "horizon",
"bots": 25,
"behaviour": "walk",
"start_seconds": 9.82,
"mspt": {
"average": 15.01,
"p50": 13.12,
"p95": 31.12,
"p99": 50.62
},
"mspt_worst_p99": 101.6,
"tps": 20.0,
"rss_mb": {
"mean": 799.52,
"max": 799.7
},
"cpu_percent": 161.02,
"chunks_per_second": 234.3,
"unique_per_second": 8.49,
"forgotten_per_second": 234.35,
"waited_ticks_per_second": 0.0,
"chunks_total": 65926,
"chunks_per_bot": {
"walk": 2637.04
},
"unique_chunks_per_bot": {
"walk": 683.96
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 25,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "vanilla",
"bots": 25,
"behaviour": "walk",
"start_seconds": 14.95,
"mspt": {
"average": 18.78,
"p50": 17.47,
"p95": 28.16,
"p99": 51.03
},
"mspt_worst_p99": 107.6,
"tps": 20.0,
"rss_mb": {
"mean": 3520.17,
"max": 3573.8
},
"cpu_percent": 154.9,
"chunks_per_second": 252.39,
"unique_per_second": 11.89,
"forgotten_per_second": 251.75,
"waited_ticks_per_second": 0.0,
"chunks_total": 66469,
"chunks_per_bot": {
"walk": 2658.76
},
"unique_chunks_per_bot": {
"walk": 689.68
},
"resent": 0,
"pulled_back": 10,
"moved_too_quickly": 10,
"moved_wrongly": 0,
"bots_joined": 25,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "horizon",
"bots": 50,
"behaviour": "walk",
"start_seconds": 9.52,
"mspt": {
"average": 22.72,
"p50": 16.98,
"p95": 60.19,
"p99": 84.16
},
"mspt_worst_p99": 105.4,
"tps": 20.0,
"rss_mb": {
"mean": 813.5,
"max": 815.8
},
"cpu_percent": 179.85,
"chunks_per_second": 464.75,
"unique_per_second": 16.94,
"forgotten_per_second": 464.69,
"waited_ticks_per_second": 0.0,
"chunks_total": 134839,
"chunks_per_bot": {
"walk": 2696.78
},
"unique_chunks_per_bot": {
"walk": 685.48
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 50,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "vanilla",
"bots": 50,
"behaviour": "walk",
"start_seconds": 16.36,
"mspt": {
"average": 34.3,
"p50": 31.07,
"p95": 55.55,
"p99": 92.32
},
"mspt_worst_p99": 452.1,
"tps": 20.0,
"rss_mb": {
"mean": 3577.02,
"max": 3611.2
},
"cpu_percent": 267.05,
"chunks_per_second": 496.77,
"unique_per_second": 26.74,
"forgotten_per_second": 496.2,
"waited_ticks_per_second": 0.0,
"chunks_total": 133505,
"chunks_per_bot": {
"walk": 2670.1
},
"unique_chunks_per_bot": {
"walk": 688.8
},
"resent": 0,
"pulled_back": 39,
"moved_too_quickly": 39,
"moved_wrongly": 0,
"bots_joined": 50,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "horizon",
"bots": 100,
"behaviour": "walk",
"start_seconds": 9.51,
"mspt": {
"average": 63.33,
"p50": 58.89,
"p95": 125.04,
"p99": 175.12
},
"mspt_worst_p99": 261.5,
"tps": 15.79,
"rss_mb": {
"mean": 868.12,
"max": 869.9
},
"cpu_percent": 245.64,
"chunks_per_second": 678.18,
"unique_per_second": 31.8,
"forgotten_per_second": 676.4,
"waited_ticks_per_second": 0.0,
"chunks_total": 219787,
"chunks_per_bot": {
"walk": 2197.87
},
"unique_chunks_per_bot": {
"walk": 637.91
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 100,
"bots_failed": [],
"tick_queries": 34
},
{
"server": "vanilla",
"bots": 100,
"behaviour": "walk",
"start_seconds": 15.14,
"error": "[Errno 32] Broken pipe"
},
{
"server": "horizon",
"bots": 1,
"behaviour": "explore",
"start_seconds": 9.72,
"mspt": {
"average": 9.73,
"p50": 8.75,
"p95": 20.99,
"p99": 34.17
},
"mspt_worst_p99": 73.9,
"tps": 20.0,
"rss_mb": {
"mean": 836.18,
"max": 891.0
},
"cpu_percent": 329.64,
"chunks_per_second": 41.11,
"unique_per_second": 35.96,
"forgotten_per_second": 41.08,
"waited_ticks_per_second": 0.16,
"chunks_total": 9460,
"chunks_per_bot": {
"explore": 9460.0
},
"unique_chunks_per_bot": {
"explore": 8479.0
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 1,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "vanilla",
"bots": 1,
"behaviour": "explore",
"start_seconds": 15.54,
"mspt": {
"average": 9.71,
"p50": 7.86,
"p95": 20.37,
"p99": 76.15
},
"mspt_worst_p99": 388.6,
"tps": 20.0,
"rss_mb": {
"mean": 3614.97,
"max": 3688.5
},
"cpu_percent": 247.15,
"chunks_per_second": 35.47,
"unique_per_second": 33.61,
"forgotten_per_second": 35.45,
"waited_ticks_per_second": 0.0,
"chunks_total": 7912,
"chunks_per_bot": {
"explore": 7912.0
},
"unique_chunks_per_bot": {
"explore": 7580.0
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 1,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "horizon",
"bots": 10,
"behaviour": "explore",
"start_seconds": 9.31,
"mspt": {
"average": 21.27,
"p50": 19.5,
"p95": 39.15,
"p99": 54.23
},
"mspt_worst_p99": 105.5,
"tps": 20.0,
"rss_mb": {
"mean": 1177.66,
"max": 1392.9
},
"cpu_percent": 321.97,
"chunks_per_second": 46.47,
"unique_per_second": 46.11,
"forgotten_per_second": 43.12,
"waited_ticks_per_second": 111.25,
"chunks_total": 14103,
"chunks_per_bot": {
"explore": 1410.3
},
"unique_chunks_per_bot": {
"explore": 1402.4
},
"resent": 0,
"pulled_back": 9856,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 10,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "vanilla",
"bots": 10,
"behaviour": "explore",
"start_seconds": 15.24,
"mspt": {
"average": 16.94,
"p50": 14.24,
"p95": 30.28,
"p99": 109.7
},
"mspt_worst_p99": 1861.2,
"tps": 20.0,
"rss_mb": {
"mean": 3953.29,
"max": 4232.0
},
"cpu_percent": 254.11,
"chunks_per_second": 24.99,
"unique_per_second": 25.01,
"forgotten_per_second": 25.33,
"waited_ticks_per_second": 77.93,
"chunks_total": 7707,
"chunks_per_bot": {
"explore": 770.7
},
"unique_chunks_per_bot": {
"explore": 769.5
},
"resent": 0,
"pulled_back": 11123,
"moved_too_quickly": 20,
"moved_wrongly": 0,
"bots_joined": 10,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "horizon",
"bots": 25,
"behaviour": "explore",
"start_seconds": 9.92,
"mspt": {
"average": 25.76,
"p50": 23.58,
"p95": 45.01,
"p99": 61.43
},
"mspt_worst_p99": 108.8,
"tps": 20.0,
"rss_mb": {
"mean": 1172.1,
"max": 1482.3
},
"cpu_percent": 314.47,
"chunks_per_second": 61.38,
"unique_per_second": 60.7,
"forgotten_per_second": 55.11,
"waited_ticks_per_second": 304.54,
"chunks_total": 24085,
"chunks_per_bot": {
"explore": 963.4
},
"unique_chunks_per_bot": {
"explore": 954.76
},
"resent": 0,
"pulled_back": 31264,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 25,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "vanilla",
"bots": 25,
"behaviour": "explore",
"start_seconds": 15.76,
"mspt": {
"average": 24.93,
"p50": 22.04,
"p95": 37.09,
"p99": 168.46
},
"mspt_worst_p99": 1724.8,
"tps": 20.0,
"rss_mb": {
"mean": 4040.42,
"max": 4447.6
},
"cpu_percent": 305.47,
"chunks_per_second": 37.91,
"unique_per_second": 37.84,
"forgotten_per_second": 38.39,
"waited_ticks_per_second": 258.66,
"chunks_total": 16865,
"chunks_per_bot": {
"explore": 674.6
},
"unique_chunks_per_bot": {
"explore": 671.28
},
"resent": 0,
"pulled_back": 33467,
"moved_too_quickly": 23,
"moved_wrongly": 0,
"bots_joined": 25,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "horizon",
"bots": 50,
"behaviour": "explore",
"start_seconds": 9.41,
"mspt": {
"average": 39.31,
"p50": 36.98,
"p95": 65.33,
"p99": 88.36
},
"mspt_worst_p99": 157.6,
"tps": 20.0,
"rss_mb": {
"mean": 1250.32,
"max": 1517.1
},
"cpu_percent": 324.77,
"chunks_per_second": 114.99,
"unique_per_second": 112.25,
"forgotten_per_second": 104.41,
"waited_ticks_per_second": 543.77,
"chunks_total": 50402,
"chunks_per_bot": {
"explore": 1008.04
},
"unique_chunks_per_bot": {
"explore": 997.42
},
"resent": 0,
"pulled_back": 64163,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 50,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "vanilla",
"bots": 50,
"behaviour": "explore",
"start_seconds": 14.55,
"error": "[Errno 32] Broken pipe"
},
{
"server": "horizon",
"bots": 100,
"behaviour": "explore",
"start_seconds": 9.31,
"mspt": {
"average": 52.7,
"p50": 49.49,
"p95": 85.73,
"p99": 115.34
},
"mspt_worst_p99": 219.0,
"tps": 18.97,
"rss_mb": {
"mean": 1358.32,
"max": 3047.2
},
"cpu_percent": 327.95,
"chunks_per_second": 154.2,
"unique_per_second": 151.83,
"forgotten_per_second": 141.65,
"waited_ticks_per_second": 1132.9,
"chunks_total": 84433,
"chunks_per_bot": {
"explore": 844.33
},
"unique_chunks_per_bot": {
"explore": 839.47
},
"resent": 0,
"pulled_back": 116161,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 100,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "vanilla",
"bots": 100,
"behaviour": "explore",
"start_seconds": 16.47,
"error": "[Errno 32] Broken pipe"
}
]
}
@@ -0,0 +1,137 @@
# Horizon's server against vanilla 26.3: mixed bots, equal work (2026-10-18)
This reruns `../server-mixed-2026-10-18/` with the bots' load made the same on both servers. In that run the
`explore` bots flew as fast as each server sent them chunks. Horizon kept up and vanilla didn't, so Horizon
sent 2.9 to 5.9 times as many chunks a second, and vanilla's walking bots got a fifth of the chunks Horizon's
got. The comparison of tick times wasn't fair.
## Summary
- **The work was equal** at 1 and 10 bots. At 25 bots it was equal in chunks sent a second (158.6 against 157.6);
vanilla was still catching up on chunks its bots hadn't had yet, see "Equal work" below. Every bot joined, none
was dropped, no chunk was sent twice, and no bot ever waited for the chunk it stood in.
- **Tick time (MSPT), same work:**
- 1 bot: 1.8 ms on Horizon against 11.3 on vanilla;
- 10 bots: 4.7 against 34.3 ms;
- 25 bots: 11.7 against 50.5 ms. Vanilla dropped just under 20 TPS (19.8) and its worst 5-second P99 was
1388 ms, against Horizon's 130;
- 50 bots: Horizon ran at 23.8 ms (20 TPS). Vanilla's watchdog stopped its server 30 s after the last bot
joined ("A single server tick took 60.01 seconds").
- **Memory:** Horizon's RSS was 4.1 to 4.9 times smaller: 0.69 against 3.40 GB with one bot, 0.95 against
3.87 GB with 25, and 1.03 GB with 50. Vanilla's heap is fixed at 4 GB, so its RSS is mostly that heap.
- **CPU** (100 % is one of the machine's 4 threads): 30 against 128 % at 10 bots, 58 against 192 % at 25.
- **What changed since the first run:** the first run's vanilla held 50 bots at 29.7 ms because its walking
bots got 640 to 1100 chunks each. With the explore bots paced, vanilla serves the walking bots in full
(5400 to 6100 chunks each, against Horizon's 6100 to 6600), and that is the load it can't carry at 50.
## Results
MSPT is milliseconds per tick from each server's own `/tick query` (50 is the limit for 20 TPS). P95 and P99
are over the 300 measured seconds. "Worst P99" is the highest P99 of a single 5-second query. RSS and CPU
cover the server's process tree. "First-time chunks/s" counts chunks a bot hadn't received before (summed
over the bots). "Waited" is ticks a second that bots stood still waiting for their own chunk. "Pulled back"
counts position packets that moved a bot back.
| Bots | Server | MSPT | P95 | P99 | Worst P99 | TPS | RSS (mean / max) | CPU | Start | Chunks/s | First-time chunks/s | Waited | Pulled back |
|---:|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|
| 1 | Horizon | **1.77** | 4.21 | 13.04 | 47.5 | 20 | **0.69 / 0.69 GB** | 28 % | 19.7 s | 9.94 | 0.01 | 0 | 0 |
| 1 | vanilla | 11.34 | 17.12 | 33.22 | 119.1 | 20 | 3.40 / 3.42 GB | 36 % | 20.4 s | 9.98 | 0.01 | 0 | 0 |
| 10 | Horizon | **4.74** | 10.48 | 19.69 | 120.1 | 20 | **0.86 / 0.88 GB** | 30 % | 19.5 s | 61.35 | 1.79 | 0 | 4159 |
| 10 | vanilla | 34.30 | 53.72 | 89.08 | 375.8 | 20 | 3.71 / 3.75 GB | 128 % | 19.9 s | 61.66 | 1.93 | 0 | 3976 |
| 25 | Horizon | **11.72** | 24.45 | 43.79 | 129.8 | 20 | **0.95 / 1.09 GB** | 58 % | 9.6 s | 158.57 | 5.78 | 0 | 3496 |
| 25 | vanilla | 50.45 | 74.09 | 191.21 | 1388.1 | 19.82 | 3.87 / 4.75 GB | 192 % | 37.8 s | 157.63 | 8.25 | 0 | 3629 |
| 50 | Horizon | **23.82** | 50.98 | 74.64 | 195.9 | 20 | **1.03 / 1.20 GB** | 98 % | 14.9 s | 302.81 | 10.96 | 0 | 7565 |
| 50 | vanilla | stopped by its watchdog | | | | | | | 11.2 s | | | | |
Chunks each bot received over the whole run, by behaviour (mean):
| Bots | Server | walk | explore | idle |
|---:|---|---:|---:|---:|
| 1 | Horizon | 6092 | | |
| 1 | vanilla | 6084 | | |
| 10 | Horizon | 6248 | 1165 | 473 |
| 10 | vanilla | 5729 | 1124 | 473 |
| 25 | Horizon | 6629 | 1267 | 473 |
| 25 | vanilla | 5387 | 1119 | 473 |
| 50 | Horizon | 6497 | 1256 | 473 |
Both servers pulled bots back about as often (`moved too quickly` 20 and 296 times on vanilla at 10 and 25
bots).
## Equal work
`tools/server_bench.py` now prints a work table after the runs. At each bot count it compares every server's
chunks sent a second and first-time chunks a second with the busiest server's. Work counts as equal when
both are within 10 % (or within a chunk a second when there are few). The report keeps the table as `work`.
| Bots | Chunks/s (Horizon, vanilla) | First-time chunks/s (Horizon, vanilla) | Equal |
|---:|---|---|---|
| 1 | 9.94, 9.98 | 0.01, 0.01 | yes |
| 10 | 61.35, 61.66 | 1.79, 1.93 | yes |
| 25 | 158.57, 157.63 | 5.78, 8.25 | no: vanilla sent 2.5 more first-time chunks a second |
Two settings made the load the same:
- **Explore bots paced at 0.8 blocks a second** (`--explore-speed 0.8`), the rate the 2026-10-17 run showed
both servers keep up with. Every bot then follows the same path on both servers. The path is deterministic
per bot, and a bot only stops while the chunk it stands in hasn't arrived, which never happened here
(`Waited` 0). So both servers had the same chunks to send.
- **A 300-second warm-up** before the 300 measured seconds. A first pass with the old 60-second warm-up
(aborted after 10 bots) sent the same chunks a second on both (63.1 against 61.4). But vanilla was still
delivering the bots' first views during the measurement (12.9 first-time chunks a second against Horizon's
4.1), so its window held more work than Horizon's. With 300 seconds both were in steady state at 1 and 10
bots. At 25, vanilla still sent 2.5 more first-time chunks a second than Horizon: 1.6 % of all it sent, and
its walking bots ended the run with 19 % fewer chunks. Read the 25-bot tick times with that in mind: the
extra first-time chunks are work vanilla had still to do because it fell behind.
## Method
`tools/server_bench.py` ran each configuration on a fresh server:
- the same `server.properties` on both: seed 12345, view and simulation distance 10, offline, creative,
difficulty easy, `sync-chunk-writes=true`, white-list off;
- `gamerule respawn_radius 0`, and otherwise vanilla's default game rules (the locator bar on for both);
- the server waits for `Done`, the bots join one a second, warm up for 300 seconds and are measured for 300,
with `/tick query` every 5 seconds and RSS and CPU every second.
The bots are `horizon-bots --behaviour mixed`: 60 % walk, 20 % explore and 20 % idle, each choice deterministic
per bot. All are creative and fly 24 blocks above spawn. `walk` flies to random points within 48 blocks at
5.6 blocks a second, `explore` flies straight out at 0.8 blocks a second, and `idle` hovers.
**Machine.** The ThinkPad: Intel Core i7-7500U (2 cores, 4 threads), 32 GB, NixOS. The desktop (24 threads)
was meant to run it, but it didn't wake (`~/fleet/power wake desktop` timed out, as it did for the first run).
No other session ran on the ThinkPad during the runs (`load.txt`, a sample a minute). The servers ran under
`nice -n 19`. The bots ran on the Mac mini (M4 Pro, shared with other work at 70 to 90 % CPU) and reached the
ThinkPad over the tailnet.
**What ran.**
- Horizon: the server build from #916 (`horizon-client --no-default-features`, no sound or controllers) at
519ec121, packaged with `xtask package-server` and installed with `install.sh --archive`, as Linux owners
will get it. `horizon-server` ran it as `horizon-engine`. The game files were prepared from the owner's own
Minecraft. `horizon-bots` and `server_bench.py` came from this PR.
- Vanilla: the pinned 26.3 server jar (the owner's own copy) on OpenJDK 25.0.4.1, `-Xms4g -Xmx4g`.
`server_bench.py` stopped after vanilla's 50-bot server crashed and before it wrote its report.
`mixed-equal.json` holds the run lines it printed. The vanilla 50 entry and its error come from that server's
log, and the `work` table was computed with `server_bench.work`. `load.txt` is the fleet monitor's view of the
machines during the runs.
## Reproduce
curl -fsSL https://raw.githubusercontent.com/ludovic111/horizon/main/packaging/server/install.sh | sh
horizon-prepare --prepare --server-jar /path/to/server.jar # your own copy
cargo build --release -p horizon-bots # on the machine that runs the bots
# in a checkout whose .local/minecraft holds the pinned vanilla jars (tools/lab.py prepare):
HORIZON_BENCH_BIN=~/.local/share/horizon/current \
python3 tools/server_bench.py --servers horizon,vanilla --bots 1,10,25,50 --behaviour mixed \
--explore-speed 0.8 --warmup 300 --nice 19 \
--bots-via 'ssh <bots machine>' --bots-bin /path/to/horizon-bots --connect <this machine's address> \
--out mixed-equal.json
## Next
- One pass per configuration ran. Vanilla at 50 bots, in particular, needs a second pass, ideally on the
desktop (24 threads), where neither server is CPU-bound.
- 100 bots, and Paper and Fabric + Lithium (docs/server.md section 9).
- A longer warm-up, or measuring from when the first-time chunk rate settles, so the 25-bot work is equal too.
@@ -0,0 +1,702 @@
2026-10-18T19:32:39Z
ludothinkpad ok: 27.2 GB free, 4 threads 17% busy, 0 sessions
desktop offline
macmini ok: 25.9 GB free, 14 threads 73% busy, 1 sessions
macbook offline
RAM 27.0 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 42% busy (agents 0.0, rest 1.6 cores) · 80°C · load 5.9
· disk 289.5 GB free · battery 96% · Ludovic: away
2026-10-18T19:33:44Z
ludothinkpad ok: 26.7 GB free, 4 threads 19% busy, 0 sessions
desktop offline
macmini busy: 26.7 GB free, 14 threads 93% busy, 1 sessions
macbook offline
RAM 26.7 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 34% busy (agents 0.0, rest 1.7 cores) · 81°C · load 4.0
· disk 289.5 GB free · battery 96% · Ludovic: away
2026-10-18T19:34:48Z
ludothinkpad ok: 26.4 GB free, 4 threads 56% busy, 0 sessions
desktop offline
macmini busy: 26.9 GB free, 14 threads 90% busy, 1 sessions
macbook offline
RAM 26.4 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 54% busy (agents 0.0, rest 2.2 cores) · 79°C · load 2.8
· disk 289.5 GB free · battery 96% · Ludovic: away
2026-10-18T19:35:52Z
ludothinkpad ok: 26.7 GB free, 4 threads 27% busy, 0 sessions
desktop offline
macmini ok: 25.4 GB free, 14 threads 85% busy, 1 sessions
macbook offline
RAM 26.6 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 30% busy (agents 0.0, rest 1.1 cores) · 82°C · load 2.2
· disk 289.5 GB free · battery 96% · Ludovic: away
2026-10-18T19:36:57Z
ludothinkpad ok: 26.7 GB free, 4 threads 25% busy, 0 sessions
desktop offline
macmini busy: 25.4 GB free, 14 threads 93% busy, 1 sessions
macbook offline
RAM 26.7 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 37% busy (agents 0.0, rest 1.2 cores) · 80°C · load 1.5
· disk 289.5 GB free · battery 96% · Ludovic: away
2026-10-18T19:38:01Z
ludothinkpad ok: 26.6 GB free, 4 threads 54% busy, 0 sessions
desktop offline
macmini ok: 26.7 GB free, 14 threads 64% busy, 1 sessions
macbook offline
RAM 26.6 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 21% busy (agents 0.0, rest 1.2 cores) · 81°C · load 1.9
· disk 289.5 GB free · battery 96% · Ludovic: away
2026-10-18T19:39:05Z
ludothinkpad ok: 26.7 GB free, 4 threads 20% busy, 0 sessions
desktop offline
macmini ok: 26.1 GB free, 14 threads 70% busy, 1 sessions
macbook offline
RAM 26.7 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 28% busy (agents 0.0, rest 1.0 cores) · 81°C · load 1.3
· disk 289.5 GB free · battery 96% · Ludovic: away
2026-10-18T19:40:10Z
ludothinkpad ok: 26.7 GB free, 4 threads 39% busy, 0 sessions
desktop offline
macmini ok: 29.2 GB free, 14 threads 73% busy, 1 sessions
macbook offline
RAM 26.6 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 32% busy (agents 0.0, rest 1.7 cores) · 80°C · load 0.9
· disk 289.5 GB free · battery 96% · Ludovic: away
2026-10-18T19:41:14Z
ludothinkpad ok: 26.7 GB free, 4 threads 31% busy, 0 sessions
desktop offline
macmini ok: 28.8 GB free, 14 threads 77% busy, 1 sessions
macbook offline
RAM 26.7 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 15% busy (agents 0.0, rest 1.1 cores) · 82°C · load 1.3
· disk 289.5 GB free · battery 96% · Ludovic: away
2026-10-18T19:42:19Z
ludothinkpad ok: 26.5 GB free, 4 threads 26% busy, 0 sessions
desktop offline
macmini busy: 26.1 GB free, 14 threads 94% busy, 1 sessions
macbook offline
RAM 26.2 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 30% busy (agents 0.0, rest 1.3 cores) · 78°C · load 1.1
· disk 289.6 GB free · battery 96% · Ludovic: away
2026-10-18T19:43:23Z
ludothinkpad busy: 26.7 GB free, 4 threads 99% busy, 0 sessions
desktop offline
macmini ok: 28.2 GB free, 14 threads 83% busy, 1 sessions
macbook offline
RAM 26.3 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 99% busy (agents 0.0, rest 3.8 cores) · 86°C · load 2.4
· disk 289.6 GB free · battery 96% · Ludovic: away
2026-10-18T19:44:27Z
ludothinkpad busy: 25.2 GB free, 4 threads 98% busy, 0 sessions
desktop offline
macmini ok: 29.0 GB free, 14 threads 73% busy, 1 sessions
macbook offline
RAM 24.9 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 83% busy (agents 0.0, rest 3.7 cores) · 83°C · load 5.5
· disk 289.5 GB free · battery 96% · Ludovic: away
2026-10-18T19:45:31Z
ludothinkpad ok: 24.5 GB free, 4 threads 39% busy, 0 sessions
desktop offline
macmini busy: 27.0 GB free, 14 threads 93% busy, 1 sessions
macbook offline
RAM 24.6 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 34% busy (agents 0.0, rest 1.5 cores) · 84°C · load 3.5
· disk 289.5 GB free · battery 96% · Ludovic: away
2026-10-18T19:46:35Z
ludothinkpad ok: 23.7 GB free, 4 threads 69% busy, 0 sessions
desktop offline
macmini busy: 26.2 GB free, 14 threads 90% busy, 1 sessions
macbook offline
RAM 23.8 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 75% busy (agents 0.0, rest 2.8 cores) · 87°C · load 2.7
· disk 289.5 GB free · battery 96% · Ludovic: away
2026-10-18T19:47:40Z
ludothinkpad ok: 23.8 GB free, 4 threads 66% busy, 0 sessions
desktop offline
macmini busy: 25.9 GB free, 14 threads 85% busy, 1 sessions
macbook offline
RAM 23.8 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 59% busy (agents 0.0, rest 2.5 cores) · 83°C · load 2.9
· disk 291.7 GB free · battery 96% · Ludovic: away
2026-10-18T19:48:44Z
ludothinkpad ok: 24.2 GB free, 4 threads 61% busy, 0 sessions
desktop offline
macmini busy: 29.0 GB free, 14 threads 90% busy, 1 sessions
macbook offline
RAM 24.3 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 62% busy (agents 0.0, rest 2.5 cores) · 84°C · load 3.2
· disk 291.6 GB free · battery 96% · Ludovic: away
2026-10-18T19:49:48Z
ludothinkpad ok: 25.1 GB free, 4 threads 59% busy, 0 sessions
desktop offline
macmini ok: 31.1 GB free, 14 threads 81% busy, 1 sessions
macbook offline
RAM 25.1 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 57% busy (agents 0.0, rest 2.4 cores) · 85°C · load 2.9
· disk 291.6 GB free · battery 96% · Ludovic: away
2026-10-18T19:50:52Z
ludothinkpad ok: 25.7 GB free, 4 threads 39% busy, 0 sessions
desktop offline
macmini busy: 28.4 GB free, 14 threads 88% busy, 2 sessions
macbook offline
RAM 25.8 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 18% busy (agents 0.0, rest 1.4 cores) · 67°C · load 2.4
· disk 291.6 GB free · battery 96% · Ludovic: away
2026-10-18T19:51:57Z
ludothinkpad ok: 23.7 GB free, 4 threads 40% busy, 0 sessions
desktop offline
macmini ok: 27.9 GB free, 14 threads 83% busy, 1 sessions
macbook offline
RAM 23.7 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 23% busy (agents 0.0, rest 1.4 cores) · 81°C · load 1.8
· disk 291.6 GB free · battery 96% · Ludovic: away
2026-10-18T19:53:01Z
ludothinkpad ok: 25.3 GB free, 4 threads 25% busy, 0 sessions
desktop offline
macmini ok: 27.1 GB free, 14 threads 84% busy, 1 sessions
macbook offline
RAM 25.1 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 31% busy (agents 0.0, rest 1.2 cores) · 80°C · load 1.8
· disk 291.6 GB free · battery 96% · Ludovic: away
2026-10-18T19:54:05Z
ludothinkpad ok: 26.5 GB free, 4 threads 75% busy, 0 sessions
desktop offline
macmini busy: 27.1 GB free, 14 threads 91% busy, 1 sessions
macbook offline
RAM 26.5 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 69% busy (agents 0.0, rest 2.5 cores) · 78°C · load 2.3
· disk 291.6 GB free · battery 96% · Ludovic: away
2026-10-18T19:55:09Z
ludothinkpad busy: 26.4 GB free, 4 threads 89% busy, 0 sessions
desktop offline
macmini busy: 26.3 GB free, 14 threads 86% busy, 1 sessions
macbook offline
RAM 26.3 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 32% busy (agents 0.0, rest 2.4 cores) · 79°C · load 2.9
· disk 291.6 GB free · battery 96% · Ludovic: away
2026-10-18T19:56:14Z
ludothinkpad ok: 26.4 GB free, 4 threads 39% busy, 0 sessions
desktop offline
macmini ok: 27.1 GB free, 14 threads 68% busy, 1 sessions
macbook offline
RAM 26.4 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 63% busy (agents 0.0, rest 1.8 cores) · 82°C · load 2.3
· disk 291.6 GB free · battery 96% · Ludovic: away
2026-10-18T19:57:18Z
ludothinkpad ok: 26.3 GB free, 4 threads 20% busy, 0 sessions
desktop offline
macmini busy: 29.2 GB free, 14 threads 85% busy, 1 sessions
macbook offline
RAM 26.3 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 19% busy (agents 0.0, rest 1.0 cores) · 80°C · load 1.8
· disk 291.6 GB free · battery 96% · Ludovic: away
2026-10-18T19:58:22Z
ludothinkpad ok: 26.3 GB free, 4 threads 28% busy, 0 sessions
desktop offline
macmini busy: 27.3 GB free, 14 threads 88% busy, 1 sessions
macbook offline
RAM 26.1 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 93% busy (agents 0.0, rest 2.5 cores) · 80°C · load 2.0
· disk 291.6 GB free · battery 96% · Ludovic: away
2026-10-18T19:59:27Z
ludothinkpad ok: 26.2 GB free, 4 threads 52% busy, 0 sessions
desktop offline
macmini ok: 35.0 GB free, 14 threads 85% busy, 1 sessions
macbook offline
RAM 26.2 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 22% busy (agents 0.0, rest 1.8 cores) · 80°C · load 3.6
· disk 291.4 GB free · battery 96% · Ludovic: away
2026-10-18T20:00:31Z
ludothinkpad busy: 25.8 GB free, 4 threads 87% busy, 0 sessions
desktop offline
macmini busy: 33.0 GB free, 14 threads 87% busy, 1 sessions
macbook offline
RAM 27.5 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 51% busy (agents 0.0, rest 2.9 cores) · 79°C · load 3.6
· disk 291.5 GB free · battery 96% · Ludovic: away
2026-10-18T20:01:35Z
ludothinkpad ok: 25.6 GB free, 4 threads 35% busy, 0 sessions
desktop offline
macmini ok: 32.4 GB free, 14 threads 81% busy, 1 sessions
macbook ok: 7.2 GB free, 12 threads 32% busy, 0 sessions
RAM 25.6 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 34% busy (agents 0.0, rest 1.3 cores) · 81°C · load 2.5
· disk 291.6 GB free · battery 96% · Ludovic: away
2026-10-18T20:02:40Z
ludothinkpad ok: 26.6 GB free, 4 threads 35% busy, 0 sessions
desktop offline
macmini ok: 29.2 GB free, 14 threads 79% busy, 1 sessions
macbook ok: 6.5 GB free, 12 threads 39% busy, 0 sessions
RAM 26.5 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 33% busy (agents 0.0, rest 1.3 cores) · 81°C · load 2.7
· disk 291.6 GB free · battery 96% · Ludovic: away
2026-10-18T20:03:44Z
ludothinkpad ok: 25.5 GB free, 4 threads 56% busy, 0 sessions
desktop offline
macmini busy: 31.1 GB free, 14 threads 89% busy, 1 sessions
macbook ok: 6.8 GB free, 12 threads 6% busy, 0 sessions
RAM 25.2 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 66% busy (agents 0.0, rest 2.5 cores) · 87°C · load 2.5
· disk 291.3 GB free · battery 96% · Ludovic: away
2026-10-18T20:04:48Z
ludothinkpad busy: 26.2 GB free, 4 threads 99% busy, 0 sessions
desktop offline
macmini busy: 30.0 GB free, 14 threads 89% busy, 1 sessions
macbook ok: 6.7 GB free, 12 threads 31% busy, 0 sessions
RAM 25.8 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 84% busy (agents 0.0, rest 3.7 cores) · 85°C · load 3.4
· disk 291.3 GB free · battery 96% · Ludovic: away
2026-10-18T20:05:52Z
ludothinkpad busy: 24.5 GB free, 4 threads 100% busy, 0 sessions
desktop offline
macmini busy: 34.1 GB free, 14 threads 89% busy, 1 sessions
macbook ok: 6.6 GB free, 12 threads 10% busy, 0 sessions
RAM 24.6 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 100% busy (agents 0.0, rest 4.0 cores) · 83°C · load 9.0
· disk 291.0 GB free · battery 96% · Ludovic: away
2026-10-18T20:06:57Z
ludothinkpad busy: 23.5 GB free, 4 threads 100% busy, 0 sessions
desktop offline
macmini busy: 28.3 GB free, 14 threads 88% busy, 1 sessions
macbook ok: 6.7 GB free, 12 threads 5% busy, 0 sessions
RAM 23.6 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 99% busy (agents 0.0, rest 4.0 cores) · 82°C · load 10.1
· disk 309.2 GB free · battery 96% · Ludovic: away
2026-10-18T20:08:01Z
ludothinkpad busy: 23.4 GB free, 4 threads 88% busy, 0 sessions
desktop offline
macmini ok: 29.6 GB free, 14 threads 83% busy, 1 sessions
macbook ok: 6.6 GB free, 12 threads 2% busy, 0 sessions
RAM 23.4 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 77% busy (agents 0.0, rest 3.4 cores) · 84°C · load 7.5
· disk 309.2 GB free · battery 96% · Ludovic: away
2026-10-18T20:09:05Z
ludothinkpad busy: 24.0 GB free, 4 threads 89% busy, 0 sessions
desktop offline
macmini ok: 25.9 GB free, 14 threads 83% busy, 1 sessions
macbook ok: 7.1 GB free, 12 threads 19% busy, 0 sessions
RAM 23.9 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 91% busy (agents 0.0, rest 3.6 cores) · 82°C · load 5.9
· disk 309.1 GB free · battery 96% · Ludovic: away
2026-10-18T20:10:09Z
ludothinkpad busy: 24.0 GB free, 4 threads 98% busy, 0 sessions
desktop offline
macmini busy: 27.1 GB free, 14 threads 88% busy, 1 sessions
macbook ok: 6.7 GB free, 12 threads 19% busy, 0 sessions
RAM 24.0 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 92% busy (agents 0.0, rest 3.7 cores) · 84°C · load 6.6
· disk 309.1 GB free · battery 96% · Ludovic: away
2026-10-18T20:11:14Z
ludothinkpad busy: 24.3 GB free, 4 threads 92% busy, 0 sessions
desktop offline
macmini ok: 26.3 GB free, 14 threads 84% busy, 1 sessions
macbook ok: 6.6 GB free, 12 threads 18% busy, 0 sessions
RAM 24.4 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 99% busy (agents 0.0, rest 3.7 cores) · 85°C · load 6.5
· disk 309.0 GB free · battery 96% · Ludovic: away
2026-10-18T20:12:18Z
ludothinkpad busy: 24.7 GB free, 4 threads 89% busy, 0 sessions
desktop offline
macmini ok: 28.6 GB free, 14 threads 78% busy, 1 sessions
macbook ok: 7.3 GB free, 12 threads 20% busy, 0 sessions
RAM 24.7 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 82% busy (agents 0.0, rest 3.4 cores) · 84°C · load 6.1
· disk 309.0 GB free · battery 96% · Ludovic: away
2026-10-18T20:13:22Z
ludothinkpad ok: 23.4 GB free, 4 threads 53% busy, 0 sessions
desktop offline
macmini ok: 28.1 GB free, 14 threads 77% busy, 1 sessions
macbook ok: 7.3 GB free, 12 threads 20% busy, 0 sessions
RAM 23.4 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 48% busy (agents 0.0, rest 2.4 cores) · 80°C · load 4.2
· disk 309.0 GB free · battery 96% · Ludovic: away
2026-10-18T20:14:27Z
ludothinkpad ok: 24.0 GB free, 4 threads 44% busy, 0 sessions
desktop offline
macmini ok: 28.0 GB free, 14 threads 71% busy, 1 sessions
macbook ok: 6.9 GB free, 12 threads 2% busy, 0 sessions
RAM 24.1 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 23% busy (agents 0.0, rest 2.0 cores) · 77°C · load 4.3
· disk 309.0 GB free · battery 96% · Ludovic: away
2026-10-18T20:15:31Z
ludothinkpad ok: 22.7 GB free, 4 threads 31% busy, 0 sessions
desktop offline
macmini busy: 27.2 GB free, 14 threads 86% busy, 1 sessions
macbook ok: 6.9 GB free, 12 threads 19% busy, 0 sessions
RAM 22.5 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 30% busy (agents 0.0, rest 1.3 cores) · 77°C · load 2.6
· disk 309.1 GB free · battery 96% · Ludovic: away
2026-10-18T20:16:35Z
ludothinkpad ok: 26.8 GB free, 4 threads 37% busy, 0 sessions
desktop offline
macmini ok: 28.6 GB free, 14 threads 82% busy, 1 sessions
macbook ok: 6.8 GB free, 12 threads 9% busy, 0 sessions
RAM 26.8 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 42% busy (agents 0.0, rest 1.7 cores) · 81°C · load 2.3
· disk 309.1 GB free · battery 96% · Ludovic: away
2026-10-18T20:17:39Z
ludothinkpad ok: 26.3 GB free, 4 threads 63% busy, 0 sessions
desktop offline
macmini ok: 26.8 GB free, 14 threads 78% busy, 1 sessions
macbook ok: 7.0 GB free, 12 threads 19% busy, 0 sessions
RAM 26.4 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 64% busy (agents 0.0, rest 2.6 cores) · 87°C · load 1.9
· disk 309.0 GB free · battery 96% · Ludovic: away
2026-10-18T20:18:43Z
ludothinkpad ok: 26.9 GB free, 4 threads 37% busy, 0 sessions
desktop offline
macmini busy: 32.2 GB free, 14 threads 87% busy, 1 sessions
macbook ok: 7.0 GB free, 12 threads 10% busy, 0 sessions
RAM 26.9 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 56% busy (agents 0.0, rest 1.9 cores) · 84°C · load 3.9
· disk 309.0 GB free · battery 96% · Ludovic: away
2026-10-18T20:19:48Z
ludothinkpad ok: 26.0 GB free, 4 threads 77% busy, 0 sessions
desktop offline
macmini ok: 29.2 GB free, 14 threads 83% busy, 1 sessions
macbook ok: 7.0 GB free, 12 threads 10% busy, 0 sessions
RAM 26.0 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 66% busy (agents 0.0, rest 2.9 cores) · 86°C · load 3.2
· disk 309.0 GB free · battery 96% · Ludovic: away
2026-10-18T20:20:52Z
ludothinkpad ok: 25.9 GB free, 4 threads 80% busy, 0 sessions
desktop offline
macmini ok: 26.8 GB free, 14 threads 84% busy, 1 sessions
macbook ok: 7.0 GB free, 12 threads 17% busy, 0 sessions
RAM 25.9 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 68% busy (agents 0.0, rest 3.2 cores) · 86°C · load 4.4
· disk 309.0 GB free · battery 96% · Ludovic: away
2026-10-18T20:21:56Z
ludothinkpad ok: 26.3 GB free, 4 threads 63% busy, 0 sessions
desktop offline
macmini ok: 26.3 GB free, 14 threads 77% busy, 1 sessions
macbook ok: 6.8 GB free, 12 threads 14% busy, 0 sessions
RAM 26.3 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 66% busy (agents 0.0, rest 2.8 cores) · 84°C · load 3.1
· disk 309.0 GB free · battery 96% · Ludovic: away
2026-10-18T20:23:00Z
ludothinkpad ok: 26.8 GB free, 4 threads 74% busy, 0 sessions
desktop offline
macmini ok: 26.1 GB free, 14 threads 82% busy, 1 sessions
macbook ok: 6.7 GB free, 12 threads 11% busy, 0 sessions
RAM 26.7 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 65% busy (agents 0.0, rest 2.8 cores) · 84°C · load 5.0
· disk 308.9 GB free · battery 96% · Ludovic: away
2026-10-18T20:24:05Z
ludothinkpad ok: 26.9 GB free, 4 threads 64% busy, 0 sessions
desktop offline
macmini busy: 28.3 GB free, 14 threads 90% busy, 1 sessions
macbook ok: 6.8 GB free, 12 threads 8% busy, 0 sessions
RAM 26.9 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 64% busy (agents 0.0, rest 2.7 cores) · 86°C · load 4.9
· disk 308.9 GB free · battery 96% · Ludovic: away
2026-10-18T20:25:09Z
ludothinkpad ok: 26.0 GB free, 4 threads 56% busy, 0 sessions
desktop offline
macmini busy: 26.2 GB free, 14 threads 88% busy, 1 sessions
macbook ok: 6.7 GB free, 12 threads 12% busy, 0 sessions
RAM 26.0 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 49% busy (agents 0.0, rest 2.1 cores) · 72°C · load 5.5
· disk 309.0 GB free · battery 96% · Ludovic: away
2026-10-18T20:26:13Z
ludothinkpad ok: 25.8 GB free, 4 threads 47% busy, 0 sessions
desktop offline
macmini ok: 27.8 GB free, 14 threads 78% busy, 1 sessions
macbook ok: 6.7 GB free, 12 threads 8% busy, 0 sessions
RAM 25.8 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 56% busy (agents 0.0, rest 2.1 cores) · 78°C · load 3.1
· disk 308.9 GB free · battery 96% · Ludovic: away
2026-10-18T20:27:18Z
ludothinkpad busy: 25.5 GB free, 4 threads 94% busy, 0 sessions
desktop offline
macmini ok: 26.6 GB free, 14 threads 82% busy, 1 sessions
macbook ok: 6.7 GB free, 12 threads 17% busy, 0 sessions
RAM 25.4 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 100% busy (agents 0.0, rest 3.9 cores) · 78°C · load 4.7
· disk 308.9 GB free · battery 96% · Ludovic: away
2026-10-18T20:28:22Z
ludothinkpad busy: 24.6 GB free, 4 threads 100% busy, 0 sessions
desktop offline
macmini ok: 26.4 GB free, 14 threads 80% busy, 1 sessions
macbook ok: 6.7 GB free, 12 threads 8% busy, 0 sessions
RAM 24.2 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 99% busy (agents 0.0, rest 4.0 cores) · 77°C · load 10.5
· disk 308.9 GB free · battery 96% · Ludovic: away
2026-10-18T20:29:27Z
ludothinkpad busy: 24.3 GB free, 4 threads 100% busy, 0 sessions
desktop offline
macmini ok: 29.8 GB free, 14 threads 80% busy, 1 sessions
macbook ok: 6.7 GB free, 12 threads 8% busy, 0 sessions
RAM 24.0 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 99% busy (agents 0.0, rest 4.0 cores) · 82°C · load 10.0
· disk 308.9 GB free · battery 96% · Ludovic: away
2026-10-18T20:30:31Z
ludothinkpad busy: 23.7 GB free, 4 threads 91% busy, 0 sessions
desktop offline
macmini ok: 30.1 GB free, 14 threads 71% busy, 1 sessions
macbook ok: 6.7 GB free, 12 threads 9% busy, 0 sessions
RAM 23.7 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 94% busy (agents 0.0, rest 3.8 cores) · 80°C · load 10.3
· disk 308.9 GB free · battery 96% · Ludovic: away
2026-10-18T20:31:36Z
ludothinkpad busy: 23.6 GB free, 4 threads 93% busy, 0 sessions
desktop offline
macmini ok: 28.0 GB free, 14 threads 81% busy, 1 sessions
macbook ok: 6.6 GB free, 12 threads 14% busy, 0 sessions
RAM 23.6 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 95% busy (agents 0.0, rest 3.7 cores) · 80°C · load 10.7
· disk 308.9 GB free · battery 96% · Ludovic: away
2026-10-18T20:32:40Z
ludothinkpad busy: 23.6 GB free, 4 threads 94% busy, 0 sessions
desktop offline
macmini ok: 26.9 GB free, 14 threads 76% busy, 1 sessions
macbook ok: 6.8 GB free, 12 threads 22% busy, 0 sessions
RAM 23.6 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 84% busy (agents 0.0, rest 3.5 cores) · 80°C · load 9.7
· disk 308.9 GB free · battery 96% · Ludovic: away
2026-10-18T20:33:45Z
ludothinkpad ok: 25.2 GB free, 4 threads 51% busy, 0 sessions
desktop offline
macmini ok: 27.1 GB free, 14 threads 82% busy, 1 sessions
macbook ok: 6.7 GB free, 12 threads 17% busy, 0 sessions
RAM 25.2 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 48% busy (agents 0.0, rest 2.1 cores) · 87°C · load 6.5
· disk 309.0 GB free · battery 96% · Ludovic: away
2026-10-18T20:34:49Z
ludothinkpad ok: 25.1 GB free, 4 threads 56% busy, 0 sessions
desktop offline
macmini busy: 28.2 GB free, 14 threads 87% busy, 1 sessions
macbook ok: 6.7 GB free, 12 threads 17% busy, 0 sessions
RAM 24.5 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 78% busy (agents 0.0, rest 2.7 cores) · 88°C · load 4.0
· disk 309.0 GB free · battery 96% · Ludovic: away
2026-10-18T20:35:53Z
ludothinkpad busy: 23.6 GB free, 4 threads 86% busy, 0 sessions
desktop offline
macmini busy: 27.9 GB free, 14 threads 86% busy, 1 sessions
macbook ok: 6.7 GB free, 12 threads 17% busy, 0 sessions
RAM 23.7 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 88% busy (agents 0.0, rest 3.3 cores) · 85°C · load 4.9
· disk 309.0 GB free · battery 96% · Ludovic: away
2026-10-18T20:36:58Z
ludothinkpad ok: 23.6 GB free, 4 threads 75% busy, 0 sessions
desktop offline
macmini ok: 29.4 GB free, 14 threads 84% busy, 1 sessions
macbook ok: 6.5 GB free, 12 threads 18% busy, 0 sessions
RAM 23.6 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 85% busy (agents 0.0, rest 3.2 cores) · 85°C · load 5.1
· disk 309.0 GB free · battery 96% · Ludovic: away
2026-10-18T20:38:02Z
ludothinkpad ok: 22.6 GB free, 4 threads 55% busy, 0 sessions
desktop offline
macmini ok: 26.5 GB free, 14 threads 80% busy, 1 sessions
macbook ok: 6.7 GB free, 12 threads 10% busy, 0 sessions
RAM 22.3 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 54% busy (agents 0.0, rest 2.2 cores) · 85°C · load 4.5
· disk 308.9 GB free · battery 96% · Ludovic: away
2026-10-18T20:39:06Z
ludothinkpad ok: 27.2 GB free, 4 threads 55% busy, 0 sessions
desktop offline
macmini busy: 29.5 GB free, 14 threads 85% busy, 1 sessions
macbook ok: 6.7 GB free, 12 threads 9% busy, 0 sessions
RAM 27.3 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 48% busy (agents 0.0, rest 2.2 cores) · 87°C · load 4.0
· disk 308.9 GB free · battery 96% · Ludovic: away
2026-10-18T20:40:10Z
ludothinkpad ok: 26.2 GB free, 4 threads 72% busy, 0 sessions
desktop offline
macmini ok: 27.7 GB free, 14 threads 84% busy, 1 sessions
macbook ok: 6.6 GB free, 12 threads 19% busy, 0 sessions
RAM 26.3 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 80% busy (agents 0.0, rest 3.1 cores) · 86°C · load 10.8
· disk 308.9 GB free · battery 96% · Ludovic: away
2026-10-18T20:41:15Z
ludothinkpad busy: 26.4 GB free, 4 threads 86% busy, 0 sessions
desktop offline
macmini ok: 28.5 GB free, 14 threads 79% busy, 1 sessions
macbook ok: 6.6 GB free, 12 threads 10% busy, 0 sessions
RAM 26.4 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 90% busy (agents 0.0, rest 3.4 cores) · 86°C · load 12.8
· disk 308.9 GB free · battery 96% · Ludovic: away
2026-10-18T20:42:19Z
ludothinkpad ok: 26.2 GB free, 4 threads 82% busy, 0 sessions
desktop offline
macmini ok: 26.9 GB free, 14 threads 78% busy, 1 sessions
macbook ok: 6.6 GB free, 12 threads 11% busy, 0 sessions
RAM 26.2 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 74% busy (agents 0.0, rest 3.3 cores) · 85°C · load 8.8
· disk 308.9 GB free · battery 96% · Ludovic: away
2026-10-18T20:43:23Z
ludothinkpad busy: 26.7 GB free, 4 threads 93% busy, 0 sessions
desktop offline
macmini ok: 27.3 GB free, 14 threads 81% busy, 1 sessions
macbook ok: 6.6 GB free, 12 threads 10% busy, 0 sessions
RAM 26.7 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 88% busy (agents 0.0, rest 3.4 cores) · 85°C · load 5.4
· disk 308.9 GB free · battery 96% · Ludovic: away
2026-10-18T20:44:27Z
ludothinkpad ok: 27.0 GB free, 4 threads 82% busy, 0 sessions
desktop offline
macmini busy: 26.9 GB free, 14 threads 87% busy, 1 sessions
macbook ok: 6.7 GB free, 12 threads 13% busy, 0 sessions
RAM 27.0 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 75% busy (agents 0.0, rest 3.3 cores) · 82°C · load 9.8
· disk 308.8 GB free · battery 96% · Ludovic: away
2026-10-18T20:45:32Z
ludothinkpad ok: 27.2 GB free, 4 threads 76% busy, 0 sessions
desktop offline
macmini ok: 26.8 GB free, 14 threads 80% busy, 1 sessions
macbook ok: 6.7 GB free, 12 threads 10% busy, 0 sessions
RAM 27.2 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 84% busy (agents 0.0, rest 3.2 cores) · 85°C · load 6.9
· disk 308.8 GB free · battery 96% · Ludovic: away
2026-10-18T20:46:36Z
ludothinkpad busy: 27.1 GB free, 4 threads 87% busy, 0 sessions
desktop offline
macmini busy: 26.8 GB free, 14 threads 92% busy, 1 sessions
macbook ok: 6.7 GB free, 12 threads 9% busy, 0 sessions
RAM 26.8 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 85% busy (agents 0.0, rest 3.2 cores) · 82°C · load 4.4
· disk 308.8 GB free · battery 96% · Ludovic: away
2026-10-18T20:47:40Z
ludothinkpad busy: 25.3 GB free, 4 threads 86% busy, 0 sessions
desktop offline
macmini ok: 26.5 GB free, 14 threads 72% busy, 1 sessions
macbook ok: 6.7 GB free, 12 threads 9% busy, 0 sessions
RAM 25.5 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 85% busy (agents 0.0, rest 3.4 cores) · 86°C · load 3.7
· disk 308.8 GB free · battery 96% · Ludovic: away
2026-10-18T20:48:45Z
ludothinkpad busy: 26.1 GB free, 4 threads 100% busy, 0 sessions
desktop offline
macmini ok: 28.5 GB free, 14 threads 84% busy, 1 sessions
macbook ok: 6.7 GB free, 12 threads 15% busy, 0 sessions
RAM 26.1 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 98% busy (agents 0.0, rest 3.9 cores) · 80°C · load 6.5
· disk 308.8 GB free · battery 96% · Ludovic: away
2026-10-18T20:49:50Z
ludothinkpad ok: 26.8 GB free, 4 threads 71% busy, 0 sessions
desktop offline
macmini busy: 27.7 GB free, 14 threads 93% busy, 1 sessions
macbook ok: 6.7 GB free, 12 threads 10% busy, 0 sessions
RAM 26.6 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 92% busy (agents 0.0, rest 3.4 cores) · 81°C · load 5.2
· disk 308.7 GB free · battery 96% · Ludovic: away
2026-10-18T20:50:54Z
ludothinkpad busy: 25.8 GB free, 4 threads 100% busy, 0 sessions
desktop offline
macmini busy: 31.3 GB free, 14 threads 95% busy, 1 sessions
macbook ok: 6.7 GB free, 12 threads 11% busy, 0 sessions
RAM 25.8 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 99% busy (agents 0.0, rest 4.0 cores) · 82°C · load 8.1
· disk 308.7 GB free · battery 96% · Ludovic: away
2026-10-18T20:51:59Z
ludothinkpad ok: 27.2 GB free, 4 threads 12% busy, 0 sessions
desktop offline
macmini ok: 31.2 GB free, 14 threads 76% busy, 1 sessions
macbook ok: 6.7 GB free, 12 threads 8% busy, 0 sessions
RAM 27.2 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 15% busy (agents 0.0, rest 1.3 cores) · 81°C · load 7.9
· disk 308.7 GB free · battery 96% · Ludovic: away
2026-10-18T20:53:03Z
ludothinkpad ok: 27.3 GB free, 4 threads 42% busy, 0 sessions
desktop offline
macmini busy: 27.8 GB free, 14 threads 87% busy, 1 sessions
macbook ok: 6.7 GB free, 12 threads 9% busy, 0 sessions
RAM 27.2 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 14% busy (agents 0.0, rest 0.8 cores) · 82°C · load 3.1
· disk 308.7 GB free · battery 96% · Ludovic: away
2026-10-18T20:54:08Z
ludothinkpad ok: 28.9 GB free, 4 threads 4% busy, 0 sessions
desktop offline
macmini ok: 29.2 GB free, 14 threads 80% busy, 1 sessions
macbook ok: 6.7 GB free, 12 threads 9% busy, 0 sessions
RAM 28.9 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 2% busy (agents 0.0, rest 0.3 cores) · 82°C · load 1.3
· disk 308.7 GB free · battery 96% · Ludovic: away
2026-10-18T20:55:12Z
ludothinkpad ok: 27.7 GB free, 4 threads 27% busy, 0 sessions
desktop offline
macmini ok: 31.2 GB free, 14 threads 70% busy, 1 sessions
macbook ok: 6.7 GB free, 12 threads 10% busy, 0 sessions
RAM 27.6 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 26% busy (agents 0.0, rest 1.2 cores) · 69°C · load 1.0
· disk 308.8 GB free · battery 96% · Ludovic: away
@@ -0,0 +1,421 @@
{
"kind": "horizon-server-benchmark",
"date": "2026-10-18T19:32:00+00:00",
"machine": {
"system": "Linux",
"machine": "x86_64",
"cpus": 4,
"name": "ThinkPad, Intel Core i7-7500U, 32 GB, NixOS"
},
"commit": "f1d5bf520986b00a5b968a84d85809fca1a039fa",
"engine": "horizon-engine: the server build of #916 (horizon-client --no-default-features) at 519ec121, as install.sh --archive installed it",
"java": [
"openjdk 25.0.4.1 2026-08-18"
],
"settings": {
"servers": "horizon,vanilla",
"bots": "1,10,25,50",
"behaviour": "mixed",
"join_every": 1.0,
"warmup": 300,
"seconds": 300,
"seed": 12345,
"explore_speed": 0.8,
"gamerule": "",
"heap": "4g",
"nice": 19,
"bots_via": "ssh macmini",
"connect": "the ThinkPad's tailnet address",
"properties": {
"level-seed": "12345",
"view-distance": "10",
"simulation-distance": "10",
"online-mode": "false",
"white-list": "false",
"gamemode": "creative",
"force-gamemode": "true",
"spawn-protection": "0",
"max-players": "200",
"difficulty": "easy",
"sync-chunk-writes": "true"
}
},
"note": "server_bench.py was stopped after vanilla's 50-bot server crashed, before it wrote this file; the runs are the lines it printed, and the vanilla 50 entry is taken from that server's log.",
"runs": [
{
"server": "horizon",
"bots": 1,
"behaviour": "mixed",
"start_seconds": 19.73,
"mspt": {
"average": 1.77,
"p50": 1.34,
"p95": 4.21,
"p99": 13.04
},
"mspt_worst_p99": 47.5,
"tps": 20.0,
"rss_mb": {
"mean": 686.54,
"max": 686.7
},
"cpu_percent": 27.92,
"chunks_per_second": 9.94,
"unique_per_second": 0.01,
"forgotten_per_second": 9.9,
"waited_ticks_per_second": 0.0,
"chunks_total": 6092,
"chunks_per_bot": {
"walk": 6092.0
},
"unique_chunks_per_bot": {
"walk": 699.0
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 1,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "vanilla",
"bots": 1,
"behaviour": "mixed",
"start_seconds": 20.45,
"mspt": {
"average": 11.34,
"p50": 10.6,
"p95": 17.12,
"p99": 33.22
},
"mspt_worst_p99": 119.1,
"tps": 20.0,
"rss_mb": {
"mean": 3404.16,
"max": 3416.3
},
"cpu_percent": 35.93,
"chunks_per_second": 9.98,
"unique_per_second": 0.01,
"forgotten_per_second": 9.93,
"waited_ticks_per_second": 0.0,
"chunks_total": 6084,
"chunks_per_bot": {
"walk": 6084.0
},
"unique_chunks_per_bot": {
"walk": 699.0
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 1,
"bots_failed": [],
"tick_queries": 59
},
{
"server": "horizon",
"bots": 10,
"behaviour": "mixed",
"start_seconds": 19.54,
"mspt": {
"average": 4.74,
"p50": 3.83,
"p95": 10.48,
"p99": 19.69
},
"mspt_worst_p99": 120.1,
"tps": 20.0,
"rss_mb": {
"mean": 855.26,
"max": 882.7
},
"cpu_percent": 29.78,
"chunks_per_second": 61.35,
"unique_per_second": 1.79,
"forgotten_per_second": 61.47,
"waited_ticks_per_second": 0.0,
"chunks_total": 40762,
"chunks_per_bot": {
"walk": 6247.67,
"explore": 1165.0,
"idle": 473.0
},
"unique_chunks_per_bot": {
"walk": 701.83,
"explore": 1086.0,
"idle": 473.0
},
"resent": 0,
"pulled_back": 4159,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 10,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "vanilla",
"bots": 10,
"behaviour": "mixed",
"start_seconds": 19.95,
"mspt": {
"average": 34.3,
"p50": 32.2,
"p95": 53.72,
"p99": 89.08
},
"mspt_worst_p99": 375.8,
"tps": 20.0,
"rss_mb": {
"mean": 3713.5,
"max": 3754.9
},
"cpu_percent": 128.07,
"chunks_per_second": 61.66,
"unique_per_second": 1.93,
"forgotten_per_second": 61.71,
"waited_ticks_per_second": 0.0,
"chunks_total": 37568,
"chunks_per_bot": {
"walk": 5729.0,
"explore": 1124.0,
"idle": 473.0
},
"unique_chunks_per_bot": {
"walk": 701.17,
"explore": 1055.0,
"idle": 473.0
},
"resent": 0,
"pulled_back": 3976,
"moved_too_quickly": 20,
"moved_wrongly": 0,
"bots_joined": 10,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "horizon",
"bots": 25,
"behaviour": "mixed",
"start_seconds": 9.61,
"mspt": {
"average": 11.72,
"p50": 9.88,
"p95": 24.45,
"p99": 43.79
},
"mspt_worst_p99": 129.8,
"tps": 20.0,
"rss_mb": {
"mean": 947.35,
"max": 1086.1
},
"cpu_percent": 57.83,
"chunks_per_second": 158.57,
"unique_per_second": 5.78,
"forgotten_per_second": 158.45,
"waited_ticks_per_second": 0.0,
"chunks_total": 108126,
"chunks_per_bot": {
"walk": 6628.53,
"explore": 1266.6,
"idle": 473.0
},
"unique_chunks_per_bot": {
"walk": 704.67,
"explore": 1189.2,
"idle": 473.0
},
"resent": 0,
"pulled_back": 3496,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 25,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "vanilla",
"bots": 25,
"behaviour": "mixed",
"start_seconds": 37.78,
"mspt": {
"average": 50.45,
"p50": 46.39,
"p95": 74.09,
"p99": 191.21
},
"mspt_worst_p99": 1388.1,
"tps": 19.82,
"rss_mb": {
"mean": 3866.18,
"max": 4745.0
},
"cpu_percent": 192.42,
"chunks_per_second": 157.63,
"unique_per_second": 8.25,
"forgotten_per_second": 156.12,
"waited_ticks_per_second": 0.0,
"chunks_total": 88763,
"chunks_per_bot": {
"walk": 5387.0,
"explore": 1118.6,
"idle": 473.0
},
"unique_chunks_per_bot": {
"walk": 699.0,
"explore": 1075.0,
"idle": 473.0
},
"resent": 2,
"pulled_back": 3629,
"moved_too_quickly": 296,
"moved_wrongly": 0,
"bots_joined": 25,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "horizon",
"bots": 50,
"behaviour": "mixed",
"start_seconds": 14.93,
"mspt": {
"average": 23.82,
"p50": 20.11,
"p95": 50.98,
"p99": 74.64
},
"mspt_worst_p99": 195.9,
"tps": 20.0,
"rss_mb": {
"mean": 1031.41,
"max": 1201.2
},
"cpu_percent": 98.21,
"chunks_per_second": 302.81,
"unique_per_second": 10.96,
"forgotten_per_second": 303.47,
"waited_ticks_per_second": 0.0,
"chunks_total": 212181,
"chunks_per_bot": {
"walk": 6496.53,
"explore": 1255.5,
"idle": 473.0
},
"unique_chunks_per_bot": {
"walk": 701.9,
"explore": 1184.7,
"idle": 473.0
},
"resent": 0,
"pulled_back": 7565,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 50,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "vanilla",
"bots": 50,
"behaviour": "mixed",
"start_seconds": 11.16,
"error": "stopped by its watchdog 30 s after the last bot joined: \"A single server tick took 60.01 seconds (should be max 0.05)\" (Player.tick)"
}
],
"work": [
{
"behaviour": "mixed",
"bots": 1,
"servers": {
"horizon": {
"chunks_per_second": {
"value": 9.94,
"share": 0.996
},
"unique_per_second": {
"value": 0.01,
"share": 1.0
},
"waited_ticks_per_second": 0.0
},
"vanilla": {
"chunks_per_second": {
"value": 9.98,
"share": 1.0
},
"unique_per_second": {
"value": 0.01,
"share": 1.0
},
"waited_ticks_per_second": 0.0
}
},
"equal": true
},
{
"behaviour": "mixed",
"bots": 10,
"servers": {
"horizon": {
"chunks_per_second": {
"value": 61.35,
"share": 0.995
},
"unique_per_second": {
"value": 1.79,
"share": 0.927
},
"waited_ticks_per_second": 0.0
},
"vanilla": {
"chunks_per_second": {
"value": 61.66,
"share": 1.0
},
"unique_per_second": {
"value": 1.93,
"share": 1.0
},
"waited_ticks_per_second": 0.0
}
},
"equal": true
},
{
"behaviour": "mixed",
"bots": 25,
"servers": {
"horizon": {
"chunks_per_second": {
"value": 158.57,
"share": 1.0
},
"unique_per_second": {
"value": 5.78,
"share": 0.701
},
"waited_ticks_per_second": 0.0
},
"vanilla": {
"chunks_per_second": {
"value": 157.63,
"share": 0.994
},
"unique_per_second": {
"value": 8.25,
"share": 1.0
},
"waited_ticks_per_second": 0.0
}
},
"equal": false
}
]
}
@@ -0,0 +1,168 @@
# Horizon's server against vanilla 26.3, equal work (2026-10-17)
The same bots on Horizon's dedicated server and on vanilla 26.3's, one server at a time, on the same
machine, with the work each server does made the same before the numbers are compared. This replaces the
first run in docs/server.md section 9, where the work wasn't equal.
## What was made equal
The first run (PR #556) had three differences that made the comparison unfair. Each is fixed in the code
that ran here:
- **Refused moves.** Vanilla pulls a player back when a move is too fast ("moved too quickly") or ends
inside a block ("moved wrongly"). Horizon now runs both checks the way vanilla does (PR #586), so a bot
that flies too fast is pulled back on both servers.
- **Chunk sending.** Vanilla paces chunks by the client's acknowledgements (PlayerChunkSender: one
unacknowledged batch at first, then ten, each at most the client's desired chunks per tick). Horizon used
to send twice as many per batch; it now paces them the same way (PR #586), and the bots acknowledge
batches and ask for a rate the way vanilla's client does, so both servers send at the pace the client
asks for. Chunks sent again while the bot still held
them (`resent`) are counted: 0 on both servers in every run.
- **Movement gated on chunks.** A bot doesn't move while the chunk it stands in hasn't arrived (vanilla's
client waits the same way), so a slower server isn't asked to teleport bots into terrain it hasn't sent.
One difference isn't in Horizon yet: vanilla 26.3's locator bar (waypoints), which costs vanilla O(players²)
work per move. Every vanilla run here sets `gamerule locator_bar false`, so neither server does it.
## Method
`tools/server_bench.py` runs each configuration on a fresh server:
- the same `server.properties` on both (seed 12345, view and simulation distance 10, offline, creative,
difficulty easy, `sync-chunk-writes=true`), and `gamerule respawn_radius 0`;
- the server waits for `Done`, the bots join one a second, a 60-second warm-up, then 300 seconds of
measurement;
- MSPT (average, P50, P95, P99) comes from each server's own `/tick query`, asked every 5 seconds on the
console, so both measure themselves the same way (vanilla's ServerTickRateManager numbers, which Horizon
reproduces); TPS is 20 while the average fits in 50 ms, else 1000 / MSPT;
- RSS and CPU are the server's whole process tree, once a second (100 % is one core);
- chunk counts come from the bots: chunks received per second, chunks no bot had before (new terrain), and
pull-backs (position packets that move a bot back).
The bots are `horizon-bots` (creative, flying 24 blocks above spawn, 20 moves a second):
- `walk`: flies to random points within 48 blocks of spawn at sprint speed (5.6 blocks a second);
- `explore`: flies straight outwards, by default at about 22 blocks a second, as fast as the server sends
chunks.
Machine: the ThinkPad (Intel Core i7-7500U, 2 cores / 4 threads, 32 GB, NixOS), servers under `nice -n 19`
because it also runs other services; vanilla on OpenJDK 25.0.4.1 with a 4 GB heap (`-Xms4g -Xmx4g`). The
bots ran on the Mac mini and reached the ThinkPad over the tailnet, so they took none of the server's CPU.
What ran: Horizon from `origin/main` at bf851bfe with #586 (move checks and chunk pacing), the bots and
`server_bench.py` from this PR, #623 (sections encoded once, its first commit) and #649 (a tick's packets
handed to each connection together). The ThinkPad copy wasn't a git checkout, so the JSON reports have an
empty `commit`. Vanilla: the pinned 26.3 server jar.
## Results
MSPT is the average milliseconds per tick (50 is the limit for 20 TPS); P95 is the 95th percentile. CPU is
the whole process tree. "New chunks/s" are chunks no bot had before.
### Walk (the same work on both servers)
| Bots | Server | MSPT | P95 | TPS | RSS | CPU | Chunks/s | New chunks/s |
|---:|---|---:|---:|---:|---:|---:|---:|---:|
| 1 | Horizon | 5.5 | 12.1 | 20.0 | 1.37 GB | 57 % | 9 | 0.3 |
| 1 | Vanilla | 18.3 | 27.3 | 20.0 | 3.35 GB | 66 % | 10 | 0.3 |
| 10 | Horizon | 11.5 | 22.1 | 20.0 | 1.41 GB | 124 % | 94 | 2.4 |
| 10 | Vanilla | 16.8 | 30.5 | 20.0 | 3.41 GB | 84 % | 100 | 2.8 |
| 25 | Horizon | 20.7 | 40.4 | 20.0 | 1.45 GB | 170 % | 236 | 5.7 |
| 25 | Vanilla | 16.0 | 26.4 | 20.0 | 3.47 GB | 115 % | 252 | 6.7 |
| 50 | Horizon | 31.4 | 79.0 | 20.0 | 1.52 GB | 173 % | 451 | 10.3 |
| 50 | Vanilla | 20.5 | 29.7 | 20.0 | 3.55 GB | 194 % | 502 | 12.2 |
| 100 | Horizon | 114.5 | 256.2 | 8.7 | 1.33 GB | 181 % | 484 | 30.8 |
| 100 | Vanilla | stopped by its watchdog | | | | | | |
Both servers sent the same chunks (about 10 a second per bot, the churn of flying around) and sent none
twice. Vanilla pulled back a few bots for moving too quickly when a tick ran long (5 to 33 per run);
Horizon none. At 100 bots vanilla's watchdog stopped the server (a tick ran over 60 s, in
`ChunkMap$TrackedEntity.updatePlayer`); Horizon kept running at 8.7 TPS.
### Explore at a rate both servers keep up with (equal work)
The bots fly outwards at 0.8 blocks a second (`--explore-speed 0.8`), slow enough that neither server makes
them wait for terrain, so both generate and send the same new chunks.
| Bots | Server | MSPT | P95 | TPS | RSS | CPU | Chunks/s | New chunks/s |
|---:|---|---:|---:|---:|---:|---:|---:|---:|
| 1 | Horizon | 4.0 | 6.6 | 20.0 | 1.36 GB | 25 % | 2 | 1.3 |
| 1 | Vanilla | 18.1 | 27.1 | 20.0 | 3.32 GB | 73 % | 2 | 1.4 |
| 10 | Horizon | 13.5 | 24.8 | 20.0 | 1.64 GB | 89 % | 13 | 11.8 |
| 10 | Vanilla | 29.2 | 43.0 | 20.0 | 3.52 GB | 143 % | 13 | 11.9 |
| 25 | Horizon | 20.2 | 36.8 | 20.0 | 1.74 GB | 121 % | 32 | 28.9 |
| 25 | Vanilla | 37.0 | 49.4 | 20.0 | 3.73 GB | 169 % | 32 | 28.9 |
### Explore as fast as each server sends chunks (not equal work)
The default 22 blocks a second is faster than either server can generate, so each server sets the pace and
a faster server is given more work: from 10 bots Horizon generated 1.5 to 2 times vanilla's new chunks a
second. Read it as throughput, not as a like-for-like tick time.
| Bots | Server | MSPT | P95 | TPS | RSS | CPU | Chunks/s | New chunks/s |
|---:|---|---:|---:|---:|---:|---:|---:|---:|
| 1 | Horizon | 10.6 | 24.6 | 20.0 | 1.34 GB | 322 % | 39 | 34.6 |
| 1 | Vanilla | 9.9 | 21.3 | 20.0 | 3.48 GB | 245 % | 36 | 33.4 |
| 10 | Horizon | 30.4 | 52.3 | 20.0 | 2.18 GB | 345 % | 47 | 46.7 |
| 10 | Vanilla | 17.0 | 29.6 | 20.0 | 4.04 GB | 242 % | 22 | 22.4 |
| 25 | Horizon | 50.7 | 89.3 | 19.7 | 2.30 GB | 286 % | 50 | 49.7 |
| 25 | Vanilla | 24.2 | 36.5 | 20.0 | 4.40 GB | 320 % | 34 | 33.7 |
| 50 | Horizon | 50.7 | 102.7 | 19.7 | 1.72 GB | 213 % | 97 | 96.5 |
| 50 | Vanilla | stopped by its watchdog | | | | | | |
| 100 | Horizon | 92.9 | 139.6 | 10.8 | 1.70 GB | 322 % | 114 | 113.6 |
| 100 | Vanilla | stopped by its watchdog | | | | | | |
Vanilla's watchdog stopped it at 50 bots (in `ClientboundLightUpdatePacketData`, copying light for a chunk
packet) and at 100 (in `Entity.move` from a move packet).
## Findings
- Horizon's RSS is 1.9 to 2.6 times smaller in every run, and it starts in 11 s against vanilla's 15 s.
Vanilla's heap is fixed at 4 GB (`-Xms4g`), so part of its RSS is that setting rather than need.
- With 1 bot Horizon's tick is 3.3 times shorter walking and 4.5 times shorter exploring at equal work;
at 10 bots it is still shorter in both.
- At equal exploring work Horizon's tick is about half of vanilla's at 10 and 25 bots.
- When the bots only fly around spawn (`walk`), vanilla's tick hardly grows with players (16 to 20 ms from
1 to 50 bots) while Horizon's grows by about 0.5 ms per bot, so vanilla is ahead from about 20 bots: 16.0
against 20.7 ms at 25 and 20.5 against 31.4 ms at 50. Both keep 20 TPS up to 50.
- With 100 bots vanilla's watchdog stops it in every pass, and Horizon keeps running at 9 to 11 TPS.
### Where Horizon's walk tick goes at 25 bots
Profiled on the ThinkPad with `perf` (the server thread, 20 s, self time):
| Share | What | Whose |
|---:|---|---|
| 18 % | `Streaming::update`: every tick it walks all loaded columns and proto chunks and tests each against every player's area (unloading, dropping protos, finishing and decoration candidates), so the cost grows with players times chunks | streaming / engine tick |
| 20 % | `PalettedContainer::get` and `World::block`, mostly from block entity ticks: on the Mac profile `tick_hoppers` looks up the block under every block entity in the loaded world each tick and compares names as strings, and spawners rebuild their display id | engine tick |
| 8 % | `light_data` when a chunk is sent: the light layers were built twice per chunk; a follow-up commit on #623 builds them once and keeps each section's packed layers | server (this work) |
| 7 % | encoding chunk sections and heightmaps for the chunks each bot newly sees | server |
| 4 % | `Hub::tick` (moves, tracking) | server |
| 2 % | the moved-wrongly collision check | server |
The first two grow with the number of players but aren't specific to the server: they are the
engine's tick and streaming, reported to perf-server-2. Before #623 and #649, encoding every chunk section
once per player and waking each connection's writer thread once per packet cost more than everything above
(at 50 walking bots the tick was 281 ms; see the earlier runs).
## Earlier runs (same machine and method)
- `run-1-before-chunk-cache.json`: before #623, with vanilla's locator bar on. Horizon's walk tick was
27.5, 40.7, 281 and 467 ms at 10, 25, 50 and 100 bots, and vanilla's 33.2 ms at 50 (the locator bar).
- `run-2-chunk-cache.json`: Horizon with #623 only: 13.2, 23.5, 31.4 and 84.4 ms walking at 10 to 100
bots.
## Files
- `horizon.json`, `vanilla.json`: the walk and explore passes (vanilla with `locator_bar=false`).
- `explore-equal-rate.json`: the explore pass at 0.8 blocks a second, both servers.
- `run-1-before-chunk-cache.json`, `run-2-chunk-cache.json`: the earlier runs.
To run it again (bots on another machine reached at `--connect`):
cargo build --release -p horizon-client --bin horizon-client -p horizon-server -p horizon-bots
python3 tools/server_bench.py --servers horizon,vanilla --bots 1,10,25,50,100 --behaviour walk,explore \
--gamerule locator_bar=false --nice 19 --bots-via 'ssh macmini' --bots-bin /path/to/horizon-bots \
--connect <this machine's address> --out results.json
python3 tools/server_bench.py --servers horizon,vanilla --bots 1,10,25 --behaviour explore \
--explore-speed 0.8 --gamerule locator_bar=false --nice 19 ... --out explore-equal-rate.json
@@ -0,0 +1,267 @@
{
"kind": "horizon-server-benchmark",
"date": "2026-10-17T05:47:12+00:00",
"machine": {
"system": "Linux",
"machine": "x86_64",
"cpus": 4
},
"commit": "",
"java": [
"openjdk 25.0.4.1 2026-08-18"
],
"settings": {
"servers": "horizon,vanilla",
"bots": "1,10,25",
"behaviour": "explore",
"join_every": 1.0,
"warmup": 60,
"seconds": 300,
"seed": 12345,
"explore_speed": 0.8,
"gamerule": "locator_bar=false",
"heap": "4g",
"out": "/home/ludovic/scratch/sh2-bench5.json",
"nice": 19,
"bots_via": "ssh -o BatchMode=yes macmini",
"bots_bin": "/tmp/sh2-horizon-bots",
"connect": "100.72.51.8",
"properties": {
"level-seed": "12345",
"view-distance": "10",
"simulation-distance": "10",
"online-mode": "false",
"white-list": "false",
"gamemode": "creative",
"force-gamemode": "true",
"spawn-protection": "0",
"max-players": "200",
"difficulty": "easy",
"sync-chunk-writes": "true"
}
},
"runs": [
{
"server": "horizon",
"bots": 1,
"behaviour": "explore",
"start_seconds": 10.91,
"mspt": {
"average": 4.02,
"p50": 3.75,
"p95": 6.63,
"p99": 14.55
},
"mspt_worst_p99": 63.3,
"tps": 20.0,
"rss_mb": {
"mean": 1392.42,
"max": 1746.1
},
"cpu_percent": 24.52,
"chunks_per_second": 1.69,
"unique_per_second": 1.34,
"forgotten_per_second": 1.62,
"waited_ticks_per_second": 0.0,
"chunks_total": 1048,
"chunks_per_bot": {
"explore": 1048.0
},
"unique_chunks_per_bot": {
"explore": 952.0
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 1,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "vanilla",
"bots": 1,
"behaviour": "explore",
"start_seconds": 15.13,
"mspt": {
"average": 18.12,
"p50": 18.78,
"p95": 27.07,
"p99": 60.81
},
"mspt_worst_p99": 1397.6,
"tps": 20.0,
"rss_mb": {
"mean": 3397.31,
"max": 3430.2
},
"cpu_percent": 72.99,
"chunks_per_second": 1.69,
"unique_per_second": 1.38,
"forgotten_per_second": 1.62,
"waited_ticks_per_second": 0.0,
"chunks_total": 1048,
"chunks_per_bot": {
"explore": 1048.0
},
"unique_chunks_per_bot": {
"explore": 952.0
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 1,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "horizon",
"bots": 10,
"behaviour": "explore",
"start_seconds": 11.12,
"mspt": {
"average": 13.49,
"p50": 12.09,
"p95": 24.81,
"p99": 40.9
},
"mspt_worst_p99": 101.4,
"tps": 20.0,
"rss_mb": {
"mean": 1684.04,
"max": 2660.6
},
"cpu_percent": 89.04,
"chunks_per_second": 13.08,
"unique_per_second": 11.84,
"forgotten_per_second": 13.02,
"waited_ticks_per_second": 0.0,
"chunks_total": 9277,
"chunks_per_bot": {
"explore": 927.7
},
"unique_chunks_per_bot": {
"explore": 889.9
},
"resent": 0,
"pulled_back": 3381,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 10,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "vanilla",
"bots": 10,
"behaviour": "explore",
"start_seconds": 14.84,
"mspt": {
"average": 29.18,
"p50": 26.76,
"p95": 43.02,
"p99": 80.16
},
"mspt_worst_p99": 425.5,
"tps": 20.0,
"rss_mb": {
"mean": 3609.51,
"max": 3791.6
},
"cpu_percent": 142.79,
"chunks_per_second": 13.1,
"unique_per_second": 11.88,
"forgotten_per_second": 13.12,
"waited_ticks_per_second": 0.0,
"chunks_total": 9245,
"chunks_per_bot": {
"explore": 924.5
},
"unique_chunks_per_bot": {
"explore": 885.3
},
"resent": 0,
"pulled_back": 3388,
"moved_too_quickly": 6,
"moved_wrongly": 0,
"bots_joined": 10,
"bots_failed": [],
"tick_queries": 59
},
{
"server": "horizon",
"bots": 25,
"behaviour": "explore",
"start_seconds": 11.02,
"mspt": {
"average": 20.25,
"p50": 18.73,
"p95": 36.78,
"p99": 55.76
},
"mspt_worst_p99": 92.3,
"tps": 20.0,
"rss_mb": {
"mean": 1782.26,
"max": 2741.9
},
"cpu_percent": 120.8,
"chunks_per_second": 32.22,
"unique_per_second": 28.89,
"forgotten_per_second": 32.19,
"waited_ticks_per_second": 0.0,
"chunks_total": 23357,
"chunks_per_bot": {
"explore": 934.28
},
"unique_chunks_per_bot": {
"explore": 890.32
},
"resent": 0,
"pulled_back": 11580,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 25,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "vanilla",
"bots": 25,
"behaviour": "explore",
"start_seconds": 14.54,
"mspt": {
"average": 37.04,
"p50": 34.77,
"p95": 49.42,
"p99": 101.67
},
"mspt_worst_p99": 735.2,
"tps": 20.0,
"rss_mb": {
"mean": 3815.46,
"max": 4202.0
},
"cpu_percent": 169.15,
"chunks_per_second": 32.34,
"unique_per_second": 28.93,
"forgotten_per_second": 32.31,
"waited_ticks_per_second": 0.0,
"chunks_total": 23306,
"chunks_per_bot": {
"explore": 932.24
},
"unique_chunks_per_bot": {
"explore": 886.96
},
"resent": 0,
"pulled_back": 11539,
"moved_too_quickly": 6,
"moved_wrongly": 0,
"bots_joined": 25,
"bots_failed": [],
"tick_queries": 59
}
]
}
@@ -0,0 +1,415 @@
{
"kind": "horizon-server-benchmark",
"date": "2026-10-17T05:04:57+00:00",
"machine": {
"system": "Linux",
"machine": "x86_64",
"cpus": 4
},
"commit": "",
"java": [
"openjdk 25.0.4.1 2026-08-18"
],
"settings": {
"servers": "horizon",
"bots": "1,10,25,50,100",
"behaviour": "walk,explore",
"join_every": 1.0,
"warmup": 60,
"seconds": 300,
"seed": 12345,
"explore_speed": 0,
"gamerule": "locator_bar=false",
"heap": "4g",
"out": "/home/ludovic/scratch/sh2-bench4.json",
"nice": 19,
"bots_via": "ssh -o BatchMode=yes macmini",
"bots_bin": "/tmp/sh2-horizon-bots",
"connect": "100.72.51.8",
"properties": {
"level-seed": "12345",
"view-distance": "10",
"simulation-distance": "10",
"online-mode": "false",
"white-list": "false",
"gamemode": "creative",
"force-gamemode": "true",
"spawn-protection": "0",
"max-players": "200",
"difficulty": "easy",
"sync-chunk-writes": "true"
}
},
"runs": [
{
"server": "horizon",
"bots": 1,
"behaviour": "walk",
"start_seconds": 11.02,
"mspt": {
"average": 5.45,
"p50": 4.63,
"p95": 12.14,
"p99": 30.63
},
"mspt_worst_p99": 91.5,
"tps": 20.0,
"rss_mb": {
"mean": 1398.5,
"max": 1708.4
},
"cpu_percent": 57.03,
"chunks_per_second": 9.17,
"unique_per_second": 0.26,
"forgotten_per_second": 9.2,
"waited_ticks_per_second": 0.0,
"chunks_total": 3708,
"chunks_per_bot": {
"walk": 3708.0
},
"unique_chunks_per_bot": {
"walk": 694.0
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 1,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "horizon",
"bots": 10,
"behaviour": "walk",
"start_seconds": 11.02,
"mspt": {
"average": 11.51,
"p50": 10.6,
"p95": 22.13,
"p99": 36.92
},
"mspt_worst_p99": 71.9,
"tps": 20.0,
"rss_mb": {
"mean": 1440.2,
"max": 1832.4
},
"cpu_percent": 124.19,
"chunks_per_second": 94.04,
"unique_per_second": 2.38,
"forgotten_per_second": 93.95,
"waited_ticks_per_second": 0.0,
"chunks_total": 37269,
"chunks_per_bot": {
"walk": 3726.9
},
"unique_chunks_per_bot": {
"walk": 688.9
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 10,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "horizon",
"bots": 25,
"behaviour": "walk",
"start_seconds": 10.91,
"mspt": {
"average": 20.74,
"p50": 19.27,
"p95": 40.42,
"p99": 57.12
},
"mspt_worst_p99": 102.0,
"tps": 20.0,
"rss_mb": {
"mean": 1485.4,
"max": 1871.9
},
"cpu_percent": 169.52,
"chunks_per_second": 235.81,
"unique_per_second": 5.68,
"forgotten_per_second": 235.83,
"waited_ticks_per_second": 0.0,
"chunks_total": 94456,
"chunks_per_bot": {
"walk": 3778.24
},
"unique_chunks_per_bot": {
"walk": 692.96
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 25,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "horizon",
"bots": 50,
"behaviour": "walk",
"start_seconds": 11.12,
"mspt": {
"average": 31.41,
"p50": 25.39,
"p95": 78.97,
"p99": 110.73
},
"mspt_worst_p99": 192.9,
"tps": 20.0,
"rss_mb": {
"mean": 1558.34,
"max": 1915.3
},
"cpu_percent": 173.27,
"chunks_per_second": 450.85,
"unique_per_second": 10.3,
"forgotten_per_second": 451.86,
"waited_ticks_per_second": 0.0,
"chunks_total": 186885,
"chunks_per_bot": {
"walk": 3737.7
},
"unique_chunks_per_bot": {
"walk": 688.9
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 50,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "horizon",
"bots": 100,
"behaviour": "walk",
"start_seconds": 18.16,
"mspt": {
"average": 114.55,
"p50": 101.37,
"p95": 256.19,
"p99": 338.64
},
"mspt_worst_p99": 442.6,
"tps": 8.73,
"rss_mb": {
"mean": 1357.65,
"max": 1358.4
},
"cpu_percent": 181.21,
"chunks_per_second": 484.16,
"unique_per_second": 30.76,
"forgotten_per_second": 478.77,
"waited_ticks_per_second": 0.0,
"chunks_total": 212947,
"chunks_per_bot": {
"walk": 2129.47
},
"unique_chunks_per_bot": {
"walk": 583.02
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 100,
"bots_failed": [],
"tick_queries": 57
},
{
"server": "horizon",
"bots": 1,
"behaviour": "explore",
"start_seconds": 17.45,
"mspt": {
"average": 10.62,
"p50": 9.2,
"p95": 24.61,
"p99": 38.18
},
"mspt_worst_p99": 81.1,
"tps": 20.0,
"rss_mb": {
"mean": 1372.73,
"max": 1568.7
},
"cpu_percent": 322.38,
"chunks_per_second": 39.21,
"unique_per_second": 34.6,
"forgotten_per_second": 38.55,
"waited_ticks_per_second": 0.51,
"chunks_total": 12549,
"chunks_per_bot": {
"explore": 12549.0
},
"unique_chunks_per_bot": {
"explore": 11168.0
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 1,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "horizon",
"bots": 10,
"behaviour": "explore",
"start_seconds": 11.32,
"mspt": {
"average": 30.38,
"p50": 27.84,
"p95": 52.31,
"p99": 77.89
},
"mspt_worst_p99": 158.3,
"tps": 20.0,
"rss_mb": {
"mean": 2231.56,
"max": 4022.6
},
"cpu_percent": 344.99,
"chunks_per_second": 47.34,
"unique_per_second": 46.73,
"forgotten_per_second": 44.57,
"waited_ticks_per_second": 112.27,
"chunks_total": 19870,
"chunks_per_bot": {
"explore": 1987.0
},
"unique_chunks_per_bot": {
"explore": 1966.5
},
"resent": 0,
"pulled_back": 16130,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 10,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "horizon",
"bots": 25,
"behaviour": "explore",
"start_seconds": 11.42,
"mspt": {
"average": 50.67,
"p50": 47.61,
"p95": 89.3,
"p99": 119.15
},
"mspt_worst_p99": 222.3,
"tps": 19.73,
"rss_mb": {
"mean": 2358.4,
"max": 4273.7
},
"cpu_percent": 286.32,
"chunks_per_second": 50.39,
"unique_per_second": 49.67,
"forgotten_per_second": 47.45,
"waited_ticks_per_second": 311.85,
"chunks_total": 28954,
"chunks_per_bot": {
"explore": 1158.16
},
"unique_chunks_per_bot": {
"explore": 1147.56
},
"resent": 0,
"pulled_back": 36319,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 25,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "horizon",
"bots": 50,
"behaviour": "explore",
"start_seconds": 20.06,
"mspt": {
"average": 50.69,
"p50": 47.48,
"p95": 102.67,
"p99": 133.27
},
"mspt_worst_p99": 274.4,
"tps": 19.73,
"rss_mb": {
"mean": 1764.01,
"max": 2542.0
},
"cpu_percent": 212.83,
"chunks_per_second": 97.33,
"unique_per_second": 96.47,
"forgotten_per_second": 76.74,
"waited_ticks_per_second": 750.49,
"chunks_total": 38538,
"chunks_per_bot": {
"explore": 770.76
},
"unique_chunks_per_bot": {
"explore": 763.84
},
"resent": 0,
"pulled_back": 39880,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 50,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "horizon",
"bots": 100,
"behaviour": "explore",
"start_seconds": 11.82,
"mspt": {
"average": 92.93,
"p50": 87.84,
"p95": 139.6,
"p99": 178.38
},
"mspt_worst_p99": 272.9,
"tps": 10.76,
"rss_mb": {
"mean": 1736.38,
"max": 1855.7
},
"cpu_percent": 321.53,
"chunks_per_second": 113.89,
"unique_per_second": 113.56,
"forgotten_per_second": 101.68,
"waited_ticks_per_second": 1179.4,
"chunks_total": 84617,
"chunks_per_bot": {
"explore": 846.17
},
"unique_chunks_per_bot": {
"explore": 845.01
},
"resent": 0,
"pulled_back": 128104,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 100,
"bots_failed": [],
"tick_queries": 58
}
]
}
@@ -0,0 +1,693 @@
{
"kind": "horizon-server-benchmark",
"date": "2026-10-17T00:37:35+00:00",
"machine": {
"system": "Linux",
"machine": "x86_64",
"cpus": 4
},
"commit": "",
"java": [
"openjdk 25.0.4.1 2026-08-18"
],
"settings": {
"servers": "horizon,vanilla",
"bots": "1,10,25,50,100",
"behaviour": "walk,explore",
"join_every": 1.0,
"warmup": 60,
"seconds": 300,
"seed": 12345,
"heap": "4g",
"out": "/home/ludovic/scratch/sh2-bench.json",
"nice": 19,
"bots_via": "ssh -o BatchMode=yes macmini",
"bots_bin": "/tmp/sh2-horizon-bots",
"connect": "100.72.51.8",
"properties": {
"level-seed": "12345",
"view-distance": "10",
"simulation-distance": "10",
"online-mode": "false",
"white-list": "false",
"gamemode": "creative",
"force-gamemode": "true",
"spawn-protection": "0",
"max-players": "200",
"difficulty": "easy",
"sync-chunk-writes": "true"
}
},
"runs": [
{
"server": "horizon",
"bots": 1,
"behaviour": "walk",
"start_seconds": 11.12,
"mspt": {
"average": 7.63,
"p50": 5.51,
"p95": 17.46,
"p99": 36.79
},
"mspt_worst_p99": 79.6,
"tps": 20.0,
"rss_mb": {
"mean": 1391.94,
"max": 1700.7
},
"cpu_percent": 60.18,
"chunks_per_second": 9.15,
"unique_per_second": 0.29,
"forgotten_per_second": 9.18,
"waited_ticks_per_second": 0.0,
"chunks_total": 3682,
"chunks_per_bot": {
"walk": 3682.0
},
"unique_chunks_per_bot": {
"walk": 692.0
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 1,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "vanilla",
"bots": 1,
"behaviour": "walk",
"start_seconds": 15.35,
"mspt": {
"average": 18.08,
"p50": 18.66,
"p95": 28.05,
"p99": 46.94
},
"mspt_worst_p99": 456.4,
"tps": 20.0,
"rss_mb": {
"mean": 3455.72,
"max": 3471.7
},
"cpu_percent": 65.67,
"chunks_per_second": 9.89,
"unique_per_second": 0.29,
"forgotten_per_second": 9.92,
"waited_ticks_per_second": 0.0,
"chunks_total": 3827,
"chunks_per_bot": {
"walk": 3827.0
},
"unique_chunks_per_bot": {
"walk": 699.0
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 1,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "horizon",
"bots": 10,
"behaviour": "walk",
"start_seconds": 11.22,
"mspt": {
"average": 27.53,
"p50": 24.93,
"p95": 53.93,
"p99": 77.03
},
"mspt_worst_p99": 111.4,
"tps": 20.0,
"rss_mb": {
"mean": 1433.85,
"max": 1831.9
},
"cpu_percent": 138.65,
"chunks_per_second": 95.98,
"unique_per_second": 2.38,
"forgotten_per_second": 95.78,
"waited_ticks_per_second": 0.0,
"chunks_total": 38041,
"chunks_per_bot": {
"walk": 3804.1
},
"unique_chunks_per_bot": {
"walk": 690.8
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 10,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "vanilla",
"bots": 10,
"behaviour": "walk",
"start_seconds": 14.83,
"mspt": {
"average": 18.92,
"p50": 16.02,
"p95": 39.0,
"p99": 56.13
},
"mspt_worst_p99": 180.3,
"tps": 20.0,
"rss_mb": {
"mean": 3479.45,
"max": 3535.7
},
"cpu_percent": 99.39,
"chunks_per_second": 99.93,
"unique_per_second": 2.94,
"forgotten_per_second": 100.04,
"waited_ticks_per_second": 0.0,
"chunks_total": 38411,
"chunks_per_bot": {
"walk": 3841.1
},
"unique_chunks_per_bot": {
"walk": 697.1
},
"resent": 0,
"pulled_back": 7,
"moved_too_quickly": 7,
"moved_wrongly": 0,
"bots_joined": 10,
"bots_failed": [],
"tick_queries": 59
},
{
"server": "horizon",
"bots": 25,
"behaviour": "walk",
"start_seconds": 11.52,
"mspt": {
"average": 40.69,
"p50": 37.28,
"p95": 81.16,
"p99": 108.42
},
"mspt_worst_p99": 149.9,
"tps": 20.0,
"rss_mb": {
"mean": 1444.12,
"max": 1882.6
},
"cpu_percent": 176.94,
"chunks_per_second": 244.11,
"unique_per_second": 6.51,
"forgotten_per_second": 243.96,
"waited_ticks_per_second": 0.0,
"chunks_total": 95219,
"chunks_per_bot": {
"walk": 3808.76
},
"unique_chunks_per_bot": {
"walk": 695.64
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 25,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "vanilla",
"bots": 25,
"behaviour": "walk",
"start_seconds": 15.23,
"mspt": {
"average": 18.81,
"p50": 17.26,
"p95": 30.16,
"p99": 53.9
},
"mspt_worst_p99": 390.0,
"tps": 20.0,
"rss_mb": {
"mean": 3517.36,
"max": 3555.7
},
"cpu_percent": 148.52,
"chunks_per_second": 252.34,
"unique_per_second": 7.75,
"forgotten_per_second": 251.95,
"waited_ticks_per_second": 0.0,
"chunks_total": 96645,
"chunks_per_bot": {
"walk": 3865.8
},
"unique_chunks_per_bot": {
"walk": 698.4
},
"resent": 0,
"pulled_back": 11,
"moved_too_quickly": 11,
"moved_wrongly": 0,
"bots_joined": 25,
"bots_failed": [],
"tick_queries": 59
},
{
"server": "horizon",
"bots": 50,
"behaviour": "walk",
"start_seconds": 12.72,
"mspt": {
"average": 281.42,
"p50": 278.28,
"p95": 343.64,
"p99": 384.23
},
"mspt_worst_p99": 475.1,
"tps": 3.55,
"rss_mb": {
"mean": 1334.32,
"max": 1335.4
},
"cpu_percent": 197.11,
"chunks_per_second": 391.5,
"unique_per_second": 21.24,
"forgotten_per_second": 388.33,
"waited_ticks_per_second": 0.0,
"chunks_total": 153136,
"chunks_per_bot": {
"walk": 3062.72
},
"unique_chunks_per_bot": {
"walk": 661.82
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 50,
"bots_failed": [],
"tick_queries": 57
},
{
"server": "vanilla",
"bots": 50,
"behaviour": "walk",
"start_seconds": 15.74,
"mspt": {
"average": 33.21,
"p50": 29.9,
"p95": 53.37,
"p99": 109.14
},
"mspt_worst_p99": 833.3,
"tps": 20.0,
"rss_mb": {
"mean": 3568.07,
"max": 3594.9
},
"cpu_percent": 266.59,
"chunks_per_second": 496.3,
"unique_per_second": 18.64,
"forgotten_per_second": 495.77,
"waited_ticks_per_second": 0.0,
"chunks_total": 191965,
"chunks_per_bot": {
"walk": 3839.3
},
"unique_chunks_per_bot": {
"walk": 696.22
},
"resent": 0,
"pulled_back": 184,
"moved_too_quickly": 183,
"moved_wrongly": 0,
"bots_joined": 50,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "horizon",
"bots": 100,
"behaviour": "walk",
"start_seconds": 11.22,
"mspt": {
"average": 467.37,
"p50": 464.62,
"p95": 524.07,
"p99": 548.77
},
"mspt_worst_p99": 581.1,
"tps": 2.14,
"rss_mb": {
"mean": 1353.68,
"max": 1354.9
},
"cpu_percent": 191.96,
"chunks_per_second": 377.62,
"unique_per_second": 55.8,
"forgotten_per_second": 348.88,
"waited_ticks_per_second": 0.0,
"chunks_total": 165719,
"chunks_per_bot": {
"walk": 1657.19
},
"unique_chunks_per_bot": {
"walk": 549.8
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 100,
"bots_failed": [],
"tick_queries": 56
},
{
"server": "vanilla",
"bots": 100,
"behaviour": "walk",
"start_seconds": 14.84,
"error": "[Errno 32] Broken pipe"
},
{
"server": "horizon",
"bots": 1,
"behaviour": "explore",
"start_seconds": 11.12,
"mspt": {
"average": 17.07,
"p50": 16.6,
"p95": 32.82,
"p99": 50.3
},
"mspt_worst_p99": 113.9,
"tps": 20.0,
"rss_mb": {
"mean": 1391.72,
"max": 1566.6
},
"cpu_percent": 333.8,
"chunks_per_second": 39.42,
"unique_per_second": 34.77,
"forgotten_per_second": 39.34,
"waited_ticks_per_second": 0.0,
"chunks_total": 13860,
"chunks_per_bot": {
"explore": 13860.0
},
"unique_chunks_per_bot": {
"explore": 12379.0
},
"resent": 0,
"pulled_back": 192,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 1,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "vanilla",
"bots": 1,
"behaviour": "explore",
"start_seconds": 16.03,
"mspt": {
"average": 9.9,
"p50": 8.04,
"p95": 20.41,
"p99": 63.62
},
"mspt_worst_p99": 351.1,
"tps": 20.0,
"rss_mb": {
"mean": 3709.92,
"max": 3776.2
},
"cpu_percent": 246.64,
"chunks_per_second": 36.06,
"unique_per_second": 33.33,
"forgotten_per_second": 35.73,
"waited_ticks_per_second": 0.0,
"chunks_total": 12369,
"chunks_per_bot": {
"explore": 12369.0
},
"unique_chunks_per_bot": {
"explore": 11559.0
},
"resent": 0,
"pulled_back": 222,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 1,
"bots_failed": [],
"tick_queries": 59
},
{
"server": "horizon",
"bots": 10,
"behaviour": "explore",
"start_seconds": 11.42,
"mspt": {
"average": 33.72,
"p50": 31.08,
"p95": 61.07,
"p99": 89.09
},
"mspt_worst_p99": 142.6,
"tps": 20.0,
"rss_mb": {
"mean": 2239.97,
"max": 4108.8
},
"cpu_percent": 350.83,
"chunks_per_second": 46.49,
"unique_per_second": 46.08,
"forgotten_per_second": 44.33,
"waited_ticks_per_second": 111.28,
"chunks_total": 19915,
"chunks_per_bot": {
"explore": 1991.5
},
"unique_chunks_per_bot": {
"explore": 1974.4
},
"resent": 0,
"pulled_back": 17516,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 10,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "vanilla",
"bots": 10,
"behaviour": "explore",
"start_seconds": 15.15,
"mspt": {
"average": 16.25,
"p50": 13.67,
"p95": 28.92,
"p99": 115.9
},
"mspt_worst_p99": 1202.1,
"tps": 20.0,
"rss_mb": {
"mean": 4202.72,
"max": 4502.7
},
"cpu_percent": 247.47,
"chunks_per_second": 22.3,
"unique_per_second": 22.25,
"forgotten_per_second": 22.64,
"waited_ticks_per_second": 78.89,
"chunks_total": 10092,
"chunks_per_bot": {
"explore": 1009.2
},
"unique_chunks_per_bot": {
"explore": 1007.4
},
"resent": 0,
"pulled_back": 16286,
"moved_too_quickly": 12,
"moved_wrongly": 0,
"bots_joined": 10,
"bots_failed": [],
"tick_queries": 59
},
{
"server": "horizon",
"bots": 25,
"behaviour": "explore",
"start_seconds": 11.82,
"mspt": {
"average": 48.03,
"p50": 44.99,
"p95": 83.06,
"p99": 109.63
},
"mspt_worst_p99": 191.8,
"tps": 20.0,
"rss_mb": {
"mean": 2214.16,
"max": 4598.6
},
"cpu_percent": 356.03,
"chunks_per_second": 64.2,
"unique_per_second": 63.5,
"forgotten_per_second": 60.9,
"waited_ticks_per_second": 301.65,
"chunks_total": 32247,
"chunks_per_bot": {
"explore": 1289.88
},
"unique_chunks_per_bot": {
"explore": 1279.4
},
"resent": 0,
"pulled_back": 42159,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 25,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "vanilla",
"bots": 25,
"behaviour": "explore",
"start_seconds": 14.73,
"mspt": {
"average": 25.73,
"p50": 22.75,
"p95": 37.72,
"p99": 201.98
},
"mspt_worst_p99": 4051.5,
"tps": 20.0,
"rss_mb": {
"mean": 4355.9,
"max": 4983.2
},
"cpu_percent": 309.3,
"chunks_per_second": 33.33,
"unique_per_second": 33.21,
"forgotten_per_second": 34.22,
"waited_ticks_per_second": 258.68,
"chunks_total": 19426,
"chunks_per_bot": {
"explore": 777.04
},
"unique_chunks_per_bot": {
"explore": 772.92
},
"resent": 0,
"pulled_back": 53918,
"moved_too_quickly": 93,
"moved_wrongly": 0,
"bots_joined": 25,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "horizon",
"bots": 50,
"behaviour": "explore",
"start_seconds": 11.01,
"mspt": {
"average": 108.61,
"p50": 107.37,
"p95": 151.96,
"p99": 177.41
},
"mspt_worst_p99": 271.6,
"tps": 9.21,
"rss_mb": {
"mean": 1775.98,
"max": 1956.9
},
"cpu_percent": 359.7,
"chunks_per_second": 100.32,
"unique_per_second": 98.91,
"forgotten_per_second": 95.42,
"waited_ticks_per_second": 518.15,
"chunks_total": 55956,
"chunks_per_bot": {
"explore": 1119.12
},
"unique_chunks_per_bot": {
"explore": 1111.42
},
"resent": 0,
"pulled_back": 69967,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 50,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "vanilla",
"bots": 50,
"behaviour": "explore",
"start_seconds": 14.74,
"error": "[Errno 32] Broken pipe"
},
{
"server": "horizon",
"bots": 100,
"behaviour": "explore",
"start_seconds": 11.12,
"mspt": {
"average": 244.43,
"p50": 243.58,
"p95": 308.39,
"p99": 360.12
},
"mspt_worst_p99": 470.2,
"tps": 4.09,
"rss_mb": {
"mean": 1806.85,
"max": 2084.5
},
"cpu_percent": 310.46,
"chunks_per_second": 186.87,
"unique_per_second": 182.99,
"forgotten_per_second": 161.75,
"waited_ticks_per_second": 1131.65,
"chunks_total": 100389,
"chunks_per_bot": {
"explore": 1003.89
},
"unique_chunks_per_bot": {
"explore": 991.9
},
"resent": 0,
"pulled_back": 56791,
"moved_too_quickly": 344,
"moved_wrongly": 0,
"bots_joined": 100,
"bots_failed": [],
"tick_queries": 57
},
{
"server": "vanilla",
"bots": 100,
"behaviour": "explore",
"start_seconds": 14.83,
"error": "[Errno 32] Broken pipe"
}
]
}
@@ -0,0 +1,413 @@
{
"kind": "horizon-server-benchmark",
"date": "2026-10-17T02:22:59+00:00",
"machine": {
"system": "Linux",
"machine": "x86_64",
"cpus": 4
},
"commit": "",
"java": [
"openjdk 25.0.4.1 2026-08-18"
],
"settings": {
"servers": "horizon",
"bots": "1,10,25,50,100",
"behaviour": "walk,explore",
"join_every": 1.0,
"warmup": 60,
"seconds": 300,
"seed": 12345,
"heap": "4g",
"out": "/home/ludovic/scratch/sh2-bench2.json",
"nice": 19,
"bots_via": "ssh -o BatchMode=yes macmini",
"bots_bin": "/tmp/sh2-horizon-bots",
"connect": "100.72.51.8",
"properties": {
"level-seed": "12345",
"view-distance": "10",
"simulation-distance": "10",
"online-mode": "false",
"white-list": "false",
"gamemode": "creative",
"force-gamemode": "true",
"spawn-protection": "0",
"max-players": "200",
"difficulty": "easy",
"sync-chunk-writes": "true"
}
},
"runs": [
{
"server": "horizon",
"bots": 1,
"behaviour": "walk",
"start_seconds": 11.12,
"mspt": {
"average": 5.41,
"p50": 4.63,
"p95": 11.61,
"p99": 30.5
},
"mspt_worst_p99": 64.6,
"tps": 20.0,
"rss_mb": {
"mean": 1398.84,
"max": 1708.5
},
"cpu_percent": 56.84,
"chunks_per_second": 9.19,
"unique_per_second": 0.27,
"forgotten_per_second": 9.22,
"waited_ticks_per_second": 0.0,
"chunks_total": 3713,
"chunks_per_bot": {
"walk": 3713.0
},
"unique_chunks_per_bot": {
"walk": 694.0
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 1,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "horizon",
"bots": 10,
"behaviour": "walk",
"start_seconds": 10.92,
"mspt": {
"average": 13.18,
"p50": 11.74,
"p95": 26.5,
"p99": 44.82
},
"mspt_worst_p99": 87.6,
"tps": 20.0,
"rss_mb": {
"mean": 1439.83,
"max": 1831.6
},
"cpu_percent": 132.94,
"chunks_per_second": 93.87,
"unique_per_second": 2.38,
"forgotten_per_second": 93.77,
"waited_ticks_per_second": 0.0,
"chunks_total": 37240,
"chunks_per_bot": {
"walk": 3724.0
},
"unique_chunks_per_bot": {
"walk": 688.7
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 10,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "horizon",
"bots": 25,
"behaviour": "walk",
"start_seconds": 10.92,
"mspt": {
"average": 23.52,
"p50": 21.92,
"p95": 44.7,
"p99": 63.12
},
"mspt_worst_p99": 119.1,
"tps": 20.0,
"rss_mb": {
"mean": 1485.82,
"max": 1872.0
},
"cpu_percent": 173.82,
"chunks_per_second": 235.63,
"unique_per_second": 5.64,
"forgotten_per_second": 235.72,
"waited_ticks_per_second": 0.0,
"chunks_total": 94549,
"chunks_per_bot": {
"walk": 3781.96
},
"unique_chunks_per_bot": {
"walk": 692.8
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 25,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "horizon",
"bots": 50,
"behaviour": "walk",
"start_seconds": 10.91,
"mspt": {
"average": 31.36,
"p50": 26.08,
"p95": 70.6,
"p99": 92.83
},
"mspt_worst_p99": 125.5,
"tps": 20.0,
"rss_mb": {
"mean": 1556.81,
"max": 1913.0
},
"cpu_percent": 192.16,
"chunks_per_second": 468.69,
"unique_per_second": 10.43,
"forgotten_per_second": 468.78,
"waited_ticks_per_second": 0.0,
"chunks_total": 193547,
"chunks_per_bot": {
"walk": 3870.94
},
"unique_chunks_per_bot": {
"walk": 692.84
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 50,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "horizon",
"bots": 100,
"behaviour": "walk",
"start_seconds": 11.02,
"mspt": {
"average": 84.38,
"p50": 81.5,
"p95": 132.92,
"p99": 168.38
},
"mspt_worst_p99": 245.5,
"tps": 11.85,
"rss_mb": {
"mean": 1395.64,
"max": 1941.3
},
"cpu_percent": 271.28,
"chunks_per_second": 850.05,
"unique_per_second": 21.79,
"forgotten_per_second": 851.1,
"waited_ticks_per_second": 0.0,
"chunks_total": 368464,
"chunks_per_bot": {
"walk": 3684.64
},
"unique_chunks_per_bot": {
"walk": 680.58
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 100,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "horizon",
"bots": 1,
"behaviour": "explore",
"start_seconds": 11.02,
"mspt": {
"average": 11.63,
"p50": 10.27,
"p95": 24.52,
"p99": 40.53
},
"mspt_worst_p99": 99.9,
"tps": 20.0,
"rss_mb": {
"mean": 1413.81,
"max": 1596.1
},
"cpu_percent": 329.84,
"chunks_per_second": 39.47,
"unique_per_second": 34.81,
"forgotten_per_second": 39.42,
"waited_ticks_per_second": 0.0,
"chunks_total": 13798,
"chunks_per_bot": {
"explore": 13798.0
},
"unique_chunks_per_bot": {
"explore": 12330.0
},
"resent": 0,
"pulled_back": 185,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 1,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "horizon",
"bots": 10,
"behaviour": "explore",
"start_seconds": 11.02,
"mspt": {
"average": 28.49,
"p50": 26.2,
"p95": 48.58,
"p99": 72.04
},
"mspt_worst_p99": 125.7,
"tps": 20.0,
"rss_mb": {
"mean": 2231.06,
"max": 4077.8
},
"cpu_percent": 344.95,
"chunks_per_second": 46.31,
"unique_per_second": 45.88,
"forgotten_per_second": 44.14,
"waited_ticks_per_second": 111.95,
"chunks_total": 19722,
"chunks_per_bot": {
"explore": 1972.2
},
"unique_chunks_per_bot": {
"explore": 1957.0
},
"resent": 0,
"pulled_back": 18533,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 10,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "horizon",
"bots": 25,
"behaviour": "explore",
"start_seconds": 11.12,
"mspt": {
"average": 40.04,
"p50": 37.61,
"p95": 62.39,
"p99": 83.64
},
"mspt_worst_p99": 147.0,
"tps": 20.0,
"rss_mb": {
"mean": 2466.85,
"max": 4504.5
},
"cpu_percent": 340.13,
"chunks_per_second": 60.61,
"unique_per_second": 59.62,
"forgotten_per_second": 56.28,
"waited_ticks_per_second": 299.57,
"chunks_total": 32877,
"chunks_per_bot": {
"explore": 1315.08
},
"unique_chunks_per_bot": {
"explore": 1300.72
},
"resent": 0,
"pulled_back": 46272,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 25,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "horizon",
"bots": 50,
"behaviour": "explore",
"start_seconds": 11.32,
"mspt": {
"average": 61.82,
"p50": 59.21,
"p95": 90.46,
"p99": 115.58
},
"mspt_worst_p99": 320.9,
"tps": 16.18,
"rss_mb": {
"mean": 2634.54,
"max": 4883.1
},
"cpu_percent": 356.96,
"chunks_per_second": 95.26,
"unique_per_second": 94.31,
"forgotten_per_second": 85.86,
"waited_ticks_per_second": 528.08,
"chunks_total": 59251,
"chunks_per_bot": {
"explore": 1185.02
},
"unique_chunks_per_bot": {
"explore": 1173.96
},
"resent": 0,
"pulled_back": 99493,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 50,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "horizon",
"bots": 100,
"behaviour": "explore",
"start_seconds": 10.81,
"mspt": {
"average": 103.24,
"p50": 100.03,
"p95": 144.77,
"p99": 178.8
},
"mspt_worst_p99": 258.6,
"tps": 9.69,
"rss_mb": {
"mean": 1784.48,
"max": 1922.0
},
"cpu_percent": 336.94,
"chunks_per_second": 115.06,
"unique_per_second": 114.86,
"forgotten_per_second": 109.43,
"waited_ticks_per_second": 1127.57,
"chunks_total": 93839,
"chunks_per_bot": {
"explore": 938.39
},
"unique_chunks_per_bot": {
"explore": 935.28
},
"resent": 0,
"pulled_back": 133404,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 100,
"bots_failed": [],
"tick_queries": 58
}
]
}
@@ -0,0 +1,324 @@
{
"kind": "horizon-server-benchmark",
"date": "2026-10-17T03:29:30+00:00",
"machine": {
"system": "Linux",
"machine": "x86_64",
"cpus": 4
},
"commit": "",
"java": [
"openjdk 25.0.4.1 2026-08-18"
],
"settings": {
"servers": "vanilla",
"bots": "1,10,25,50,100",
"behaviour": "walk,explore",
"join_every": 1.0,
"warmup": 60,
"seconds": 300,
"seed": 12345,
"gamerule": "locator_bar=false",
"heap": "4g",
"out": "/home/ludovic/scratch/sh2-bench3.json",
"nice": 19,
"bots_via": "ssh -o BatchMode=yes macmini",
"bots_bin": "/tmp/sh2-horizon-bots",
"connect": "100.72.51.8",
"properties": {
"level-seed": "12345",
"view-distance": "10",
"simulation-distance": "10",
"online-mode": "false",
"white-list": "false",
"gamemode": "creative",
"force-gamemode": "true",
"spawn-protection": "0",
"max-players": "200",
"difficulty": "easy",
"sync-chunk-writes": "true"
}
},
"runs": [
{
"server": "vanilla",
"bots": 1,
"behaviour": "walk",
"start_seconds": 15.43,
"mspt": {
"average": 18.32,
"p50": 19.26,
"p95": 27.34,
"p99": 45.48
},
"mspt_worst_p99": 393.3,
"tps": 20.0,
"rss_mb": {
"mean": 3428.43,
"max": 3437.7
},
"cpu_percent": 66.47,
"chunks_per_second": 9.92,
"unique_per_second": 0.29,
"forgotten_per_second": 9.95,
"waited_ticks_per_second": 0.0,
"chunks_total": 3850,
"chunks_per_bot": {
"walk": 3850.0
},
"unique_chunks_per_bot": {
"walk": 699.0
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 1,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "vanilla",
"bots": 10,
"behaviour": "walk",
"start_seconds": 14.76,
"mspt": {
"average": 16.8,
"p50": 14.55,
"p95": 30.52,
"p99": 50.01
},
"mspt_worst_p99": 205.2,
"tps": 20.0,
"rss_mb": {
"mean": 3490.15,
"max": 3580.7
},
"cpu_percent": 83.7,
"chunks_per_second": 100.01,
"unique_per_second": 2.83,
"forgotten_per_second": 100.04,
"waited_ticks_per_second": 0.0,
"chunks_total": 38623,
"chunks_per_bot": {
"walk": 3862.3
},
"unique_chunks_per_bot": {
"walk": 697.2
},
"resent": 0,
"pulled_back": 5,
"moved_too_quickly": 5,
"moved_wrongly": 0,
"bots_joined": 10,
"bots_failed": [],
"tick_queries": 59
},
{
"server": "vanilla",
"bots": 25,
"behaviour": "walk",
"start_seconds": 14.84,
"mspt": {
"average": 16.01,
"p50": 14.55,
"p95": 26.39,
"p99": 51.26
},
"mspt_worst_p99": 397.8,
"tps": 20.0,
"rss_mb": {
"mean": 3550.02,
"max": 3593.5
},
"cpu_percent": 114.96,
"chunks_per_second": 251.62,
"unique_per_second": 6.72,
"forgotten_per_second": 251.16,
"waited_ticks_per_second": 0.0,
"chunks_total": 98701,
"chunks_per_bot": {
"walk": 3948.04
},
"unique_chunks_per_bot": {
"walk": 698.84
},
"resent": 0,
"pulled_back": 10,
"moved_too_quickly": 10,
"moved_wrongly": 0,
"bots_joined": 25,
"bots_failed": [],
"tick_queries": 59
},
{
"server": "vanilla",
"bots": 50,
"behaviour": "walk",
"start_seconds": 14.43,
"mspt": {
"average": 20.52,
"p50": 19.55,
"p95": 29.72,
"p99": 49.04
},
"mspt_worst_p99": 600.4,
"tps": 20.0,
"rss_mb": {
"mean": 3633.12,
"max": 3658.7
},
"cpu_percent": 193.84,
"chunks_per_second": 501.86,
"unique_per_second": 12.15,
"forgotten_per_second": 501.8,
"waited_ticks_per_second": 0.0,
"chunks_total": 203429,
"chunks_per_bot": {
"walk": 4068.58
},
"unique_chunks_per_bot": {
"walk": 698.66
},
"resent": 0,
"pulled_back": 33,
"moved_too_quickly": 32,
"moved_wrongly": 0,
"bots_joined": 50,
"bots_failed": [],
"tick_queries": 59
},
{
"server": "vanilla",
"bots": 100,
"behaviour": "walk",
"start_seconds": 15.34,
"error": "[Errno 32] Broken pipe"
},
{
"server": "vanilla",
"bots": 1,
"behaviour": "explore",
"start_seconds": 15.55,
"mspt": {
"average": 9.93,
"p50": 8.12,
"p95": 21.33,
"p99": 60.19
},
"mspt_worst_p99": 373.0,
"tps": 20.0,
"rss_mb": {
"mean": 3564.11,
"max": 3655.0
},
"cpu_percent": 245.18,
"chunks_per_second": 35.93,
"unique_per_second": 33.35,
"forgotten_per_second": 35.64,
"waited_ticks_per_second": 0.0,
"chunks_total": 12340,
"chunks_per_bot": {
"explore": 12340.0
},
"unique_chunks_per_bot": {
"explore": 11569.0
},
"resent": 0,
"pulled_back": 261,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 1,
"bots_failed": [],
"tick_queries": 59
},
{
"server": "vanilla",
"bots": 10,
"behaviour": "explore",
"start_seconds": 15.03,
"mspt": {
"average": 16.99,
"p50": 14.46,
"p95": 29.61,
"p99": 132.18
},
"mspt_worst_p99": 1244.0,
"tps": 20.0,
"rss_mb": {
"mean": 4135.2,
"max": 4453.1
},
"cpu_percent": 242.32,
"chunks_per_second": 22.39,
"unique_per_second": 22.41,
"forgotten_per_second": 22.78,
"waited_ticks_per_second": 78.78,
"chunks_total": 10177,
"chunks_per_bot": {
"explore": 1017.7
},
"unique_chunks_per_bot": {
"explore": 1016.2
},
"resent": 0,
"pulled_back": 17314,
"moved_too_quickly": 19,
"moved_wrongly": 0,
"bots_joined": 10,
"bots_failed": [],
"tick_queries": 59
},
{
"server": "vanilla",
"bots": 25,
"behaviour": "explore",
"start_seconds": 14.84,
"mspt": {
"average": 24.19,
"p50": 21.22,
"p95": 36.51,
"p99": 174.82
},
"mspt_worst_p99": 1532.8,
"tps": 20.0,
"rss_mb": {
"mean": 4504.55,
"max": 4988.9
},
"cpu_percent": 319.55,
"chunks_per_second": 33.78,
"unique_per_second": 33.66,
"forgotten_per_second": 34.64,
"waited_ticks_per_second": 269.22,
"chunks_total": 20522,
"chunks_per_bot": {
"explore": 820.88
},
"unique_chunks_per_bot": {
"explore": 815.76
},
"resent": 0,
"pulled_back": 46246,
"moved_too_quickly": 28,
"moved_wrongly": 0,
"bots_joined": 25,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "vanilla",
"bots": 50,
"behaviour": "explore",
"start_seconds": 14.73,
"error": "[Errno 32] Broken pipe"
},
{
"server": "vanilla",
"bots": 100,
"behaviour": "explore",
"start_seconds": 14.54,
"error": "[Errno 32] Broken pipe"
}
]
}
@@ -0,0 +1,167 @@
# Horizon's server against vanilla 26.3, Paper and Fabric + Lithium: mixed bots, equal work (2026-10-19)
This is the same load as `../server-equal-mixed-2026-10-18/`, now with two more servers: Paper, and vanilla on
Fabric with Lithium. It also uses a newer Horizon release (v2026.10.19). That release spawns mobs around every
player (#950) and ticks blocks around every player (#931), as vanilla does. The 2026-10-18 build did neither for
players away from spawn, so it had less work to do than vanilla, and its tick times there were lower than
they would have been with equal work. These numbers replace them.
## Summary
- **The work was equal** at every count, on all four servers. Chunks sent a second and first-time chunks a
second were within 4 % of each other, and the walking bots got about the same chunks each (6050 to 6510).
Every bot joined and none was dropped. Vanilla's 50-bot server is the exception: its watchdog stopped it.
- **Memory:** Horizon used 0.72 GB with one player and 1.11 GB with 50. The three Java servers used 3.4 to
4.5 GB (a 4 GB heap each), so Horizon was **3.8 to 4.8 times smaller** at every count.
- **Start:** **8.2 to 8.5 s** to `Done` for Horizon, against 14.7 to 15.5 s for vanilla, 16.6 to 17.6 s for
Fabric + Lithium and 19.0 to 22.4 s for Paper.
- **CPU** (100 % is one of the machine's 4 threads): from 10 players up, Horizon used the least: 143 % at 50,
against Paper's 210 % and Fabric's 236 %. With one player Paper used less (32 % against 46 %).
- **Average tick (MSPT), where Horizon loses:**
- With 1 player, Paper (10.3 ms) and Fabric + Lithium (12.5) were faster than Horizon (13.7). Vanilla was
slowest (17.3).
- With 10, Fabric (20.2) and Paper (20.6) were ahead of Horizon (22.9), which tied vanilla (22.9).
- With 25, Paper (28.3) was ahead of Horizon (30.9), and Horizon was ahead of Fabric (31.4) and vanilla (37.4).
- With 50, Paper (46.9) and Horizon (47.8) were close. Fabric + Lithium fell to 17.2 TPS (58.3 ms), and
vanilla's watchdog stopped its server.
- **Slow ticks, where Horizon wins:** Horizon's P99 was the lowest from 10 players up (72.0 ms at 25 and 73.2
at 50, against Paper's 90.4 and 109.7). Its worst 5-second P99 was the lowest at every count (133 ms at
50, against 213 for Paper and 888 for Fabric).
So on this 4-thread laptop CPU, Horizon is not yet the fastest on average tick time. Paper is ahead at every
count: Horizon's average tick is a third longer than Paper's with one player and 2 % longer with 50. Fabric +
Lithium is ahead with 1 and 10 players.
Horizon is ahead on memory, start time, CPU from 10 players up, and the steadiness of its ticks (P99). Its
average tick is what perf-server-11 is working on.
## Results
MSPT is milliseconds per tick from each server's own `/tick query` (50 is the limit for 20 TPS). P95 and P99
are means over the 5-second queries of the 300 measured seconds. "Worst P99" is the highest P99 of a single
query. RSS and CPU cover the server's process tree. "First-time chunks/s" counts chunks a bot hadn't
received before (summed over the bots). "Pulled back" counts position packets that moved a bot back.
| Bots | Server | MSPT | P95 | P99 | Worst P99 | TPS | RSS (mean / max) | CPU | Start | Chunks/s | First-time chunks/s | Pulled back |
|---:|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|
| 1 | Horizon | 13.70 | 19.06 | 27.38 | **43.4** | 20 | **0.72 / 0.72 GB** | 46 % | **8.3 s** | 9.94 | 0.01 | 0 |
| 1 | vanilla | 17.31 | 24.68 | 36.43 | 71.0 | 20 | 3.44 / 3.44 GB | 55 % | 14.9 s | 9.99 | 0.00 | 0 |
| 1 | Paper | **10.31** | 13.90 | 22.50 | 81.2 | 20 | 4.24 / 4.25 GB | **32 %** | 22.3 s | 9.81 | 0.02 | 0 |
| 1 | Fabric + Lithium | 12.46 | 17.69 | 32.74 | 70.1 | 20 | 3.43 / 3.43 GB | 46 % | 17.6 s | 9.98 | 0.00 | 0 |
| 10 | Horizon | 22.93 | 39.83 | 53.33 | **89.5** | 20 | **0.92 / 0.97 GB** | **67 %** | **8.2 s** | 61.21 | 1.78 | 3702 |
| 10 | vanilla | 22.94 | 36.92 | 56.48 | 196.6 | 20 | 3.71 / 3.76 GB | 101 % | 14.7 s | 61.63 | 1.84 | 3370 |
| 10 | Paper | 20.64 | 35.94 | 56.29 | 166.9 | 20 | 4.07 / 4.08 GB | 74 % | 20.5 s | 60.76 | 1.85 | 3974 |
| 10 | Fabric + Lithium | **20.18** | 37.04 | 56.08 | 217.9 | 20 | 3.73 / 3.78 GB | 94 % | 16.6 s | 61.65 | 1.84 | 4002 |
| 25 | Horizon | 30.92 | 48.25 | **72.00** | **121.1** | 20 | **1.01 / 1.12 GB** | **99 %** | **8.5 s** | 155.81 | 5.78 | 3008 |
| 25 | vanilla | 37.35 | 48.20 | 82.66 | 579.8 | 20 | 3.94 / 4.16 GB | 191 % | 15.5 s | 158.52 | 5.71 | 3762 |
| 25 | Paper | **28.30** | 61.68 | 90.40 | 124.3 | 20 | 4.16 / 4.25 GB | 124 % | 19.0 s | 156.35 | 5.72 | 4337 |
| 25 | Fabric + Lithium | 31.44 | 52.45 | 93.35 | 409.3 | 20 | 3.88 / 4.19 GB | 174 % | 17.5 s | 158.89 | 5.75 | 4194 |
| 50 | Horizon | 47.83 | 62.42 | **73.21** | **133.0** | 20 | **1.11 / 1.30 GB** | **143 %** | **8.3 s** | 302.68 | 11.02 | 8562 |
| 50 | vanilla | stopped by its watchdog | | | | | | | | | | |
| 50 | Paper | **46.86** | 61.16 | 109.72 | 213.4 | 20 | 4.35 / 4.52 GB | 210 % | 19.4 s | 310.51 | 11.16 | 7344 |
| 50 | Fabric + Lithium | 58.30 | 83.06 | 174.37 | 887.6 | 17.15 | 4.52 / 4.74 GB | 236 % | 17.5 s | 311.52 | 11.28 | 6350 |
Vanilla at 50: all 50 bots joined by 05:22:28 UTC. The server fell further and further behind ("Can't keep up!
… Running 16229ms or 324 ticks behind"), and at 05:28:17, during the warm-up, its watchdog stopped it: "A
single server tick took 60.00 seconds". That is the same as on 2026-10-18.
Chunks each bot received over the whole run, by behaviour (mean):
| Bots | Server | walk | explore | idle |
|---:|---|---:|---:|---:|
| 1 | Horizon | 6280 | | |
| 1 | vanilla | 6210 | | |
| 1 | Paper | 6049 | | |
| 1 | Fabric + Lithium | 6188 | | |
| 10 | Horizon | 6460 | 1195 | 473 |
| 10 | vanilla | 6272 | 1180 | 473 |
| 10 | Paper | 6177 | 1171 | 473 |
| 10 | Fabric + Lithium | 6291 | 1181 | 473 |
| 25 | Horizon | 6506 | 1262 | 473 |
| 25 | vanilla | 6307 | 1236 | 473 |
| 25 | Paper | 6280 | 1229 | 473 |
| 25 | Fabric + Lithium | 6357 | 1239 | 473 |
| 50 | Horizon | 6468 | 1251 | 473 |
| 50 | Paper | 6411 | 1224 | 473 |
| 50 | Fabric + Lithium | 6332 | 1228 | 473 |
## Equal work
`server_bench.py`'s work table compares, at each bot count, every server's chunks sent a second and
first-time chunks a second with the busiest server's. The work is equal when both are within 10 % (or within
a chunk a second when there are few).
| Bots | Chunks/s sent, first-time (Horizon; vanilla; Paper; Fabric + Lithium) | Equal |
|---:|---|---|
| 1 | 9.94, 0.01; 9.99, 0.00; 9.81, 0.02; 9.98, 0.00 | yes |
| 10 | 61.21, 1.78; 61.63, 1.84; 60.76, 1.85; 61.65, 1.84 | yes |
| 25 | 155.81, 5.78; 158.52, 5.71; 156.35, 5.72; 158.89, 5.75 | yes |
| 50 | 302.68, 11.02; (stopped); 310.51, 11.16; 311.52, 11.28 | yes |
Horizon pulled no bot back for moving too quickly (`moved too quickly`: 0 at every count). The Java servers
logged 0 to 46 such lines a run. All four sent position corrections ("Pulled back") at similar rates.
## Method
`tools/server_bench.py` ran each configuration on a fresh server:
- the same `server.properties` on all four: seed 12345, view and simulation distance 10, offline, creative,
difficulty easy, `sync-chunk-writes=true`, white-list off;
- `gamerule respawn_radius 0`, and otherwise each server's default game rules;
- the server waits for `Done`, the bots join one a second, warm up for 300 seconds and are measured for 300,
with `/tick query` every 5 seconds and RSS and CPU every second.
The bots are `horizon-bots --behaviour mixed`: 60 % walk, 20 % explore and 20 % idle, each choice deterministic
per bot. All are creative and fly 24 blocks above spawn. `walk` flies to random points within 48 blocks at
5.6 blocks a second, `explore` flies straight out at 0.8 blocks a second (`--explore-speed 0.8`), and `idle`
hovers.
**Machine.** The ThinkPad: Intel Core i7-7500U (2 cores, 4 threads), 32 GB, NixOS. The desktop was meant to
run it but didn't wake (`~/fleet/power wake desktop` timed out twice). No other session ran on the ThinkPad
(`load.txt`, a sample a minute: 0 other sessions throughout). The benchmark ran under `nice -n 19` and
`ionice -c3`. The bots ran on the Mac mini (M4 Pro, shared with other work at 51 to 98 % CPU, 82 % on
average) and reached the ThinkPad over the tailnet.
**What ran.**
- Horizon: release v2026.10.19 (07f2913d), the server archive `horizon-server-linux-x86_64.tar.gz` installed
with `install.sh`. `horizon-server` ran its `horizon-engine` with game files prepared from the owner's own
Minecraft.
- Vanilla: the pinned 26.3 server jar (the owner's own copy) on OpenJDK 25.0.4.1, `-Xms4g -Xmx4g`.
- Paper: 26.3 build 205 (`26.3-205-main@a90108f`), from PaperMC's download API, checked against its SHA-256.
It ran with Paper's defaults except `connection-throttle: -1` in `bukkit.yml`. Every bot connects from one
address, and the default throttle (4 s) refused all but the first. Paperclip patched the pinned server jar
from its cache. Same Java and heap.
- Fabric + Lithium: the pinned server jar on Fabric loader 0.19.5 (`tools/benchmark-deps/fabric.lock.json`) with Lithium
`mc26.3-0.26.2-fabric` (`tools/benchmark-deps/mods.lock.json`, checked against its SHA-512). Lithium needs no Fabric API.
Same Java and heap.
The Paper and Fabric jars are dev-only downloads into `.local/server-bench/jars` and are never committed or
shipped.
**How it ran.** The first pass ran all four servers at 1 and 10 bots, but its Paper 10-bot run hit the
connection throttle (1 of 10 bots joined), and it was stopped after that. The 1-bot and 10-bot lines for
Horizon, vanilla and Fabric come from that pass, along with Paper's 1-bot line. A second pass with the
throttle off ran all four at 25 and 50 bots, then Paper at 10. All runs were on the same machine on the
same morning (02:57 to 06:02 UTC). `runs.jsonl` holds the 16 run lines used here.
`report-25-50.json` and `report-paper-10.json` are the second pass's reports. The first pass, stopped
early, wrote no report. Its lines come from its output.
## Reproduce
curl -fsSL https://raw.githubusercontent.com/ludovic111/horizon/main/packaging/server/install.sh | sh
horizon-prepare --prepare --server-jar /path/to/server.jar # your own copy
cargo build --release -p horizon-bots # on the machine that runs the bots
# in a checkout whose .local/minecraft holds the pinned vanilla jars (tools/lab.py prepare):
HORIZON_BENCH_BIN=~/.local/share/horizon/current \
python3 tools/server_bench.py --servers horizon,vanilla,paper,fabric --bots 1,10,25,50 --behaviour mixed \
--explore-speed 0.8 --warmup 300 --nice 19 \
--bots-via 'ssh <bots machine>' --bots-bin /path/to/horizon-bots --connect <this machine's address> \
--out four-way.json
## Next
- One pass per configuration ran, on a 4-thread laptop CPU. A rerun on a bigger machine (the desktop's
24 threads) would show which server uses more cores best, and give vanilla at 50 a second chance.
- Horizon's average tick: where its 1-player tick goes (13.7 ms against Paper's 10.3). That work is
perf-server-11's.
- 100 bots.
@@ -0,0 +1,870 @@
2026-10-19T02:57:31Z
ludothinkpad ok: 29.0 GB free, 4 threads 1% busy, 0 sessions
desktop offline
macmini busy: 28.7 GB free, 14 threads 89% busy, 1 sessions
macbook offline
2026-10-19T02:58:36Z
ludothinkpad ok: 28.3 GB free, 4 threads 20% busy, 0 sessions
desktop offline
macmini busy: 26.0 GB free, 14 threads 92% busy, 1 sessions
macbook offline
2026-10-19T02:59:40Z
ludothinkpad ok: 28.3 GB free, 4 threads 20% busy, 0 sessions
desktop offline
macmini busy: 26.8 GB free, 14 threads 96% busy, 1 sessions
macbook offline
2026-10-19T03:00:44Z
ludothinkpad ok: 28.3 GB free, 4 threads 15% busy, 0 sessions
desktop offline
macmini busy: 25.8 GB free, 14 threads 97% busy, 1 sessions
macbook offline
2026-10-19T03:01:48Z
ludothinkpad ok: 27.8 GB free, 4 threads 27% busy, 0 sessions
desktop offline
macmini ok: 26.8 GB free, 14 threads 84% busy, 1 sessions
macbook offline
2026-10-19T03:02:52Z
ludothinkpad ok: 28.4 GB free, 4 threads 18% busy, 0 sessions
desktop offline
macmini busy: 25.7 GB free, 14 threads 93% busy, 1 sessions
macbook offline
2026-10-19T03:03:56Z
ludothinkpad ok: 28.4 GB free, 4 threads 14% busy, 0 sessions
desktop offline
macmini busy: 29.6 GB free, 14 threads 96% busy, 1 sessions
macbook offline
2026-10-19T03:05:00Z
ludothinkpad ok: 28.4 GB free, 4 threads 11% busy, 0 sessions
desktop offline
macmini ok: 29.4 GB free, 14 threads 84% busy, 1 sessions
macbook offline
2026-10-19T03:06:04Z
ludothinkpad ok: 28.3 GB free, 4 threads 22% busy, 0 sessions
desktop offline
macmini ok: 26.8 GB free, 14 threads 82% busy, 1 sessions
macbook offline
2026-10-19T03:07:09Z
ludothinkpad ok: 28.4 GB free, 4 threads 11% busy, 0 sessions
desktop offline
macmini busy: 26.1 GB free, 14 threads 91% busy, 1 sessions
macbook offline
2026-10-19T03:08:13Z
ludothinkpad busy: 26.9 GB free, 4 threads 92% busy, 0 sessions
desktop offline
macmini ok: 26.4 GB free, 14 threads 76% busy, 1 sessions
macbook offline
2026-10-19T03:09:17Z
ludothinkpad ok: 25.8 GB free, 4 threads 19% busy, 0 sessions
desktop offline
macmini busy: 29.5 GB free, 14 threads 87% busy, 1 sessions
macbook offline
2026-10-19T03:10:21Z
ludothinkpad ok: 25.8 GB free, 4 threads 49% busy, 0 sessions
desktop offline
macmini ok: 25.8 GB free, 14 threads 79% busy, 1 sessions
macbook offline
2026-10-19T03:11:25Z
ludothinkpad ok: 25.8 GB free, 4 threads 19% busy, 0 sessions
desktop offline
macmini busy: 26.7 GB free, 14 threads 85% busy, 1 sessions
macbook offline
2026-10-19T03:12:29Z
ludothinkpad ok: 25.8 GB free, 4 threads 18% busy, 0 sessions
desktop offline
macmini busy: 26.2 GB free, 14 threads 88% busy, 1 sessions
macbook offline
2026-10-19T03:13:33Z
ludothinkpad ok: 25.8 GB free, 4 threads 17% busy, 0 sessions
desktop offline
macmini ok: 28.0 GB free, 14 threads 84% busy, 1 sessions
macbook offline
2026-10-19T03:14:37Z
ludothinkpad ok: 25.8 GB free, 4 threads 17% busy, 0 sessions
desktop offline
macmini ok: 28.7 GB free, 14 threads 76% busy, 1 sessions
macbook offline
2026-10-19T03:15:41Z
ludothinkpad ok: 25.8 GB free, 4 threads 18% busy, 0 sessions
desktop offline
macmini busy: 26.2 GB free, 14 threads 87% busy, 1 sessions
macbook offline
2026-10-19T03:16:46Z
ludothinkpad ok: 25.8 GB free, 4 threads 18% busy, 0 sessions
desktop offline
macmini busy: 25.0 GB free, 14 threads 91% busy, 1 sessions
macbook offline
2026-10-19T03:17:50Z
ludothinkpad ok: 25.8 GB free, 4 threads 19% busy, 0 sessions
desktop offline
macmini ok: 25.6 GB free, 14 threads 78% busy, 1 sessions
macbook offline
2026-10-19T03:18:54Z
ludothinkpad ok: 26.7 GB free, 4 threads 71% busy, 0 sessions
desktop offline
macmini busy: 26.2 GB free, 14 threads 91% busy, 1 sessions
macbook offline
2026-10-19T03:19:58Z
ludothinkpad ok: 25.1 GB free, 4 threads 16% busy, 0 sessions
desktop offline
macmini busy: 25.6 GB free, 14 threads 85% busy, 1 sessions
macbook offline
2026-10-19T03:21:02Z
ludothinkpad ok: 25.1 GB free, 4 threads 15% busy, 0 sessions
desktop offline
macmini busy: 25.7 GB free, 14 threads 87% busy, 1 sessions
macbook offline
2026-10-19T03:22:06Z
ludothinkpad ok: 25.1 GB free, 4 threads 12% busy, 0 sessions
desktop offline
macmini busy: 26.8 GB free, 14 threads 87% busy, 1 sessions
macbook offline
2026-10-19T03:23:10Z
ludothinkpad ok: 25.1 GB free, 4 threads 12% busy, 0 sessions
desktop offline
macmini ok: 25.6 GB free, 14 threads 81% busy, 1 sessions
macbook offline
2026-10-19T03:24:14Z
ludothinkpad ok: 25.1 GB free, 4 threads 19% busy, 0 sessions
desktop offline
macmini ok: 27.1 GB free, 14 threads 82% busy, 1 sessions
macbook offline
2026-10-19T03:25:19Z
ludothinkpad ok: 25.1 GB free, 4 threads 9% busy, 0 sessions
desktop offline
macmini busy: 28.6 GB free, 14 threads 86% busy, 1 sessions
macbook offline
2026-10-19T03:26:23Z
ludothinkpad ok: 25.1 GB free, 4 threads 13% busy, 0 sessions
desktop offline
macmini ok: 25.9 GB free, 14 threads 71% busy, 1 sessions
macbook offline
2026-10-19T03:27:27Z
ludothinkpad ok: 25.1 GB free, 4 threads 14% busy, 0 sessions
desktop offline
macmini busy: 26.0 GB free, 14 threads 94% busy, 1 sessions
macbook offline
2026-10-19T03:28:31Z
ludothinkpad ok: 25.1 GB free, 4 threads 13% busy, 0 sessions
desktop offline
macmini ok: 26.1 GB free, 14 threads 79% busy, 1 sessions
macbook offline
2026-10-19T03:29:35Z
ludothinkpad ok: 27.5 GB free, 4 threads 36% busy, 0 sessions
desktop offline
macmini ok: 25.5 GB free, 14 threads 84% busy, 1 sessions
macbook offline
2026-10-19T03:30:39Z
ludothinkpad ok: 25.8 GB free, 4 threads 22% busy, 0 sessions
desktop offline
macmini ok: 26.2 GB free, 14 threads 73% busy, 1 sessions
macbook offline
2026-10-19T03:31:43Z
ludothinkpad ok: 25.8 GB free, 4 threads 20% busy, 0 sessions
desktop offline
macmini busy: 26.1 GB free, 14 threads 86% busy, 1 sessions
macbook offline
2026-10-19T03:32:47Z
ludothinkpad ok: 25.8 GB free, 4 threads 21% busy, 0 sessions
desktop offline
macmini busy: 28.4 GB free, 14 threads 90% busy, 1 sessions
macbook offline
2026-10-19T03:33:52Z
ludothinkpad ok: 25.8 GB free, 4 threads 18% busy, 0 sessions
desktop offline
macmini ok: 26.9 GB free, 14 threads 84% busy, 2 sessions
macbook offline
2026-10-19T03:34:56Z
ludothinkpad ok: 25.8 GB free, 4 threads 14% busy, 0 sessions
desktop offline
macmini ok: 26.1 GB free, 14 threads 76% busy, 1 sessions
macbook offline
2026-10-19T03:36:00Z
ludothinkpad ok: 25.8 GB free, 4 threads 16% busy, 0 sessions
desktop offline
macmini ok: 27.4 GB free, 14 threads 78% busy, 1 sessions
macbook offline
2026-10-19T03:37:04Z
ludothinkpad ok: 25.8 GB free, 4 threads 15% busy, 0 sessions
desktop offline
macmini ok: 26.6 GB free, 14 threads 77% busy, 1 sessions
macbook offline
2026-10-19T03:38:08Z
ludothinkpad ok: 25.8 GB free, 4 threads 14% busy, 0 sessions
desktop offline
macmini busy: 25.8 GB free, 14 threads 94% busy, 1 sessions
macbook offline
2026-10-19T03:39:12Z
ludothinkpad ok: 25.8 GB free, 4 threads 17% busy, 0 sessions
desktop offline
macmini busy: 27.1 GB free, 14 threads 91% busy, 1 sessions
macbook offline
2026-10-19T03:40:16Z
ludothinkpad ok: 28.4 GB free, 4 threads 73% busy, 0 sessions
desktop offline
macmini ok: 25.5 GB free, 14 threads 78% busy, 1 sessions
macbook offline
2026-10-19T03:41:20Z
ludothinkpad ok: 28.3 GB free, 4 threads 41% busy, 0 sessions
desktop offline
macmini busy: 25.8 GB free, 14 threads 86% busy, 1 sessions
macbook offline
2026-10-19T03:42:24Z
ludothinkpad ok: 28.3 GB free, 4 threads 24% busy, 0 sessions
desktop offline
macmini busy: 26.9 GB free, 14 threads 88% busy, 1 sessions
macbook offline
2026-10-19T03:43:29Z
ludothinkpad ok: 28.3 GB free, 4 threads 26% busy, 0 sessions
desktop offline
macmini busy: 25.0 GB free, 14 threads 87% busy, 1 sessions
macbook offline
2026-10-19T03:44:33Z
ludothinkpad ok: 28.2 GB free, 4 threads 22% busy, 0 sessions
desktop offline
macmini ok: 25.6 GB free, 14 threads 81% busy, 1 sessions
macbook offline
2026-10-19T03:45:37Z
ludothinkpad ok: 28.2 GB free, 4 threads 27% busy, 0 sessions
desktop offline
macmini ok: 27.8 GB free, 14 threads 76% busy, 1 sessions
macbook offline
2026-10-19T03:46:41Z
ludothinkpad ok: 28.1 GB free, 4 threads 37% busy, 0 sessions
desktop offline
macmini ok: 27.3 GB free, 14 threads 81% busy, 1 sessions
macbook offline
2026-10-19T03:47:45Z
ludothinkpad ok: 28.1 GB free, 4 threads 21% busy, 0 sessions
desktop offline
macmini ok: 27.1 GB free, 14 threads 82% busy, 1 sessions
macbook offline
2026-10-19T03:48:49Z
ludothinkpad ok: 28.1 GB free, 4 threads 32% busy, 0 sessions
desktop offline
macmini ok: 26.2 GB free, 14 threads 82% busy, 1 sessions
macbook offline
2026-10-19T03:49:53Z
ludothinkpad ok: 28.1 GB free, 4 threads 25% busy, 0 sessions
desktop offline
macmini busy: 25.7 GB free, 14 threads 85% busy, 1 sessions
macbook offline
2026-10-19T03:50:57Z
ludothinkpad busy: 26.6 GB free, 4 threads 94% busy, 0 sessions
desktop offline
macmini ok: 25.1 GB free, 14 threads 85% busy, 1 sessions
macbook offline
2026-10-19T03:52:01Z
ludothinkpad ok: 25.8 GB free, 4 threads 28% busy, 0 sessions
desktop offline
macmini busy: 25.1 GB free, 14 threads 91% busy, 1 sessions
macbook offline
2026-10-19T03:53:06Z
ludothinkpad ok: 25.8 GB free, 4 threads 50% busy, 0 sessions
desktop offline
macmini busy: 26.4 GB free, 14 threads 87% busy, 1 sessions
macbook offline
2026-10-19T03:54:10Z
ludothinkpad ok: 25.7 GB free, 4 threads 41% busy, 0 sessions
desktop offline
macmini busy: 25.8 GB free, 14 threads 87% busy, 1 sessions
macbook offline
2026-10-19T03:55:14Z
ludothinkpad ok: 25.6 GB free, 4 threads 27% busy, 0 sessions
desktop offline
macmini busy: 31.4 GB free, 14 threads 90% busy, 1 sessions
macbook offline
2026-10-19T03:56:18Z
ludothinkpad ok: 25.6 GB free, 4 threads 26% busy, 0 sessions
desktop offline
macmini busy: 31.0 GB free, 14 threads 88% busy, 1 sessions
macbook offline
2026-10-19T03:57:22Z
ludothinkpad ok: 25.6 GB free, 4 threads 33% busy, 0 sessions
desktop offline
macmini busy: 26.5 GB free, 14 threads 91% busy, 1 sessions
macbook offline
2026-10-19T03:58:26Z
ludothinkpad ok: 25.5 GB free, 4 threads 45% busy, 0 sessions
desktop offline
macmini busy: 26.7 GB free, 14 threads 89% busy, 1 sessions
macbook offline
2026-10-19T03:59:30Z
ludothinkpad ok: 25.5 GB free, 4 threads 24% busy, 0 sessions
desktop offline
macmini ok: 25.6 GB free, 14 threads 77% busy, 1 sessions
macbook offline
2026-10-19T04:00:34Z
ludothinkpad ok: 25.5 GB free, 4 threads 31% busy, 0 sessions
desktop offline
macmini ok: 25.0 GB free, 14 threads 81% busy, 1 sessions
macbook offline
2026-10-19T04:01:38Z
ludothinkpad busy: 27.0 GB free, 4 threads 99% busy, 0 sessions
desktop offline
macmini ok: 28.3 GB free, 14 threads 82% busy, 1 sessions
macbook offline
2026-10-19T04:02:42Z
ludothinkpad ok: 26.2 GB free, 4 threads 19% busy, 0 sessions
desktop offline
macmini busy: 26.5 GB free, 14 threads 87% busy, 1 sessions
macbook offline
2026-10-19T04:03:47Z
ludothinkpad ok: 25.9 GB free, 4 threads 14% busy, 0 sessions
desktop offline
macmini ok: 27.9 GB free, 14 threads 78% busy, 1 sessions
macbook offline
2026-10-19T04:04:51Z
ludothinkpad ok: 25.7 GB free, 4 threads 11% busy, 0 sessions
desktop offline
macmini busy: 30.0 GB free, 14 threads 91% busy, 1 sessions
macbook offline
2026-10-19T04:05:55Z
ludothinkpad ok: 25.3 GB free, 4 threads 10% busy, 0 sessions
desktop offline
macmini busy: 27.5 GB free, 14 threads 88% busy, 1 sessions
macbook offline
2026-10-19T04:06:59Z
ludothinkpad ok: 25.3 GB free, 4 threads 13% busy, 0 sessions
desktop offline
macmini ok: 26.9 GB free, 14 threads 77% busy, 1 sessions
macbook offline
2026-10-19T04:08:03Z
ludothinkpad ok: 25.3 GB free, 4 threads 12% busy, 0 sessions
desktop offline
macmini ok: 25.8 GB free, 14 threads 81% busy, 1 sessions
macbook offline
2026-10-19T04:09:07Z
ludothinkpad ok: 25.3 GB free, 4 threads 10% busy, 0 sessions
desktop offline
macmini busy: 26.9 GB free, 14 threads 86% busy, 1 sessions
macbook offline
2026-10-19T04:10:11Z
ludothinkpad ok: 25.3 GB free, 4 threads 8% busy, 0 sessions
desktop offline
macmini busy: 26.6 GB free, 14 threads 92% busy, 1 sessions
macbook offline
2026-10-19T04:11:15Z
ludothinkpad ok: 25.3 GB free, 4 threads 9% busy, 0 sessions
desktop offline
macmini ok: 27.3 GB free, 14 threads 77% busy, 1 sessions
macbook offline
2026-10-19T04:12:20Z
ludothinkpad busy: 27.5 GB free, 4 threads 88% busy, 0 sessions
desktop offline
macmini busy: 26.5 GB free, 14 threads 86% busy, 1 sessions
macbook offline
2026-10-19T04:13:24Z
ludothinkpad ok: 25.8 GB free, 4 threads 44% busy, 0 sessions
desktop offline
macmini busy: 27.6 GB free, 14 threads 90% busy, 1 sessions
macbook offline
2026-10-19T04:14:28Z
ludothinkpad ok: 25.7 GB free, 4 threads 28% busy, 0 sessions
desktop offline
macmini busy: 27.8 GB free, 14 threads 87% busy, 1 sessions
macbook offline
2026-10-19T04:15:32Z
ludothinkpad ok: 25.7 GB free, 4 threads 26% busy, 0 sessions
desktop offline
macmini ok: 26.7 GB free, 14 threads 73% busy, 1 sessions
macbook offline
2026-10-19T04:16:36Z
ludothinkpad ok: 25.6 GB free, 4 threads 28% busy, 0 sessions
desktop offline
macmini ok: 29.3 GB free, 14 threads 80% busy, 1 sessions
macbook offline
2026-10-19T04:17:40Z
ludothinkpad ok: 25.6 GB free, 4 threads 32% busy, 0 sessions
desktop offline
macmini busy: 26.3 GB free, 14 threads 90% busy, 1 sessions
macbook offline
2026-10-19T04:18:44Z
ludothinkpad ok: 25.5 GB free, 4 threads 76% busy, 0 sessions
desktop offline
macmini ok: 28.4 GB free, 14 threads 84% busy, 1 sessions
macbook offline
2026-10-19T04:19:48Z
ludothinkpad ok: 25.5 GB free, 4 threads 26% busy, 0 sessions
desktop offline
macmini ok: 30.4 GB free, 14 threads 79% busy, 1 sessions
macbook offline
2026-10-19T04:20:52Z
ludothinkpad ok: 25.5 GB free, 4 threads 26% busy, 0 sessions
desktop offline
macmini ok: 28.1 GB free, 14 threads 73% busy, 1 sessions
macbook offline
2026-10-19T04:21:57Z
ludothinkpad ok: 25.5 GB free, 4 threads 28% busy, 0 sessions
desktop offline
macmini ok: 31.0 GB free, 14 threads 84% busy, 1 sessions
macbook offline
2026-10-19T04:23:01Z
ludothinkpad ok: 25.5 GB free, 4 threads 10% busy, 0 sessions
desktop offline
macmini ok: 31.7 GB free, 14 threads 83% busy, 1 sessions
macbook offline
2026-10-19T04:24:05Z
ludothinkpad ok: 28.3 GB free, 4 threads 41% busy, 0 sessions
desktop offline
macmini ok: 27.1 GB free, 14 threads 71% busy, 1 sessions
macbook offline
2026-10-19T04:25:15Z
ludothinkpad ok: 28.8 GB free, 4 threads 4% busy, 0 sessions
desktop offline
macmini busy: 25.7 GB free, 14 threads 98% busy, 1 sessions
macbook offline
2026-10-19T04:26:19Z
ludothinkpad ok: 28.3 GB free, 4 threads 40% busy, 0 sessions
desktop offline
macmini ok: 32.1 GB free, 14 threads 65% busy, 1 sessions
macbook offline
2026-10-19T04:27:23Z
ludothinkpad ok: 28.2 GB free, 4 threads 41% busy, 0 sessions
desktop offline
macmini busy: 29.4 GB free, 14 threads 89% busy, 1 sessions
macbook offline
2026-10-19T04:28:27Z
ludothinkpad ok: 28.2 GB free, 4 threads 44% busy, 0 sessions
desktop offline
macmini ok: 28.9 GB free, 14 threads 77% busy, 1 sessions
macbook offline
2026-10-19T04:29:32Z
ludothinkpad ok: 28.1 GB free, 4 threads 33% busy, 0 sessions
desktop offline
macmini ok: 25.3 GB free, 14 threads 71% busy, 1 sessions
macbook offline
2026-10-19T04:30:36Z
ludothinkpad ok: 28.1 GB free, 4 threads 31% busy, 0 sessions
desktop offline
macmini ok: 30.0 GB free, 14 threads 63% busy, 1 sessions
macbook offline
2026-10-19T04:31:40Z
ludothinkpad ok: 28.0 GB free, 4 threads 40% busy, 0 sessions
desktop offline
macmini ok: 27.4 GB free, 14 threads 73% busy, 1 sessions
macbook offline
2026-10-19T04:32:44Z
ludothinkpad ok: 28.0 GB free, 4 threads 35% busy, 0 sessions
desktop offline
macmini ok: 29.3 GB free, 14 threads 51% busy, 1 sessions
macbook offline
2026-10-19T04:33:48Z
ludothinkpad ok: 28.0 GB free, 4 threads 31% busy, 0 sessions
desktop offline
macmini ok: 27.5 GB free, 14 threads 67% busy, 1 sessions
macbook offline
2026-10-19T04:34:52Z
ludothinkpad ok: 28.0 GB free, 4 threads 35% busy, 0 sessions
desktop offline
macmini ok: 26.6 GB free, 14 threads 82% busy, 1 sessions
macbook offline
2026-10-19T04:35:56Z
ludothinkpad ok: 27.9 GB free, 4 threads 34% busy, 0 sessions
desktop offline
macmini ok: 28.9 GB free, 14 threads 66% busy, 1 sessions
macbook offline
2026-10-19T04:37:00Z
ludothinkpad busy: 25.8 GB free, 4 threads 98% busy, 0 sessions
desktop offline
macmini ok: 26.9 GB free, 14 threads 75% busy, 1 sessions
macbook offline
2026-10-19T04:38:04Z
ludothinkpad ok: 25.7 GB free, 4 threads 64% busy, 0 sessions
desktop offline
macmini ok: 25.6 GB free, 14 threads 74% busy, 1 sessions
macbook offline
2026-10-19T04:39:09Z
ludothinkpad ok: 25.6 GB free, 4 threads 64% busy, 0 sessions
desktop offline
macmini ok: 26.8 GB free, 14 threads 77% busy, 1 sessions
macbook offline
2026-10-19T04:40:13Z
ludothinkpad ok: 25.5 GB free, 4 threads 51% busy, 0 sessions
desktop offline
macmini ok: 26.3 GB free, 14 threads 70% busy, 1 sessions
macbook offline
2026-10-19T04:41:17Z
ludothinkpad ok: 25.4 GB free, 4 threads 62% busy, 0 sessions
desktop offline
macmini ok: 26.3 GB free, 14 threads 83% busy, 1 sessions
macbook offline
2026-10-19T04:42:21Z
ludothinkpad ok: 25.4 GB free, 4 threads 47% busy, 0 sessions
desktop offline
macmini ok: 26.2 GB free, 14 threads 68% busy, 1 sessions
macbook offline
2026-10-19T04:43:25Z
ludothinkpad ok: 25.4 GB free, 4 threads 67% busy, 0 sessions
desktop offline
macmini busy: 26.9 GB free, 14 threads 88% busy, 1 sessions
macbook offline
2026-10-19T04:44:29Z
ludothinkpad ok: 25.3 GB free, 4 threads 64% busy, 0 sessions
desktop offline
macmini ok: 30.4 GB free, 14 threads 79% busy, 1 sessions
macbook offline
2026-10-19T04:45:33Z
ludothinkpad ok: 25.2 GB free, 4 threads 66% busy, 0 sessions
desktop offline
macmini ok: 25.0 GB free, 14 threads 69% busy, 1 sessions
macbook offline
2026-10-19T04:46:37Z
ludothinkpad ok: 25.1 GB free, 4 threads 48% busy, 0 sessions
desktop offline
macmini busy: 26.4 GB free, 14 threads 87% busy, 1 sessions
macbook offline
2026-10-19T04:47:42Z
ludothinkpad ok: 27.5 GB free, 4 threads 69% busy, 0 sessions
desktop offline
macmini ok: 28.0 GB free, 14 threads 83% busy, 1 sessions
macbook offline
2026-10-19T04:48:46Z
ludothinkpad ok: 25.3 GB free, 4 threads 83% busy, 0 sessions
desktop offline
macmini busy: 25.6 GB free, 14 threads 85% busy, 1 sessions
macbook offline
2026-10-19T04:49:50Z
ludothinkpad ok: 25.3 GB free, 4 threads 55% busy, 0 sessions
desktop offline
macmini busy: 25.3 GB free, 14 threads 88% busy, 1 sessions
macbook offline
2026-10-19T04:50:54Z
ludothinkpad ok: 25.2 GB free, 4 threads 51% busy, 0 sessions
desktop offline
macmini ok: 30.9 GB free, 14 threads 74% busy, 1 sessions
macbook offline
2026-10-19T04:51:58Z
ludothinkpad ok: 25.2 GB free, 4 threads 57% busy, 0 sessions
desktop offline
macmini ok: 29.2 GB free, 14 threads 83% busy, 1 sessions
macbook offline
2026-10-19T04:53:02Z
ludothinkpad ok: 25.1 GB free, 4 threads 59% busy, 0 sessions
desktop offline
macmini busy: 26.2 GB free, 14 threads 88% busy, 1 sessions
macbook offline
2026-10-19T04:54:06Z
ludothinkpad ok: 25.1 GB free, 4 threads 38% busy, 0 sessions
desktop offline
macmini busy: 25.4 GB free, 14 threads 90% busy, 2 sessions
macbook offline
2026-10-19T04:55:10Z
ludothinkpad ok: 25.1 GB free, 4 threads 43% busy, 0 sessions
desktop offline
macmini busy: 25.4 GB free, 14 threads 85% busy, 1 sessions
macbook offline
2026-10-19T04:56:15Z
ludothinkpad ok: 25.1 GB free, 4 threads 42% busy, 0 sessions
desktop offline
macmini ok: 27.6 GB free, 14 threads 80% busy, 1 sessions
macbook offline
2026-10-19T04:57:19Z
ludothinkpad ok: 25.0 GB free, 4 threads 45% busy, 0 sessions
desktop offline
macmini ok: 27.0 GB free, 14 threads 67% busy, 1 sessions
macbook offline
2026-10-19T04:58:23Z
ludothinkpad ok: 25.0 GB free, 4 threads 48% busy, 0 sessions
desktop offline
macmini ok: 27.4 GB free, 14 threads 74% busy, 1 sessions
macbook offline
2026-10-19T04:59:27Z
ludothinkpad busy: 26.6 GB free, 4 threads 100% busy, 0 sessions
desktop offline
macmini busy: 27.7 GB free, 14 threads 91% busy, 1 sessions
macbook offline
2026-10-19T05:00:31Z
ludothinkpad ok: 25.6 GB free, 4 threads 53% busy, 0 sessions
desktop offline
macmini ok: 32.2 GB free, 14 threads 64% busy, 1 sessions
macbook offline
2026-10-19T05:01:35Z
ludothinkpad ok: 25.7 GB free, 4 threads 54% busy, 0 sessions
desktop offline
macmini ok: 26.1 GB free, 14 threads 85% busy, 1 sessions
macbook offline
2026-10-19T05:02:39Z
ludothinkpad ok: 25.6 GB free, 4 threads 59% busy, 0 sessions
desktop offline
macmini ok: 25.4 GB free, 14 threads 84% busy, 1 sessions
macbook offline
2026-10-19T05:03:43Z
ludothinkpad ok: 25.6 GB free, 4 threads 55% busy, 0 sessions
desktop offline
macmini busy: 25.3 GB free, 14 threads 87% busy, 1 sessions
macbook offline
2026-10-19T05:04:48Z
ludothinkpad busy: 25.0 GB free, 4 threads 96% busy, 0 sessions
desktop offline
macmini ok: 26.6 GB free, 14 threads 79% busy, 1 sessions
macbook offline
2026-10-19T05:05:52Z
ludothinkpad ok: 25.4 GB free, 4 threads 51% busy, 0 sessions
desktop offline
macmini ok: 27.8 GB free, 14 threads 84% busy, 1 sessions
macbook offline
2026-10-19T05:06:56Z
ludothinkpad ok: 25.4 GB free, 4 threads 46% busy, 0 sessions
desktop offline
macmini busy: 25.9 GB free, 14 threads 92% busy, 1 sessions
macbook offline
2026-10-19T05:08:00Z
ludothinkpad ok: 25.4 GB free, 4 threads 53% busy, 0 sessions
desktop offline
macmini busy: 29.0 GB free, 14 threads 88% busy, 1 sessions
macbook offline
2026-10-19T05:09:04Z
ludothinkpad ok: 25.1 GB free, 4 threads 64% busy, 0 sessions
desktop offline
macmini ok: 27.3 GB free, 14 threads 81% busy, 1 sessions
macbook offline
2026-10-19T05:10:08Z
ludothinkpad ok: 28.6 GB free, 4 threads 28% busy, 0 sessions
desktop offline
macmini ok: 26.8 GB free, 14 threads 78% busy, 1 sessions
macbook offline
2026-10-19T05:11:12Z
ludothinkpad ok: 28.2 GB free, 4 threads 48% busy, 0 sessions
desktop offline
macmini busy: 28.5 GB free, 14 threads 88% busy, 1 sessions
macbook offline
2026-10-19T05:12:16Z
ludothinkpad ok: 28.2 GB free, 4 threads 52% busy, 0 sessions
desktop offline
macmini ok: 27.3 GB free, 14 threads 74% busy, 1 sessions
macbook offline
2026-10-19T05:13:21Z
ludothinkpad ok: 28.1 GB free, 4 threads 41% busy, 0 sessions
desktop offline
macmini busy: 25.9 GB free, 14 threads 87% busy, 1 sessions
macbook offline
2026-10-19T05:14:25Z
ludothinkpad ok: 28.1 GB free, 4 threads 56% busy, 0 sessions
desktop offline
macmini busy: 26.7 GB free, 14 threads 88% busy, 1 sessions
macbook offline
2026-10-19T05:15:29Z
ludothinkpad ok: 28.0 GB free, 4 threads 50% busy, 0 sessions
desktop offline
macmini busy: 25.4 GB free, 14 threads 90% busy, 1 sessions
macbook offline
2026-10-19T05:16:33Z
ludothinkpad ok: 27.9 GB free, 4 threads 47% busy, 0 sessions
desktop offline
macmini ok: 29.1 GB free, 14 threads 77% busy, 1 sessions
macbook offline
2026-10-19T05:17:37Z
ludothinkpad ok: 27.9 GB free, 4 threads 61% busy, 0 sessions
desktop offline
macmini ok: 27.5 GB free, 14 threads 75% busy, 1 sessions
macbook offline
2026-10-19T05:18:41Z
ludothinkpad ok: 27.9 GB free, 4 threads 68% busy, 0 sessions
desktop offline
macmini ok: 27.3 GB free, 14 threads 76% busy, 1 sessions
macbook offline
2026-10-19T05:19:45Z
ludothinkpad ok: 27.8 GB free, 4 threads 45% busy, 0 sessions
desktop offline
macmini busy: 27.5 GB free, 14 threads 85% busy, 1 sessions
macbook offline
2026-10-19T05:20:49Z
ludothinkpad ok: 27.5 GB free, 4 threads 60% busy, 0 sessions
desktop offline
macmini busy: 26.6 GB free, 14 threads 95% busy, 1 sessions
macbook offline
2026-10-19T05:21:53Z
ludothinkpad busy: 26.7 GB free, 4 threads 99% busy, 0 sessions
desktop offline
macmini ok: 29.8 GB free, 14 threads 72% busy, 1 sessions
macbook offline
2026-10-19T05:22:58Z
ludothinkpad busy: 25.7 GB free, 4 threads 97% busy, 0 sessions
desktop offline
macmini busy: 26.6 GB free, 14 threads 86% busy, 1 sessions
macbook offline
2026-10-19T05:24:02Z
ludothinkpad busy: 25.4 GB free, 4 threads 90% busy, 0 sessions
desktop offline
macmini busy: 28.2 GB free, 14 threads 85% busy, 1 sessions
macbook offline
2026-10-19T05:25:06Z
ludothinkpad ok: 25.3 GB free, 4 threads 73% busy, 0 sessions
desktop offline
macmini busy: 27.8 GB free, 14 threads 89% busy, 1 sessions
macbook offline
2026-10-19T05:26:10Z
ludothinkpad ok: 25.2 GB free, 4 threads 69% busy, 0 sessions
desktop offline
macmini ok: 28.8 GB free, 14 threads 74% busy, 1 sessions
macbook offline
2026-10-19T05:27:14Z
ludothinkpad ok: 25.2 GB free, 4 threads 82% busy, 0 sessions
desktop offline
macmini ok: 29.6 GB free, 14 threads 80% busy, 1 sessions
macbook offline
2026-10-19T05:28:18Z
ludothinkpad ok: 25.2 GB free, 4 threads 66% busy, 0 sessions
desktop offline
macmini busy: 33.7 GB free, 14 threads 88% busy, 1 sessions
macbook offline
2026-10-19T05:29:22Z
ludothinkpad busy: 26.4 GB free, 4 threads 99% busy, 0 sessions
desktop offline
macmini ok: 33.7 GB free, 14 threads 81% busy, 1 sessions
macbook offline
2026-10-19T05:30:27Z
ludothinkpad ok: 25.0 GB free, 4 threads 73% busy, 0 sessions
desktop offline
macmini busy: 31.8 GB free, 14 threads 87% busy, 1 sessions
macbook offline
2026-10-19T05:31:31Z
ludothinkpad ok: 24.9 GB free, 4 threads 66% busy, 0 sessions
desktop offline
macmini ok: 29.7 GB free, 14 threads 77% busy, 1 sessions
macbook offline
2026-10-19T05:32:35Z
ludothinkpad busy: 25.0 GB free, 4 threads 94% busy, 0 sessions
desktop offline
macmini busy: 27.8 GB free, 14 threads 87% busy, 1 sessions
macbook offline
2026-10-19T05:33:39Z
ludothinkpad ok: 24.9 GB free, 4 threads 70% busy, 0 sessions
desktop offline
macmini busy: 25.7 GB free, 14 threads 90% busy, 1 sessions
macbook offline
2026-10-19T05:34:43Z
ludothinkpad busy: 24.9 GB free, 4 threads 88% busy, 0 sessions
desktop offline
macmini busy: 32.7 GB free, 14 threads 88% busy, 1 sessions
macbook offline
2026-10-19T05:35:47Z
ludothinkpad ok: 24.9 GB free, 4 threads 72% busy, 0 sessions
desktop offline
macmini busy: 33.3 GB free, 14 threads 86% busy, 1 sessions
macbook offline
2026-10-19T05:36:51Z
ludothinkpad ok: 24.8 GB free, 4 threads 76% busy, 0 sessions
desktop offline
macmini ok: 32.6 GB free, 14 threads 83% busy, 1 sessions
macbook offline
2026-10-19T05:37:55Z
ludothinkpad ok: 24.7 GB free, 4 threads 71% busy, 0 sessions
desktop offline
macmini ok: 31.5 GB free, 14 threads 65% busy, 1 sessions
macbook offline
2026-10-19T05:39:00Z
ludothinkpad busy: 24.7 GB free, 4 threads 92% busy, 0 sessions
desktop offline
macmini ok: 29.7 GB free, 14 threads 69% busy, 1 sessions
macbook offline
2026-10-19T05:40:04Z
ludothinkpad ok: 28.6 GB free, 4 threads 55% busy, 0 sessions
desktop offline
macmini ok: 30.0 GB free, 14 threads 73% busy, 1 sessions
macbook offline
2026-10-19T05:41:08Z
ludothinkpad busy: 25.6 GB free, 4 threads 100% busy, 0 sessions
desktop offline
macmini busy: 28.3 GB free, 14 threads 88% busy, 1 sessions
macbook offline
2026-10-19T05:42:12Z
ludothinkpad busy: 25.5 GB free, 4 threads 97% busy, 0 sessions
desktop offline
macmini ok: 27.6 GB free, 14 threads 80% busy, 1 sessions
macbook offline
2026-10-19T05:43:16Z
ludothinkpad busy: 25.3 GB free, 4 threads 98% busy, 0 sessions
desktop offline
macmini ok: 29.5 GB free, 14 threads 76% busy, 1 sessions
macbook offline
2026-10-19T05:44:20Z
ludothinkpad ok: 25.2 GB free, 4 threads 85% busy, 0 sessions
desktop offline
macmini ok: 28.6 GB free, 14 threads 64% busy, 1 sessions
macbook offline
2026-10-19T05:45:25Z
ludothinkpad ok: 25.2 GB free, 4 threads 74% busy, 0 sessions
desktop offline
macmini ok: 26.6 GB free, 14 threads 78% busy, 1 sessions
macbook offline
2026-10-19T05:46:29Z
ludothinkpad ok: 25.2 GB free, 4 threads 68% busy, 0 sessions
desktop offline
macmini ok: 26.5 GB free, 14 threads 82% busy, 1 sessions
macbook offline
2026-10-19T05:47:33Z
ludothinkpad ok: 24.5 GB free, 4 threads 84% busy, 0 sessions
desktop offline
macmini busy: 26.5 GB free, 14 threads 90% busy, 1 sessions
macbook offline
2026-10-19T05:48:37Z
ludothinkpad busy: 24.4 GB free, 4 threads 86% busy, 0 sessions
desktop offline
macmini ok: 30.6 GB free, 14 threads 61% busy, 1 sessions
macbook offline
2026-10-19T05:49:41Z
ludothinkpad ok: 24.2 GB free, 4 threads 80% busy, 0 sessions
desktop offline
macmini busy: 26.9 GB free, 14 threads 85% busy, 3 sessions
macbook offline
2026-10-19T05:50:45Z
ludothinkpad ok: 24.2 GB free, 4 threads 72% busy, 0 sessions
desktop offline
macmini busy: 25.7 GB free, 14 threads 89% busy, 1 sessions
macbook offline
2026-10-19T05:51:49Z
ludothinkpad ok: 24.1 GB free, 4 threads 17% busy, 0 sessions
desktop offline
macmini busy: 29.1 GB free, 14 threads 87% busy, 1 sessions
macbook offline
2026-10-19T05:52:54Z
ludothinkpad ok: 27.5 GB free, 4 threads 83% busy, 0 sessions
desktop offline
macmini ok: 26.9 GB free, 14 threads 76% busy, 1 sessions
macbook offline
2026-10-19T05:53:58Z
ludothinkpad ok: 25.8 GB free, 4 threads 56% busy, 0 sessions
desktop offline
macmini ok: 27.3 GB free, 14 threads 82% busy, 1 sessions
macbook offline
2026-10-19T05:55:02Z
ludothinkpad ok: 25.8 GB free, 4 threads 35% busy, 0 sessions
desktop offline
macmini busy: 27.2 GB free, 14 threads 95% busy, 1 sessions
macbook offline
2026-10-19T05:56:06Z
ludothinkpad ok: 25.0 GB free, 4 threads 52% busy, 0 sessions
desktop offline
macmini ok: 29.6 GB free, 14 threads 56% busy, 1 sessions
macbook offline
2026-10-19T05:57:10Z
ludothinkpad ok: 25.1 GB free, 4 threads 30% busy, 0 sessions
desktop offline
macmini busy: 26.2 GB free, 14 threads 91% busy, 1 sessions
macbook offline
2026-10-19T05:58:14Z
ludothinkpad ok: 25.1 GB free, 4 threads 36% busy, 0 sessions
desktop offline
macmini busy: 27.8 GB free, 14 threads 94% busy, 1 sessions
macbook offline
2026-10-19T05:59:18Z
ludothinkpad ok: 25.0 GB free, 4 threads 27% busy, 0 sessions
desktop offline
macmini ok: 30.3 GB free, 14 threads 51% busy, 1 sessions
macbook offline
2026-10-19T06:00:22Z
ludothinkpad ok: 25.0 GB free, 4 threads 24% busy, 0 sessions
desktop offline
macmini busy: 26.7 GB free, 14 threads 86% busy, 1 sessions
macbook offline
2026-10-19T06:01:26Z
ludothinkpad ok: 24.4 GB free, 4 threads 38% busy, 0 sessions
desktop offline
macmini busy: 27.1 GB free, 14 threads 89% busy, 1 sessions
macbook offline
2026-10-19T06:02:31Z
ludothinkpad ok: 25.2 GB free, 4 threads 27% busy, 0 sessions
desktop offline
macmini ok: 29.1 GB free, 14 threads 55% busy, 1 sessions
macbook offline
@@ -0,0 +1,437 @@
{
"kind": "horizon-server-benchmark",
"date": "2026-10-19T05:52:44+00:00",
"machine": {
"system": "Linux",
"machine": "x86_64",
"cpus": 4
},
"commit": "38505a07b5334f3ea2c467efb4a00e5557f910f8",
"java": [
"openjdk 25.0.4.1 2026-08-18"
],
"versions": {
"paper_build": 205,
"fabric_loader": "0.19.5",
"lithium": "mc26.3-0.26.2-fabric"
},
"settings": {
"servers": "horizon,vanilla,paper,fabric",
"bots": "25,50",
"behaviour": "mixed",
"join_every": 1.0,
"warmup": 300,
"seconds": 300,
"seed": 12345,
"explore_speed": 0.8,
"gamerule": "",
"heap": "4g",
"out": "/home/ludovic/sh10/out/mixed-equal-25-50.json",
"nice": 19,
"bots_via": "ssh macmini",
"bots_bin": "/Users/ludovicmarie/code/horizon/.claude/worktrees/sh10-bench/target/release/horizon-bots",
"connect": "the ThinkPad's tailnet address",
"properties": {
"level-seed": "12345",
"view-distance": "10",
"simulation-distance": "10",
"online-mode": "false",
"white-list": "false",
"gamemode": "creative",
"force-gamemode": "true",
"spawn-protection": "0",
"max-players": "200",
"difficulty": "easy",
"sync-chunk-writes": "true"
}
},
"runs": [
{
"server": "horizon",
"bots": 25,
"behaviour": "mixed",
"start_seconds": 8.51,
"mspt": {
"average": 30.92,
"p50": 27.88,
"p95": 48.25,
"p99": 72.0
},
"mspt_worst_p99": 121.1,
"tps": 20.0,
"rss_mb": {
"mean": 1012.01,
"max": 1121.3
},
"cpu_percent": 98.57,
"chunks_per_second": 155.81,
"unique_per_second": 5.78,
"forgotten_per_second": 155.62,
"waited_ticks_per_second": 0.0,
"chunks_total": 106268,
"chunks_per_bot": {
"walk": 6506.33,
"explore": 1261.6,
"idle": 473.0
},
"unique_chunks_per_bot": {
"walk": 702.13,
"explore": 1184.4,
"idle": 473.0
},
"resent": 0,
"pulled_back": 3008,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 25,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "vanilla",
"bots": 25,
"behaviour": "mixed",
"start_seconds": 15.47,
"mspt": {
"average": 37.35,
"p50": 35.64,
"p95": 48.2,
"p99": 82.66
},
"mspt_worst_p99": 579.8,
"tps": 20.0,
"rss_mb": {
"mean": 3935.5,
"max": 4157.4
},
"cpu_percent": 191.44,
"chunks_per_second": 158.52,
"unique_per_second": 5.71,
"forgotten_per_second": 158.39,
"waited_ticks_per_second": 0.0,
"chunks_total": 103148,
"chunks_per_bot": {
"walk": 6306.87,
"explore": 1236.0,
"idle": 473.0
},
"unique_chunks_per_bot": {
"walk": 704.2,
"explore": 1160.2,
"idle": 473.0
},
"resent": 0,
"pulled_back": 3762,
"moved_too_quickly": 11,
"moved_wrongly": 0,
"bots_joined": 25,
"bots_failed": [],
"tick_queries": 59
},
{
"server": "paper",
"bots": 25,
"behaviour": "mixed",
"start_seconds": 18.95,
"mspt": {
"average": 28.3,
"p50": 23.7,
"p95": 61.68,
"p99": 90.4
},
"mspt_worst_p99": 124.3,
"tps": 20.0,
"rss_mb": {
"mean": 4160.34,
"max": 4246.4
},
"cpu_percent": 124.24,
"chunks_per_second": 156.35,
"unique_per_second": 5.72,
"forgotten_per_second": 156.25,
"waited_ticks_per_second": 0.0,
"chunks_total": 102715,
"chunks_per_bot": {
"walk": 6280.4,
"explore": 1228.8,
"idle": 473.0
},
"unique_chunks_per_bot": {
"walk": 703.07,
"explore": 1155.6,
"idle": 473.0
},
"resent": 0,
"pulled_back": 4337,
"moved_too_quickly": 9,
"moved_wrongly": 0,
"bots_joined": 25,
"bots_failed": [],
"tick_queries": 59
},
{
"server": "fabric",
"bots": 25,
"behaviour": "mixed",
"start_seconds": 17.45,
"mspt": {
"average": 31.44,
"p50": 28.33,
"p95": 52.45,
"p99": 93.35
},
"mspt_worst_p99": 409.3,
"tps": 20.0,
"rss_mb": {
"mean": 3884.61,
"max": 4186.4
},
"cpu_percent": 174.04,
"chunks_per_second": 158.89,
"unique_per_second": 5.75,
"forgotten_per_second": 158.54,
"waited_ticks_per_second": 0.0,
"chunks_total": 103913,
"chunks_per_bot": {
"walk": 6356.8,
"explore": 1239.2,
"idle": 473.0
},
"unique_chunks_per_bot": {
"walk": 704.27,
"explore": 1163.0,
"idle": 473.0
},
"resent": 0,
"pulled_back": 4194,
"moved_too_quickly": 11,
"moved_wrongly": 0,
"bots_joined": 25,
"bots_failed": [],
"tick_queries": 59
},
{
"server": "horizon",
"bots": 50,
"behaviour": "mixed",
"start_seconds": 8.31,
"mspt": {
"average": 47.83,
"p50": 46.02,
"p95": 62.42,
"p99": 73.21
},
"mspt_worst_p99": 133.0,
"tps": 20.0,
"rss_mb": {
"mean": 1112.46,
"max": 1296.4
},
"cpu_percent": 143.22,
"chunks_per_second": 302.68,
"unique_per_second": 11.02,
"forgotten_per_second": 303.15,
"waited_ticks_per_second": 0.0,
"chunks_total": 211296,
"chunks_per_bot": {
"walk": 6468.47,
"explore": 1251.2,
"idle": 473.0
},
"unique_chunks_per_bot": {
"walk": 701.67,
"explore": 1180.5,
"idle": 473.0
},
"resent": 0,
"pulled_back": 8562,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 50,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "vanilla",
"bots": 50,
"behaviour": "mixed",
"start_seconds": 15.45,
"error": "[Errno 32] Broken pipe"
},
{
"server": "paper",
"bots": 50,
"behaviour": "mixed",
"start_seconds": 19.35,
"mspt": {
"average": 46.86,
"p50": 45.11,
"p95": 61.16,
"p99": 109.72
},
"mspt_worst_p99": 213.4,
"tps": 20.0,
"rss_mb": {
"mean": 4353.02,
"max": 4516.1
},
"cpu_percent": 210.21,
"chunks_per_second": 310.51,
"unique_per_second": 11.16,
"forgotten_per_second": 310.64,
"waited_ticks_per_second": 0.0,
"chunks_total": 209303,
"chunks_per_bot": {
"walk": 6411.0,
"explore": 1224.3,
"idle": 473.0
},
"unique_chunks_per_bot": {
"walk": 703.47,
"explore": 1155.8,
"idle": 473.0
},
"resent": 0,
"pulled_back": 7344,
"moved_too_quickly": 35,
"moved_wrongly": 0,
"bots_joined": 50,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "fabric",
"bots": 50,
"behaviour": "mixed",
"start_seconds": 17.45,
"mspt": {
"average": 58.3,
"p50": 54.73,
"p95": 83.06,
"p99": 174.37
},
"mspt_worst_p99": 887.6,
"tps": 17.15,
"rss_mb": {
"mean": 4521.47,
"max": 4744.2
},
"cpu_percent": 236.04,
"chunks_per_second": 311.52,
"unique_per_second": 11.28,
"forgotten_per_second": 311.74,
"waited_ticks_per_second": 0.0,
"chunks_total": 206965,
"chunks_per_bot": {
"walk": 6331.7,
"explore": 1228.4,
"idle": 473.0
},
"unique_chunks_per_bot": {
"walk": 703.3,
"explore": 1160.3,
"idle": 473.0
},
"resent": 0,
"pulled_back": 6350,
"moved_too_quickly": 46,
"moved_wrongly": 0,
"bots_joined": 50,
"bots_failed": [],
"tick_queries": 58
}
],
"work": [
{
"behaviour": "mixed",
"bots": 25,
"servers": {
"horizon": {
"chunks_per_second": {
"value": 155.81,
"share": 0.981
},
"unique_per_second": {
"value": 5.78,
"share": 1.0
},
"waited_ticks_per_second": 0.0
},
"vanilla": {
"chunks_per_second": {
"value": 158.52,
"share": 0.998
},
"unique_per_second": {
"value": 5.71,
"share": 0.988
},
"waited_ticks_per_second": 0.0
},
"paper": {
"chunks_per_second": {
"value": 156.35,
"share": 0.984
},
"unique_per_second": {
"value": 5.72,
"share": 0.99
},
"waited_ticks_per_second": 0.0
},
"fabric": {
"chunks_per_second": {
"value": 158.89,
"share": 1.0
},
"unique_per_second": {
"value": 5.75,
"share": 0.995
},
"waited_ticks_per_second": 0.0
}
},
"equal": true
},
{
"behaviour": "mixed",
"bots": 50,
"servers": {
"horizon": {
"chunks_per_second": {
"value": 302.68,
"share": 0.972
},
"unique_per_second": {
"value": 11.02,
"share": 0.977
},
"waited_ticks_per_second": 0.0
},
"paper": {
"chunks_per_second": {
"value": 310.51,
"share": 0.997
},
"unique_per_second": {
"value": 11.16,
"share": 0.989
},
"waited_ticks_per_second": 0.0
},
"fabric": {
"chunks_per_second": {
"value": 311.52,
"share": 1.0
},
"unique_per_second": {
"value": 11.28,
"share": 1.0
},
"waited_ticks_per_second": 0.0
}
},
"equal": true
}
]
}
@@ -0,0 +1,90 @@
{
"kind": "horizon-server-benchmark",
"date": "2026-10-19T06:03:34+00:00",
"machine": {
"system": "Linux",
"machine": "x86_64",
"cpus": 4
},
"commit": "38505a07b5334f3ea2c467efb4a00e5557f910f8",
"java": [
"openjdk 25.0.4.1 2026-08-18"
],
"versions": {
"paper_build": 205
},
"settings": {
"servers": "paper",
"bots": "10",
"behaviour": "mixed",
"join_every": 1.0,
"warmup": 300,
"seconds": 300,
"seed": 12345,
"explore_speed": 0.8,
"gamerule": "",
"heap": "4g",
"out": "/home/ludovic/sh10/out/mixed-equal-paper-10.json",
"nice": 19,
"bots_via": "ssh macmini",
"bots_bin": "/Users/ludovicmarie/code/horizon/.claude/worktrees/sh10-bench/target/release/horizon-bots",
"connect": "the ThinkPad's tailnet address",
"properties": {
"level-seed": "12345",
"view-distance": "10",
"simulation-distance": "10",
"online-mode": "false",
"white-list": "false",
"gamemode": "creative",
"force-gamemode": "true",
"spawn-protection": "0",
"max-players": "200",
"difficulty": "easy",
"sync-chunk-writes": "true"
}
},
"runs": [
{
"server": "paper",
"bots": 10,
"behaviour": "mixed",
"start_seconds": 20.48,
"mspt": {
"average": 20.64,
"p50": 18.1,
"p95": 35.94,
"p99": 56.29
},
"mspt_worst_p99": 166.9,
"tps": 20.0,
"rss_mb": {
"mean": 4069.94,
"max": 4080.2
},
"cpu_percent": 73.56,
"chunks_per_second": 60.76,
"unique_per_second": 1.85,
"forgotten_per_second": 60.96,
"waited_ticks_per_second": 0.0,
"chunks_total": 40348,
"chunks_per_bot": {
"walk": 6176.67,
"explore": 1171.0,
"idle": 473.0
},
"unique_chunks_per_bot": {
"walk": 701.0,
"explore": 1091.5,
"idle": 473.0
},
"resent": 0,
"pulled_back": 3974,
"moved_too_quickly": 7,
"moved_wrongly": 0,
"bots_joined": 10,
"bots_failed": [],
"tick_queries": 59
}
],
"work": []
}
@@ -0,0 +1,16 @@
{"server": "horizon", "bots": 1, "behaviour": "mixed", "start_seconds": 8.31, "mspt": {"average": 13.7, "p50": 14.18, "p95": 19.06, "p99": 27.38}, "mspt_worst_p99": 43.4, "tps": 20.0, "rss_mb": {"mean": 715.63, "max": 719.6}, "cpu_percent": 46.22, "chunks_per_second": 9.94, "unique_per_second": 0.01, "forgotten_per_second": 9.89, "waited_ticks_per_second": 0.0, "chunks_total": 6280, "chunks_per_bot": {"walk": 6280.0}, "unique_chunks_per_bot": {"walk": 700.0}, "resent": 0, "pulled_back": 0, "moved_too_quickly": 0, "moved_wrongly": 0, "bots_joined": 1, "bots_failed": [], "tick_queries": 58}
{"server": "vanilla", "bots": 1, "behaviour": "mixed", "start_seconds": 14.85, "mspt": {"average": 17.31, "p50": 18.3, "p95": 24.68, "p99": 36.43}, "mspt_worst_p99": 71.0, "tps": 20.0, "rss_mb": {"mean": 3442.51, "max": 3444.5}, "cpu_percent": 55.03, "chunks_per_second": 9.99, "unique_per_second": 0.0, "forgotten_per_second": 9.95, "waited_ticks_per_second": 0.0, "chunks_total": 6210, "chunks_per_bot": {"walk": 6210.0}, "unique_chunks_per_bot": {"walk": 700.0}, "resent": 0, "pulled_back": 0, "moved_too_quickly": 0, "moved_wrongly": 0, "bots_joined": 1, "bots_failed": [], "tick_queries": 58}
{"server": "paper", "bots": 1, "behaviour": "mixed", "start_seconds": 22.27, "mspt": {"average": 10.31, "p50": 10.4, "p95": 13.9, "p99": 22.5}, "mspt_worst_p99": 81.2, "tps": 20.0, "rss_mb": {"mean": 4239.22, "max": 4245.1}, "cpu_percent": 31.68, "chunks_per_second": 9.81, "unique_per_second": 0.02, "forgotten_per_second": 9.77, "waited_ticks_per_second": 0.0, "chunks_total": 6049, "chunks_per_bot": {"walk": 6049.0}, "unique_chunks_per_bot": {"walk": 698.0}, "resent": 0, "pulled_back": 0, "moved_too_quickly": 0, "moved_wrongly": 0, "bots_joined": 1, "bots_failed": [], "tick_queries": 58}
{"server": "fabric", "bots": 1, "behaviour": "mixed", "start_seconds": 17.55, "mspt": {"average": 12.46, "p50": 12.62, "p95": 17.69, "p99": 32.74}, "mspt_worst_p99": 70.1, "tps": 20.0, "rss_mb": {"mean": 3428.55, "max": 3430.2}, "cpu_percent": 45.86, "chunks_per_second": 9.98, "unique_per_second": 0.0, "forgotten_per_second": 9.93, "waited_ticks_per_second": 0.0, "chunks_total": 6188, "chunks_per_bot": {"walk": 6188.0}, "unique_chunks_per_bot": {"walk": 700.0}, "resent": 0, "pulled_back": 0, "moved_too_quickly": 0, "moved_wrongly": 0, "bots_joined": 1, "bots_failed": [], "tick_queries": 58}
{"server": "horizon", "bots": 10, "behaviour": "mixed", "start_seconds": 8.21, "mspt": {"average": 22.93, "p50": 20.03, "p95": 39.83, "p99": 53.33}, "mspt_worst_p99": 89.5, "tps": 20.0, "rss_mb": {"mean": 922.28, "max": 973.7}, "cpu_percent": 66.6, "chunks_per_second": 61.21, "unique_per_second": 1.78, "forgotten_per_second": 61.34, "waited_ticks_per_second": 0.0, "chunks_total": 42095, "chunks_per_bot": {"walk": 6460.0, "explore": 1194.5, "idle": 473.0}, "unique_chunks_per_bot": {"walk": 702.17, "explore": 1112.5, "idle": 473.0}, "resent": 0, "pulled_back": 3702, "moved_too_quickly": 0, "moved_wrongly": 0, "bots_joined": 10, "bots_failed": [], "tick_queries": 58}
{"server": "vanilla", "bots": 10, "behaviour": "mixed", "start_seconds": 14.65, "mspt": {"average": 22.94, "p50": 20.61, "p95": 36.92, "p99": 56.48}, "mspt_worst_p99": 196.6, "tps": 20.0, "rss_mb": {"mean": 3706.57, "max": 3761.6}, "cpu_percent": 100.83, "chunks_per_second": 61.63, "unique_per_second": 1.84, "forgotten_per_second": 61.68, "waited_ticks_per_second": 0.0, "chunks_total": 40938, "chunks_per_bot": {"walk": 6272.17, "explore": 1179.5, "idle": 473.0}, "unique_chunks_per_bot": {"walk": 702.0, "explore": 1097.0, "idle": 473.0}, "resent": 0, "pulled_back": 3370, "moved_too_quickly": 7, "moved_wrongly": 0, "bots_joined": 10, "bots_failed": [], "tick_queries": 59}
{"server": "paper", "bots": 10, "behaviour": "mixed", "start_seconds": 20.48, "mspt": {"average": 20.64, "p50": 18.1, "p95": 35.94, "p99": 56.29}, "mspt_worst_p99": 166.9, "tps": 20.0, "rss_mb": {"mean": 4069.94, "max": 4080.2}, "cpu_percent": 73.56, "chunks_per_second": 60.76, "unique_per_second": 1.85, "forgotten_per_second": 60.96, "waited_ticks_per_second": 0.0, "chunks_total": 40348, "chunks_per_bot": {"walk": 6176.67, "explore": 1171.0, "idle": 473.0}, "unique_chunks_per_bot": {"walk": 701.0, "explore": 1091.5, "idle": 473.0}, "resent": 0, "pulled_back": 3974, "moved_too_quickly": 7, "moved_wrongly": 0, "bots_joined": 10, "bots_failed": [], "tick_queries": 59}
{"server": "fabric", "bots": 10, "behaviour": "mixed", "start_seconds": 16.64, "mspt": {"average": 20.18, "p50": 17.63, "p95": 37.04, "p99": 56.08}, "mspt_worst_p99": 217.9, "tps": 20.0, "rss_mb": {"mean": 3732.43, "max": 3776.5}, "cpu_percent": 94.48, "chunks_per_second": 61.65, "unique_per_second": 1.84, "forgotten_per_second": 61.76, "waited_ticks_per_second": 0.0, "chunks_total": 41055, "chunks_per_bot": {"walk": 6291.17, "explore": 1181.0, "idle": 473.0}, "unique_chunks_per_bot": {"walk": 701.83, "explore": 1098.0, "idle": 473.0}, "resent": 0, "pulled_back": 4002, "moved_too_quickly": 3, "moved_wrongly": 0, "bots_joined": 10, "bots_failed": [], "tick_queries": 59}
{"server": "horizon", "bots": 25, "behaviour": "mixed", "start_seconds": 8.51, "mspt": {"average": 30.92, "p50": 27.88, "p95": 48.25, "p99": 72.0}, "mspt_worst_p99": 121.1, "tps": 20.0, "rss_mb": {"mean": 1012.01, "max": 1121.3}, "cpu_percent": 98.57, "chunks_per_second": 155.81, "unique_per_second": 5.78, "forgotten_per_second": 155.62, "waited_ticks_per_second": 0.0, "chunks_total": 106268, "chunks_per_bot": {"walk": 6506.33, "explore": 1261.6, "idle": 473.0}, "unique_chunks_per_bot": {"walk": 702.13, "explore": 1184.4, "idle": 473.0}, "resent": 0, "pulled_back": 3008, "moved_too_quickly": 0, "moved_wrongly": 0, "bots_joined": 25, "bots_failed": [], "tick_queries": 58}
{"server": "vanilla", "bots": 25, "behaviour": "mixed", "start_seconds": 15.47, "mspt": {"average": 37.35, "p50": 35.64, "p95": 48.2, "p99": 82.66}, "mspt_worst_p99": 579.8, "tps": 20.0, "rss_mb": {"mean": 3935.5, "max": 4157.4}, "cpu_percent": 191.44, "chunks_per_second": 158.52, "unique_per_second": 5.71, "forgotten_per_second": 158.39, "waited_ticks_per_second": 0.0, "chunks_total": 103148, "chunks_per_bot": {"walk": 6306.87, "explore": 1236.0, "idle": 473.0}, "unique_chunks_per_bot": {"walk": 704.2, "explore": 1160.2, "idle": 473.0}, "resent": 0, "pulled_back": 3762, "moved_too_quickly": 11, "moved_wrongly": 0, "bots_joined": 25, "bots_failed": [], "tick_queries": 59}
{"server": "paper", "bots": 25, "behaviour": "mixed", "start_seconds": 18.95, "mspt": {"average": 28.3, "p50": 23.7, "p95": 61.68, "p99": 90.4}, "mspt_worst_p99": 124.3, "tps": 20.0, "rss_mb": {"mean": 4160.34, "max": 4246.4}, "cpu_percent": 124.24, "chunks_per_second": 156.35, "unique_per_second": 5.72, "forgotten_per_second": 156.25, "waited_ticks_per_second": 0.0, "chunks_total": 102715, "chunks_per_bot": {"walk": 6280.4, "explore": 1228.8, "idle": 473.0}, "unique_chunks_per_bot": {"walk": 703.07, "explore": 1155.6, "idle": 473.0}, "resent": 0, "pulled_back": 4337, "moved_too_quickly": 9, "moved_wrongly": 0, "bots_joined": 25, "bots_failed": [], "tick_queries": 59}
{"server": "fabric", "bots": 25, "behaviour": "mixed", "start_seconds": 17.45, "mspt": {"average": 31.44, "p50": 28.33, "p95": 52.45, "p99": 93.35}, "mspt_worst_p99": 409.3, "tps": 20.0, "rss_mb": {"mean": 3884.61, "max": 4186.4}, "cpu_percent": 174.04, "chunks_per_second": 158.89, "unique_per_second": 5.75, "forgotten_per_second": 158.54, "waited_ticks_per_second": 0.0, "chunks_total": 103913, "chunks_per_bot": {"walk": 6356.8, "explore": 1239.2, "idle": 473.0}, "unique_chunks_per_bot": {"walk": 704.27, "explore": 1163.0, "idle": 473.0}, "resent": 0, "pulled_back": 4194, "moved_too_quickly": 11, "moved_wrongly": 0, "bots_joined": 25, "bots_failed": [], "tick_queries": 59}
{"server": "horizon", "bots": 50, "behaviour": "mixed", "start_seconds": 8.31, "mspt": {"average": 47.83, "p50": 46.02, "p95": 62.42, "p99": 73.21}, "mspt_worst_p99": 133.0, "tps": 20.0, "rss_mb": {"mean": 1112.46, "max": 1296.4}, "cpu_percent": 143.22, "chunks_per_second": 302.68, "unique_per_second": 11.02, "forgotten_per_second": 303.15, "waited_ticks_per_second": 0.0, "chunks_total": 211296, "chunks_per_bot": {"walk": 6468.47, "explore": 1251.2, "idle": 473.0}, "unique_chunks_per_bot": {"walk": 701.67, "explore": 1180.5, "idle": 473.0}, "resent": 0, "pulled_back": 8562, "moved_too_quickly": 0, "moved_wrongly": 0, "bots_joined": 50, "bots_failed": [], "tick_queries": 58}
{"server": "vanilla", "bots": 50, "behaviour": "mixed", "start_seconds": 15.45, "error": "[Errno 32] Broken pipe"}
{"server": "paper", "bots": 50, "behaviour": "mixed", "start_seconds": 19.35, "mspt": {"average": 46.86, "p50": 45.11, "p95": 61.16, "p99": 109.72}, "mspt_worst_p99": 213.4, "tps": 20.0, "rss_mb": {"mean": 4353.02, "max": 4516.1}, "cpu_percent": 210.21, "chunks_per_second": 310.51, "unique_per_second": 11.16, "forgotten_per_second": 310.64, "waited_ticks_per_second": 0.0, "chunks_total": 209303, "chunks_per_bot": {"walk": 6411.0, "explore": 1224.3, "idle": 473.0}, "unique_chunks_per_bot": {"walk": 703.47, "explore": 1155.8, "idle": 473.0}, "resent": 0, "pulled_back": 7344, "moved_too_quickly": 35, "moved_wrongly": 0, "bots_joined": 50, "bots_failed": [], "tick_queries": 58}
{"server": "fabric", "bots": 50, "behaviour": "mixed", "start_seconds": 17.45, "mspt": {"average": 58.3, "p50": 54.73, "p95": 83.06, "p99": 174.37}, "mspt_worst_p99": 887.6, "tps": 17.15, "rss_mb": {"mean": 4521.47, "max": 4744.2}, "cpu_percent": 236.04, "chunks_per_second": 311.52, "unique_per_second": 11.28, "forgotten_per_second": 311.74, "waited_ticks_per_second": 0.0, "chunks_total": 206965, "chunks_per_bot": {"walk": 6331.7, "explore": 1228.4, "idle": 473.0}, "unique_chunks_per_bot": {"walk": 703.3, "explore": 1160.3, "idle": 473.0}, "resent": 0, "pulled_back": 6350, "moved_too_quickly": 46, "moved_wrongly": 0, "bots_joined": 50, "bots_failed": [], "tick_queries": 58}
@@ -0,0 +1,125 @@
# Horizon's server against vanilla 26.3: mixed bots (2026-10-18)
The first benchmark from docs/server.md section 10 (item 4): the same bots on Horizon's dedicated server, as
`install.sh` installs it today, and on vanilla 26.3's, one server at a time on the same quiet machine.
There were 1, 10, 25 and 50 bots, all with the `mixed` behaviour.
## Summary
- **Both servers held 20 TPS in every run.**
- **Memory:** Horizon's RSS was 3.7 to 5.2 times smaller: 0.65 GB against 3.4 GB with one player, and
1.0 GB against 4.0 GB with 50. Vanilla runs with the usual fixed heap (`-Xms4g -Xmx4g`), so its RSS
is mostly that heap.
- **Tick time (MSPT):**
- with 1, 10 and 25 players, Horizon's average tick was shorter (2.4 against 17.9 ms, 12.4 against 34.5,
24.0 against 31.6);
- with 50, it was longer (35.2 against 29.7 ms);
- Horizon's P99 was lower at every count, and so were its worst 5-second windows (241 ms at most,
against 424 to 2320 ms for vanilla).
- **The work was not equal, and the difference counts against Horizon.** In `mixed`, 20 % of the bots
fly straight out at 22 blocks a second, as fast as each server sends them chunks. Horizon kept up:
- with 10 or more players it sent 2.9 to 5.9 times as many chunks a second, and generated 1.9 to 2.9
times as much new terrain a second;
- its walking bots each received 3300 to 4000 chunks, against 640 to 1100 on vanilla once 10 or more
were online.
So at 50 players Horizon's tick is 18 % longer while it generates twice the new terrain. The
equal-rate explore pass of `../server-equal-work-2026-10-17/` is the fair comparison for exploring.
- **Start (fresh world, to `Done`):** 10 to 20 s for Horizon, 14.5 to 28.6 s for vanilla.
## Results
MSPT is milliseconds per tick from each server's own `/tick query` (50 is the limit for 20 TPS). P95 and
P99 are over the whole run. "Worst P99" is the highest P99 of a single 5-second query. RSS and CPU cover
the server's whole process tree (100 % is one thread; the machine has 4). "New chunks/s" counts chunks
no bot had before. "Pulled back" counts position packets that moved a bot back.
| Bots | Server | MSPT | P95 | P99 | Worst P99 | TPS | RSS (mean / max) | CPU | Start | Chunks/s | New chunks/s | Pulled back |
|---:|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|
| 1 | Horizon | **2.4** | 5.2 | 18.2 | 45.6 | 20 | **0.65 / 0.66 GB** | 25 % | 10.9 s | 9.9 | 0.3 | 0 |
| 1 | vanilla | 17.9 | 27.6 | 47.3 | 423.8 | 20 | 3.38 / 3.39 GB | 64 % | 14.8 s | 9.9 | 0.3 | 0 |
| 10 | Horizon | **12.4** | 36.0 | 65.9 | 117.1 | 20 | **0.94 / 1.01 GB** | 210 % | 10.2 s | 98.2 | 36.5 | 6267 |
| 10 | vanilla | 34.5 | 63.5 | 138.6 | 884.7 | 20 | 3.59 / 3.66 GB | 217 % | 14.5 s | 24.3 | 19.1 | 5500 |
| 25 | Horizon | **24.0** | 49.7 | 80.6 | 168.8 | 20 | **1.01 / 1.18 GB** | 192 % | 14.7 s | 178.2 | 31.9 | 5532 |
| 25 | vanilla | 31.6 | 70.0 | 222.7 | 2319.6 | 20 | 3.74 / 4.06 GB | 218 % | 28.6 s | 30.4 | 10.8 | 4524 |
| 50 | Horizon | 35.2 | 69.0 | 96.6 | 240.8 | 20 | **1.01 / 1.25 GB** | 218 % | 19.8 s | 273.2 | 34.6 | 8369 |
| 50 | vanilla | **29.7** | 62.3 | 137.4 | 489.3 | 20 | 4.00 / 4.60 GB | 294 % | 20.6 s | 94.4 | 17.8 | 12034 |
Chunks each bot received, by behaviour (mean):
| Bots | Server | walk | explore | idle |
|---:|---|---:|---:|---:|
| 10 | Horizon | 3992 | 7360 | 473 |
| 10 | vanilla | 680 | 2824 | 358 |
| 25 | Horizon | 3928 | 2475 | 473 |
| 25 | vanilla | 640 | 738 | 180 |
| 50 | Horizon | 3300 | 1192 | 460 |
| 50 | vanilla | 1105 | 629 | 220 |
No bot failed to join or was dropped, and no chunk was sent twice (`resent` 0), on either server. Vanilla
also logged "moved too quickly" 12, 324 and 2193 times at 10, 25 and 50 bots.
## Method
`tools/server_bench.py`, unchanged, runs each configuration on a fresh server:
- the same `server.properties` on both: seed 12345, view and simulation distance 10, offline, creative,
difficulty easy, `sync-chunk-writes=true`, white-list off;
- `gamerule respawn_radius 0`, and otherwise vanilla's default game rules on both. The locator bar is on
for both: Horizon's server has run it since v2026.10.18.13. The 2026-10-17 runs turned it off on vanilla
because Horizon had none then;
- the server waits for `Done`, then the bots join one a second, warm up for 60 seconds, and are measured
for 300 seconds, with `/tick query` every 5 seconds and RSS and CPU every second.
The bots are `horizon-bots --behaviour mixed`: 60 % walk, 20 % explore and 20 % idle, each choice
deterministic per bot. All are creative and fly 24 blocks above their spawn:
- `walk`: random points within 48 blocks at 5.6 blocks a second;
- `explore`: straight out at up to 22 blocks a second, as fast as chunks come;
- `idle`: hovers.
A bot doesn't move while the chunk it stands in hasn't arrived, and it acknowledges chunk batches as
vanilla's client does.
**Machine.** The ThinkPad: Intel Core i7-7500U (2 cores, 4 threads), 32 GB, NixOS (Linux 6.18). No other
session ran on it (`load.txt`: the fleet monitor shows 0 sessions throughout). The busy periods are the
servers themselves, which used 2 to 3 of its 4 threads from 10 bots up, so both servers were CPU-bound
there. The servers ran under `nice -n 19`. The desktop was meant to run this (24 threads) but didn't
wake. The bots ran on the Mac mini (M4 Pro) and reached the ThinkPad over the tailnet, so they took none
of the servers' CPU. The Mac mini was shared with other work (70 to 90 % busy), which only affects the
bots' timing.
**What ran.**
- Horizon: the published v2026.10.18.15 release, `horizon-client` (the engine) at 6c078b28, as
`install.sh` installs it. Its game files were prepared from the owner's own Minecraft by the release's
`horizon-prepare`. `horizon-server` (the supervisor, which takes no part in the tick) came from #898
(4ea2243e), because the release's copy can't find game files prepared without sounds (#893).
`horizon-bots` came from main.
- Vanilla: the pinned 26.3 server jar (the owner's own copy) on OpenJDK 25.0.4.1, `-Xms4g -Xmx4g`.
On Linux the release's engine needs `libasound.so.2` and `libudev.so.1`. The ThinkPad has them only in
the Nix store, so the run set `LD_LIBRARY_PATH` to them (see #898).
## Reproduce
curl -fsSL https://raw.githubusercontent.com/ludovic111/horizon/main/packaging/server/install.sh | sh
horizon-prepare --prepare --server-jar /path/to/server.jar # your own copy
cargo build --release -p horizon-bots # on the machine that runs the bots
# in a checkout whose .local/minecraft holds the pinned vanilla jars (tools/lab.py prepare):
HORIZON_BENCH_BIN=<a folder with horizon-server and horizon-client> \
python3 tools/server_bench.py --servers horizon,vanilla --bots 1,10,25,50 --behaviour mixed --nice 19 \
--bots-via 'ssh <bots machine>' --bots-bin /path/to/horizon-bots --connect <this machine's address> \
--out mixed.json
`mixed.json` is the report (`horizon-server-benchmark`: machine, versions, settings and every run), and
`load.txt` is the ThinkPad's load before and during the run.
## Next
- An equal-work `mixed` pass (`--explore-speed` at a rate both servers keep up with), then 100 bots.
- The same run on a bigger machine, so neither server is CPU-bound at 25 to 50 bots.
- Paper and Fabric + Lithium (docs/server.md section 9).
- At 50 players Horizon's tick is longer than vanilla's while it generates and sends far more. The
profile of the 2026-10-17 run (streaming and block entity ticks growing with players) is
perf-server's to take on.
+286
View File
@@ -0,0 +1,286 @@
ludothinkpad — OK
RAM 29.1 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 2% busy (agents 0.0, rest 0.1 cores) · 67°C · load 2.1
· disk 295.5 GB free · battery 96%
Sessions, most important first:
(finished session) 0.0 GB leftovers: node 0.0 GB
Other machines (≤ 1 min old): desktop offline · macbook offline · macmini ok: 26.6 GB free, 14 threads 69% busy, 1 sessions
# During the run, once a minute (uptime, then ~/fleet/load fleet)
18:25:30
18:25:30 up 8 days 3:08, 0 users, load average: 2.13, 1.53, 1.29
ludothinkpad ok: 29.1 GB free, 4 threads 2% busy, 0 sessions
desktop offline
macmini busy: 27.6 GB free, 14 threads 87% busy, 1 sessions
18:26:34
18:26:34 up 8 days 3:09, 0 users, load average: 1.70, 1.56, 1.31
ludothinkpad ok: 28.5 GB free, 4 threads 23% busy, 0 sessions
desktop offline
macmini busy: 26.4 GB free, 14 threads 85% busy, 1 sessions
18:27:38
18:27:38 up 8 days 3:10, 0 users, load average: 1.10, 1.43, 1.28
ludothinkpad ok: 28.5 GB free, 4 threads 4% busy, 0 sessions
desktop offline
macmini ok: 27.7 GB free, 14 threads 80% busy, 1 sessions
18:28:42
18:28:42 up 8 days 3:11, 0 users, load average: 0.93, 1.29, 1.24
ludothinkpad ok: 28.5 GB free, 4 threads 22% busy, 0 sessions
desktop offline
macmini ok: 26.0 GB free, 14 threads 82% busy, 1 sessions
18:29:46
18:29:46 up 8 days 3:12, 0 users, load average: 0.62, 1.12, 1.18
ludothinkpad ok: 28.5 GB free, 4 threads 12% busy, 0 sessions
desktop offline
macmini ok: 25.7 GB free, 14 threads 81% busy, 1 sessions
18:30:50
18:30:51 up 8 days 3:13, 0 users, load average: 0.79, 1.05, 1.15
ludothinkpad ok: 28.5 GB free, 4 threads 7% busy, 0 sessions
desktop offline
macmini busy: 27.0 GB free, 14 threads 88% busy, 1 sessions
18:31:55
18:31:55 up 8 days 3:14, 0 users, load average: 0.67, 0.96, 1.11
ludothinkpad ok: 28.6 GB free, 4 threads 73% busy, 0 sessions
desktop offline
macmini ok: 27.3 GB free, 14 threads 79% busy, 1 sessions
18:32:59
18:32:59 up 8 days 3:15, 0 users, load average: 4.01, 1.91, 1.44
ludothinkpad ok: 25.9 GB free, 4 threads 24% busy, 0 sessions
desktop offline
macmini ok: 26.4 GB free, 14 threads 85% busy, 1 sessions
18:34:03
18:34:03 up 8 days 3:16, 0 users, load average: 2.24, 1.84, 1.44
ludothinkpad ok: 25.9 GB free, 4 threads 21% busy, 0 sessions
desktop offline
macmini busy: 29.2 GB free, 14 threads 86% busy, 1 sessions
18:35:07
18:35:07 up 8 days 3:17, 0 users, load average: 1.28, 1.63, 1.40
ludothinkpad ok: 25.8 GB free, 4 threads 21% busy, 0 sessions
desktop offline
macmini busy: 31.0 GB free, 14 threads 86% busy, 1 sessions
18:36:11
18:36:11 up 8 days 3:18, 0 users, load average: 0.79, 1.41, 1.33
ludothinkpad ok: 25.9 GB free, 4 threads 18% busy, 0 sessions
desktop offline
macmini busy: 26.3 GB free, 14 threads 93% busy, 1 sessions
18:37:15
18:37:15 up 8 days 3:20, 0 users, load average: 1.44, 1.47, 1.36
ludothinkpad ok: 25.9 GB free, 4 threads 23% busy, 0 sessions
desktop offline
macmini busy: 26.8 GB free, 14 threads 86% busy, 1 sessions
18:38:19
18:38:19 up 8 days 3:21, 0 users, load average: 1.37, 1.45, 1.36
ludothinkpad ok: 25.9 GB free, 4 threads 13% busy, 0 sessions
desktop offline
macmini busy: 26.7 GB free, 14 threads 86% busy, 1 sessions
18:39:24
18:39:24 up 8 days 3:22, 0 users, load average: 2.27, 1.66, 1.44
ludothinkpad ok: 28.3 GB free, 4 threads 62% busy, 0 sessions
desktop offline
macmini ok: 27.1 GB free, 14 threads 70% busy, 1 sessions
18:40:28
18:40:28 up 8 days 3:23, 0 users, load average: 1.95, 1.72, 1.48
ludothinkpad ok: 28.3 GB free, 4 threads 63% busy, 0 sessions
desktop offline
macmini ok: 26.7 GB free, 14 threads 84% busy, 1 sessions
18:41:32
18:41:32 up 8 days 3:24, 0 users, load average: 3.32, 2.22, 1.67
ludothinkpad ok: 28.2 GB free, 4 threads 61% busy, 0 sessions
desktop offline
macmini busy: 26.8 GB free, 14 threads 85% busy, 1 sessions
18:42:36
18:42:36 up 8 days 3:25, 0 users, load average: 3.13, 2.33, 1.75
ludothinkpad ok: 28.2 GB free, 4 threads 60% busy, 0 sessions
desktop offline
macmini busy: 27.1 GB free, 14 threads 91% busy, 1 sessions
18:43:40
18:43:40 up 8 days 3:26, 0 users, load average: 3.27, 2.48, 1.83
ludothinkpad busy: 28.1 GB free, 4 threads 90% busy, 0 sessions
desktop offline
macmini busy: 28.3 GB free, 14 threads 92% busy, 1 sessions
18:44:44
18:44:44 up 8 days 3:27, 0 users, load average: 2.14, 2.32, 1.82
ludothinkpad ok: 28.1 GB free, 4 threads 55% busy, 0 sessions
desktop offline
macmini busy: 30.4 GB free, 14 threads 88% busy, 1 sessions
18:45:48
18:45:48 up 8 days 3:28, 0 users, load average: 4.94, 3.06, 2.11
ludothinkpad busy: 25.9 GB free, 4 threads 86% busy, 0 sessions
desktop offline
macmini ok: 28.7 GB free, 14 threads 84% busy, 1 sessions
18:46:53
18:46:53 up 8 days 3:29, 0 users, load average: 6.55, 3.92, 2.47
ludothinkpad busy: 25.4 GB free, 4 threads 99% busy, 0 sessions
desktop offline
macmini ok: 29.3 GB free, 14 threads 81% busy, 1 sessions
18:47:57
18:47:57 up 8 days 3:30, 0 users, load average: 8.50, 4.96, 2.93
ludothinkpad busy: 24.0 GB free, 4 threads 100% busy, 0 sessions
desktop offline
macmini busy: 27.9 GB free, 14 threads 90% busy, 1 sessions
18:49:01
18:49:01 up 8 days 3:31, 0 users, load average: 8.59, 5.68, 3.31
ludothinkpad busy: 22.7 GB free, 4 threads 99% busy, 0 sessions
desktop offline
macmini ok: 28.2 GB free, 14 threads 85% busy, 1 sessions
18:50:05
18:50:05 up 8 days 3:32, 0 users, load average: 7.81, 6.07, 3.61
ludothinkpad busy: 24.5 GB free, 4 threads 90% busy, 0 sessions
desktop offline
macmini busy: 28.6 GB free, 14 threads 96% busy, 1 sessions
18:51:09
18:51:09 up 8 days 3:33, 0 users, load average: 8.29, 6.50, 3.93
ludothinkpad busy: 24.3 GB free, 4 threads 92% busy, 0 sessions
desktop offline
macmini busy: 28.0 GB free, 14 threads 94% busy, 1 sessions
18:52:14
18:52:14 up 8 days 3:35, 0 users, load average: 5.58, 6.06, 3.95
ludothinkpad busy: 26.0 GB free, 4 threads 100% busy, 0 sessions
desktop offline
macmini busy: 26.3 GB free, 14 threads 87% busy, 1 sessions
18:53:18
18:53:18 up 8 days 3:36, 0 users, load average: 4.53, 5.69, 3.97
ludothinkpad busy: 27.2 GB free, 4 threads 89% busy, 0 sessions
desktop offline
macmini busy: 26.8 GB free, 14 threads 87% busy, 1 sessions
18:54:22
18:54:22 up 8 days 3:37, 0 users, load average: 5.31, 5.64, 4.07
ludothinkpad busy: 26.3 GB free, 4 threads 100% busy, 0 sessions
desktop offline
macmini busy: 29.3 GB free, 14 threads 91% busy, 1 sessions
18:55:26
18:55:26 up 8 days 3:38, 0 users, load average: 10.47, 6.99, 4.63
ludothinkpad busy: 26.1 GB free, 4 threads 100% busy, 0 sessions
desktop offline
macmini busy: 26.7 GB free, 14 threads 90% busy, 1 sessions
18:56:30
18:56:30 up 8 days 3:39, 0 users, load average: 9.27, 7.45, 4.96
ludothinkpad busy: 26.2 GB free, 4 threads 100% busy, 0 sessions
desktop offline
macmini busy: 28.5 GB free, 14 threads 92% busy, 1 sessions
18:57:35
18:57:35 up 8 days 3:40, 0 users, load average: 7.79, 7.39, 5.10
ludothinkpad busy: 26.1 GB free, 4 threads 100% busy, 0 sessions
desktop offline
macmini ok: 27.1 GB free, 14 threads 80% busy, 1 sessions
18:58:39
18:58:39 up 8 days 3:41, 0 users, load average: 8.06, 7.55, 5.32
ludothinkpad busy: 26.4 GB free, 4 threads 100% busy, 0 sessions
desktop offline
macmini busy: 31.8 GB free, 14 threads 95% busy, 1 sessions
18:59:43
18:59:43 up 8 days 3:42, 0 users, load average: 8.44, 7.65, 5.50
ludothinkpad busy: 26.2 GB free, 4 threads 100% busy, 0 sessions
desktop offline
macmini busy: 31.4 GB free, 14 threads 94% busy, 1 sessions
19:00:47
19:00:47 up 8 days 3:43, 0 users, load average: 11.04, 8.66, 6.00
ludothinkpad busy: 24.9 GB free, 4 threads 100% busy, 0 sessions
desktop offline
macmini busy: 31.8 GB free, 14 threads 88% busy, 1 sessions
19:01:51
19:01:51 up 8 days 3:44, 0 users, load average: 11.91, 9.37, 6.41
ludothinkpad busy: 24.4 GB free, 4 threads 100% busy, 0 sessions
desktop offline
macmini ok: 32.9 GB free, 14 threads 77% busy, 1 sessions
19:02:56
19:02:56 up 8 days 3:45, 0 users, load average: 11.64, 9.85, 6.79
ludothinkpad ok: 25.5 GB free, 4 threads 70% busy, 0 sessions
desktop offline
macmini busy: 30.9 GB free, 14 threads 91% busy, 1 sessions
19:04:00
19:04:00 up 8 days 3:46, 0 users, load average: 11.42, 10.06, 7.06
ludothinkpad busy: 23.4 GB free, 4 threads 99% busy, 0 sessions
desktop offline
macmini busy: 28.4 GB free, 14 threads 94% busy, 1 sessions
19:05:04
19:05:04 up 8 days 3:47, 0 users, load average: 9.83, 9.96, 7.24
ludothinkpad busy: 23.2 GB free, 4 threads 100% busy, 0 sessions
desktop offline
macmini ok: 31.4 GB free, 14 threads 80% busy, 1 sessions
19:06:09
19:06:09 up 8 days 3:48, 0 users, load average: 8.38, 9.68, 7.34
ludothinkpad ok: 22.7 GB free, 4 threads 17% busy, 0 sessions
desktop offline
macmini busy: 29.8 GB free, 14 threads 86% busy, 1 sessions
19:07:13
19:07:13 up 8 days 3:49, 0 users, load average: 4.51, 8.27, 7.00
ludothinkpad ok: 25.7 GB free, 4 threads 75% busy, 0 sessions
desktop offline
macmini busy: 26.5 GB free, 14 threads 89% busy, 1 sessions
19:08:17
19:08:17 up 8 days 3:51, 0 users, load average: 6.77, 8.13, 7.04
ludothinkpad busy: 25.8 GB free, 4 threads 95% busy, 0 sessions
desktop offline
macmini busy: 30.8 GB free, 14 threads 91% busy, 1 sessions
19:09:22
19:09:22 up 8 days 3:52, 0 users, load average: 8.27, 8.25, 7.14
ludothinkpad busy: 25.2 GB free, 4 threads 100% busy, 0 sessions
desktop offline
macmini busy: 28.3 GB free, 14 threads 91% busy, 1 sessions
19:10:26
19:10:26 up 8 days 3:53, 0 users, load average: 8.88, 8.57, 7.34
ludothinkpad busy: 24.8 GB free, 4 threads 99% busy, 0 sessions
desktop offline
macmini ok: 32.4 GB free, 14 threads 72% busy, 1 sessions
19:11:30
19:11:30 up 8 days 3:54, 0 users, load average: 13.33, 10.05, 7.94
ludothinkpad busy: 24.7 GB free, 4 threads 97% busy, 0 sessions
desktop offline
macmini busy: 30.1 GB free, 14 threads 87% busy, 1 sessions
19:12:35
19:12:35 up 8 days 3:55, 0 users, load average: 10.92, 10.13, 8.12
ludothinkpad ok: 26.4 GB free, 4 threads 76% busy, 0 sessions
desktop offline
macmini busy: 29.3 GB free, 14 threads 86% busy, 1 sessions
19:13:39
19:13:39 up 8 days 3:56, 0 users, load average: 9.98, 9.89, 8.16
ludothinkpad busy: 25.8 GB free, 4 threads 90% busy, 0 sessions
desktop offline
macmini busy: 28.8 GB free, 14 threads 93% busy, 1 sessions
19:14:43
19:14:43 up 8 days 3:57, 0 users, load average: 8.95, 9.63, 8.19
ludothinkpad busy: 25.1 GB free, 4 threads 100% busy, 0 sessions
desktop offline
macmini ok: 29.6 GB free, 14 threads 71% busy, 1 sessions
19:15:47
19:15:47 up 8 days 3:58, 0 users, load average: 12.02, 10.38, 8.55
ludothinkpad busy: 24.5 GB free, 4 threads 100% busy, 0 sessions
desktop offline
macmini busy: 28.1 GB free, 14 threads 93% busy, 1 sessions
19:16:52
19:16:52 up 8 days 3:59, 0 users, load average: 13.82, 11.21, 8.95
ludothinkpad busy: 23.9 GB free, 4 threads 100% busy, 0 sessions
desktop offline
macmini ok: 28.4 GB free, 14 threads 75% busy, 1 sessions
19:17:56
19:17:56 up 8 days 4:00, 0 users, load average: 11.95, 11.20, 9.10
ludothinkpad busy: 25.0 GB free, 4 threads 99% busy, 0 sessions
desktop offline
macmini ok: 28.4 GB free, 14 threads 81% busy, 1 sessions
19:19:00
19:19:00 up 8 days 4:01, 0 users, load average: 11.46, 11.14, 9.22
ludothinkpad busy: 24.6 GB free, 4 threads 95% busy, 0 sessions
desktop offline
macmini ok: 26.4 GB free, 14 threads 83% busy, 1 sessions
19:20:04
19:20:04 up 8 days 4:02, 0 users, load average: 11.11, 10.97, 9.28
ludothinkpad busy: 24.6 GB free, 4 threads 93% busy, 0 sessions
desktop offline
macmini ok: 26.7 GB free, 14 threads 85% busy, 1 sessions
19:21:08
19:21:08 up 8 days 4:03, 0 users, load average: 9.81, 10.60, 9.27
ludothinkpad busy: 24.4 GB free, 4 threads 91% busy, 0 sessions
desktop offline
macmini ok: 27.8 GB free, 14 threads 75% busy, 1 sessions
19:22:13
19:22:13 up 8 days 4:04, 0 users, load average: 5.98, 9.51, 9.00
ludothinkpad ok: 23.8 GB free, 4 threads 18% busy, 0 sessions
desktop offline
macmini busy: 29.7 GB free, 14 threads 85% busy, 1 sessions
19:23:17
19:23:17 up 8 days 4:06, 0 users, load average: 4.01, 8.27, 8.60
ludothinkpad ok: 23.1 GB free, 4 threads 56% busy, 0 sessions
desktop offline
macmini ok: 28.2 GB free, 14 threads 79% busy, 1 sessions
@@ -0,0 +1,367 @@
{
"kind": "horizon-server-benchmark",
"date": "2026-10-18T17:23:30+00:00",
"machine": {
"system": "Linux",
"machine": "x86_64",
"cpus": 4,
"note": "ThinkPad: Intel Core i7-7500U (2 cores, 4 threads), 32 GB, NixOS, Linux 6.18; the bots on the Mac mini (M4 Pro) over the tailnet"
},
"commit": "4ea2243e4d1a44f910857bd759845db70e3690a8",
"java": [
"openjdk 25.0.4.1 2026-08-18"
],
"settings": {
"servers": "horizon,vanilla",
"bots": "1,10,25,50",
"behaviour": "mixed",
"join_every": 1.0,
"warmup": 60,
"seconds": 300,
"seed": 12345,
"explore_speed": 0,
"gamerule": "",
"heap": "4g",
"out": "mixed.json",
"nice": 19,
"bots_via": "ssh macmini",
"bots_bin": "horizon-bots (on the Mac mini)",
"connect": "<the ThinkPad's tailnet address>",
"properties": {
"level-seed": "12345",
"view-distance": "10",
"simulation-distance": "10",
"online-mode": "false",
"white-list": "false",
"gamemode": "creative",
"force-gamemode": "true",
"spawn-protection": "0",
"max-players": "200",
"difficulty": "easy",
"sync-chunk-writes": "true"
}
},
"runs": [
{
"server": "horizon",
"bots": 1,
"behaviour": "mixed",
"start_seconds": 10.92,
"mspt": {
"average": 2.38,
"p50": 1.97,
"p95": 5.17,
"p99": 18.2
},
"mspt_worst_p99": 45.6,
"tps": 20.0,
"rss_mb": {
"mean": 653.7,
"max": 659.2
},
"cpu_percent": 24.51,
"chunks_per_second": 9.9,
"unique_per_second": 0.3,
"forgotten_per_second": 9.93,
"waited_ticks_per_second": 0.0,
"chunks_total": 3951,
"chunks_per_bot": {
"walk": 3951.0
},
"unique_chunks_per_bot": {
"walk": 699.0
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 1,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "vanilla",
"bots": 1,
"behaviour": "mixed",
"start_seconds": 14.75,
"mspt": {
"average": 17.9,
"p50": 18.39,
"p95": 27.58,
"p99": 47.28
},
"mspt_worst_p99": 423.8,
"tps": 20.0,
"rss_mb": {
"mean": 3382.06,
"max": 3394.1
},
"cpu_percent": 64.02,
"chunks_per_second": 9.92,
"unique_per_second": 0.29,
"forgotten_per_second": 9.95,
"waited_ticks_per_second": 0.0,
"chunks_total": 3846,
"chunks_per_bot": {
"walk": 3846.0
},
"unique_chunks_per_bot": {
"walk": 699.0
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 1,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "horizon",
"bots": 10,
"behaviour": "mixed",
"start_seconds": 10.22,
"mspt": {
"average": 12.39,
"p50": 8.33,
"p95": 35.99,
"p99": 65.9
},
"mspt_worst_p99": 117.1,
"tps": 20.0,
"rss_mb": {
"mean": 935.85,
"max": 1008.6
},
"cpu_percent": 210.37,
"chunks_per_second": 98.22,
"unique_per_second": 36.49,
"forgotten_per_second": 98.25,
"waited_ticks_per_second": 0.0,
"chunks_total": 39618,
"chunks_per_bot": {
"walk": 3992.17,
"explore": 7359.5,
"idle": 473.0
},
"unique_chunks_per_bot": {
"walk": 696.83,
"explore": 6869.0,
"idle": 473.0
},
"resent": 0,
"pulled_back": 6267,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 10,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "vanilla",
"bots": 10,
"behaviour": "mixed",
"start_seconds": 14.54,
"mspt": {
"average": 34.47,
"p50": 29.67,
"p95": 63.52,
"p99": 138.58
},
"mspt_worst_p99": 884.7,
"tps": 20.0,
"rss_mb": {
"mean": 3594.26,
"max": 3661.7
},
"cpu_percent": 217.13,
"chunks_per_second": 24.34,
"unique_per_second": 19.13,
"forgotten_per_second": 20.12,
"waited_ticks_per_second": 0.62,
"chunks_total": 10443,
"chunks_per_bot": {
"walk": 679.83,
"explore": 2824.0,
"idle": 358.0
},
"unique_chunks_per_bot": {
"walk": 389.17,
"explore": 2824.0,
"idle": 358.0
},
"resent": 0,
"pulled_back": 5500,
"moved_too_quickly": 12,
"moved_wrongly": 0,
"bots_joined": 10,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "horizon",
"bots": 25,
"behaviour": "mixed",
"start_seconds": 14.73,
"mspt": {
"average": 24.0,
"p50": 21.07,
"p95": 49.72,
"p99": 80.57
},
"mspt_worst_p99": 168.8,
"tps": 20.0,
"rss_mb": {
"mean": 1013.07,
"max": 1179.4
},
"cpu_percent": 192.47,
"chunks_per_second": 178.23,
"unique_per_second": 31.85,
"forgotten_per_second": 177.93,
"waited_ticks_per_second": 60.59,
"chunks_total": 73657,
"chunks_per_bot": {
"walk": 3927.87,
"explore": 2474.8,
"idle": 473.0
},
"unique_chunks_per_bot": {
"walk": 691.73,
"explore": 2447.8,
"idle": 473.0
},
"resent": 0,
"pulled_back": 5532,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 25,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "vanilla",
"bots": 25,
"behaviour": "mixed",
"start_seconds": 28.57,
"mspt": {
"average": 31.59,
"p50": 25.18,
"p95": 70.03,
"p99": 222.74
},
"mspt_worst_p99": 2319.6,
"tps": 20.0,
"rss_mb": {
"mean": 3742.51,
"max": 4055.0
},
"cpu_percent": 218.34,
"chunks_per_second": 30.37,
"unique_per_second": 10.84,
"forgotten_per_second": 29.44,
"waited_ticks_per_second": 50.91,
"chunks_total": 14193,
"chunks_per_bot": {
"walk": 640.2,
"explore": 738.0,
"idle": 180.0
},
"unique_chunks_per_bot": {
"walk": 211.33,
"explore": 737.6,
"idle": 180.0
},
"resent": 0,
"pulled_back": 4524,
"moved_too_quickly": 324,
"moved_wrongly": 0,
"bots_joined": 25,
"bots_failed": [],
"tick_queries": 56
},
{
"server": "horizon",
"bots": 50,
"behaviour": "mixed",
"start_seconds": 19.84,
"mspt": {
"average": 35.2,
"p50": 32.12,
"p95": 69.04,
"p99": 96.56
},
"mspt_worst_p99": 240.8,
"tps": 20.0,
"rss_mb": {
"mean": 1013.2,
"max": 1249.1
},
"cpu_percent": 217.68,
"chunks_per_second": 273.18,
"unique_per_second": 34.58,
"forgotten_per_second": 268.19,
"waited_ticks_per_second": 138.29,
"chunks_total": 115527,
"chunks_per_bot": {
"walk": 3300.1,
"explore": 1192.4,
"idle": 460.0
},
"unique_chunks_per_bot": {
"walk": 632.67,
"explore": 1186.9,
"idle": 460.0
},
"resent": 0,
"pulled_back": 8369,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 50,
"bots_failed": [],
"tick_queries": 57
},
{
"server": "vanilla",
"bots": 50,
"behaviour": "mixed",
"start_seconds": 20.58,
"mspt": {
"average": 29.74,
"p50": 23.6,
"p95": 62.25,
"p99": 137.41
},
"mspt_worst_p99": 489.3,
"tps": 20.0,
"rss_mb": {
"mean": 3995.71,
"max": 4597.8
},
"cpu_percent": 294.31,
"chunks_per_second": 94.36,
"unique_per_second": 17.76,
"forgotten_per_second": 94.69,
"waited_ticks_per_second": 101.48,
"chunks_total": 41637,
"chunks_per_bot": {
"walk": 1104.97,
"explore": 628.8,
"idle": 220.0
},
"unique_chunks_per_bot": {
"walk": 279.4,
"explore": 626.4,
"idle": 220.0
},
"resent": 0,
"pulled_back": 12034,
"moved_too_quickly": 2193,
"moved_wrongly": 0,
"bots_joined": 50,
"bots_failed": [],
"tick_queries": 57
}
],
"horizon_build": "v2026.10.18.15 release binaries (horizon-client, the engine, at 6c078b28); horizon-server from #898 (4ea2243e, the supervisor only)"
}
+2 -2
View File
@@ -4,10 +4,10 @@ version = "0.1.0"
edition.workspace = true
license.workspace = true
rust-version = "1.86"
description = "cargo horizon-mod new/build/run: make Horizon mods in Rust (docs/mods.md)"
description = "cargo horizon-mod new/build/run/check: make Horizon mods in Rust (docs/mods.md)"
[dependencies]
horizon-mods = { path = "../horizon-mods", default-features = false, features = ["pack"] }
horizon-mods = { path = "../horizon-mods", default-features = false, features = ["pack", "runtime", "index"] }
serde_json = "1.0.151"
[lints]
+337
View File
@@ -0,0 +1,337 @@
//! `cargo horizon-mod check <file>`: what the registry checks of a submission,
//! run anywhere: its kind's own checks (docs/api/mod-index.md), its signature
//! when given a key, and for a mod a trial run in the sandbox: it must start,
//! survive ticks, frames, a chat line and each of its commands (a server mod:
//! a player joining, chatting, typing them and leaving), and import no more
//! than its capabilities.
use horizon_mods::{
Clock, GameMode, Mods, Output, Package, Player, ServerPlayer, Snapshot, Status,
index::{self, Facts, Kind},
manifest::component_imports,
};
use serde_json::{Value, json};
use std::path::Path;
/// How long the trial run plays: two seconds of game.
const TICKS: u64 = 40;
const FRAMES_PER_TICK: u64 = 3;
pub struct Report {
pub kind: Kind,
pub facts: Facts,
pub sha256: String,
pub size: u64,
pub signed: bool,
/// A mod has code to run (a content mod may have none).
pub code: bool,
/// A mod's component imports.
pub imports: Vec<String>,
/// A mod's commands, each run once.
pub commands: Vec<String>,
/// What the mod showed during the trial run.
pub output: Vec<String>,
}
impl Report {
pub fn json(&self) -> Value {
let requires: Vec<Value> = self
.facts
.requires
.iter()
.map(|r| json!({"id": r.id, "version": r.version, "sha256": r.sha256}))
.collect();
json!({
"kind": self.kind.name(),
"id": self.facts.id,
"version": self.facts.version,
"api": self.facts.api,
"side": self.facts.side,
"capabilities": self.facts.capabilities,
"api_level": self.facts.api_level,
"game": self.facts.game,
"requires": requires,
"sha256": self.sha256,
"size": self.size,
"signed": self.signed,
"code": self.code,
"imports": self.imports,
"commands": self.commands,
"output": self.output,
})
}
pub fn text(&self) -> String {
let f = &self.facts;
let mut out = format!(
"{} {} ({}",
f.id.as_deref().unwrap_or("resource pack"),
f.version.as_deref().unwrap_or(""),
self.kind.name()
);
if let Some(side) = f.side {
let side = serde_json::to_value(side).unwrap_or_default();
out += &format!(", API {}, {}", f.api, side.as_str().unwrap_or("?"));
} else if self.kind == Kind::Shader {
out += &format!(", shader API {}", f.api);
} else if !f.api.is_empty() {
out += &format!(", format {}", f.api);
}
if let Some(game) = &f.game {
out += &format!(", for {game}");
}
let signed = if self.signed {
"signature verified"
} else {
"unsigned"
};
out += &format!(
")\n {} bytes, SHA-256 {}, {signed}\n",
self.size, self.sha256
);
if let Some(level) = f.api_level {
out += &format!(" needs mod API level {level} or newer\n");
}
for c in &f.capabilities {
out += &format!(" may {}\n", c.describe());
}
for r in &f.requires {
out += &format!(" needs {} {} ({})\n", r.id, r.version, &r.sha256[..12]);
}
if self.kind == Kind::Mod && !self.code {
out += " no code: content only, nothing to run\n";
} else if self.kind == Kind::Mod {
let commands = if self.commands.is_empty() {
String::new()
} else {
format!(", /{}", self.commands.join(", /"))
};
out += &format!(
" trial run: started, {TICKS} ticks, {} frames, a chat line{commands}: ok\n",
TICKS * FRAMES_PER_TICK
);
}
out
}
}
/// The kind a file's extension says.
pub fn kind_of(path: &Path) -> Option<Kind> {
match path.extension()?.to_str()? {
"hmod" => Some(Kind::Mod),
"hpack" => Some(Kind::Modpack),
"zip" => Some(Kind::ResourcePack),
"hshader" => Some(Kind::Shader),
_ => None,
}
}
/// Checks the file at `path` as `kind`; `key` and `signature` (the
/// `.minisig` text) check its signature.
pub fn check(
path: &Path,
kind: Kind,
key: Option<&str>,
signature: Option<&str>,
) -> Result<Report, String> {
let size = std::fs::metadata(path)
.map_err(|e| format!("{}: {e}", path.display()))?
.len();
if size > index::MAX_FILE {
return Err(format!("over {} MiB", index::MAX_FILE >> 20));
}
index::open(&kind)?;
let facts = index::inspect(&kind, path)?;
let signed = match (key, signature) {
(Some(key), Some(sig)) => {
index::verify_signature(key, sig, path)?;
true
}
(None, None) => false,
_ => return Err("--key and --signature go together".into()),
};
let mut report = Report {
kind,
facts,
sha256: index::sha256_file(path)?,
size,
signed,
code: false,
imports: Vec::new(),
commands: Vec::new(),
output: Vec::new(),
};
if report.kind == Kind::Mod {
trial_run(path, &mut report)?;
}
Ok(report)
}
/// Runs a mod in the sandbox, as the game does, from a scratch copy so its
/// storage lands in a folder removed afterwards. A content mod has no code to
/// run: `inspect` already read its content as the game does.
fn trial_run(path: &Path, report: &mut Report) -> Result<(), String> {
let package = Package::read(path)?;
let Some(wasm) = package.wasm.as_deref() else {
return Ok(());
};
report.code = true;
report.imports = component_imports(wasm)?;
package
.manifest
.check_imports(report.imports.iter().map(String::as_str))?;
let dir = std::env::temp_dir().join(format!("horizon-mod-check-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?;
let copy = dir.join("mod.hmod");
let result = std::fs::copy(path, &copy)
.map_err(|e| e.to_string())
.and_then(|_| play(&copy, report));
let _ = std::fs::remove_dir_all(&dir);
result
}
fn play(path: &Path, report: &mut Report) -> Result<(), String> {
let mut mods = Mods::load(&[path.to_path_buf()], |_| true, |_| false);
let failed = |mods: &Mods| match &mods.entries()[0].status {
Status::Failed(reason) => Some(reason.clone()),
_ => None,
};
if let Some(reason) = failed(&mods) {
return Err(format!("it didn't start: {reason}"));
}
let client: Vec<String> = mods.commands().map(str::to_owned).collect();
let server: Vec<String> = mods.server_commands().map(|c| c.name.clone()).collect();
report.commands = [client.clone(), server.clone()].concat();
// A server mod plays with one player on, who joins, chats and leaves.
let steve = ServerPlayer {
id: "00000000-0000-4000-8000-000000000001".into(),
name: "Steve".into(),
x: 0.5,
y: 64.0,
z: 0.5,
dimension: "minecraft:overworld".into(),
game_mode: GameMode::Survival,
health: 20.0,
};
let snapshot = |t: u64| Snapshot {
player: Some(Player {
x: 0.5 + t as f64 * 0.1,
y: 64.0,
z: 0.5,
yaw: 90.0,
pitch: 0.0,
health: 20.0,
food: 20,
dimension: "minecraft:overworld".into(),
}),
clock: Some(Clock {
game_time: t,
day_time: 1000 + t,
}),
players: vec![steve.clone()],
// The GUI of a 1280 x 720 window at scale 3.
gui_size: (427.0, 240.0),
..Snapshot::default()
};
mods.set_snapshot(snapshot(0));
mods.player_joined(&steve);
for t in 0..TICKS {
mods.set_snapshot(snapshot(t));
mods.tick(t);
mods.server_tick(t);
for _ in 0..FRAMES_PER_TICK {
mods.frame(1.0 / 60.0);
}
}
mods.chat("<Steve> hello");
mods.player_chat(&steve, "hello");
for name in &client {
mods.command(name);
}
for name in &server {
mods.server_command(&steve, name);
}
mods.set_snapshot(Snapshot::default());
mods.player_left(&steve);
for out in mods.drain() {
report.output.push(match out {
Output::Chat { text, .. } => format!("chat: {text}"),
Output::Tell { text, .. } => format!("tell: {text}"),
Output::Broadcast { text, .. } => format!("broadcast: {text}"),
Output::Feedback { text, error } | Output::Reply { text, error, .. } => {
format!("{}: {text}", if error { "error" } else { "feedback" })
}
Output::Command { line } => format!("command: /{line}"),
Output::QuietCommands { lines, .. } => {
format!("{} quiet command(s)", lines.len())
}
Output::Stopped { reason, .. } => return Err(format!("it stopped: {reason}")),
Output::SetBlocks {
dimension, blocks, ..
} => {
format!("set {} block(s) in {dimension}", blocks.len())
}
Output::OpenScreen { .. } => "opened a screen".to_string(),
Output::CloseScreen { .. } => "closed its screen".to_string(),
Output::PlaceRecipe { book_id, all } => {
format!("placed recipe {book_id}{}", if all { " (all)" } else { "" })
}
Output::Give { item, count, .. } => format!("gave {count} {item}"),
Output::Shake { strength, seconds } => {
format!("shook the camera: {strength:.2} for {seconds:.1} s")
}
});
}
let shapes = mods.hud_shapes().count();
if shapes > 0 {
report.output.push(format!("drew {shapes} HUD shape(s)"));
}
match failed(&mods) {
Some(reason) => Err(format!("it stopped: {reason}")),
None => Ok(()),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn fixture(name: &str) -> std::path::PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../horizon-mods/tests/index")
.join(name)
}
#[test]
fn checks_each_kind_its_own_way() {
let pack = fixture("cozy-1.0.0.hpack");
let report = check(&pack, kind_of(&pack).unwrap(), None, None).unwrap();
assert_eq!(report.facts.requires[0].id, "coords_hud");
assert!(
report
.text()
.contains("cozy 1.0.0 (modpack, format 1, for 26.3)")
);
// The fixture mod's code is a stand-in: its manifest reads, its run fails.
let hmod = fixture("coords_hud-0.1.0.hmod");
let key = std::fs::read_to_string(fixture("test.pub")).unwrap();
let sig = std::fs::read_to_string(fixture("coords_hud-0.1.0.hmod.minisig")).unwrap();
let key = key.lines().last().unwrap();
let e = check(&hmod, Kind::Mod, Some(key), Some(&sig))
.err()
.unwrap();
assert!(e.contains("didn't start"), "{e}");
let e = check(
&hmod,
Kind::Mod,
Some(key),
Some("untrusted comment: x\nRUQ=\n"),
)
.err()
.unwrap();
assert!(e.contains("signature"), "{e}");
let e = check(&pack, Kind::Shader, None, None).err().unwrap();
assert!(e.contains("horizon-shaders.toml"), "{e}");
assert!(check(&pack, Kind::Mod, None, None).is_err());
}
}
+253 -21
View File
@@ -1,13 +1,24 @@
//! `cargo horizon-mod`: make Horizon mods in Rust (`docs/mods.md`).
//!
//! - `new <path> [--name "Name"] [--sdk-path <dir>]`: a mod crate from the template.
//! - `build [--debug]`: the `.hmod` in `target/horizon-mod/`.
//! - `new <path> [--template code|content] [--name "Name"] [--sdk-path <dir>]`:
//! a mod from a template: a Rust crate, or a content mod (no code).
//! - `build [--debug]`: the `.hmod` in `target/horizon-mod/`, with the mod's
//! `assets/` and `data/`; a folder without `Cargo.toml` is content only.
//! - `run [--game <horizon-client>] [-- <game args>]`: build, then play with
//! the mod; run `build` again and the game reloads it.
//! - `test [--debug] [--hmod <file>] [<name>]`: build, then run the mod's
//! `tests/*.toml` scripts in the sandbox, with no game.
//! - `check <file> [--json]`: what the registry checks of a submission
//! (`.hmod`, `.hpack`, resource pack `.zip`), with a trial run for a mod.
//! - `registry build|review|check|list`: the registry repository's CI.
use horizon_mods::{
Manifest, Package,
Capability, Content, Manifest, Package,
manifest::{MANIFEST, component_imports, componentize},
testing::{Script, TestGame},
};
mod check;
mod registry;
use std::{
path::{Path, PathBuf},
process::{Command, ExitCode, Stdio},
@@ -28,7 +39,10 @@ fn main() -> ExitCode {
println!("Built {}", p.display());
}),
Some("run") => run(&args[1..]),
_ => Err("usage: cargo horizon-mod new <path> | build [--debug] | run [--game <horizon-client>] [-- <game args>]".into()),
Some("test") => test(&args[1..]),
Some("check") => check(&args[1..]),
Some("registry") => registry::main(&args[1..]),
_ => Err("usage: cargo horizon-mod new <path> [--template code|content] | build [--debug] | run [--game <horizon-client>] [-- <game args>] | test [--debug] [--hmod <file>] [<name>] | check <file> [--kind <kind>] [--key <public key> --signature <file.minisig>] [--json] | registry build|review|check|list".into()),
};
match result {
Ok(()) => ExitCode::SUCCESS,
@@ -82,19 +96,72 @@ fn new(args: &[String]) -> Result<(), String> {
.replace("{name}", &name)
.replace("{sdk}", &sdk)
};
let files = [
let code = [
("Cargo.toml", include_str!("../template/Cargo.toml.in")),
(MANIFEST, include_str!("../template/horizon-mod.toml")),
("src/lib.rs", include_str!("../template/src/lib.rs")),
(".gitignore", include_str!("../template/gitignore")),
(
"tests/hello.toml",
include_str!("../template/tests/hello.toml"),
),
];
for (file, text) in files {
// A block copying a vanilla one, with its model, name, loot, recipe and tag.
let content = [
(
MANIFEST,
include_str!("../template/content/horizon-mod.toml"),
),
(
"data/{id}/horizon/block/glowing_amethyst.json",
include_str!("../template/content/block.json"),
),
(
"assets/{id}/blockstates/glowing_amethyst.json",
include_str!("../template/content/blockstate.json"),
),
(
"assets/{id}/models/block/glowing_amethyst.json",
include_str!("../template/content/model.json"),
),
(
"assets/{id}/items/glowing_amethyst.json",
include_str!("../template/content/item.json"),
),
(
"assets/{id}/lang/en_us.json",
include_str!("../template/content/lang.json"),
),
(
"data/{id}/loot_table/blocks/glowing_amethyst.json",
include_str!("../template/content/loot.json"),
),
(
"data/{id}/recipe/glowing_amethyst.json",
include_str!("../template/content/recipe.json"),
),
(
"data/minecraft/tags/block/mineable/pickaxe.json",
include_str!("../template/content/pickaxe.json"),
),
(".gitignore", include_str!("../template/gitignore")),
];
let files: &[(&str, &str)] = match flag(args, "--template").unwrap_or("code") {
"code" => &code,
"content" => &content,
other => return Err(format!("no template {other}: code or content")),
};
for &(file, text) in files {
let file = fill(file);
let file = file.as_str();
let to = dir.join(file);
std::fs::create_dir_all(to.parent().expect("a file in the mod"))
.map_err(|e| e.to_string())?;
std::fs::write(&to, fill(text)).map_err(|e| format!("{}: {e}", to.display()))?;
}
Manifest::parse(&std::fs::read_to_string(dir.join(MANIFEST)).map_err(|e| e.to_string())?)?;
let manifest =
Manifest::parse(&std::fs::read_to_string(dir.join(MANIFEST)).map_err(|e| e.to_string())?)?;
Content::read(dir, &manifest)?;
println!(
"Created {} (mod id {id}). Next: cd {path} && cargo horizon-mod run",
dir.display()
@@ -102,14 +169,53 @@ fn new(args: &[String]) -> Result<(), String> {
Ok(())
}
/// Builds the crate in the current folder and packs `target/horizon-mod/<id>.hmod`.
/// Builds the mod in the current folder and packs `target/horizon-mod/<id>.hmod`:
/// the manifest, the crate's component (none without a `Cargo.toml`: a
/// content mod) and the files in `assets/` and `data/`.
fn build(release: bool) -> Result<PathBuf, String> {
let manifest_text = std::fs::read_to_string(MANIFEST)
build_in(Path::new("."), release)
}
fn build_in(root: &Path, release: bool) -> Result<PathBuf, String> {
let manifest_text = std::fs::read_to_string(root.join(MANIFEST))
.map_err(|e| format!("{MANIFEST}: {e} (run this in a mod's folder)"))?;
let manifest = Manifest::parse(&manifest_text)?;
let (wasm, target_dir) = if root.join("Cargo.toml").exists() {
let (wasm, target) = compile(root, release)?;
// The game refuses a component importing capabilities the manifest lacks.
manifest.check_imports(component_imports(&wasm)?.iter().map(String::as_str))?;
(Some(wasm), target)
} else if manifest.capabilities.contains(&Capability::Registries) {
(None, root.join("target"))
} else {
return Err(format!(
"no Cargo.toml: only a content mod (capability registries in {MANIFEST}) has no code"
));
};
// The same checks as the game's, and the files in the order it lists them.
let content = Content::read(root, &manifest)?;
let mut reader = content.reader()?;
let mut files = Vec::with_capacity(content.files.len());
for name in &content.files {
files.push((name.as_str(), reader.read(name)?));
}
let files: Vec<(&str, &[u8])> = files.iter().map(|(n, b)| (*n, b.as_slice())).collect();
let dir = target_dir.join("horizon-mod");
std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?;
let path = dir.join(format!("{}.hmod", manifest.id));
// Write then rename, so a running game never reads half a file.
let partial = path.with_extension("hmod.partial");
let bytes = Package::write_files(&manifest_text, wasm.as_deref(), &files)?;
std::fs::write(&partial, bytes).map_err(|e| e.to_string())?;
std::fs::rename(&partial, &path).map_err(|e| e.to_string())?;
Ok(path)
}
/// Builds the crate for WebAssembly: its component, and Cargo's target folder.
fn compile(root: &Path, release: bool) -> Result<(Vec<u8>, PathBuf), String> {
let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".into());
let mut cmd = Command::new(cargo);
cmd.args([
cmd.current_dir(root).args([
"build",
"--lib",
"--target",
@@ -137,17 +243,96 @@ fn build(release: bool) -> Result<PathBuf, String> {
.rfind(|f| f.extension().is_some_and(|e| e == "wasm"))
.ok_or("the build made no .wasm: is the crate a cdylib?")?;
let wasm = componentize(&std::fs::read(&core).map_err(|e| e.to_string())?)?;
// The game refuses a component importing capabilities the manifest lacks.
manifest.check_imports(component_imports(&wasm)?.iter().map(String::as_str))?;
let target_dir = core.ancestors().nth(3).ok_or("unexpected target folder")?;
let dir = target_dir.join("horizon-mod");
std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?;
let path = dir.join(format!("{}.hmod", manifest.id));
// Write then rename, so a running game never reads half a file.
let partial = path.with_extension("hmod.partial");
std::fs::write(&partial, Package::write(&manifest_text, &wasm)?).map_err(|e| e.to_string())?;
std::fs::rename(&partial, &path).map_err(|e| e.to_string())?;
Ok(path)
let target = core.ancestors().nth(3).ok_or("unexpected target folder")?;
Ok((wasm, target.to_path_buf()))
}
/// `check <file>`: the registry's checks of a submission, printed; fails
/// when one does.
fn check(args: &[String]) -> Result<(), String> {
let file = args
.first()
.filter(|a| !a.starts_with("--"))
.ok_or("cargo horizon-mod check <file>")?;
let path = Path::new(file);
let kind = match flag(args, "--kind") {
Some(k) => horizon_mods::index::Kind::from(k.to_string()),
None => check::kind_of(path)
.ok_or("pass --kind mod|modpack|resourcepack: the extension doesn't say")?,
};
let signature = flag(args, "--signature")
.map(|p| std::fs::read_to_string(p).map_err(|e| format!("{p}: {e}")))
.transpose()?;
let report = check::check(path, kind, flag(args, "--key"), signature.as_deref());
if args.iter().any(|a| a == "--json") {
let json = match &report {
Ok(r) => serde_json::json!({"ok": true, "report": r.json()}),
Err(e) => serde_json::json!({"ok": false, "error": e}),
};
println!("{json}");
}
let report = report?;
if !args.iter().any(|a| a == "--json") {
print!("{}", report.text());
}
Ok(())
}
/// `test`: every `tests/*.toml` script (those whose name holds `<name>`)
/// against the built mod, or the `.hmod` given with `--hmod`.
fn test(args: &[String]) -> Result<(), String> {
let hmod = match flag(args, "--hmod") {
Some(p) => PathBuf::from(p),
None => build(!args.iter().any(|a| a == "--debug"))?,
};
let filter = args
.iter()
.enumerate()
.find(|(i, a)| !a.starts_with("--") && (*i == 0 || args[i - 1] != "--hmod"))
.map(|(_, a)| a.as_str());
let mut scripts: Vec<PathBuf> = std::fs::read_dir("tests")
.map_err(|e| {
format!(
"tests/: {e} (a mod's tests are tests/<name>.toml: docs/mods.md, \"Testing a mod\")"
)
})?
.filter_map(|e| Some(e.ok()?.path()))
.filter(|p| p.extension().is_some_and(|e| e == "toml"))
.filter(|p| filter.is_none_or(|f| p.to_string_lossy().contains(f)))
.collect();
scripts.sort();
if scripts.is_empty() {
return Err("no tests/*.toml to run".into());
}
println!(
"running {} scripts against {}",
scripts.len(),
hmod.display()
);
let mut failed = 0;
for path in &scripts {
let result = std::fs::read_to_string(path)
.map_err(|e| e.to_string())
.and_then(|text| Script::parse(&text))
.and_then(|script| {
let mut game = TestGame::load(&hmod)?;
script.run(&mut game)?;
Ok(script.steps.len())
});
match result {
Ok(steps) => println!("test {} ... ok ({steps} steps)", path.display()),
Err(e) => {
failed += 1;
println!("test {} ... FAILED\n {e}", path.display());
}
}
}
let passed = scripts.len() - failed;
if failed > 0 {
return Err(format!("{failed} failed, {passed} passed"));
}
println!("test result: ok. {passed} passed");
Ok(())
}
fn run(args: &[String]) -> Result<(), String> {
@@ -192,10 +377,57 @@ mod tests {
);
let cargo = std::fs::read_to_string(path.join("Cargo.toml")).unwrap();
assert!(cargo.contains("name = \"my-mod\"") && cargo.contains(SDK_GIT));
let script = std::fs::read_to_string(path.join("tests/hello.toml")).unwrap();
assert_eq!(Script::parse(&script).unwrap().steps.len(), 3);
assert!(
new(&[path.display().to_string()]).is_err(),
"never overwrites"
);
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn the_content_template_builds_and_checks_with_no_code() {
let dir = std::env::temp_dir().join(format!("horizon-mod-content-{}", std::process::id()));
let path = dir.join("gems");
let new_args = [
path.display().to_string(),
"--template".into(),
"content".into(),
];
new(&new_args).unwrap();
assert!(!path.join("Cargo.toml").exists());
let hmod = build_in(&path, true).unwrap();
assert_eq!(hmod, path.join("target/horizon-mod/gems.hmod"));
let package = Package::read(&hmod).unwrap();
assert!(package.wasm.is_none());
let content = Content::read(&hmod, &package.manifest).unwrap();
assert!(
content
.files
.contains(&"data/gems/horizon/block/glowing_amethyst.json".into())
);
assert_eq!(content.files.len(), 8, "{:?}", content.files);
let report = check::check(&hmod, check::kind_of(&hmod).unwrap(), None, None).unwrap();
assert!(!report.code);
assert!(
report.text().contains("no code: content only"),
"{}",
report.text()
);
// Without registries, a mod needs code.
let manifest = std::fs::read_to_string(path.join(MANIFEST)).unwrap();
let manifest = manifest.replace("[\"registries\"]", "[]");
std::fs::write(path.join(MANIFEST), manifest).unwrap();
assert!(build_in(&path, true).unwrap_err().contains("no Cargo.toml"));
assert!(
new(&[
dir.join("x").display().to_string(),
"--template".into(),
"lua".into()
])
.is_err()
);
std::fs::remove_dir_all(&dir).unwrap();
}
}
+159
View File
@@ -0,0 +1,159 @@
//! `cargo horizon-mod registry build|review|check|list`: the registry repository's CI
//! (`ludovic111/horizon-mods`, docs/api/mod-index.md). Network-free: the
//! workflow downloads the files and publishes what this writes.
use horizon_mods::index::{
FILE_HOST,
registry::{self, Sources},
};
use std::path::Path;
/// Where the registry's site is published (its icons).
const SITE: &str = "https://ludovic111.github.io/horizon-mods/";
const USAGE: &str = "usage: cargo horizon-mod registry build <checkout> --files <dir> --out <index.json> [--release <url>] [--site <url>] [--generated <time>]
cargo horizon-mod registry review <base checkout> <head checkout> --author <login> [--json]
cargo horizon-mod registry check <base checkout> <head checkout> --files <dir>
cargo horizon-mod registry list <checkout>";
fn flag<'a>(args: &'a [String], name: &str) -> Option<&'a str> {
super::flag(args, name)
}
fn positional(args: &[String]) -> Vec<&str> {
let mut out = Vec::new();
let mut skip = false;
for a in args {
if skip {
skip = false;
} else if a == "--json" {
} else if a.starts_with("--") {
skip = true;
} else {
out.push(a.as_str());
}
}
out
}
fn lines(errors: Vec<String>) -> String {
format!("{} problem(s):\n {}", errors.len(), errors.join("\n "))
}
pub fn main(args: &[String]) -> Result<(), String> {
let rest = args.get(1..).unwrap_or_default();
let pos = positional(rest);
match args.first().map(String::as_str) {
Some("build") => {
let [checkout] = pos[..] else {
return Err(USAGE.into());
};
let (files, out) = (flag(rest, "--files"), flag(rest, "--out"));
let (Some(files), Some(out)) = (files, out) else {
return Err(USAGE.into());
};
let generated = flag(rest, "--generated")
.map(str::to_owned)
.or_else(|| std::env::var("REGISTRY_GENERATED").ok())
.unwrap_or_default();
let index = registry::build(&Sources {
registry: Path::new(checkout),
files: Path::new(files),
release: flag(rest, "--release").unwrap_or(FILE_HOST),
site: flag(rest, "--site").unwrap_or(SITE),
generated,
})
.map_err(lines)?;
let text = serde_json::to_string_pretty(&index).map_err(|e| e.to_string())?;
std::fs::write(out, text + "\n").map_err(|e| format!("{out}: {e}"))?;
println!(
"Wrote {out}: {} project(s), {} taken down",
index.projects.len(),
index.revoked.len()
);
Ok(())
}
Some("review") => {
let [base, head] = pos[..] else {
return Err(USAGE.into());
};
let author = flag(rest, "--author").ok_or(USAGE)?;
let (base, _) = registry::load(Path::new(base)).map_err(lines)?;
let (head, _) = registry::load(Path::new(head)).map_err(lines)?;
let review = registry::review(&base, &head, author);
if rest.iter().any(|a| a == "--json") {
let json = serde_json::json!({"errors": review.errors, "notes": review.notes});
println!("{json}");
} else {
for n in &review.notes {
println!("note: {n}");
}
for e in &review.errors {
println!("error: {e}");
}
}
if review.errors.is_empty() {
Ok(())
} else {
Err(format!(
"{} change(s) the registry refuses",
review.errors.len()
))
}
}
// `check` (and its trial run) on each file `head` lists and `base`
// doesn't: a pull request's new files.
Some("check") => {
let [base, head] = pos[..] else {
return Err(USAGE.into());
};
let files = Path::new(flag(rest, "--files").ok_or(USAGE)?);
let old: Vec<String> = match registry::load(Path::new(base)) {
Ok((entries, _)) => entries
.iter()
.flat_map(|e| e.versions.iter().map(|v| v.sha256.clone()))
.collect(),
// The first pull request has no entries to compare with.
Err(_) => Vec::new(),
};
let (entries, _) = registry::load(Path::new(head)).map_err(lines)?;
let mut failed = 0;
for e in &entries {
for v in e.versions.iter().filter(|v| !old.contains(&v.sha256)) {
println!("{} {} ({}):", e.id, v.version, v.file);
let report = crate::check::check(
&files.join(&v.sha256),
e.kind.clone(),
e.key.as_deref(),
v.signature.as_deref(),
);
match report {
Ok(r) => print!("{}", r.text()),
Err(err) => {
failed += 1;
println!(" error: {err}");
}
}
}
}
if failed == 0 {
Ok(())
} else {
Err(format!("{failed} file(s) failed their check"))
}
}
// The files CI must download for `build`: `<sha256> <id> <version> <file>`.
Some("list") => {
let [checkout] = pos[..] else {
return Err(USAGE.into());
};
let (entries, _) = registry::load(Path::new(checkout)).map_err(lines)?;
for e in &entries {
for v in &e.versions {
println!("{} {} {} {}", v.sha256, e.id, v.version, v.file);
}
}
Ok(())
}
_ => Err(USAGE.into()),
}
}
@@ -0,0 +1,4 @@
{
"template": "minecraft:amethyst_block",
"properties": { "hardness": 2.0, "light": 7 }
}
@@ -0,0 +1,5 @@
{
"variants": {
"": { "model": "{id}:block/glowing_amethyst" }
}
}
@@ -0,0 +1,10 @@
id = "{id}"
name = "{name}"
version = "0.1.0"
description = ""
authors = []
# A content mod: blocks and items from data/ and assets/, no code (API 0.2).
api = "0.2"
side = "both"
# registries: "May add blocks and items to your worlds".
capabilities = ["registries"]
@@ -0,0 +1,3 @@
{
"model": { "type": "minecraft:model", "model": "{id}:block/glowing_amethyst" }
}
@@ -0,0 +1,3 @@
{
"block.{id}.glowing_amethyst": "Glowing Amethyst"
}
@@ -0,0 +1,11 @@
{
"type": "minecraft:block",
"pools": [
{
"condition": { "type": "minecraft:survives_explosion" },
"entries": [{ "type": "minecraft:item", "name": "{id}:glowing_amethyst" }],
"rolls": 1
}
],
"random_sequence": "{id}:blocks/glowing_amethyst"
}
@@ -0,0 +1,4 @@
{
"parent": "minecraft:block/cube_all",
"textures": { "all": "minecraft:block/amethyst_block" }
}
@@ -0,0 +1,3 @@
{
"values": ["{id}:glowing_amethyst"]
}
@@ -0,0 +1,6 @@
{
"type": "minecraft:crafting_shapeless",
"category": "building",
"ingredients": ["minecraft:amethyst_block", "minecraft:glowstone_dust"],
"result": { "id": "{id}:glowing_amethyst" }
}
@@ -0,0 +1,14 @@
# `cargo horizon-mod test` runs every tests/*.toml against the built mod, with no game:
# steps happen in order, and `expect` checks what the mod showed during the step.
# Every key: docs/mods.md, "Testing a mod".
[[step]]
expect.commands = ["hello"]
[[step]]
command = "hello world"
expect.chat = ["Hello world! (1 so far)"]
[[step]]
command = "hello again"
expect.chat = ["Hello again! (2 so far)"]
+62 -14
View File
@@ -4,15 +4,15 @@ use crate::json::{Result, error};
use horizon_core::random::RandomSource;
#[derive(Clone, Debug)]
enum Choice {
Single(Variant),
enum Choice<T> {
Single(T),
Weighted {
variants: Vec<Variant>,
variants: Vec<T>,
ends: Vec<i32>,
total: i32,
},
}
impl Choice {
impl Choice<Variant> {
fn new(value: &VariantSet) -> Result<Self> {
match value {
VariantSet::Single(value) => Ok(Self::Single(value.clone())),
@@ -41,7 +41,23 @@ impl Choice {
}
}
}
fn select(&self, random: &mut impl RandomSource) -> &Variant {
}
impl<T> Choice<T> {
fn map<U>(&self, f: &mut impl FnMut(&T) -> U) -> Choice<U> {
match self {
Self::Single(value) => Choice::Single(f(value)),
Self::Weighted {
variants,
ends,
total,
} => Choice::Weighted {
variants: variants.iter().map(&mut *f).collect(),
ends: ends.clone(),
total: *total,
},
}
}
fn select(&self, random: &mut impl RandomSource) -> &T {
match self {
Self::Single(value) => value,
Self::Weighted {
@@ -60,10 +76,11 @@ impl Choice {
}
/// Compile once per definition and share across its bound block states. Prefix
/// sums avoid rebuilding weight totals or scanning all entries during selection.
/// `map` swaps each variant for what it stands for (its baked model).
#[derive(Clone, Debug)]
pub struct CompiledChoices {
variants: Vec<Choice>,
multipart: Vec<Choice>,
pub struct CompiledChoices<T = Variant> {
variants: Vec<Choice<T>>,
multipart: Vec<Choice<T>>,
}
impl CompiledChoices {
pub fn new(definition: &BlockStateDefinition) -> Result<Self> {
@@ -82,14 +99,45 @@ impl CompiledChoices {
.collect::<Result<_>>()?,
})
}
/// Append selected variants without allocating intermediate part lists. A
/// multipart root consumes nextLong even for an empty selected subset, then
/// resets the same random source to that seed before each selected part.
}
impl<T> CompiledChoices<T> {
/// The same choices, with `f` of each variant in its place.
pub fn map<U>(&self, mut f: impl FnMut(&T) -> U) -> CompiledChoices<U> {
CompiledChoices {
variants: self.variants.iter().map(|c| c.map(&mut f)).collect(),
multipart: self.multipart.iter().map(|c| c.map(&mut f)).collect(),
}
}
/// The variant `particleMaterial` comes from: the first entry of a weighted
/// list (`WeightedVariants`) and, for a multipart root, of its first
/// selector whether or not that selector applies (`MultiPartModel`).
pub fn particle_variant(&self, selected: &SelectedModels) -> Option<&T> {
let choice = match selected {
SelectedModels::Variant(index) => self.variants.get(*index)?,
SelectedModels::Multipart(_) => self.multipart.first()?,
};
Some(match choice {
Choice::Single(value) => value,
Choice::Weighted { variants, .. } => variants.first()?,
})
}
/// Append selected variants without allocating intermediate part lists.
pub fn collect<'a>(
&'a self,
selected: &SelectedModels,
random: &mut impl RandomSource,
output: &mut Vec<&'a Variant>,
output: &mut Vec<&'a T>,
) -> Result<()> {
self.for_each(selected, random, |v| output.push(v))
}
/// Each selected variant, in order. A multipart root consumes nextLong
/// even for an empty selected subset, then resets the same random source
/// to that seed before each selected part.
pub fn for_each<'a>(
&'a self,
selected: &SelectedModels,
random: &mut impl RandomSource,
mut f: impl FnMut(&'a T),
) -> Result<()> {
match selected {
SelectedModels::Variant(index) => {
@@ -97,7 +145,7 @@ impl CompiledChoices {
.variants
.get(*index)
.ok_or_else(|| error("invalid variant selection"))?;
output.push(choice.select(random));
f(choice.select(random));
}
SelectedModels::Multipart(indices) => {
if indices.iter().any(|index| *index >= self.multipart.len()) {
@@ -106,7 +154,7 @@ impl CompiledChoices {
let seed = random.next_long();
for index in indices {
random.set_seed(seed);
output.push(self.multipart[*index].select(random));
f(self.multipart[*index].select(random));
}
}
}
+10 -7
View File
@@ -1031,19 +1031,22 @@ pub struct ItemModels {
}
impl ItemModels {
pub fn components(&self, stack: &StackView) -> Components {
let mut out = self
.prototypes
.get(&stack.item)
.cloned()
.unwrap_or_default();
/// The stack's components: its item's defaults with the stack's patch on top. A stack
/// with no patch (most stacks a screen draws) borrows the defaults instead of copying them.
pub fn components(&self, stack: &StackView) -> std::borrow::Cow<'_, Components> {
static NONE: Components = BTreeMap::new();
let prototype = self.prototypes.get(&stack.item).unwrap_or(&NONE);
if stack.patch.is_empty() {
return std::borrow::Cow::Borrowed(prototype);
}
let mut out = prototype.clone();
for (key, value) in &stack.patch {
match value {
Some(v) => out.insert(key.clone(), v.clone()),
None => out.remove(key),
};
}
out
std::borrow::Cow::Owned(out)
}
/// `ItemModelResolver.updateForTopItem`.
pub fn resolve(&self, stack: &StackView, ctx: &Context, env: &dyn ItemEnv) -> ResolvedItem {
+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 }
+52 -27
View File
@@ -1,8 +1,7 @@
//! The machine a report was measured on. CPU, RAM and OS are detected here; the GPU and the display
//! come from the program that rendered (the game knows its adapter, the launcher its window).
use std::process::Command;
use horizon_telemetry::system::command_output as output;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
@@ -16,8 +15,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 +46,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 +63,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 +131,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).
@@ -141,12 +172,6 @@ pub fn cpu_seconds(pid: u32) -> Option<f64> {
Some(ticks / 100.0)
}
fn output(program: &str, args: &[&str]) -> Option<String> {
let out = Command::new(program).args(args).output().ok()?;
let text = String::from_utf8(out.stdout).ok()?.trim().to_owned();
(out.status.success() && !text.is_empty()).then_some(text)
}
#[cfg(target_os = "macos")]
fn os_version() -> Option<String> {
output("sw_vers", &["-productVersion"])
+7 -1
View File
@@ -245,13 +245,19 @@ pub struct Report {
impl Report {
/// An empty report for this machine, created now.
pub fn new(kind: Kind, app: App) -> Report {
Self::with_hardware(kind, app, Hardware::detect())
}
/// Starts a report using supplied hardware, without running system probes.
/// Interactive apps can collect the remaining machine details after their first frame.
pub fn with_hardware(kind: Kind, app: App, hardware: Hardware) -> Report {
Report {
format: FORMAT.to_owned(),
version: VERSION,
kind,
created: utc_now(),
app,
hardware: Hardware::detect(),
hardware,
conditions: Conditions::default(),
metrics: BTreeMap::new(),
frames: Vec::new(),
+142 -34
View File
@@ -9,6 +9,11 @@
//! horizon-bots --server 127.0.0.1:25565 --bots 25 --behaviour mixed --seconds 360 --report bots.json
//!
//! It prints one JSON line a second (bots online, chunks received) and writes a summary with `--report`.
//! A bot moves only while it holds the chunk it stands in: vanilla's client stops ticking its player in a chunk
//! it hasn't received, so a player can't outrun the server's chunk sending, on any server.
//! Each bot also keeps the chunks it holds, as a client's chunk cache does, so the report tells chunks that
//! entered the view apart from chunks sent again while the bot still held them (`resent`), and counts the
//! times the server pulled a bot back (`pulled_back`: a teleport after the first, as for "moved too quickly").
use horizon_protocol::{
PROTOCOL_VERSION,
@@ -25,6 +30,7 @@ use horizon_protocol::{
};
use serde_json::json;
use std::{
collections::HashSet,
sync::{
Arc, Mutex,
atomic::{AtomicBool, AtomicU64, Ordering},
@@ -43,8 +49,9 @@ horizon-bots: load-test bots for a Minecraft Java Edition 26.3 server (offline m
--seconds N how long to run after the first bot joins; default 60
--view-distance N what the bots ask for; default 10
--seed N default 12345
--explore-speed B blocks a second an explore bot flies; default 22 (as fast as chunks come)
--name PREFIX bot names: PREFIX0, PREFIX1...; default Bot
--report FILE a JSON summary at the end
--report FILE a JSON summary at the end (- prints it after the per-second lines)
";
#[derive(Clone, Copy, Debug, PartialEq)]
@@ -73,6 +80,7 @@ struct Options {
seconds: u64,
view_distance: i8,
seed: u64,
explore_speed: f64,
name: String,
report: Option<String>,
}
@@ -87,6 +95,7 @@ fn options() -> Result<Options, String> {
seconds: 60,
view_distance: 10,
seed: 12345,
explore_speed: 22.0,
name: "Bot".into(),
report: None,
};
@@ -110,6 +119,9 @@ fn options() -> Result<Options, String> {
"--seconds" => o.seconds = value.parse().map_err(|_| number("seconds"))?,
"--view-distance" => o.view_distance = value.parse().map_err(|_| number("2 to 32"))?,
"--seed" => o.seed = value.parse().map_err(|_| number("a number"))?,
"--explore-speed" => {
o.explore_speed = value.parse().map_err(|_| number("blocks a second"))?;
}
"--name" => o.name = value,
"--report" => o.report = Some(value),
_ => return Err(format!("unknown option {arg}; --help lists them")),
@@ -129,6 +141,17 @@ fn options() -> Result<Options, String> {
struct Bot {
online: AtomicBool,
chunks: AtomicU64,
/// Chunks received while the bot already held them (no ForgetLevelChunk in between).
resent: AtomicU64,
/// Different chunks received over the run.
unique: AtomicU64,
forgotten: AtomicU64,
/// Teleports after the spawn one: the server refused a move.
pulled_back: AtomicU64,
/// Ticks the bot couldn't move because it didn't hold its chunk yet.
waited: AtomicU64,
/// The chunks the bot holds (ClientChunkCache).
held: Mutex<HashSet<(i32, i32)>>,
/// Where the server last put the bot (a teleport), to start from.
teleport: Mutex<Option<[f64; 3]>>,
error: Mutex<Option<String>>,
@@ -150,6 +173,11 @@ impl Random {
}
}
/// One bot thread that panics poisons the locks it held; the others and the report go on.
fn lock<T>(m: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
m.lock().unwrap_or_else(std::sync::PoisonError::into_inner)
}
fn raw(id: i32, write: impl FnOnce(&mut Writer)) -> Vec<u8> {
let mut w = Writer::new();
w.varint(id);
@@ -268,6 +296,36 @@ fn join(o: &Options, name: &str) -> Result<Connection, String> {
}
}
/// ChunkBatchSizeCalculator: the chunks per tick a vanilla client asks for, from how long its batches take
/// to arrive.
struct BatchSize {
nanos_per_chunk: f64,
old_samples_weight: u32,
started: Instant,
}
impl BatchSize {
fn new() -> Self {
Self {
nanos_per_chunk: 2_000_000.0,
old_samples_weight: 1,
started: Instant::now(),
}
}
fn finished(&mut self, size: i32) {
if size > 0 {
let per_chunk = self.started.elapsed().as_nanos() as f64 / f64::from(size);
let clamped = per_chunk.clamp(self.nanos_per_chunk / 3.0, self.nanos_per_chunk * 3.0);
let weight = f64::from(self.old_samples_weight);
self.nanos_per_chunk = (self.nanos_per_chunk * weight + clamped) / (weight + 1.0);
self.old_samples_weight = (self.old_samples_weight + 1).min(49);
}
}
fn desired_chunks_per_tick(&self) -> f32 {
(7_000_000.0 / self.nanos_per_chunk) as f32
}
}
/// Reads the play state: answers what a client must answer, counts chunks, records teleports.
fn read(
mut reader: horizon_protocol::connection::ConnReader,
@@ -276,13 +334,16 @@ fn read(
) {
use ids::play::clientbound as cb;
use ids::play::serverbound as sb;
let send = |body: Vec<u8>| writer.lock().unwrap().send(&body);
let send = |body: Vec<u8>| lock(&writer).send(&body);
let mut seen = HashSet::new();
let mut batch = BatchSize::new();
let mut teleports = 0u64;
loop {
let (id, body) = match reader.recv() {
Ok(packet) => packet,
Err(e) => {
if bot.online.swap(false, Ordering::SeqCst) {
*bot.error.lock().unwrap() = Some(e.to_string());
*lock(&bot.error) = Some(e.to_string());
}
return;
}
@@ -299,17 +360,41 @@ fn read(
},
cb::LEVEL_CHUNK_WITH_LIGHT => {
bot.chunks.fetch_add(1, Ordering::Relaxed);
if let (Ok(x), Ok(z)) = (r.i32(), r.i32()) {
if !lock(&bot.held).insert((x, z)) {
bot.resent.fetch_add(1, Ordering::Relaxed);
}
if seen.insert((x, z)) {
bot.unique.fetch_add(1, Ordering::Relaxed);
}
}
Ok(())
}
cb::CHUNK_BATCH_FINISHED => send(
play::Serverbound::ChunkBatchReceived(play::ChunkBatchReceived {
chunks_per_tick: 25.0,
})
.encode(),
),
cb::FORGET_LEVEL_CHUNK => {
if let Ok(play::Clientbound::ForgetLevelChunk(p)) =
play::Clientbound::decode(id, &body)
&& lock(&bot.held).remove(&(p.pos[0], p.pos[1]))
{
bot.forgotten.fetch_add(1, Ordering::Relaxed);
}
Ok(())
}
cb::CHUNK_BATCH_START => {
batch.started = Instant::now();
Ok(())
}
cb::CHUNK_BATCH_FINISHED => {
batch.finished(r.varint().unwrap_or(0));
send(
play::Serverbound::ChunkBatchReceived(play::ChunkBatchReceived {
chunks_per_tick: batch.desired_chunks_per_tick(),
})
.encode(),
)
}
cb::PLAYER_POSITION => match play::Clientbound::decode(id, &body) {
Ok(play::Clientbound::PlayerPosition(p)) => {
let mut at = bot.teleport.lock().unwrap();
let mut at = lock(&bot.teleport);
let previous = at.unwrap_or([0.0; 3]);
// Relative flags: X 1, Y 2, Z 4.
let pos: [f64; 3] = std::array::from_fn(|i| {
@@ -320,6 +405,10 @@ fn read(
}
});
*at = Some(pos);
teleports += 1;
if teleports > 1 {
bot.pulled_back.fetch_add(1, Ordering::Relaxed);
}
send(
play::Serverbound::AcceptTeleportation(play::AcceptTeleportation {
id: p.teleport,
@@ -336,7 +425,7 @@ fn read(
},
cb::DISCONNECT | cb::START_CONFIGURATION => {
bot.online.store(false, Ordering::SeqCst);
*bot.error.lock().unwrap() = Some(if id == cb::DISCONNECT {
*lock(&bot.error) = Some(if id == cb::DISCONNECT {
format!(
"disconnected: {}",
String::from_utf8_lossy(&body)
@@ -371,7 +460,7 @@ fn run_bot(
let connection = match join(&o, &name) {
Ok(c) => c,
Err(e) => {
*bot.error.lock().unwrap() = Some(e);
*lock(&bot.error) = Some(e);
return;
}
};
@@ -385,12 +474,12 @@ fn run_bot(
use ids::play::serverbound as sb;
// Wait for the spawn position.
let spawn = loop {
if let Some(at) = *bot.teleport.lock().unwrap() {
if let Some(at) = *lock(&bot.teleport) {
break at;
}
if !bot.online.load(Ordering::SeqCst) || asked.elapsed() > Duration::from_secs(60) {
if bot.error.lock().unwrap().is_none() {
*bot.error.lock().unwrap() = Some("no spawn position within 60 s".into());
if lock(&bot.error).is_none() {
*lock(&bot.error) = Some("no spawn position within 60 s".into());
}
return;
}
@@ -398,7 +487,7 @@ fn run_bot(
};
bot.joined_after_ms
.store(asked.elapsed().as_millis() as u64, Ordering::SeqCst);
let send = |body: Vec<u8>| writer.lock().unwrap().send(&body).is_ok();
let send = |body: Vec<u8>| lock(&writer).send(&body).is_ok();
send(raw(sb::PLAYER_LOADED, |_| {}));
let mut random = Random(o.seed ^ (index as u64).wrapping_mul(0x2545_F491_4F6C_DD1D));
let mut pos = spawn;
@@ -407,16 +496,17 @@ fn run_bot(
let mut target = pos;
let mut next = Instant::now();
loop {
if until.lock().unwrap().is_some_and(|u| Instant::now() >= u)
|| !bot.online.load(Ordering::SeqCst)
{
if lock(&until).is_some_and(|u| Instant::now() >= u) || !bot.online.load(Ordering::SeqCst) {
break;
}
// A teleport from the server wins over where the bot thought it was.
if let Some(at) = bot.teleport.lock().unwrap().take() {
if let Some(at) = lock(&bot.teleport).take() {
pos = at;
}
if pos[1] < cruise {
let here = ((pos[0].floor() as i32) >> 4, (pos[2].floor() as i32) >> 4);
if !lock(&bot.held).contains(&here) {
bot.waited.fetch_add(1, Ordering::Relaxed);
} else if pos[1] < cruise {
pos[1] = (pos[1] + 0.5).min(cruise);
} else {
match behaviour {
@@ -440,9 +530,11 @@ fn run_bot(
}
}
Behaviour::Explore => {
// Flying fast in creative: about 22 blocks a second, which keeps new chunks coming.
pos[0] += heading.cos() * 1.1;
pos[2] += heading.sin() * 1.1;
// Flying in creative, by default about 22 blocks a second, which keeps new
// chunks coming as fast as the server sends them.
let step = o.explore_speed / 20.0;
pos[0] += heading.cos() * step;
pos[2] += heading.sin() * step;
}
}
}
@@ -469,7 +561,7 @@ fn run_bot(
}
// Offline first, so the reader doesn't count the closed connection as a failure.
bot.online.store(false, Ordering::SeqCst);
writer.lock().unwrap().shutdown();
lock(&writer).shutdown();
}
fn behaviour_of(mode: &str, index: usize) -> Behaviour {
@@ -520,13 +612,13 @@ fn main() {
.filter(|b| b.online.load(Ordering::SeqCst))
.count();
let chunks: u64 = bots.iter().map(|b| b.chunks.load(Ordering::Relaxed)).sum();
let failed = bots
.iter()
.filter(|b| b.error.lock().unwrap().is_some())
.count();
let total = |f: fn(&Bot) -> &AtomicU64| -> u64 {
bots.iter().map(|b| f(b).load(Ordering::Relaxed)).sum()
};
let failed = bots.iter().filter(|b| lock(&b.error).is_some()).count();
if first.is_none() && online > 0 {
first = Some(Instant::now());
*until.lock().unwrap() = Some(Instant::now() + Duration::from_secs(o.seconds));
*lock(&until) = Some(Instant::now() + Duration::from_secs(o.seconds));
}
println!(
"{}",
@@ -536,13 +628,18 @@ fn main() {
"failed": failed,
"chunks": chunks,
"chunks_per_second": chunks - previous_chunks,
"unique": total(|b| &b.unique),
"forgotten": total(|b| &b.forgotten),
"resent": total(|b| &b.resent),
"pulled_back": total(|b| &b.pulled_back),
"waited_ticks": total(|b| &b.waited),
})
);
previous_chunks = chunks;
let over = until.lock().unwrap().is_some_and(|u| Instant::now() >= u);
let over = lock(&until).is_some_and(|u| Instant::now() >= u);
let all_failed = failed == o.bots;
if over || all_failed || (first.is_none() && start.elapsed() > Duration::from_secs(120)) {
*until.lock().unwrap() = Some(Instant::now());
*lock(&until) = Some(Instant::now());
break;
}
}
@@ -554,19 +651,30 @@ fn main() {
"bots": o.bots,
"behaviour": o.behaviour,
"seed": o.seed,
"explore_speed": o.explore_speed,
"view_distance": o.view_distance,
"seconds": o.seconds,
"joined": bots.iter().filter(|b| b.joined_after_ms.load(Ordering::SeqCst) > 0).count(),
"chunks": bots.iter().map(|b| b.chunks.load(Ordering::Relaxed)).sum::<u64>(),
"resent": bots.iter().map(|b| b.resent.load(Ordering::Relaxed)).sum::<u64>(),
"pulled_back": bots.iter().map(|b| b.pulled_back.load(Ordering::Relaxed)).sum::<u64>(),
"waited_ticks": bots.iter().map(|b| b.waited.load(Ordering::Relaxed)).sum::<u64>(),
"per_bot": bots.iter().enumerate().map(|(i, b)| json!({
"name": format!("{}{i}", o.name),
"behaviour": behaviour_of(&o.behaviour, i).name(),
"joined_after_ms": b.joined_after_ms.load(Ordering::SeqCst),
"chunks": b.chunks.load(Ordering::Relaxed),
"error": *b.error.lock().unwrap(),
"unique_chunks": b.unique.load(Ordering::Relaxed),
"resent": b.resent.load(Ordering::Relaxed),
"forgotten": b.forgotten.load(Ordering::Relaxed),
"pulled_back": b.pulled_back.load(Ordering::Relaxed),
"waited_ticks": b.waited.load(Ordering::Relaxed),
"error": *lock(&b.error),
})).collect::<Vec<_>>(),
});
if let Some(path) = &o.report {
if o.report.as_deref() == Some("-") {
println!("{summary:#}");
} else if let Some(path) = &o.report {
if let Err(e) = std::fs::write(path, format!("{summary:#}\n")) {
eprintln!("horizon-bots: {path}: {e}");
}
+14
View File
@@ -0,0 +1,14 @@
[package]
name = "horizon-client-core"
version = "0.1.0"
edition.workspace = true
license.workspace = true
rust-version = "1.90"
description = "The client's lowest layer: chat text and translations, SNBT and NBT paths, the scoreboard, boss bars, game rules and the registry and particle tables commands name"
[dependencies]
horizon-world = { path = "../horizon-world" }
serde_json = { version = "1.0.151", features = ["raw_value"] }
[lints]
workspace = true
@@ -259,7 +259,7 @@ fn is_pos(tag: &Tag) -> bool {
matches!(tag, Tag::IntArray(v) if v.len() == 3)
}
/// The loaded `SignText` of one side, or the empty text.
pub(crate) fn sign_side(tag: Option<&Tag>) -> Tag {
pub fn sign_side(tag: Option<&Tag>) -> Tag {
let empty = || string("");
let mut messages: Vec<Tag> = match tag.and_then(|t| t.get("messages")) {
Some(Tag::List(list)) => list.iter().filter_map(component).collect(),
@@ -664,7 +664,9 @@ pub fn canonical(tag: &Tag) -> Option<Tag> {
}
"minecraft:trial_spawner" => {
// `TrialSpawnerStateData.Packed` and `FullConfig`: every key is optional
// and defaulted by vanilla; they are kept as loaded.
// and defaulted by vanilla; they are kept as loaded. No `spawn_data` is
// added: an absent one loads as Optional.empty and is not stored, and
// an empty entity would keep the spawner from ever picking a mob.
if let Tag::Compound(c) = tag {
for (k, v) in c {
let key = k.to_string_lossy();
@@ -676,12 +678,6 @@ pub fn canonical(tag: &Tag) -> Option<Tag> {
}
}
}
if !f.out.contains_key(&"spawn_data".into()) {
f.put(
"spawn_data",
Tag::compound([("entity", Tag::Compound(Compound::new()))]),
);
}
}
"minecraft:vault" => {
// `VaultConfig.DEFAULT` (when the config is absent): the trial key and the
@@ -827,6 +823,21 @@ mod tests {
canonical(&snbt(s)).expect("known type")
}
#[test]
fn a_trial_spawner_without_spawn_data_saves_none() {
let fresh = canon(
r#"{id:"minecraft:trial_spawner",x:1,y:2,z:3,normal_config:"minecraft:trial_chamber/ranged/skeleton/normal"}"#,
);
assert_eq!(fresh.get("spawn_data"), None);
let picked = canon(
r#"{id:"minecraft:trial_spawner",x:1,y:2,z:3,spawn_data:{entity:{id:"minecraft:skeleton"}}}"#,
);
assert_eq!(
picked.get("spawn_data"),
Some(&snbt(r#"{entity:{id:"minecraft:skeleton"}}"#))
);
}
#[test]
fn every_type_has_defaults() {
for id in TYPES.iter().chain(NEWEST.iter()) {
@@ -14,40 +14,64 @@ pub const STYLES: [&str; 5] = [
"notched_12",
"notched_20",
];
/// `BossBarColor.getFormatting`.
/// `BossBarColor.getFormatting`. Purple is dark blue in 26.3.
pub fn color_rgb(color: &str) -> u32 {
let name = match color {
"pink" => "red",
"red" => "dark_red",
"purple" => "dark_purple",
"purple" => "dark_blue",
other => other,
};
text::named_color(name).unwrap_or(text::WHITE)
}
/// A version-4 shaped id from a resource id. Saved boss events do not keep
/// their UUID (`CustomBossEvents` mints a new one on load).
pub fn stable_uuid(id: &str) -> u128 {
let mut h = 0xcbf29ce484222325u64;
for b in id.as_bytes() {
h ^= u64::from(*b);
h = h.wrapping_mul(0x100000001b3);
}
let most = (h as i64) & -61441 | 16384;
let least = (h.rotate_left(17) as i64) & 4_611_686_018_427_387_903 | i64::MIN;
((most as u64 as u128) << 64) | (least as u64 as u128)
}
#[derive(Clone, Debug, PartialEq)]
pub struct BossBar {
pub id: String,
/// The `ServerBossEvent` UUID. Not saved.
pub uuid: u128,
pub name: Comp,
pub color: String,
pub style: String,
pub value: i32,
pub max: i32,
/// `ServerBossEvent.visible`. A new bar is visible; nobody sees it until
/// a player is added.
pub visible: bool,
/// Whether the (single) local player is in the bar's player set.
pub player: bool,
pub darken: bool,
pub music: bool,
pub fog: bool,
/// `CustomBossEvent.players`, the UUIDs told about the bar.
pub players: Vec<[i32; 4]>,
}
impl BossBar {
pub fn new(id: &str, name: Comp) -> Self {
Self {
id: id.to_owned(),
uuid: stable_uuid(id),
name,
color: "white".into(),
style: "progress".into(),
value: 0,
max: 100,
visible: false,
player: false,
visible: true,
darken: false,
music: false,
fog: false,
players: Vec::new(),
}
}
/// `CustomBossEvent.getDisplayName`.
@@ -91,31 +115,34 @@ impl BossBars {
pub fn ids(&self) -> Vec<String> {
self.bars.iter().map(|b| b.id.clone()).collect()
}
/// The saved compound; the player's UUID stands for "the local player".
pub fn pack(&self, player: Option<[i32; 4]>) -> Tag {
/// The saved compound (`CustomBossEvent.Packed`).
pub fn pack(&self) -> Tag {
let mut c = Compound::new();
for b in &self.bars {
let mut e = vec![
("Name", json_to_tag(&b.name)),
("Visible", Tag::Byte(i8::from(b.visible))),
("Value", Tag::Int(b.value)),
("Max", Tag::Int(b.max)),
("Color", Tag::String(b.color.as_str().into())),
("Overlay", Tag::String(b.style.as_str().into())),
("DarkenScreen", Tag::Byte(0)),
("PlayBossMusic", Tag::Byte(0)),
("CreateWorldFog", Tag::Byte(0)),
];
let players = match (b.player, player) {
(true, Some(u)) => vec![Tag::IntArray(u.to_vec())],
_ => Vec::new(),
};
e.push(("Players", Tag::List(players)));
c.insert(b.id.as_str().into(), Tag::compound(e));
let players = b
.players
.iter()
.map(|u| Tag::IntArray(u.to_vec()))
.collect();
c.insert(
b.id.as_str().into(),
Tag::compound([
("Name", json_to_tag(&b.name)),
("Visible", Tag::Byte(i8::from(b.visible))),
("Value", Tag::Int(b.value)),
("Max", Tag::Int(b.max)),
("Color", Tag::String(b.color.as_str().into())),
("Overlay", Tag::String(b.style.as_str().into())),
("DarkenScreen", Tag::Byte(i8::from(b.darken))),
("PlayBossMusic", Tag::Byte(i8::from(b.music))),
("CreateWorldFog", Tag::Byte(i8::from(b.fog))),
("Players", Tag::List(players)),
]),
);
}
Tag::Compound(c)
}
pub fn unpack(data: &Tag, player: Option<[i32; 4]>) -> Self {
pub fn unpack(data: &Tag) -> Self {
let mut bars = Vec::new();
if let Tag::Compound(c) = data {
for (id, e) in c {
@@ -130,21 +157,30 @@ impl BossBars {
Some(Tag::Byte(v)) => i32::from(*v),
_ => d,
};
let in_players = match (get("Players"), player) {
(Some(Tag::List(l)), Some(u)) => l
let players = match get("Players") {
Some(Tag::List(l)) => l
.iter()
.any(|t| matches!(t, Tag::IntArray(a) if a.as_slice() == u)),
_ => false,
.filter_map(|t| match t {
Tag::IntArray(a) if a.len() == 4 => Some([a[0], a[1], a[2], a[3]]),
_ => None,
})
.collect(),
_ => Vec::new(),
};
let id = id.to_string_lossy();
bars.push(BossBar {
id: id.to_string_lossy(),
uuid: stable_uuid(&id),
id,
name: get("Name").map_or_else(|| literal(""), tag_to_json),
color: text("Color", "white"),
style: text("Overlay", "progress"),
value: num("Value", 0),
max: num("Max", 100),
visible: num("Visible", 0) != 0,
player: in_players,
darken: num("DarkenScreen", 0) != 0,
music: num("PlayBossMusic", 0) != 0,
fog: num("CreateWorldFog", 0) != 0,
players,
});
}
}
@@ -167,17 +203,15 @@ mod tests {
bar.value = 25;
bar.max = 50;
bar.visible = true;
bar.player = true;
let uuid = Some([1, 2, 3, 4]);
let back = BossBars::unpack(&b.pack(uuid), uuid);
bar.darken = true;
bar.players = vec![[1, 2, 3, 4]];
let back = BossBars::unpack(&b.pack());
assert_eq!(back, b);
// Another player's UUID does not include the local player.
assert!(!BossBars::unpack(&b.pack(uuid), Some([9, 9, 9, 9])).bars[0].player);
let bar = b.get("test:x").unwrap();
assert_eq!(bar.progress(), 0.5);
assert_eq!(bar.display_name().plain(), "chat.square_brackets");
assert_eq!(color_rgb("pink"), text::RED);
assert_eq!(color_rgb("purple"), 0xAA00AA);
assert_eq!(color_rgb("purple"), text::named_color("dark_blue").unwrap());
b.remove("test:x");
assert!(b.bars.is_empty());
}
@@ -167,6 +167,26 @@ fn object_style(o: &serde_json::Map<String, Value>) -> Style {
style.underlined = flag("underlined");
style.strikethrough = flag("strikethrough");
style.obfuscated = flag("obfuscated");
style.font = o.get("font").and_then(crate::text::font_id);
// ClickEvent.CODEC: the actions a chat line acts on here. A component
// from a server never opens a file.
if let Some(e) = o.get("click_event").and_then(Value::as_object) {
let text = |k: &str| e.get(k).and_then(Value::as_str).map(str::to_owned);
style.click = match e.get("action").and_then(Value::as_str) {
Some("suggest_command") => text("command").map(crate::text::ClickEvent::SuggestCommand),
Some("copy_to_clipboard") => {
text("value").map(crate::text::ClickEvent::CopyToClipboard)
}
_ => None,
};
}
// HoverEvent.ShowText.
if let Some(e) = o.get("hover_event").and_then(Value::as_object)
&& e.get("action").and_then(Value::as_str) == Some("show_text")
&& let Some(value) = e.get("value")
{
style.hover = Some(Box::new(to_text(value, &mut |s| Text::literal(s))));
}
style
}
/// `ComponentSerialization.CODEC` decoding into text. `selector` components
@@ -232,7 +252,7 @@ pub fn to_text_with(
} else if let Some(sel) = o.get("selector").and_then(Value::as_str) {
resolve(sel)
} else if let Some(key) = o.get("keybind").and_then(Value::as_str) {
crate::keybinds::display_name(key)
crate::key_display::display_name(key)
} else {
Text::empty()
};
@@ -266,4 +286,26 @@ mod tests {
let (v, _) = parse_snbt("[\"a\",{\"text\":\"b\"}]").unwrap();
assert_eq!(to_text(&v, &mut |_| Text::empty()).plain(), "ab");
}
#[test]
fn the_font_is_inherited_and_kept() {
let (v, _) =
parse_snbt(r#"{text:"a",font:"alt",extra:["b",{text:"c",font:"minecraft:uniform"}]}"#)
.unwrap();
let t = to_text(&v, &mut |_| Text::empty());
let fonts: Vec<_> = t.chars().map(|(_, s)| s.font.clone()).collect();
let (alt, uniform) = (
Some("minecraft:alt".into()),
Some("minecraft:uniform".into()),
);
assert_eq!(fonts, [alt.clone(), alt, uniform]);
// Back to JSON with the field, which a re-read keeps.
let again = to_text(&t.component(), &mut |_| Text::empty());
let kept: Vec<_> = again.chars().map(|(_, s)| s.font.clone()).collect();
assert_eq!(kept, fonts);
// Not an identifier: no font.
let (v, _) = parse_snbt(r#"{text:"a",font:"Bad Font"}"#).unwrap();
let t = to_text(&v, &mut |_| Text::empty());
assert_eq!(t.spans[0].style.font, None);
}
}
@@ -292,27 +292,4 @@ mod tests {
assert_eq!(GameRules::load(&dir), rules);
let _ = fs::remove_dir_all(dir);
}
/// The world's save (Level.write) writes the rules as they are now, and a
/// world opened again reads them back (#74).
#[test]
fn level_save_keeps_changed_rules() {
let dir =
std::env::temp_dir().join(format!("horizon-gamerules-level-{}", std::process::id()));
let _ = fs::remove_dir_all(&dir);
let mut level = crate::save::Level::default();
level.write(&dir).unwrap();
// Rules changed while playing, then saved and loaded again.
let mut rules = GameRules::load(&dir);
assert_eq!(rules, GameRules::default());
assert!(rules.set("advance_time", Value::Bool(false)));
assert!(rules.set("advance_weather", Value::Bool(false)));
assert!(rules.set("random_tick_speed", Value::Int(0)));
level.game_rules = rules.by_name();
level.write(&dir).unwrap();
assert_eq!(GameRules::load(&dir), rules);
let read = crate::save::Level::read(&dir).unwrap();
assert_eq!(read.game_rules, rules.by_name());
assert_eq!(read.game_rule("advance_time"), 0);
let _ = fs::remove_dir_all(dir);
}
}
@@ -0,0 +1,18 @@
//! Resource identifiers (`Identifier`).
/// Identifier.parse: `ns:path` or `path`, validated.
pub fn parse_identifier(raw: &str) -> Option<String> {
let (ns, path) = match raw.split_once(':') {
Some(("", path)) => ("minecraft", path),
Some((ns, path)) => (ns, path),
None => ("minecraft", raw),
};
let ns_ok = ns != ".."
&& ns
.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || matches!(c, '_' | '.' | '-'));
let path_ok = path.chars().all(|c| {
c.is_ascii_lowercase() || c.is_ascii_digit() || matches!(c, '_' | '.' | '-' | '/')
});
(ns_ok && path_ok).then(|| format!("{ns}:{path}"))
}
@@ -0,0 +1,38 @@
//! Key display names (`InputConstants.Key.getDisplayName`), shared by key
//! mappings and `keybind` text components.
use crate::text::Text;
/// InputConstants.Key.getDisplayName: one-character keys as the upper-case
/// character, others by translation.
pub fn display_name(key: &str) -> Text {
if let Some(rest) = key.strip_prefix("key.keyboard.")
&& rest.chars().count() == 1
{
let c = rest.to_uppercase();
return Text::literal(c);
}
let symbol = match key {
"key.keyboard.minus" => Some("-"),
"key.keyboard.equal" => Some("="),
"key.keyboard.left.bracket" => Some("["),
"key.keyboard.right.bracket" => Some("]"),
"key.keyboard.backslash" => Some("\\"),
"key.keyboard.semicolon" => Some(";"),
"key.keyboard.apostrophe" => Some("'"),
"key.keyboard.grave.accent" => Some("`"),
"key.keyboard.comma" => Some(","),
"key.keyboard.period" => Some("."),
"key.keyboard.slash" => Some("/"),
_ => None,
};
if let Some(s) = symbol {
return Text::literal(s);
}
if let Some(n) = key.strip_prefix("key.mouse.")
&& n.parse::<u32>().is_ok()
&& !crate::lang::has(key)
{
return Text::translate("key.mouse", &[Text::literal(n)]);
}
Text::translate(key, &[])
}
@@ -37,6 +37,25 @@ pub trait ResourceSource {
vec!["minecraft".into()]
}
}
/// Horizon's own strings, compiled in so they need no prepared resources.
struct Bundled;
impl ResourceSource for Bundled {
fn read(&self, path: &str) -> Option<Vec<u8>> {
let body = match path {
"assets/horizon/lang/en_us.json" => {
include_str!("../../horizon-client/assets/horizon/lang/en_us.json")
}
"assets/horizon/lang/ru_ru.json" => {
include_str!("../../horizon-client/assets/horizon/lang/ru_ru.json")
}
_ => return None,
};
Some(body.as_bytes().to_vec())
}
fn namespaces(&self) -> Vec<String> {
vec!["horizon".into()]
}
}
impl ResourceSource for std::path::PathBuf {
fn read(&self, path: &str) -> Option<Vec<u8>> {
std::fs::read(self.join(path)).ok()
@@ -77,12 +96,6 @@ pub fn apply_deprecated(map: &mut HashMap<String, String>, json: &[u8]) {
}
impl Language {
pub fn from_map(code: &str, map: HashMap<String, String>) -> Self {
Self {
code: code.to_owned(),
map,
}
}
/// ClientLanguage.loadFrom: en_us first, then the selected language, each
/// from every namespace and every source in priority order (later sources
/// override).
@@ -92,6 +105,12 @@ impl Language {
if code != DEFAULT {
codes.push(code);
}
// Horizon's own strings first, so a pack or the prepared root can still
// override them. Vanilla has no key for "Controller vibration".
let bundled = Bundled;
let mut all: Vec<&dyn ResourceSource> = vec![&bundled];
all.extend_from_slice(sources);
let sources = all.as_slice();
let namespaces: std::collections::BTreeSet<String> =
sources.iter().flat_map(|s| s.namespaces()).collect();
for c in codes {
@@ -123,6 +142,11 @@ impl Language {
pub fn get(&self, key: &str) -> Option<&str> {
self.map.get(key).map(String::as_str)
}
/// The resolved map, for `horizon_protocol::text::plain`: a server's
/// components carry keys, and the network layer resolves them with this.
pub fn map(&self) -> &HashMap<String, String> {
&self.map
}
pub fn len(&self) -> usize {
self.map.len()
}
@@ -210,6 +234,16 @@ mod tests {
self.0.get(path).cloned()
}
}
#[test]
fn bundled_horizon_strings_follow_the_language() {
// No prepared resources at all: the namespace is compiled in.
let empty = Mem(HashMap::new());
let en = Language::load(&[&empty], "en_us");
assert_eq!(en.get("horizon.options.controller"), Some("Controller"));
let ru = Language::load(&[&empty], "ru_ru");
assert_eq!(ru.get("horizon.options.controller"), Some("Геймпад"));
}
#[test]
fn layers_selected_language_over_english() {
let base = Mem(HashMap::from([
+23
View File
@@ -0,0 +1,23 @@
//! The client's lowest layer, below commands and gameplay: chat text and
//! translations, SNBT and NBT paths, text components, the scoreboard, boss
//! bars, game rules, block entity NBT tables, and the registry, particle and
//! sound source tables commands name. horizon-client re-exports each module
//! under its old path (`horizon_client::text`, ...).
#![forbid(unsafe_code)]
pub mod block_entity_nbt;
pub mod bossbar;
pub mod component;
pub mod fastutil_order;
pub mod gamerules;
pub mod identifier;
pub mod key_display;
pub mod lang;
pub mod nbt_path;
pub mod particle_type;
pub mod registries;
pub mod scoreboard;
pub mod snbt;
pub mod sound_source;
pub mod text;
pub mod uuid;
pub type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
@@ -0,0 +1,209 @@
//! Particle types (`ParticleTypes`): the vanilla particle types with a client
//! implementation, by registry name, and their per-type flags.
macro_rules! particle_types {
($($variant:ident = $name:literal),* $(,)?) => {
/// Vanilla particle types with a client implementation here.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum ParticleType { $($variant),* }
impl ParticleType {
pub const ALL: &'static [ParticleType] = &[$(ParticleType::$variant),*];
/// Registry path (`minecraft:<name>`), also the sprite-set file name.
pub fn name(self) -> &'static str {
match self { $(ParticleType::$variant => $name),* }
}
pub fn from_name(name: &str) -> Option<Self> {
let name = name.strip_prefix("minecraft:").unwrap_or(name);
match name { $($name => Some(ParticleType::$variant),)* _ => None }
}
}
};
}
particle_types! {
Block = "block",
BlockCrumble = "block_crumble",
Trail = "trail",
DustPillar = "dust_pillar",
FallingDust = "falling_dust",
Smoke = "smoke",
LargeSmoke = "large_smoke",
WhiteSmoke = "white_smoke",
Flame = "flame",
SoulFireFlame = "soul_fire_flame",
CopperFireFlame = "copper_fire_flame",
SmallFlame = "small_flame",
Lava = "lava",
DrippingWater = "dripping_water",
FallingWater = "falling_water",
DrippingLava = "dripping_lava",
FallingLava = "falling_lava",
LandingLava = "landing_lava",
DrippingHoney = "dripping_honey",
FallingHoney = "falling_honey",
LandingHoney = "landing_honey",
FallingNectar = "falling_nectar",
FallingSporeBlossom = "falling_spore_blossom",
SporeBlossomAir = "spore_blossom_air",
DrippingObsidianTear = "dripping_obsidian_tear",
FallingObsidianTear = "falling_obsidian_tear",
LandingObsidianTear = "landing_obsidian_tear",
DrippingDripstoneWater = "dripping_dripstone_water",
FallingDripstoneWater = "falling_dripstone_water",
DrippingDripstoneLava = "dripping_dripstone_lava",
FallingDripstoneLava = "falling_dripstone_lava",
Splash = "splash",
Rain = "rain",
Bubble = "bubble",
SulfurBubbles = "sulfur_bubbles",
BubblePop = "bubble_pop",
BubbleColumnUp = "bubble_column_up",
CurrentDown = "current_down",
Underwater = "underwater",
Portal = "portal",
ReversePortal = "reverse_portal",
Enchant = "enchant",
Nautilus = "nautilus",
VaultConnection = "vault_connection",
Crit = "crit",
EnchantedHit = "enchanted_hit",
DamageIndicator = "damage_indicator",
Heart = "heart",
AngryVillager = "angry_villager",
HappyVillager = "happy_villager",
Composter = "composter",
Mycelium = "mycelium",
Dolphin = "dolphin",
EggCrack = "egg_crack",
Poof = "poof",
Explosion = "explosion",
ExplosionEmitter = "explosion_emitter",
CampfireCosySmoke = "campfire_cosy_smoke",
CampfireSignalSmoke = "campfire_signal_smoke",
Note = "note",
Dust = "dust",
TotemOfUndying = "totem_of_undying",
Firework = "firework",
EndRod = "end_rod",
Ash = "ash",
WhiteAsh = "white_ash",
CrimsonSpore = "crimson_spore",
WarpedSpore = "warped_spore",
CherryLeaves = "cherry_leaves",
PaleOakLeaves = "pale_oak_leaves",
TintedLeaves = "tinted_leaves",
Effect = "effect",
InstantEffect = "instant_effect",
EntityEffect = "entity_effect",
Witch = "witch",
Infested = "infested",
Cloud = "cloud",
Sneeze = "sneeze",
Snowflake = "snowflake",
Soul = "soul",
SculkSoul = "sculk_soul",
DragonBreath = "dragon_breath",
Gust = "gust",
SmallGust = "small_gust",
GustEmitterLarge = "gust_emitter_large",
GustEmitterSmall = "gust_emitter_small",
DustColorTransition = "dust_color_transition",
SculkCharge = "sculk_charge",
SculkChargePop = "sculk_charge_pop",
Shriek = "shriek",
SonicBoom = "sonic_boom",
Vibration = "vibration",
SweepAttack = "sweep_attack",
Glow = "glow",
WaxOn = "wax_on",
WaxOff = "wax_off",
ElectricSpark = "electric_spark",
Scrape = "scrape",
SquidInk = "squid_ink",
GlowSquidInk = "glow_squid_ink",
Item = "item",
ItemSlime = "item_slime",
ItemSnowball = "item_snowball",
ItemCobweb = "item_cobweb",
SulfurCubeGoo = "sulfur_cube_goo",
Flash = "flash",
Firefly = "firefly",
TrialSpawnerDetection = "trial_spawner_detection",
TrialSpawnerDetectionOminous = "trial_spawner_detection_ominous",
TrialOmen = "trial_omen",
RaidOmen = "raid_omen",
OminousSpawning = "ominous_spawning",
Spit = "spit",
Fishing = "fishing",
DustPlume = "dust_plume",
RedPoplarLeaves = "red_poplar_leaves",
OrangePoplarLeaves = "orange_poplar_leaves",
YellowPoplarLeaves = "yellow_poplar_leaves",
PauseMobGrowth = "pause_mob_growth",
ResetMobGrowth = "reset_mob_growth",
BlockMarker = "block_marker",
}
impl ParticleType {
/// `ParticleType.getOverrideLimiter` (`ParticleTypes.register(name, true)`):
/// spawned regardless of distance.
pub fn override_limiter(self) -> bool {
matches!(
self,
Self::BlockMarker
| Self::DamageIndicator
| Self::ExplosionEmitter
| Self::Explosion
| Self::Gust
| Self::GustEmitterLarge
| Self::GustEmitterSmall
| Self::SonicBoom
| Self::SculkCharge
| Self::SculkChargePop
| Self::Vibration
| Self::Poof
| Self::Spit
| Self::SquidInk
| Self::SweepAttack
| Self::CampfireCosySmoke
| Self::CampfireSignalSmoke
| Self::GlowSquidInk
| Self::Glow
| Self::WaxOn
| Self::WaxOff
| Self::ElectricSpark
| Self::Scrape
| Self::TrialSpawnerDetection
| Self::TrialSpawnerDetectionOminous
| Self::VaultConnection
| Self::OminousSpawning
)
}
/// `SingleQuadParticle.Layer.translucent`: types whose vanilla particle class
/// hardcodes the translucent layer, drawn after translucent terrain.
/// `TerrainParticle`, `BreakingItemParticle` and `BlockMarker` resolve their
/// layer from the sprite instead (`Layer.bySprite`); see the `translucent`
/// sprite set on the engine.
pub fn translucent_layer(self) -> bool {
match self {
// CampfireSmokeParticle.
Self::CampfireCosySmoke | Self::CampfireSignalSmoke
// PlayerCloudParticle.
| Self::Cloud | Self::Sneeze
// SpellParticle.
| Self::Effect | Self::InstantEffect | Self::EntityEffect
| Self::Witch | Self::Infested | Self::TrialOmen | Self::RaidOmen
// EmissiveRisingParticle.
| Self::Soul | Self::SculkSoul
// SculkChargeParticle, SculkChargePopParticle, ShriekParticle,
// VibrationSignalParticle, FireflyParticle, OverlayParticle.
| Self::SculkCharge | Self::SculkChargePop | Self::Shriek
| Self::Vibration | Self::Firefly | Self::Flash
// SimpleAnimatedParticle and its subclasses: EndRodParticle,
// SparkParticle (firework), SquidInkParticle, TotemParticle.
| Self::EndRod | Self::Firework | Self::SquidInk | Self::GlowSquidInk
| Self::TotemOfUndying
// FlyTowardsPositionParticle: its LifetimeAlpha(0, 0.6, 0.25, 1) is not opaque.
| Self::VaultConnection => true,
_ => false,
}
}
}
@@ -0,0 +1,188 @@
//! Registry contents and source state that commands validate and complete
//! arguments against (what the server's registry access holds).
use std::collections::BTreeMap;
/// Registry contents and source state used to validate and complete arguments.
#[derive(Clone, Debug, Default)]
pub struct Registries {
/// Permission level of the command source.
pub permission: u8,
pub items: Vec<String>,
pub item_tags: Vec<String>,
/// Block id -> (property, allowed values).
pub blocks: BTreeMap<String, Vec<(String, Vec<String>)>>,
pub block_tags: Vec<String>,
/// Entity types that `summon` can create.
pub summonable: Vec<String>,
/// Every entity type (selector `type=`).
pub entity_types: Vec<String>,
pub effects: Vec<String>,
pub enchantments: Vec<String>,
pub structures: Vec<String>,
pub biomes: Vec<String>,
pub dimensions: Vec<String>,
pub clocks: Vec<String>,
pub timelines: Vec<String>,
pub time_markers: Vec<String>,
pub advancements: Vec<String>,
/// Game rule name -> integer-valued.
pub game_rules: Vec<(String, bool)>,
pub players: Vec<String>,
/// Scoreboard objective names, team names and tracked score holders.
pub objectives: Vec<String>,
pub teams: Vec<String>,
pub tracked: Vec<String>,
/// Loaded datapack functions (`ns:path`) and function tags (`ns:path`).
pub functions: Vec<String>,
/// `/data ... storage` ids.
pub storages: Vec<String>,
/// Boss bar ids and datapack predicate ids.
pub bossbars: Vec<String>,
pub predicates: Vec<String>,
pub item_modifiers: Vec<String>,
pub schedules: Vec<String>,
pub stopwatches: Vec<String>,
pub recipes: Vec<String>,
pub sounds: Vec<String>,
pub damage_types: Vec<String>,
pub attributes: Vec<String>,
pub loot_tables: Vec<String>,
pub templates: Vec<String>,
pub random_sequences: Vec<String>,
pub datapacks_enabled: Vec<String>,
pub datapacks_disabled: Vec<String>,
pub function_tags: Vec<String>,
/// The block the source is looking at (relevant coordinates).
pub target_block: Option<[i32; 3]>,
/// Every registry's element and tag ids (`minecraft:item` -> ...), as
/// the server's registry access and reloadable registries hold them.
pub by_registry: BTreeMap<String, RegistryIds>,
/// Blocks whose default state has a block entity.
pub block_entities: std::collections::BTreeSet<String>,
/// Block tag -> member blocks.
pub block_tag_members: BTreeMap<String, Vec<String>>,
/// Data component types with a codec (not transient).
pub persistent_components: Vec<String>,
/// Recipes `/recipe` can give (not special).
pub learnable_recipes: Vec<String>,
/// Advancement id -> criterion names.
pub advancement_criteria: BTreeMap<String, Vec<String>>,
/// World clock -> time markers shown in commands.
pub clock_time_markers: BTreeMap<String, Vec<String>>,
/// Trigger objectives the source may use (`/trigger` suggestions).
pub trigger_objectives: Vec<String>,
/// Objectives with the `trigger` criterion -> holder -> whether its score is locked.
pub trigger_scores: BTreeMap<String, BTreeMap<String, bool>>,
}
/// The ids and tag ids of one registry.
#[derive(Clone, Debug, Default)]
pub struct RegistryIds {
pub ids: Vec<String>,
pub tags: Vec<String>,
}
const EMPTY: &[String] = &[];
impl Registries {
/// Fill what the game did not build itself from vanilla's exported
/// registries (`command-registries.json`, written by
/// `VanillaCommandParseOracle registries`): built-in registries such as
/// mob effects, data component types and dialogs, block entities and tag
/// members. Registries that data packs reload stay with the game.
pub fn fill_from_export(&mut self, r: &serde_json::Value) {
fn strings(v: &serde_json::Value) -> Vec<String> {
v.as_array()
.map(|a| {
a.iter()
.filter_map(|x| x.as_str().map(str::to_owned))
.collect()
})
.unwrap_or_default()
}
let fill = |field: &mut Vec<String>, key: &str| {
if field.is_empty() {
*field = strings(&r[key]);
}
};
fill(&mut self.effects, "effects");
fill(&mut self.enchantments, "enchantments");
fill(&mut self.attributes, "attributes");
fill(&mut self.damage_types, "damage_types");
fill(&mut self.templates, "templates");
fill(&mut self.persistent_components, "persistent_components");
fill(&mut self.learnable_recipes, "learnable_recipes");
const RELOADED: [&str; 5] = [
"minecraft:advancement",
"minecraft:recipe",
"minecraft:loot_table",
"minecraft:predicate",
"minecraft:item_modifier",
];
for (name, entry) in r["by_registry"].as_object().into_iter().flatten() {
if RELOADED.contains(&name.as_str()) || self.by_registry.contains_key(name) {
continue;
}
self.by_registry.insert(
name.clone(),
RegistryIds {
ids: strings(&entry["ids"]),
tags: strings(&entry["tags"]),
},
);
}
if self.block_entities.is_empty() {
self.block_entities = strings(&r["block_entities"]).into_iter().collect();
}
if self.block_tag_members.is_empty() {
for (tag, members) in r["block_tag_members"].as_object().into_iter().flatten() {
self.block_tag_members.insert(tag.clone(), strings(members));
}
}
if self.clock_time_markers.is_empty() {
for (clock, markers) in r["clock_time_markers"].as_object().into_iter().flatten() {
self.clock_time_markers
.insert(clock.clone(), strings(markers));
}
}
if self.advancement_criteria.is_empty() {
for (id, criteria) in r["advancement_criteria"].as_object().into_iter().flatten() {
self.advancement_criteria
.insert(id.clone(), strings(criteria));
}
}
}
/// The element ids of a registry (sorted).
pub fn ids(&self, registry: &str) -> &[String] {
if let Some(r) = self.by_registry.get(registry) {
return &r.ids;
}
match registry {
"minecraft:item" => &self.items,
"minecraft:entity_type" => &self.entity_types,
"minecraft:mob_effect" => &self.effects,
"minecraft:enchantment" => &self.enchantments,
"minecraft:worldgen/structure" => &self.structures,
"minecraft:worldgen/biome" => &self.biomes,
"minecraft:world_clock" => &self.clocks,
"minecraft:timeline" => &self.timelines,
"minecraft:attribute" => &self.attributes,
"minecraft:damage_type" => &self.damage_types,
"minecraft:sound_event" => &self.sounds,
"minecraft:predicate" => &self.predicates,
"minecraft:item_modifier" => &self.item_modifiers,
"minecraft:loot_table" => &self.loot_tables,
"minecraft:recipe" => &self.recipes,
"minecraft:advancement" => &self.advancements,
_ => EMPTY,
}
}
/// The tag ids of a registry (sorted).
pub fn tags(&self, registry: &str) -> &[String] {
if let Some(r) = self.by_registry.get(registry) {
return &r.tags;
}
match registry {
"minecraft:item" => &self.item_tags,
"minecraft:block" => &self.block_tags,
_ => EMPTY,
}
}
}
@@ -2,9 +2,9 @@
//! `ObjectiveCriteria`, `NumberFormat`) and its saved data
//! (`data/minecraft/scoreboard.dat`, `ScoreboardSaveData.Packed`).
use crate::{
command::Registries,
component,
fastutil_order::{HashSetOrder, OpenOrder},
registries::Registries,
text::{self, Style, Text},
};
use horizon_world::nbt::Tag;
@@ -118,6 +118,7 @@ pub fn style_from_json(v: &Value) -> Style {
s.underlined = flag("underlined");
s.strikethrough = flag("strikethrough");
s.obfuscated = flag("obfuscated");
s.font = o.get("font").and_then(text::font_id);
s
}
impl NumberFormat {
@@ -1265,7 +1266,7 @@ fn entity_uuids(tag: &mut Tag) {
})
.collect::<Option<Vec<i32>>>(),
Tag::String(s) => {
crate::save::uuid_from_text(&s.to_string_lossy()).map(Vec::from)
crate::uuid::uuid_from_text(&s.to_string_lossy()).map(Vec::from)
}
_ => None,
};
File renamed without changes.
@@ -0,0 +1,7 @@
//! Sound sources (`SoundSource`), by name.
/// SoundSource names in vanilla order.
pub const SOUND_SOURCES: [&str; 11] = [
"master", "music", "record", "weather", "block", "hostile", "neutral", "player", "ambient",
"voice", "ui",
];
@@ -61,6 +61,13 @@ pub struct Style {
pub click: Option<ClickEvent>,
/// Hover text.
pub hover: Option<Box<Text>>,
/// The font (`FontDescription.Resource`), a full id like
/// `minecraft:alt`; the default font when unset.
pub font: Option<String>,
}
/// A style's `font` field: an identifier (`alt` is `minecraft:alt`).
pub fn font_id(v: &Value) -> Option<String> {
crate::identifier::parse_identifier(v.as_str()?)
}
impl Style {
pub fn color(c: u32) -> Self {
@@ -83,6 +90,9 @@ impl Style {
if self.hover.is_none() {
self.hover.clone_from(&base.hover);
}
if self.font.is_none() {
self.font.clone_from(&base.font);
}
}
pub fn is_bold(&self) -> bool {
self.bold == Some(true)
@@ -233,6 +243,10 @@ impl Text {
}
/// The `Component` as vanilla's JSON (ComponentSerialization): the one
/// built through the constructors, else the spans as literal siblings.
/// The key of a `Component.translatable` built through [`Text::translate`].
pub fn translation_key(&self) -> Option<&str> {
self.source.as_deref()?.get("translate")?.as_str()
}
pub fn component(&self) -> Value {
if let Some(c) = &self.source {
return (**c).clone();
@@ -263,6 +277,24 @@ impl Text {
_ => c,
}
}
/// Style fields the spans don't model (an entity hover, an insertion),
/// set on the component sent to vanilla only.
pub fn with_component_fields(mut self, fields: Vec<(String, Value)>) -> Self {
let mut c = self.component();
if let Value::Object(o) = &mut c {
for (k, v) in fields {
o.entry(k).or_insert(v);
}
}
self.source = Some(c.into());
self
}
/// The same spans, sent to vanilla as `component` (a shape the spans'
/// own JSON would not take, such as a styled root with siblings).
pub fn with_component(mut self, component: Value) -> Self {
self.source = Some(component.into());
self
}
pub fn with_color(self, color: u32) -> Self {
self.with_style(&Style::color(color))
}
@@ -396,6 +428,43 @@ impl Text {
}
out
}
/// `resolve_legacy` borrowing `self` when no span holds a `§` code (the
/// common case).
pub fn resolved(&self) -> std::borrow::Cow<'_, Text> {
if self.spans.iter().any(|s| s.text.contains('§')) {
std::borrow::Cow::Owned(self.resolve_legacy())
} else {
std::borrow::Cow::Borrowed(self)
}
}
/// The text without its legacy `§` codes (ChatFormatting.stripFormatting:
/// a `§` and a code character, `0`-`9`, `a`-`f`, `k`-`o` or `r`).
pub fn strip_legacy(&self) -> Text {
if !self.spans.iter().any(|s| s.text.contains('§')) {
return self.clone();
}
let code =
|c: char| matches!(c.to_ascii_lowercase(), '0'..='9' | 'a'..='f' | 'k'..='o' | 'r');
let mut out = Text::default();
for span in &self.spans {
let mut text = String::with_capacity(span.text.len());
let mut chars = span.text.chars().peekable();
while let Some(c) = chars.next() {
if c == '§' && chars.peek().is_some_and(|&n| code(n)) {
chars.next();
} else {
text.push(c);
}
}
if !text.is_empty() {
out.spans.push(Span {
text,
style: span.style.clone(),
});
}
}
out
}
/// Characters with their effective styles.
pub fn chars(&self) -> impl Iterator<Item = (char, &Style)> {
self.spans
@@ -423,13 +492,10 @@ impl Text {
}
out
}
pub fn char_count(&self) -> usize {
self.spans.iter().map(|s| s.text.chars().count()).sum()
}
}
/// The style fields of a component's JSON.
const STYLE_KEYS: [&str; 8] = [
const STYLE_KEYS: [&str; 9] = [
"color",
"bold",
"italic",
@@ -438,6 +504,7 @@ const STYLE_KEYS: [&str; 8] = [
"obfuscated",
"click_event",
"hover_event",
"font",
];
/// Style.Serializer: the fields a style sets.
fn style_json(style: &Style) -> Vec<(String, Value)> {
@@ -478,6 +545,9 @@ fn style_json(style: &Style) -> Vec<(String, Value)> {
json!({ "action": "show_text", "value": hover.component() }),
));
}
if let Some(font) = &style.font {
out.push(("font".to_owned(), font.clone().into()));
}
out
}
@@ -634,4 +704,20 @@ mod tests {
assert_eq!(t.spans[1].style.color, Some(GOLD));
assert_eq!(t.slice(1, 2).plain(), "y");
}
#[test]
fn resolved_borrows_clean_text_and_restyles_codes() {
let clean = Text::colored("Steve", GOLD);
assert!(matches!(clean.resolved(), std::borrow::Cow::Borrowed(_)));
// A colour code inside a span clears its decorations (a bold
// "§cSteve" draws "Steve" red, not bold).
let styled = Style {
bold: Some(true),
..Style::default()
};
let styled = Text::styled("§cSteve", styled);
let resolved = styled.resolved();
assert_eq!(resolved.plain(), "Steve");
assert_eq!(resolved.spans[0].style.color, Some(RED));
assert_eq!(resolved.spans[0].style.bold, Some(false));
}
}
+24
View File
@@ -0,0 +1,24 @@
//! UUIDs as vanilla's four-int form, to and from their text form.
pub fn uuid_text(u: [i32; 4]) -> String {
let hex: String = u.iter().map(|v| format!("{:08x}", *v as u32)).collect();
format!(
"{}-{}-{}-{}-{}",
&hex[0..8],
&hex[8..12],
&hex[12..16],
&hex[16..20],
&hex[20..32]
)
}
pub fn uuid_from_text(s: &str) -> Option<[i32; 4]> {
let hex: String = s.chars().filter(|c| *c != '-').collect();
if hex.len() != 32 {
return None;
}
let mut out = [0i32; 4];
for (i, v) in out.iter_mut().enumerate() {
*v = u32::from_str_radix(&hex[i * 8..i * 8 + 8], 16).ok()? as i32;
}
Some(out)
}
+59 -14
View File
@@ -9,55 +9,100 @@ description = "Horizon native development client"
[dependencies]
horizon-assets = { path = "../horizon-assets" }
horizon-client-core = { path = "../horizon-client-core" }
horizon-command = { path = "../horizon-command" }
horizon-core = { path = "../horizon-core" }
# The server thread ahead of the workers (MinecraftServer.spin's Thread.setPriority).
horizon-threads = { path = "../horizon-threads" }
horizon-hdr = { path = "../horizon-hdr" }
horizon-world = { path = "../horizon-world" }
horizon-protocol = { path = "../horizon-protocol" }
# server.properties, eula.txt and the user lists, shared with horizon-server.
horizon-server-files = { path = "../horizon-server-files" }
# Shader packs: reading them and turning their GLSL into naga modules.
horizon-shaders = { path = "../horizon-shaders" }
# Horizon's own shader packs: the format, the stages and the check (docs/shaders.md).
horizon-shader-api = { path = "../horizon-shader-api" }
horizon-friends = { path = "../horizon-friends" }
# Horizon mods: sandboxed WebAssembly components, off unless installed and turned on (docs/mods.md).
horizon-mods = { path = "../horizon-mods" }
wgpu = { version = "30.0.1", default-features = false, features = ["std", "parking_lot", "vulkan", "metal", "wgsl"] }
# Their runtime is the `mods` feature.
horizon-mods = { path = "../horizon-mods", default-features = false, features = ["index"] }
wgpu = { version = "30.0.1", default-features = false, features = ["std", "parking_lot", "vulkan", "metal", "wgsl", "naga-ir"] }
winit = "0.30.13"
pollster = "0.4"
bytemuck = { version = "1.24", features = ["derive"] }
glam = "0.30"
png = "0.18"
sha2 = "0.10.9"
# Server resource packs are checked and cached by their SHA-1, as vanilla does.
sha1 = "0.10"
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"] }
# Java (OptiFine/Iris) shader packs, translated to WGSL.
horizon-shaderpack = { path = "../horizon-shaderpack" }
cpal = "0.16"
# 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"] }
# Sound (ALSA on Linux); the `audio` feature.
cpal = { version = "0.16", optional = true }
symphonia = { version = "0.5", default-features = false, features = ["ogg", "vorbis"] }
miniz_oxide = "0.8"
ab_glyph = "0.2.32"
log = "0.4"
chrono = { version = "0.4.42", default-features = false, features = ["clock", "std"] }
flate2 = { version = "1.1.10", default-features = false, features = ["rust_backend"] }
flate2 = { version = "1.1.10", default-features = false, features = ["zlib-rs", "runtime_detection"] }
horizon-theme = { path = "../horizon-theme" }
# Opt-in data collection: the game keeps, the launcher sends (docs/telemetry.md).
horizon-telemetry = { path = "../horizon-telemetry", default-features = false }
# Opt-in data collection: the game keeps, the launcher sends (docs/telemetry.md); `live` is
# the anonymous "I'm playing" ping the game sends itself (`heartbeat.rs`).
horizon-telemetry = { path = "../horizon-telemetry", default-features = false, features = ["live"] }
horizon-bench = { path = "../horizon-bench" }
arboard = { version = "3.6.1", default-features = false, features = ["wayland-data-control"] }
zip = { version = "2", default-features = false, features = ["deflate"] }
# Controllers (evdev + libudev on Linux, IOKit on macOS, Windows.Gaming.Input).
gilrs = "0.11"
# Controllers (evdev + libudev on Linux, IOKit on macOS, Windows.Gaming.Input); the `controllers` feature.
gilrs = { version = "0.11", optional = true }
# The DualSense's rumble, adaptive triggers and lights over HID (gilrs only reads it).
horizon-dualsense = { path = "../horizon-dualsense" }
horizon-dualsense = { path = "../horizon-dualsense", optional = true }
# HTTPS to the Minecraft services (online-mode session join).
ureq = { version = "2", default-features = false, features = ["tls"] }
[target.'cfg(unix)'.dependencies]
# The server thread's CPU time per tick (`HORIZON_SPIKES`, spans.rs).
rustix = { version = "1", default-features = false, features = ["std", "time"] }
[target.'cfg(windows)'.dependencies]
# DirectX 12, where Vulkan has no adapter or device, or by choice (`graphicsApi`, graphics.rs).
# Shaders compile with FXC (d3dcompiler_47.dll, part of Windows), or DXC when dxcompiler.dll is
# on the PATH.
wgpu = { version = "30.0.1", default-features = false, features = ["dx12"] }
[target.'cfg(not(target_os = "macos"))'.dependencies]
# OpenGL (WGL on Windows, EGL on Linux), the last fallback, for GPUs with no Vulkan or
# DirectX 12 driver (`graphicsApi`, graphics.rs).
wgpu = { version = "30.0.1", default-features = false, features = ["gles"] }
[target.'cfg(windows)'.build-dependencies]
# The icon resource in the .exe (build.rs).
winresource = "0.1"
[features]
default = ["audio", "controllers"]
# Sound output. Without it (and `controllers`) the program links neither libasound nor libudev on
# Linux: the server build (`--no-default-features`, released as horizon-engine; docs/server.md).
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"]
# Its own name, not `probe`: Cargo copies every crate's examples into one target/<profile>/examples.
[[example]]
name = "client_probe"
path = "examples/probe/main.rs"
[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", "msl-out", "spv-out"] }
@@ -0,0 +1,15 @@
{
"horizon.options.controller": "Controller",
"horizon.options.vibration": "Vibration",
"horizon.options.adaptive_triggers": "Adaptive Triggers",
"horizon.options.lightbar": "Lightbar",
"horizon.options.look_sensitivity": "Look Sensitivity",
"horizon.options.invert_x": "Invert Look X",
"horizon.options.invert_y": "Invert Look Y",
"horizon.options.sneak_button": "Sneak Button",
"horizon.options.move_deadzone": "Move Deadzone",
"horizon.options.look_deadzone": "Look Deadzone",
"horizon.options.button_glyphs": "Button Glyphs",
"horizon.options.menu_navigation": "Menu Navigation",
"horizon.options.on_screen_keyboard": "On-Screen Keyboard"
}
@@ -0,0 +1,15 @@
{
"horizon.options.controller": "Геймпад",
"horizon.options.vibration": "Вибрация",
"horizon.options.adaptive_triggers": "Адаптивные триггеры",
"horizon.options.lightbar": "Подсветка",
"horizon.options.look_sensitivity": "Чувствительность обзора",
"horizon.options.invert_x": "Инверсия обзора по X",
"horizon.options.invert_y": "Инверсия обзора по Y",
"horizon.options.sneak_button": "Кнопка приседания",
"horizon.options.move_deadzone": "Мёртвая зона движения",
"horizon.options.look_deadzone": "Мёртвая зона обзора",
"horizon.options.button_glyphs": "Символы кнопок",
"horizon.options.menu_navigation": "Навигация по меню",
"horizon.options.on_screen_keyboard": "Экранная клавиатура"
}
@@ -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,213 +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 facts = BlockFacts::load(&root, assets.registry)?;
let biomes = Biomes::load(&root)?;
let tables = MeshTables::new(
&assets,
&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,
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(())
}
@@ -0,0 +1,168 @@
//! What a column costs to load, on real terrain: generates and lights the
//! columns around a point with the vanilla generator, as a world being explored
//! does, then meshes every section of the inner columns on this one thread, so a
//! change to generation or meshing shows as time per column and per section.
//!
//! cargo run --release -p horizon-client --example chunk_load_bench -- \
//! .local/horizon-client/26.3 [--seed 1234] [--center CX CZ] [--radius 8]
//! [--threads N] [--rounds 3]
//!
//! The default centre is the village of `tools/perf_bench.py`. The generation
//! figure is wall time with N workers (the game's own pool); the mesh figure is
//! this thread's CPU time in `Mesher::mesh`, the best of `--rounds`, so other
//! work on the machine changes it less. The checksum of the meshes shows that an
//! A/B pair of builds made the same ones.
use horizon_client::{
assets::Assets,
biome::Biomes,
blocks::BlockFacts,
chunk::HEIGHT,
light::LightTables,
mesher::{MeshTables, Mesher, Region, borrow},
models,
spans::thread_cpu,
streaming::{Streaming, WorkerContext},
vanillagen::VanillaTerrain,
world::World,
};
use horizon_core::biome::{BiomeZoom, obfuscate_seed};
use horizon_world::registry::BlockStateId;
use std::{
hash::{DefaultHasher, Hash, Hasher},
path::PathBuf,
sync::Arc,
thread,
time::{Duration, Instant},
};
type Error = Box<dyn std::error::Error>;
fn flag<T: std::str::FromStr>(args: &[String], name: &str, default: T) -> T {
args.iter()
.position(|a| a == name)
.and_then(|i| args.get(i + 1)?.parse().ok())
.unwrap_or(default)
}
fn main() -> Result<(), Error> {
let args: Vec<String> = std::env::args().collect();
let root = PathBuf::from(args.get(1).ok_or("usage: chunk_load_bench <resources>")?);
let seed: i64 = flag(&args, "--seed", 1234);
let radius: i32 = flag(&args, "--radius", 8);
let rounds: usize = flag(&args, "--rounds", 3);
let threads: usize = flag(
&args,
"--threads",
thread::available_parallelism().map_or(2, |n| n.get()),
);
let center = args
.iter()
.position(|a| a == "--center")
.map(|i| -> Result<(i32, i32), Error> {
Ok((
args.get(i + 1).ok_or("--center CX CZ")?.parse()?,
args.get(i + 2).ok_or("--center CX CZ")?.parse()?,
))
})
.transpose()?
.unwrap_or((-60, 10));
let assets = Arc::new(Assets::load(&root)?);
let render = Arc::new(assets.reload(&root)?);
let facts = BlockFacts::load(&root, assets.registry)?;
let visibility = models::load_visibility(&root, assets.registry)?;
let biomes = Arc::new(Biomes::load(&root)?);
let context = Arc::new(WorkerContext {
tables: Arc::new(MeshTables::new(&assets, &render, &facts, visibility)?),
light: Arc::new(LightTables::load(&root, assets.registry, &facts)?),
biomes: biomes.clone(),
assets: assets.clone(),
render,
});
let terrain = VanillaTerrain::new(&root.join("worldgen"), assets.registry, &biomes, seed)?;
// Generation and light, on the game's worker pool. Meshing is left out
// here and measured on its own below.
let mut streaming = Streaming::new(Arc::new(terrain), context.clone(), threads);
streaming.mesh = false;
streaming.center = center;
streaming.radius = radius;
let mut world = World::new(BlockStateId(0), 0);
let started = Instant::now();
let mut quiet = 0;
while quiet < 100 {
let before = world.chunks.len();
let _ = streaming.update(&mut world);
thread::sleep(Duration::from_millis(2));
quiet = if streaming.pending() == 0 && before == world.chunks.len() {
quiet + 1
} else {
0
};
}
let generated = started.elapsed() - Duration::from_millis(2 * 100);
let columns = world.chunks.len();
println!(
"generated and lit {columns} columns in {:.0} ms with {threads} workers ({:.2} ms per column)",
generated.as_secs_f64() * 1e3,
generated.as_secs_f64() * 1e3 / columns.max(1) as f64
);
// Meshing every section of the columns whose neighbours are all loaded.
let inner = radius - 1;
let regions: Vec<(Region, i32)> = (center.0 - inner..=center.0 + inner)
.flat_map(|x| (center.1 - inner..=center.1 + inner).map(move |z| (x, z)))
.filter_map(|(x, z)| {
let region: Region = std::array::from_fn(|dz| {
std::array::from_fn(|dx| {
world
.chunks
.get(&(x + dx as i32 - 1, z + dz as i32 - 1))
.cloned()
})
});
region
.iter()
.flatten()
.all(Option::is_some)
.then_some(region)
})
.flat_map(|region| (0..HEIGHT / 16).map(move |sy| (region.clone(), sy)))
.collect();
let mut best = Duration::MAX;
let (mut vertices, mut checksum) = (0, 0);
for _ in 0..rounds.max(1) {
let mut mesher = Mesher {
nether_shading: false,
zoom: BiomeZoom::new(obfuscate_seed(seed)),
blend_radius: 2,
smooth_lighting: true,
cutout_leaves: true,
assets: &context.assets,
render: &context.render,
tables: &context.tables,
biomes: &context.biomes,
visibility: context.tables.visibility.clone(),
};
vertices = 0;
let mut hash = DefaultHasher::new();
let mut spent = Duration::ZERO;
for (region, sy) in &regions {
let started = thread_cpu().ok_or("no thread CPU clock here")?;
let mesh = mesher.mesh(&borrow(region), *sy);
spent += thread_cpu().ok_or("no thread CPU clock here")? - started;
vertices += mesh.solid.len() + mesh.translucent.len();
bytemuck::cast_slice::<_, u8>(&mesh.solid).hash(&mut hash);
bytemuck::cast_slice::<_, u8>(&mesh.translucent).hash(&mut hash);
}
checksum = hash.finish();
best = best.min(spent);
}
println!(
"meshed {} sections ({vertices} vertices, checksum {checksum:016x}) in {:.1} ms of CPU ({:.1} us per section)",
regions.len(),
best.as_secs_f64() * 1e3,
best.as_secs_f64() * 1e6 / regions.len().max(1) as f64
);
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(())
}
}
-141
View File
@@ -1,141 +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 as u16)
};
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,
};
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,
};
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) => println!("{line}"),
HostEvent::Left(_) => {}
}
}
std::thread::sleep(Duration::from_millis(50));
}
}
@@ -84,8 +84,10 @@ fn main() -> Result<()> {
horizon_client::collisions::load(&root, registry).map_err(step("collisions"))?;
let loaded = began.elapsed().as_secs_f64() - built;
let ours = |id: &str| {
id.split_once(':')
.is_some_and(|(ns, _)| mods.iter().any(|m| m.id == ns))
id.split_once(':').is_some_and(|(ns, _)| {
mods.iter()
.any(|m| m.content.namespaces.iter().any(|n| n == ns))
})
};
let tools: Vec<String> = serde_json::from_value(oracle["tools"].clone())?;
@@ -56,10 +56,11 @@ fn main() -> horizon_client::Result<()> {
let initial = LIVE.load(Relaxed);
let start = Instant::now();
let assets = Assets::load(root)?;
let render = assets.reload(root)?;
let block_ms = start.elapsed().as_secs_f64() * 1000.;
let block_retained = LIVE.load(Relaxed) - initial;
let start_items = Instant::now();
let items = ItemRenderer::load_with_library(root, assets.model_library.clone())?;
let items = ItemRenderer::load_with_library(root, render.model_library.clone())?;
let item_ms = start_items.elapsed().as_secs_f64() * 1000.;
let total_ms = start.elapsed().as_secs_f64() * 1000.;
let retained = LIVE.load(Relaxed) - initial;
@@ -74,7 +75,7 @@ fn main() -> horizon_client::Result<()> {
horizon_core::position::BlockPos::new(0, 0, 0),
horizon_core::position::BlockPos::new(-17, 65, 31),
] {
if let Ok(selected) = assets.model_catalog.collect(id, pos) {
if let Ok(selected) = render.model_catalog.collect(id, pos) {
for (variant, model) in selected {
format!("{variant:?}").hash(&mut hash);
model.ambient_occlusion.hash(&mut hash);
@@ -0,0 +1,263 @@
//! The `entity_portals` section scan only: `may_hold_portal` over synthetic
//! entity reaches, with the per-tick palette map against the resident cache.
//! Run with prepared client resources:
//! cargo run --profile fast -p horizon-client --example portal_scan_bench -- RESOURCES
use horizon_client::{
blocks::BlockFacts,
chunk::{Chunk, MIN_Y, SECTIONS, Section},
fast_hash::FxHashMap,
portal_sections::PortalSections,
};
use horizon_core::{
geometry::{Aabb, Vec3},
position::BlockPos,
};
use horizon_world::{level::Behaviours, registry::BlockStateId};
use std::{hint::black_box, path::Path, sync::Arc, time::Instant};
/// `Game::swept_portal`'s reach (`Entity.checkInsideBlocks`'s swept box).
fn reach(from: Vec3, to: Vec3, width: f32, height: f32) -> Aabb {
let half = f64::from(width) / 2.0;
let bounds = Aabb::new(
Vec3::new(to.x - half, to.y, to.z - half),
Vec3::new(to.x + half, to.y + f64::from(height), to.z + half),
)
.deflate(Vec3::new(1.0e-5, 1.0e-5, 1.0e-5));
bounds
.union_bounds(bounds.move_by(from.subtract(to)))
.inflate(Vec3::new(1.0, 1.0, 1.0))
}
fn sections(reach: Aabb) -> (i64, i32, i32, i32, i32, i32, i32) {
let (min, max) = (reach.min_position(), reach.max_position());
let (min, max) = (BlockPos::containing(min), BlockPos::containing(max));
let top = SECTIONS as i32 - 1;
let (x0, x1) = (min.x >> 4, max.x >> 4);
let (z0, z1) = (min.z >> 4, max.z >> 4);
let (y0, y1) = (
((min.y - MIN_Y) >> 4).clamp(0, top),
((max.y - MIN_Y) >> 4).clamp(0, top),
);
let count = (i64::from(x1) - i64::from(x0) + 1)
* (i64::from(y1) - i64::from(y0) + 1)
* (i64::from(z1) - i64::from(z0) + 1);
(count, x0, x1, y0, y1, z0, z1)
}
/// The original `Game::may_hold_portal`: per-tick `seen`, palette per section.
fn may_hold_portal_baseline(
reach: Aabb,
world: &FxHashMap<(i32, i32), Arc<Chunk>>,
seen: &mut FxHashMap<(i32, i32, i32), bool>,
behaviours: &Behaviours,
) -> bool {
let (count, x0, x1, y0, y1, z0, z1) = sections(reach);
if count > 64 {
return true;
}
(x0..=x1).any(|cx| {
(z0..=z1).any(|cz| {
(y0..=y1).any(|sy| {
*seen.entry((cx, sy, cz)).or_insert_with(|| {
world
.get(&(cx, cz))
.and_then(|c| c.sections.get(sy as usize))
.is_some_and(|s| {
s.blocks
.maybe_has(|v| behaviours.portal_axis(BlockStateId(v)).is_some())
})
})
})
})
})
}
/// The cached `Game::may_hold_portal`: one mask per resident chunk.
fn may_hold_portal_cached(
reach: Aabb,
world: &FxHashMap<(i32, i32), Arc<Chunk>>,
cache: &mut PortalSections,
behaviours: &Behaviours,
) -> bool {
let (count, x0, x1, y0, y1, z0, z1) = sections(reach);
if count > 64 {
return true;
}
let mut y_mask = 0u32;
for sy in y0..=y1 {
y_mask |= 1 << sy;
}
(x0..=x1).any(|cx| {
(z0..=z1).any(|cz| {
world.get(&(cx, cz)).is_some_and(|chunk| {
cache.get((cx, cz), chunk, |v| {
behaviours.portal_axis(BlockStateId(v)).is_some()
}) & y_mask
!= 0
})
})
})
}
fn main() -> horizon_client::Result<()> {
let root = std::env::args()
.nth(1)
.ok_or("expected prepared client resource directory")?;
let start = Instant::now();
let registry = horizon_world::level::data::load_registry(Path::new(&root))?;
let (facts, behaviours) = BlockFacts::load_pair(Path::new(&root), &registry)?;
println!(
"resource_load_ms={:.3}",
start.elapsed().as_secs_f64() * 1000.0
);
let len = registry.len() as u32;
let portal: Vec<u32> = (0..len)
.filter(|&id| behaviours.portal_axis(BlockStateId(id)).is_some())
.collect();
let inactive_states: Vec<u32> = (0..len)
.filter(|&id| behaviours.portal_axis(BlockStateId(id)).is_none())
.collect();
let _ = &facts;
let portal_state = *portal
.first()
.ok_or("no nether portal block state in the registry")?;
println!("portal_states={}", portal.len());
// 500 entities spread over a 17x17 chunk world. Their reaches touch 1-4
// sections, as an item or a mob standing in or stepping into a portal does.
let grid = 17;
let base = -(grid / 2) * 16;
let entities: Vec<(Vec3, Vec3, f32, f32)> = (0..500)
.map(|i| {
let x = base + (i % grid) * 16 + (i % 5);
let z = base + (i / grid % grid) * 16 + (i % 7);
let y = 64 + (i % 3) * 16;
let from = Vec3::new(f64::from(x), f64::from(y), f64::from(z));
let to = from.add_vector(Vec3::new(0.1, 0.0, 0.07));
(from, to, 0.25, 0.25)
})
.collect();
let reaches: Vec<Aabb> = entities
.iter()
.map(|&(from, to, w, h)| reach(from, to, w, h))
.collect();
for (name, palette, portals_every) in [
("sparse", 1usize, 0usize),
("dense", 128, 0),
("global", 300, 0),
("dense_portal", 128, 5),
] {
let start = Instant::now();
let mut world: FxHashMap<(i32, i32), Arc<Chunk>> = FxHashMap::default();
for czi in 0..grid {
for cxi in 0..grid {
let (cx, cz) = (-(grid / 2) + cxi, -(grid / 2) + czi);
let mut chunk = Chunk::empty(cx, cz);
for (i, section) in chunk.sections.iter_mut().enumerate() {
let values: [u32; 4096] = std::array::from_fn(|j| {
if portals_every > 0 && i % portals_every == 0 && j == 0 {
portal_state
} else {
inactive_states[(j * 7 + i) % palette.min(inactive_states.len())]
}
});
Arc::make_mut(section).blocks = Section::from_blocks(&values);
}
world.insert((cx, cz), Arc::new(chunk));
}
}
println!(
"{name} fixture_ms={:.3}",
start.elapsed().as_secs_f64() * 1000.0
);
let keys: Vec<_> = world.keys().copied().collect();
let portal_chunks: Vec<(i32, i32)> = keys
.iter()
.copied()
.filter(|&(cx, cz)| {
world[&(cx, cz)]
.sections
.iter()
.any(|s| s.blocks.maybe_has(|v| v == portal_state))
})
.collect();
println!("{name} portal_chunks={}", portal_chunks.len());
// The global palette's maybe_has walks the whole ID range, so rescanning
// every chunk each tick (cold/reload/edit_all) is intractable; those rows
// are only meaningful for the palettes whose scans are bounded.
let workloads: &[(&str, usize)] = if name == "global" {
&[("steady", 0), ("edit_1pct", 3)]
} else {
&[
("steady", 0),
("cold", 0),
("reload", 0),
("edit_1pct", 3),
("edit_all", 289),
]
};
for &(workload, changed) in workloads {
let mut baseline = Vec::new();
let mut candidate = Vec::new();
let mut cache = PortalSections::default();
let (mut base_hits, mut cand_hits) = (0u64, 0u64);
for sample in 0..8 {
for cached in if sample % 2 == 0 {
[false, true]
} else {
[true, false]
} {
let start = Instant::now();
let mut result = 0u64;
for tick in 0..300 {
if cached && workload == "cold" {
cache = PortalSections::default();
}
if cached && workload == "reload" {
cache.clear();
}
for key in keys.iter().take(changed) {
Arc::make_mut(world.get_mut(key).unwrap()).set(
0,
MIN_Y,
0,
inactive_states[tick % inactive_states.len()],
);
}
let mut seen = FxHashMap::default();
for reach in &reaches {
let hit = if cached {
may_hold_portal_cached(*reach, &world, &mut cache, &behaviours)
} else {
may_hold_portal_baseline(*reach, &world, &mut seen, &behaviours)
};
result += hit as u64;
}
}
if cached {
cache = PortalSections::default();
cand_hits += black_box(result);
} else {
base_hits += black_box(result);
}
if sample > 0 {
let us = start.elapsed().as_secs_f64() * 1e6 / 300.0;
if cached {
candidate.push(us);
} else {
baseline.push(us);
}
}
}
}
assert_eq!(
base_hits, cand_hits,
"{name} {workload}: cached may_hold_portal diverged"
);
println!("{name} {workload} baseline_us={baseline:.3?} candidate_us={candidate:.3?}");
}
}
println!("chunk_wrapper_bytes={}", std::mem::size_of::<Chunk>());
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,19 +50,20 @@ 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());
}
let root = PathBuf::from(&args[0]);
let dir = PathBuf::from(&args[1]);
let assets = Assets::load(&root)?;
let mut scene = World::test_scene(&assets, 1)?;
let render = assets.reload(&root)?;
let mut scene = World::test_scene(&assets, &render, 1)?;
fs::write(
dir.join("scene.json"),
serde_json::to_vec(
&serde_json::json!({"radius":1,"resident_chunks":scene.chunks.len(),"model_gallery":assets.model_gallery}),
&serde_json::json!({"radius":1,"resident_chunks":scene.chunks.len(),"model_gallery":render.model_gallery}),
)?,
)?;
if args[2] == "--describe" {
@@ -162,6 +163,8 @@ fn main() -> Result<()> {
ArrowSound::HitGround => "hit",
ArrowSound::Splash => "splash",
ArrowSound::Extinguish => "extinguish",
ArrowSound::TridentHitGround => "trident_hit_ground",
ArrowSound::TridentHit => "trident_hit",
},
volume.to_bits(),
pitch.to_bits()
@@ -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);
Loaded 100 of 2218 files, more files were not shown because too many files have changed in this diff. Show more