Compare commits

..
Author SHA1 Message Date
Ludovic Marie 3e06612e08 Merge pull request #11826 from iseaprojects/i18n/es-es-remaining
🤖 Agent: queued. es.json was merged key by key with main (91 translations kept, main's new keys too), so no Spanish string main added is lost.
2026-11-08 03:02:29 +01:00
Ludovic Marie 15c0246614 Merge pull request #12147 from RINNECODER/up2/port-occludes
🤖 Agent: queued after #12145 (its base, queued). Main merged in and the stack rebuilt on #12145's head; verify is green at this head.
2026-11-08 03:02:25 +01:00
Ludovic Marie face534db3 Merge pull request #12132 from Evan-lhm/fix/launcher-blur-zero-sharp
🤖 Agent: queued. A 0 px blur skips the blur pass, so the plates are sharp and the GPU does no work for it.
2026-11-08 03:02:22 +01:00
Ludovic Marie 0a6eca805d Merge pull request #12154 from RINNECODER/up2/pack-comment-settings
🤖 Agent: queued. Comment settings end at their line, as the Iris probes in `read_stage` show it reads them line by line; live code still reads to the `;`.
2026-11-08 02:59:34 +01:00
Ludovic Marie ed0be44325 Merge pull request #12117 from sergue212fbright/fix/sign-text-formatting
🤖 Agent: queued. Bold is drawn as the shifted second pass and italic as the skew vanilla's `BakedGlyph` uses, and after the #11863 merge styled glyphs keep the HDR emission tier; the 102 sign cases pass.
2026-11-08 02:58:24 +01:00
Ludovic Marie 97e46ce1d5 Merge pull request #12050 from TheMrZZ/fix/h140-nautilus-cooldown-memory
🤖 Agent: queued. The Nautilus cooldown memory now exists only while it counts down and is erased at 0, as `Brain.setMemoryWithExpiry` does; its harness runs in parity.
2026-11-08 02:58:20 +01:00
Ludovic Marie 5669d66de3 Merge pull request #11990 from TheMrZZ/fix/h285-adventure-predicate-saved-forms
🤖 Agent: queued. A one-block predicate now saves as the bare id and partial `custom_data` keeps only the keys given, which is how `BlockPredicate`'s codec writes them; the residual corpus documents what is left.
2026-11-08 02:58:17 +01:00
Ludovic Marie f54fba8726 Merge pull request #12123 from NikitaMGrimm/tnt-piston-ground
🤖 Agent: queued. A sideways push leaves `onGround` false, as `PistonMovingBlockEntity.moveCollidedEntities` only sets it for upward pushes; main merged in since my review, no other change.
2026-11-08 02:58:13 +01:00
Ludovic Marie f86b98b203 Merge pull request #12118 from NikitaMGrimm/minecart-drop-motion
🤖 Agent: queued. A destroyed minecart drops its item and contents with `Containers.dropContents`' motions; same items, now in vanilla's spread.
2026-11-08 02:58:10 +01:00
Ludovic Marie 9842e75046 Merge pull request #12107 from martipops/settings/shell-fixes
🤖 Agent: queued. Building the settings rows once removes the per-frame rebuild, and Down from the search field lands in the list.
2026-11-08 02:58:07 +01:00
Ludovic Marie 99956fff22 Merge pull request #12106 from martipops/controller/nintendo-buttons
🤖 Agent: queued. The Nintendo row picks by position or by letter only in the glyph table; the bindings themselves don't move.
2026-11-08 02:58:03 +01:00
Ludovic Marie b23ad347d1 Merge pull request #12105 from NikitaMGrimm/execute-block-nbt
🤖 Agent: queued. `execute if block`, `fill` and `clone` now compare the block entity's NBT part of the predicate, as `BlockPredicateArgument` does.
2026-11-08 02:58:00 +01:00
Ludovic Marie 350e156a34 Merge pull request #12088 from KBroneZ/claude/datapack-jars-launcher
🤖 Agent: queued. Last of the series, after #12087: a profile with only data packs now plays instead of reporting nothing to install.
2026-11-08 02:57:55 +01:00
Ludovic Marie 502c15aba6 Merge pull request #12086 from KBroneZ/claude/new-world-datapacks
🤖 Agent: queued. First of the 12086 → 12087 → 12088 series (each includes the one before): new worlds get the game folder's `new-world-datapacks`, copied at creation only.
2026-11-08 02:55:14 +01:00
Ludovic Marie 147980da63 Merge pull request #12082 from KBroneZ/claude/found-on-this-computer
🤖 Agent: queued. Only reads other launchers' folders (no writes), and the 86 lang files carry the new strings.
2026-11-08 02:55:09 +01:00
Ludovic Marie 97e99a45b4 Merge pull request #12073 from KyttuXD/fix/metal-cutout-leaves-derivatives
🤖 Agent: queued. Metal now always draws vanilla terrain flat, whatever the section count, which is the path that was already used under the threshold.
2026-11-08 02:55:05 +01:00
Ludovic Marie 37cda82c81 Merge pull request #12060 from ZiQu-dev/fix/oit-cull-depth-metal
🤖 Agent: queued. The 4-ulp depth tolerance fixes the speckles on Metal where the OIT pass's depths differ by a few ulps from the opaque pass's.
2026-11-08 02:55:01 +01:00
Ludovic Marie c76ed64570 Merge pull request #12058 from Mik00000/perf/dimension-tick
🤖 Agent: queued. Ending the tick on the first level skips the dimension swap that did nothing, and the ray chunk cache is per raycast, so no stale chunk outlives one ray.
2026-11-08 02:54:58 +01:00
Ludovic Marie f63ad014e1 Merge pull request #12048 from ilguerro/perf/9996-load-palres
🤖 Agent: queued. One cost to know: the registry-wide cache has 8192 slots that are never reclaimed; past that it falls back to decoding, so it only stops helping, it doesn't grow.
2026-11-08 02:54:54 +01:00
Ludovic Marie 7b33d702f5 Merge pull request #12047 from ilguerro/perf/9996-load-packed
🤖 Agent: queued. The SWAR check tests 8 palette indices per word and has an exhaustive test against the scalar loop, so a bad index is still caught at the same position.
2026-11-08 02:54:50 +01:00
Ludovic Marie dfae932fec Merge pull request #12016 from MSedgeMC/fix/drowned-trident-continue
🤖 Agent: queued. The trident attack continues while `alive && (holding a trident || !nav.done)`, which is vanilla's condition rewritten; a drowned that drops its trident now switches to melee.
2026-11-08 02:54:47 +01:00
Ludovic Marie bf3a5f162f Merge pull request #12008 from MSedgeMC/fix/cat-tempt-tamed
🤖 Agent: queued. A tamed cat no longer starts the tempt goal, as `Cat.TemptGoal`'s `canUse` returns false once tamed.
2026-11-08 02:54:43 +01:00
Ludovic Marie 14fc6d55e0 Merge pull request #11985 from TheMrZZ/ci/overlap-note-faster
🤖 Agent: queued. Tools only: one `git cat-file --batch-check` for the heads instead of a process per PR, and the diffs fetched in parallel; same notes out.
2026-11-08 02:54:39 +01:00
Ludovic Marie 4e6a8131c2 Merge pull request #11980 from MSedgeMC/fix/trade-start-conditions
🤖 Agent: queued. `TradeWithPlayerGoal.canUse` also stops on hurt and on distance > 16; it reads `cx.w.player` like the existing LookAtTradingPlayer goal does, so guests on a server share that approximation.
2026-11-08 02:54:36 +01:00
Ludovic Marie 01451da341 Merge pull request #11977 from MSedgeMC/fix/ghast-still-float
🤖 Agent: queued. The happy ghast's still timeout now ends the float goal as in vanilla, so a parked ghast stays put instead of drifting.
2026-11-08 02:54:32 +01:00
Ludovic Marie 179ebb840a Merge pull request #11965 from MSedgeMC/fix/cube-jump-passenger
🤖 Agent: queued. A ridden cube mob now skips its jump-move as vanilla's `SlimeMoveControl` does while it is a passenger, so a slime on a boat no longer hops off its seat.
2026-11-08 02:54:28 +01:00
Ludovic Marie fca63b5eb8 Merge pull request #11868 from MrCloudy2/fix/rider-teleport-look
🤖 Agent: queued. Matches `ClientPacketListener.handleMovePlayer`: a passenger keeps its seat when the server's position packet comes, instead of snapping out of the vehicle for a tick.
2026-11-08 02:54:25 +01:00
Horizon agentandClaude Opus 5.5 85d56215e6 Merge main into #11826
es.json: main's file with this PR's 91 translations applied key by key.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 02:28:45 +01:00
Ludovic Marie b13dbf0687 Merge pull request #11837 from yahya-nusseibeh/fix/control-socket-path
🤖 Agent: queued. When `/tmp` is shared the socket goes into a private `horizon-<uid>` folder, so another user on the machine can't connect to or squat on the control socket.
2026-11-08 02:20:33 +01:00
Ludovic Marie 0cdb230b54 Merge pull request #11815 from ilguerro/fix/thorns-rest
🤖 Agent: queued at a3fd3597f (main merged in; the draw now uses `random_source::entity()`). Thorns rolls in EquipmentSlot order from the wearer's own random, as `EnchantmentHelper.doPostAttackEffects` does.
2026-11-08 02:20:29 +01:00
Ludovic Marie 6e919e4356 Merge pull request #11806 from MrCloudy2/fix/shield-disable-cooldown
🤖 Agent: queued. The defender now gets the Cooldown packet the server sets on the shield (`Player.disableShield`), so its client greys the shield out for the 100 ticks instead of letting it block client-side.
2026-11-08 02:20:26 +01:00
Ludovic Marie 92cf9ac888 Merge pull request #11799 from Detimes/fix-old-attribute-names
🤖 Agent: queued at 14bfb672f (main and #11750 merged in). UPDATE/REMOVE_MOB_EFFECT ids are renumbered for older clients; effects they don't know are dropped instead of showing as another effect.
2026-11-08 02:20:22 +01:00
Ludovic Marie 0961939b71 Merge pull request #11791 from MrCloudy2/fix/spear-stab-players
🤖 Agent: queued. One cost to keep in mind: `stab_players_for` builds a Vec for every guest using any item each tick (shields, food too); small, but a follow-up could return early when the item is not a spear.
2026-11-08 02:19:16 +01:00
Ludovic Marie 2cd05e462e Merge pull request #11789 from MSedgeMC/fix/camel-sit-room
🤖 Agent: queued. Same check as `Camel.canCamelChangePose` (the standing box against the blocks above), and the test uses a block at y+2 that blocks only the 2.375-tall standing pose.
2026-11-08 02:19:11 +01:00
Ludovic Marie 541f331f1e Merge pull request #11762 from Mayonesu0/perf/launcher-write-data-file
🤖 Agent: queued. The data file is written off the UI thread after `complete_assets`, and a stale one is dropped when the assets change, so a repaired install doesn't read an old index.
2026-11-08 02:19:07 +01:00
Ludovic Marie 973287971d Merge pull request #11759 from doodlyyousuf/251107-fix-guest-left-mob-xp
🤖 Agent: queued. Matches `LivingEntity.dropExperience`/`dropCustomDeathLoot`: experience and equipment follow `lastHurtByPlayerMemoryTime > 0`, while LAST_DAMAGE_PLAYER still needs the player to be resolvable, and the new test checks both sides.
2026-11-08 02:19:04 +01:00
Ludovic Marie 7648d874b9 Merge pull request #11750 from Detimes/fix-11722-old-pick-item
🤖 Agent: queued at fee1085be (main merged in). A 1.21.3 pick_item now becomes the same container swap a 1.21.4+ client sends, so older clients keep middle-click in survival.
2026-11-08 02:19:00 +01:00
Ludovic Marie b91f5f5892 Merge pull request #11741 from yahya-nusseibeh/fix/server-undrained-packets
🤖 Agent: queued. The count goes up at the one `NetEvent::Packet` send in `conn.rs` and down at the hub's drain, so the 2× inbox cap bounds what a flooding client can queue between ticks without touching the hot path otherwise.
2026-11-08 02:18:57 +01:00
Ludovic Marie f159a106a8 Merge pull request #11713 from Akirabane/feat/transfer-stop-wait
🤖 Agent: queued. The stop waits only with `[transfer]` on, and its 90 s bound stays under the supervisor's 120 s kill, so a proxy that never answers can't hang a stop. Built with 14 other PRs on main (pr_ready --quick green apart from known NixOS-only tests).
2026-11-08 02:18:54 +01:00
Ludovic Marie c805ee70c5 Merge pull request #11782 from Teiwazik/render/native-section-fade
🤖 Agent: I moved the distance blend per pixel (it was per triangle through the flat hdr_code). Before/after screenshots and frame time are in my comment: the pack's GPU frame is 121.1 vs 122.0 ms on lavapipe, same within noise.
2026-11-08 02:18:50 +01:00
Horizon agent 14bfb672f8 Merge #11750 into #11799 (both add the last tests of client_session.rs)
Co-Authored-By: Claude Opus 5.5 <[email protected]>

# Conflicts:
#	crates/horizon-protocol/src/adapter/client_session.rs
2026-11-08 02:14:29 +01:00
Horizon agentandClaude Opus 5.5 43832ec794 Merge #12145 into #12147 (both change the map_color line)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 01:58:49 +01:00
Ludovic Marie bc6016d19d Merge pull request #12157 from horizon-rust/agent/older-servers-8718
🤖 Agent: Follow-up to #8718. On main, lan_bot's older_clients_of_each_release_join_and_are_sent_chunks fails at 1.21.1 (the reverse step never adds 1.21.2's particle status to ClientInformation); CI misses it because the test returns early without prepared resources. With this it passes, as do all 440 horizon-protocol tests and, on the union, the particle harnesses block/emitter/entity/marker/native_client_particles, particle_providers, player_effect_amplifier and spell_ticks (8/8).
2026-11-08 01:55:30 +01:00
Ludovic Marie 40e3f368da Merge pull request #11720 from Teiwazik/seat/follow-plugin-seat-chains
🤖 Agent: I pushed 9bc110bae. The new second position_riders() call after the player moves rebuilt the whole mob index on every tick for every client; it now runs only when the riding graph has something on this player. I also added the missing passenger_vehicles field to main's studio Remote initializer (it did not compile on main) and drop passengers whose vehicle is removed. Both ignored tests pass with prepared resources, and so do the riding harnesses on a union with current main: boat_authority, boat_rider, boat_paddle_reset, mob_rider_navigation, mount_submerged, server_entities, vehicle_command_damage, dedicated_server (8/8).
2026-11-08 01:42:48 +01:00
Ludovic Marie b399f0ddc5 Merge pull request #11775 from Teiwazik/older-servers/sign-update-order
🤖 Agent: Checked against the 26.3 client jar: ServerboundSignUpdatePacket's codec is pos, fixedSizeList(4) of stringUtf8(384), then SignTextSlot as an idMapper VarInt (BACK=0, FRONT=1), so reading the side last and writing it as the old boolean after the position is right. It only runs in the older-server adapter, so 26.3 servers are untouched. I merged main (the #8718 conflict in translate.rs: kept #8718's arms plus this one); 440 horizon-protocol tests and clippy pass.
2026-11-08 01:18:50 +01:00
Horizon agentandClaude Opus 5.5 a3fd3597f4 Thorns: draw from random_source::entity(), which replaced dispenser::entity_random on main
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 01:16:10 +01:00
Horizon agent 1d9f455dac Merge remote-tracking branch 'origin/main' into HEAD 2026-11-08 01:15:53 +01:00
Horizon agent f97dffe988 Merge remote-tracking branch 'origin/main' into pr-11775
# Conflicts:
#	crates/horizon-protocol/src/adapter/translate.rs
2026-11-08 00:15:30 +00:00
Horizon agentandClaude Opus 5.5 fee1085bec Merge main into fix-11722-old-pick-item
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 01:02:24 +01:00
Horizon agentandClaude Opus 5.5 272518c1f2 Merge main into fix-old-attribute-names
Main already carries the 1.21.1 and 1.20.6 attribute tables (consolidated in
d79a36101); kept main's generated tables and this branch's mob effect renumbering.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 01:02:00 +01:00
Nikita Maximilian Grimm b95e34a990 Merge remote-tracking branch 'origin/main' into tnt-piston-ground
# Conflicts:
#	crates/horizon-client/src/game/piston_push.rs
2026-11-08 00:46:47 +01:00
GlockSmithandOpenAI Codex GPT-6 e86ce41888 Merge main and retain HDR emission on styled sign text
Resolve the overlap with #11863 in the styled span and glyph loops. Keep its emission tier on glyphs, outlines and decoration quads, with regression coverage.

Co-Authored-By: OpenAI Codex GPT-6 <[email protected]>
2026-11-07 18:42:38 -05:00
Horizon agentandClaude Opus 5.5 458f0e4134 Native shader packs: blend the section fade by distance per pixel
The fade-in's 16-to-32-block distance blend was computed per vertex and
carried in the flat hdr_code, so it was one value per triangle (the
provoking vertex's) and near faces faded in steps. The flat bits now hold
only the section's own visibility (the same for the whole section), and
horizon_material_faded blends it by the interpolated spherical distance,
as terrain.wgsl's interpolated visibility does.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 23:40:34 +00:00
Kacper 29b3374b5d Merge pull request #12098 from NikitaMGrimm/boat-drop-motion
A destroyed boat drops its item with vanilla's drop motion
2026-11-08 00:40:24 +01:00
Kacper 02042bf6b4 Merge pull request #12097 from NikitaMGrimm/boat-fire-timer
A boat burns: its fire timer counts down, fire hurts it and water puts it out, as in vanilla
2026-11-08 00:40:18 +01:00
Kacper 79e277451b Merge pull request #12095 from Web-mc1/fix/telemetry-scrub-word-boundaries
telemetry: match the user name exactly when lowercasing moves boundaries
2026-11-08 00:40:10 +01:00
Kacper e5c1ea0612 Merge pull request #12092 from Web-mc1/fix/java-properties-save-convert
java_properties: write a NUL and a non-BMP character as Java does
2026-11-08 00:40:03 +01:00
Kacper 2f39e246fe Merge pull request #12084 from KBroneZ/claude/windows-unused-mut
Fix Windows-only unused_mut on the TCP keepalive builders
2026-11-08 00:39:56 +01:00
Kacper 2cd9e12ecd Merge pull request #12078 from KBroneZ/claude/oracle-windows
Run tools/neoforge_oracle.py on Windows
2026-11-08 00:39:43 +01:00
Kacper 6d32a88a50 Merge pull request #12069 from senjinthedragon/horizon-uniforms-lights-tier
Shader packs: Horizon's dynamic lights as uniforms, horizonTier for terrain and entities
2026-11-08 00:39:37 +01:00
Kacper 124e88e731 Merge pull request #12068 from NikitaMGrimm/fix/diode-moving-piston-support
A repeater or comparator on a retracting piston stays, as in vanilla
2026-11-08 00:39:30 +01:00
Kacper 1a004614f8 Merge pull request #12056 from TheMrZZ/fix/h255-mob-piston-ground
Project ground after unoccupied server NoAI Mob piston movement
2026-11-08 00:39:23 +01:00
Kacper 1edf540479 Merge pull request #12054 from TheMrZZ/fix/h234-machine-recipe-placement
Route recipe-book placement into actual furnace menus
2026-11-08 00:39:16 +01:00
Kacper 9bccc1056f Merge pull request #12053 from TheMrZZ/fix/h245-sulfur-bucket
Pick up a sulfur cube with an empty bucket
2026-11-08 00:39:09 +01:00
Kacper d144af4a11 Merge pull request #12049 from ilguerro/perf/9996-load-loadlight
Load saved light directly from its layers instead of expanding and repacking
2026-11-08 00:39:03 +01:00
Kacper 616707f3d7 Merge pull request #12033 from Vaspyyy/feat/native-muffle-sounds
Native mod API: muffle the game's sounds for a while, as ears ringing after a blast (native_muffle_sounds)
2026-11-08 00:38:56 +01:00
Kacper 1c5744ac24 Merge pull request #12014 from Vaspyyy/fix/native-mod-image-slot
Native mods: a game built with horizon-mixins gets the mod image slot, native-mods.json or not
2026-11-08 00:38:49 +01:00
Kacper c124718cbc Merge pull request #12002 from MrCloudy2/fix/sprint-hit-slowdown
lan_e2e: sprint-hit-slowdown runs up instead of teleporting, and catches a missing slowdown
2026-11-08 00:38:43 +01:00
Kacper 0b8f18dd40 Merge pull request #11992 from TheMrZZ/fix/h275-test-defs-world-packs
Load a world's test instances from its data packs when it opens
2026-11-08 00:38:36 +01:00
Kacper e51c277db7 Merge pull request #11987 from NikitaMGrimm/hdr-hud-composite
HDR float frame: the HUD and menus lay over the world's light
2026-11-08 00:38:29 +01:00
Kacper fc61e4e9ff Merge pull request #11986 from NikitaMGrimm/hdr-entity-marks
HDR: mobs get HDR lighting and held light sources shine again
2026-11-08 00:38:23 +01:00
Kacper 2c300290d5 Merge pull request #11983 from TheMrZZ/fix/h207-effective-equippable
Honor supported pumpkin Equippable overrides on item use
2026-11-08 00:38:10 +01:00
Kacper 16260920c3 Merge pull request #11969 from Mik00000/perf/mob-ai-stress-bench
mobs: reuse pathfinding allocation buffers and add stress_mob_ai benchmark
2026-11-08 00:38:03 +01:00
Kacper d84428c781 Merge pull request #11968 from TheMrZZ/fix/h237-clone-block-ticks
Copy a clone's pending block ticks to its destination
2026-11-08 00:37:57 +01:00
Kacper e5f23f8b40 Merge pull request #11958 from TheMrZZ/fix/h095-allay-liked-player-ttl
Keep an Allay's liked_player memory time to live and expire it
2026-11-08 00:37:50 +01:00
Kacper 17e4124d04 Merge pull request #11956 from Stink-O/native/hold-world
Native mod API: hold the terrain's picture while the world changes (`native_world_hold`)
2026-11-08 00:37:43 +01:00
Horizon agentandClaude Opus 5.5 37eafb6c1b Older clients: a 1.21.1 client's information gains 1.21.2's particle status
The reverse step for 1.21.1 clients passed ClientInformation through, so a
host read past its end. The older-server adapter now writes 1.21.1's real
layout (no particle status), which made lan_bot's
older_clients_of_each_release_join_and_are_sent_chunks fail at 1.21.1;
real 1.21.1 clients were refused the same way. The step appends
ParticleStatus.ALL in configuration and play.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 23:37:41 +00:00
Kacper 4df2329711 Merge pull request #11942 from glitchsnake/fix/11804-block-outline-entity-pick
client/render: omit block outline when an entity is targeted (fixes #11804)
2026-11-08 00:37:36 +01:00
Kacper 7ca3fdeedd Merge pull request #11900 from TheMrZZ/fix/h175-frozen-first-tick-water
Register a newly loaded chunk's tick queues before commands reach it
2026-11-08 00:37:29 +01:00
Kacper e4ca9aba95 Merge pull request #11899 from CocofireHD/fix/8390-some-block-entities-render-too-dark-or-have
Block entities: shade by the face normal so shader packs don't darken them twice
2026-11-08 00:37:22 +01:00
Kacper 9848261b44 Merge pull request #11897 from abit-foggy/codex/studio-b2-camera-path
Studio: free camera and camera paths
2026-11-08 00:37:15 +01:00
Kacper 7e66da154b Merge pull request #11896 from NikitaMGrimm/hdr-hud-layer
HDR float frame: HUD and menus record how much of the world they cover
2026-11-08 00:37:08 +01:00
Kacper 3b03674d14 Merge pull request #11893 from CocofireHD/fix/8875-resource-packs-doesn-t-work-well
Fonts: read a pack's missing font files from the game below the overlay
2026-11-08 00:37:01 +01:00
Kacper 3fe5ad0d19 Merge pull request #11892 from TheMrZZ/fix/h173-live-dungeon-spawner
Create missing live dungeon spawner block entities
2026-11-08 00:36:54 +01:00
Kacper a1926a4901 Merge pull request #11882 from CocofireHD/fix/10228-server-specific-resource-pack-not-loading-pr
HUD fonts: pack a bitmap sheet from any texture path and read it from the atlas
2026-11-08 00:36:47 +01:00
Kacper 8dd178353a Merge pull request #11878 from CocofireHD/fix/10095-crash-when-enabling-a-shader-pack-on-opengl
OpenGL: report the context's texture units and refuse a bad pack instead of crashing
2026-11-08 00:36:40 +01:00
Kacper 0845a33141 Merge pull request #11864 from TheMrZZ/fix/h150-direct-passenger-relations
fix: select direct mob passengers for execute relations
2026-11-08 00:36:33 +01:00
Kacper e05360eb74 Merge pull request #11863 from senjinthedragon/glow-ink-text-tier
HDR and shader packs: glowing sign text shines
2026-11-08 00:36:27 +01:00
Kacper b967051f4f Merge pull request #11861 from Joansitoh/fix/11849-home-tab-strip
fix(launcher): wrap Home's tab strip on narrow windows instead of scrolling it sideways
2026-11-08 00:36:20 +01:00
Kacper 6c723039a7 Merge pull request #11858 from TheMrZZ/fix/h170-place-feature-inline
Place valid inline features with /place feature
2026-11-08 00:36:13 +01:00
Kacper a2c4c2d4fc Merge pull request #11856 from Ragbie/fix/infested-hurt-draw-order
Infested draws at the hurt, before an enderman's or a shulker's own roll
2026-11-08 00:36:06 +01:00
Kacper 3b5b0d8a3c Merge pull request #11850 from GIGABAIT93/l10n/uk-nightly-1107
Ukrainian: the 445 strings uk_ua.json lacked (PvP & HUD, Studio, mods, cosmetics)
2026-11-08 00:35:59 +01:00
Kacper 15ebef6a69 Merge pull request #11847 from TheMrZZ/fix/h204-depleted-absorption
Remove a mob's Absorption effect once the absorption is used up
2026-11-08 00:35:52 +01:00
Kacper 40630164be Merge pull request #11839 from vnsmoreira/packs/polytone-legacy
Polytone: legacy math expressions, color and distance functions
2026-11-08 00:35:45 +01:00
Kacper c6a2fd5291 Merge pull request #11838 from vnsmoreira/packs/polytone-groups-fix
Polytone: leave keys beside the attribute groups alone
2026-11-08 00:35:37 +01:00
Kacper 29d41a899b Merge pull request #11836 from yahya-nusseibeh/fix/remote-view-distance-overflow
Multiplayer: a server's view distance and chunk centre can't overflow
2026-11-08 00:35:31 +01:00
Kacper 61d5d3bd8a Merge pull request #11835 from yahya-nusseibeh/fix/portmap-zero-lifetime
Play with Friends: refuse a port mapping granted no lifetime
2026-11-08 00:35:24 +01:00
Kacper 99bf985e0a Merge pull request #11834 from yahya-nusseibeh/fix/guest-tunnel-token
Play with Friends: the guest's loopback tunnel takes only its own client
2026-11-08 00:35:17 +01:00
Kacper dc098b606b Merge pull request #11833 from yahya-nusseibeh/fix/pack-overlay-prune
Resource packs: delete overlays no selection has used lately
2026-11-08 00:35:11 +01:00
Kacper 51f73ac425 Merge pull request #11832 from yahya-nusseibeh/fix/pack-overlay-budget
Resource packs: cap what one overlay build may unpack
2026-11-08 00:35:04 +01:00
Kacper ac9bd0527a Merge pull request #11827 from TheMrZZ/fix/h168-mob-fall-flying
Keep a mob's FallFlying flag and wear its glider as vanilla does
2026-11-08 00:34:57 +01:00
Kacper 8715111af1 Merge pull request #11825 from vnsmoreira/packs/polytone-expressions
Polytone: the scripting expression language
2026-11-08 00:34:50 +01:00
Kacper 790efa9cb7 Merge pull request #11818 from NikitaMGrimm/hdr-note-hud-first
HDR design note: the HUD's layer and the tone curve before light is written
2026-11-08 00:34:43 +01:00
Kacper ae46423a77 Merge pull request #11795 from Hiuoy/port-pack/renamed-tooltip-anvil
Port pack: RenderTooltipEvent.Color and AnvilRepairEvent under their 26.1 names
2026-11-08 00:34:36 +01:00
Kacper 9da1e6f55f Merge pull request #11794 from doodlyyousuf/251107-fix-entity-air-drag
fix: widen entity air and fluid drags from vanilla floats
2026-11-08 00:34:29 +01:00
Kacper e27e97e58b Merge pull request #11793 from TheMrZZ/fix/h149-local-coordinate-math-current
fix: match exact local command rotation arithmetic
2026-11-08 00:34:22 +01:00
Kacper 02b75ab1f3 Merge pull request #11788 from Teiwazik/render/taa-motion-clip
TAA upscaling smears the world under motion
2026-11-08 00:34:15 +01:00
Kacper 3be40a13a3 Merge pull request #11786 from Teiwazik/render/upscaling-sharpness
Upscaling Sharpness: sharpen the TAA-upscaled world
2026-11-08 00:34:07 +01:00
Kacper c69d499386 Merge pull request #11778 from Teiwazik/fix/framed-maps-slots
Framed maps: no slot taken from a map drawn this frame
2026-11-08 00:33:59 +01:00
Kacper 1a83468f0d Merge pull request #11776 from MSedgeMC/fix/frog-panic-speed
Frogs panic at AnimalPanic's 2.0 speed multiplier
2026-11-08 00:33:51 +01:00
Kacper 1abb582ab4 Merge pull request #11769 from MSedgeMC/port_map/stale-batch-two
port map: a row that survived a rename
2026-11-08 00:33:45 +01:00
Kacper d44210f9b4 Merge pull request #11765 from MSedgeMC/test/enchant-durability
test: compare enchantment durability effects
2026-11-08 00:33:38 +01:00
Kacper 3cefb44bba Merge pull request #11740 from Detimes/fix-11721-explode-radius
Older clients: explosions no longer fail for 1.21.8 and older; real radius for 1.21.1
2026-11-08 00:33:25 +01:00
Kacper 5c28d7f248 Merge pull request #11723 from TheMrZZ/fix/loaded-arrow-shot-game-event
Dispatch ordinary arrow shoot events on their first tick
2026-11-08 00:33:18 +01:00
Kacper b1dae52509 Merge pull request #11711 from MSedgeMC/test/falling-block
test: compare falling block landings
2026-11-08 00:33:11 +01:00
Kacper 341bd0ba5b Merge pull request #11709 from TheMrZZ/fix/connected-guest-attribute-commands
Route attribute commands through the selected guest body
2026-11-08 00:33:04 +01:00
Kacper 6e423b9caa Merge pull request #11701 from Alysia42/random-source
Parity: entity and RandomSource.create() randoms draw from the level's random or are hand-seeded
2026-11-08 00:32:57 +01:00
Kacper 604c3a6654 Merge pull request #11694 from TheMrZZ/fix/h289-mob-equipment-custom-data
fix: match typed custom data in mob equipment predicates
2026-11-08 00:32:51 +01:00
Kacper b229c37e45 Merge pull request #11691 from TheMrZZ/fix/warden-loaded-memory-slots
Bind loaded Warden memories to their runtime slots
2026-11-08 00:32:44 +01:00
Kacper dcb6409ebd Merge pull request #11681 from MSedgeMC/test/nether-portal-shape
test: compare nether portal frame search
2026-11-08 00:32:37 +01:00
Kacper 217b5e97f7 Merge pull request #11672 from TheMrZZ/fix/loot-kill-sheep-state
Bind actual sheep target state in loot-kill commands
2026-11-08 00:32:31 +01:00
Kacper 4fe3ed58b4 Merge pull request #11666 from Raydnde/fix/copper-chest-wax-and-viewers-11378
Waxing a double copper chest reaches both halves; a golem's open chest doesn't weather
2026-11-08 00:32:24 +01:00
Kacper c15fa6c55c Merge pull request #11659 from TheMrZZ/fix/camel-inherited-saved-fields
Save Camel inherited horse fields from modeled state
2026-11-08 00:32:17 +01:00
Kacper d6d3042086 Merge pull request #11656 from TheMrZZ/fix/h212-crafter-drop-name
fix: keep vanilla crafter drops unnamed
2026-11-08 00:32:10 +01:00
Kacper c929d96fa5 Merge pull request #11652 from ilguerro/perf/11126-structure-starts-jigsaw
Read saved jigsaw structure starts instead of regenerating them
2026-11-08 00:32:03 +01:00
Kacper 5555cd8cda Merge pull request #11634 from k3yh/cushion-selectors
Selectors find cushions
2026-11-08 00:31:56 +01:00
Kacper f85f824a27 Merge pull request #11633 from MSedgeMC/fix/old-stats-file
Move an old name-based stats file to the player's uuid
2026-11-08 00:31:49 +01:00
Kacper e1a8d50c79 Merge pull request #11625 from TheMrZZ/fix/loaded-sleeper-bed-position
Refresh loaded sleeper position at the bed on its first tick
2026-11-08 00:31:42 +01:00
Kacper b69935ff31 Merge pull request #11624 from TheMrZZ/fix/h290-pot-template-compounds
fix: save pot sherd string templates as item compounds
2026-11-08 00:31:35 +01:00
Kacper 900ce5e15b Merge pull request #11612 from k3yh/ride-vehicles
/ride takes armor stands, displays, interactions and markers as vehicles
2026-11-08 00:31:28 +01:00
Kacper 7435df4fae Merge pull request #11610 from TheMrZZ/fix/world-pack-enchantment-definitions
Load active world-pack enchantment definitions
2026-11-08 00:31:21 +01:00
Kacper afce5629b7 Merge pull request #11590 from TheMrZZ/fix/h145-required-function-tags
fix: reject function tags with unresolved required references
2026-11-08 00:31:14 +01:00
Kacper 7f22f76867 Merge pull request #11576 from TheMrZZ/fix/folder-datapack-metadata
Require folder data-pack metadata sections before discovery
2026-11-08 00:31:06 +01:00
Kacper 08cb78f385 Merge pull request #11573 from vnsmoreira/attributes/modifiers
Environment attributes: the registry and modifiers, checked against vanilla
2026-11-08 00:30:59 +01:00
Kacper ebaea75be6 Merge pull request #11565 from TheMrZZ/fix/function-command-admission
Reject functions containing invalid ordinary commands
2026-11-08 00:30:52 +01:00
Kacper ad2aabd6cf Merge pull request #11558 from Vaspyyy/perf/vault-players-lazy
Vaults: list the players only when a vault looks for them (−12.6% tick_block_entities)
2026-11-08 00:30:46 +01:00
Kacper 534feb4833 Merge pull request #11544 from voidsipp/fix/11328-trade-modifiers
Fix trade modifiers and potion payment matching
2026-11-08 00:30:39 +01:00
Kacper b7d134d2ef Merge pull request #11540 from voidsipp/fix/11330-explosion-item-health
Spill contents when explosions destroy dropped containers
2026-11-08 00:30:32 +01:00
Kacper 921f2fa118 Merge pull request #11533 from TheMrZZ/fix/loot-mine-executor-presence
Carry actual loot-mine executor presence into block drops
2026-11-08 00:30:25 +01:00
Kacper 0429cd882f Merge pull request #11526 from Vaspyyy/perf/cart-push-loot
Minecarts: keep the loot roll out of the copies a cart push makes (−52% tick_carts)
2026-11-08 00:30:17 +01:00
Kacper b68f5b5327 Merge pull request #11509 from voidsipp/fix/smithing-transform-11307
Fix smithing transform optional inputs and result templates
2026-11-08 00:30:10 +01:00
Kacper d21170f02d Merge pull request #11508 from TheMrZZ/fix/camel-loaded-pose-timestamp
Preserve loaded Camel pose timestamp boundaries
2026-11-08 00:30:03 +01:00
Kacper 6488524356 Merge pull request #11498 from TheMrZZ/fix/effective-item-component-validation
Reject stackable damageable item command inputs
2026-11-08 00:29:56 +01:00
Kacper c9e8e4e8e0 Merge pull request #11497 from voidsipp/fix/11316-stack-tool-mining
Respect stack tool mining rules and actual eye immersion
2026-11-08 00:29:49 +01:00
Kacper b0abfbd196 Merge pull request #11491 from DUzzL/fix/10041-adventure-placed-entities
Adventure mode: honor can_place_on for placed entities and fireworks
2026-11-08 00:29:42 +01:00
Kacper 2c0564db8c Merge pull request #11485 from voidsipp/fix/11317-block-crack-feedback
Match player mining crack stages and broadcast distance
2026-11-08 00:29:34 +01:00
Kacper 41a021365e Merge pull request #11483 from TheMrZZ/fix/llama-saved-temper
Preserve signed equine temper across load and save
2026-11-08 00:29:28 +01:00
Kacper ef36a05d90 Merge pull request #11480 from brasga-a/fix/11314-player-food-predicate
Player predicates test food level and saturation
2026-11-08 00:29:21 +01:00
Kacper ab4a0fd56e Merge pull request #11478 from DUzzL/fix/11379-copper-golem-name
Keep the copper golem's name on its statue
2026-11-08 00:29:14 +01:00
Kacper 0e12cc64de Merge pull request #11474 from voidsipp/fix/11319-gateway-item-motion
Preserve item motion through End gateways
2026-11-08 00:29:07 +01:00
Kacper 4a6da129ce Merge pull request #11467 from voidsipp/fix/round5-5-11321
Respect world borders for pistons and natural spawning
2026-11-08 00:29:00 +01:00
Kacper 29f5ef342c Merge pull request #11466 from MSedgeMC/fix/region-external
Region: a chunk's external stream reads even when its stub length disagrees or its .mcc is missing
2026-11-08 00:28:53 +01:00
Kacper 672c9a7974 Merge pull request #11465 from TheMrZZ/fix/h097-loaded-air-first-tick
Discard loaded air falling blocks on their first tick
2026-11-08 00:28:46 +01:00
Kacper 3d275f03bf Merge pull request #11459 from DUzzL/fix/11387-beacon-effect-overlap
Beacon: select players by bounding-box overlap
2026-11-08 00:28:38 +01:00
Kacper 1dd7423d00 Merge pull request #11457 from voidsipp/fix/round5-1-11333
Respect Silent on explosions and potion impacts
2026-11-08 00:28:31 +01:00
RINNECODERandClaude Opus 5.5 c56e9312ee Port pack: a mod's own rarity or map colour stays the template's, not a refused mod
NeoForge lets mods add item rarities (Immersive Engineering's masterwork) and some blocks carry a
map colour id past 26.3's 61 (Mekanism's gas blocks: 63). The game refuses the whole content
root for either, so one such file left every block of the mod out. They now keep the template's
and the report names them.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 17:28:23 -06:00
Kacper 9c0c0c7210 Merge pull request #11456 from MrCloudy2/fix/dropper-container-entities
Droppers insert into chest boats and container minecarts
2026-11-08 00:28:23 +01:00
Kacper 671a85a7f2 Merge pull request #11455 from NikitaMGrimm/frame-identity-no-fall
Frame identity: keep the player on a block while the scenes are built
2026-11-08 00:28:15 +01:00
Kacper 63a8290dcb Merge pull request #11445 from MSedgeMC/test/furnace-tick
furnace: direct block-entity tick parity (burn table, XP, mid-cook reset)
2026-11-08 00:28:08 +01:00
Kacper fbd9b2ce23 Merge pull request #11434 from DUzzL/fix/11306-anvil-grindstone
Read stack names and durability components in anvil and grindstone
2026-11-08 00:28:00 +01:00
Kacper e290e2c196 Merge pull request #11433 from TheMrZZ/fix/falling-live-data-adoption
Adopt live FallingBlock data updates
2026-11-08 00:27:53 +01:00
Kacper 5671fc9619 Merge pull request #11432 from MSedgeMC/fix/phantom-anchor-save
Save a phantom's circling anchor with its chunk
2026-11-08 00:27:46 +01:00
Kacper 436e839ebf Merge pull request #11425 from TheMrZZ/fix/h051-kill-vehicle-contents
Spill explicit vehicle container contents on kill removal
2026-11-08 00:27:39 +01:00
Kacper 860f3f05d5 Merge pull request #11423 from DUzzL/fix/11311-scaffold-water
Read scaffolding as its stable shape in fluid wall tests
2026-11-08 00:27:33 +01:00
Kacper 88d7c0eb2c Merge pull request #11422 from voidsipp/fix/round5-6-11320
Preserve comparator output and hopper cooldown across saves
2026-11-08 00:27:26 +01:00
Kacper 2cf505bbd7 Merge pull request #11421 from DUzzL/fix/11324-spawn-heightmap
Fix natural spawn height bound off by one
2026-11-08 00:27:19 +01:00
Kacper cd218ea58e Merge pull request #11420 from TheMrZZ/fix/falling-block-state-default
Use modern falling-block state decoding and sand fallback
2026-11-08 00:27:12 +01:00
Kacper 161c4e5677 Merge pull request #11414 from voidsipp/fix/round5-3-11315
Honor block drops and RNG order for mining XP
2026-11-08 00:27:05 +01:00
Kacper a6dc594cdf Merge pull request #11413 from ilguerro/fix/w149
A server player's glide ends on a climbable (as_guest copies fall_flying back)
2026-11-08 00:26:58 +01:00
Kacper 013c3952a7 Merge pull request #11411 from ilguerro/fix/w167
Wait for the client's load after the End credits respawn
2026-11-08 00:26:51 +01:00
Kacper 5365f2be63 Merge pull request #11407 from ilguerro/fix/w157
Drop a converting piglin's inventory like vanilla
2026-11-08 00:26:44 +01:00
Kacper 0f23639844 Merge pull request #11400 from ilguerro/fix/w144
Search neighboring columns for dolphin breathing like vanilla
2026-11-08 00:26:38 +01:00
Kacper 997fe4515d Merge pull request #11390 from UMCEKO/dims/dragon-facts
The dragon fight follows has_ender_dragon_fight (#11282, 5/9)
2026-11-08 00:26:30 +01:00
Kacper 5d16154c0b Merge pull request #11366 from UMCEKO/dims/mob-facts
Mobs and spawn rules read the dimension type (#11282, 4/9)
2026-11-08 00:26:24 +01:00
Kacper 47e562e8b9 Merge pull request #11350 from AlfaRojo/fix/sky-darken-truncates
Sky darkening truncates the light level, as updateSkyBrightness does
2026-11-08 00:26:16 +01:00
Kacper 74b8550421 Merge pull request #11344 from UMCEKO/dims/gameplay-facts
Gameplay reads the dimension type, not the dimension (#11282, 3/9)
2026-11-08 00:26:09 +01:00
RINNECODERandClaude Opus 5.5 9f52bf9a83 Port pack: a block that can't occlude (noOcclusion) carries occludes=false
The oracle reports can_occlude per state, but the carry ignored it: a
full-cube block made with noOcclusion() (one its block entity draws) took
its template's occlusion and hid the faces behind it, leaving a hole.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 17:26:03 -06:00
Kacper 54c3a21b29 Merge pull request #11116 from Delete228/content-context-geometry-upstream
Native content: resolve block geometry by entity context
2026-11-08 00:26:02 +01:00
Kacper d6f0b3042f Merge pull request #11098 from Delete228/native-state-entity-factory-upstream
Native mods: create block entities from the actual state
2026-11-08 00:25:55 +01:00
Kacper 0422dbc79f Merge pull request #10559 from rison1337/perf/cache-trim-preparation
Reuse unchanged armor-trim preparation across frames
2026-11-08 00:25:48 +01:00
Kacper 914c1e9a3b Merge pull request #10552 from ilguerro/perf/9996-surface
Write surface rule runs of one block at once
2026-11-08 00:25:41 +01:00
Kacper 62dd244cb6 Merge pull request #10511 from TheMrZZ/fix/noai-nautilus-air
Keep superclass air refill for NoAI Nautilus
2026-11-08 00:25:34 +01:00
Kacper 0e446e4336 Merge pull request #10242 from TheMrZZ/fix/h297-potion-effect-icon
Save default icon flags in custom potion effects
2026-11-08 00:25:27 +01:00
Kacper 48344d0a6b Merge pull request #10210 from TheMrZZ/fix/h103-trade-reputation-gate
Defer villager trade reputation until its AI phase
2026-11-08 00:25:20 +01:00
Kacper 87d4a87960 Merge pull request #10201 from DUzzL/fix/10147-projectile-catchup-stutter
Unstick arrows and tridents the server sets moving
2026-11-08 00:25:13 +01:00
Kacper 159b5c4be6 Merge pull request #8718 from TheApple05/feature/older-servers-1.21.8
Older servers: experimental native adapters for 1.20.2–1.21.8
2026-11-08 00:25:05 +01:00
RINNECODERandClaude Opus 5.5 cf0b0d02c8 Shader packs: a setting in a comment may end at its line
Packs list their const settings in a block comment one per line with no
';' (`const vec4 colortex6ClearColor = vec4(0.0, 0.0, 0.0, 1.0)`). The
reader looked for the next ';' and took every following line as the value,
so each such setting was refused as 'not a vec4' (Zephyr Starlight logged
85 multi-line notes) and its clear colour was lost. In a comment a
declaration now ends at the first ';' or line end; live code still reads to
the ';'.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 17:20:49 -06:00
Evan-lhmandClaude Opus 5.5 1b93764780 Launcher: a 0 px blur leaves the glass plates sharp (#12062)
With the blur at 0 the plates still read their backdrop bilinearly between
texel centres, and a texel is 3 to 6 logical pixels of pixel art, so the
scene behind the glass stayed slightly soft. With no blur, read the texel
under the point instead.

Fixes #12062

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 00:18:19 +01:00
Horizon agentandClaude Opus 5.5 52aad2205d Older servers: keep the particle markers inside the adapter's connections
The render crate read a trail duration of -1 as the pre-1.21.4 marker, and
the client read particle ids -2/-3 as the old entity-effect providers, for
any server: a 26.3 server sending those values got the legacy behaviour.
The client now decides from the connection's release (received_particle)
and passes an explicit legacy_trail_lifetime option; a current server's
-1 stays a lifetime of -1.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 23:18:13 +00:00
Horizon agentandClaude Opus 5.5 9bc110bae6 Riding: skip the extra rider pass when nothing rides the player
The second position_riders() call after the player moves rebuilt the mob
index every tick for every client; run it only when the riding graph has
something on this player. Also forget passengers whose vehicle is removed,
and add the new field to main's second (studio) Remote initializer.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 23:08:57 +00:00
Ludovic Marie a392d4b3ba Merge pull request #12101 from horizon-rust/fix/anticheat-effect-attributes
🤖 Agent: the client keeps only the server's attribute modifiers, so Speed now ends one tick after RemoveMobEffect as in vanilla (ServerPlayer.doTick runs after sendDirtyEntityData); Paper+GrimAC bench 7-17 Simulation flags at expiry -> 0, 8 selected harnesses pass.
2026-11-08 00:07:47 +01:00
Nikita Maximilian Grimm 83535ddcc1 Merge remote-tracking branch 'origin/main' into minecart-drop-motion 2026-11-07 23:58:46 +01:00
Nikita Maximilian Grimm 2c672eae2d Merge remote-tracking branch 'origin/main' into tnt-piston-ground 2026-11-07 23:58:26 +01:00
sergue212fbrightandOpenAI Codex GPT-6 796047f1e8 Render styled sign text and animate obfuscated glyphs
Follow vanilla 26.3 sign layout and baked glyph geometry, preserving styles through clipping and rebuilding animated sign meshes. Extend the block-entity oracle with styled components and glyph vertices.

Co-Authored-By: OpenAI Codex GPT-6 <[email protected]>
2026-11-07 17:53:48 -05:00
Ludovic Marie 491710cd2f Merge pull request #11880 from horizon-rust/agent/native-build-sign
🤖 Agent: verifies the per-mod-set build signature with its own Ed25519 key (separate from the release key) before the launcher runs a prebuilt binary; independent review found it sound; opt-in stays off until the rollout steps.
2026-11-07 23:53:00 +01:00
Ludovic Marie 0932cefc79 Merge pull request #12055 from horizon-rust/vanilla-split/extras-feature
🤖 Agent: the default build resolves the same features as before (390 third-party units with identical fingerprints); every new gate is cfg!(), so CI's existing --no-default-features clippy already compiles the no-extras side and no stub can drift.
2026-11-07 23:49:40 +01:00
Nikita Maximilian Grimm 2dbcb75cfa Merge remote-tracking branch 'origin/main' into execute-block-nbt 2026-11-07 23:41:15 +01:00
Nikita Maximilian GrimmandClaude Opus 5.5 34f528ffcb Drop a destroyed minecart's item and contents with vanilla's motions
VehicleEntity.destroy spawns the cart item through spawnAtLocation, with an
ItemEntity's own motion (0.2 up, random sideways). A container cart's contents
go through Containers.dropContents: a spot in the cart's block, pieces of 10 to
30 with the level random's scatter and no pickup delay, every slot drawing its
spot, and chestVehicleDestroyed drops the emptied container again. Fixes #12099.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 23:40:28 +01:00
Nikita Maximilian GrimmandClaude Opus 5.5 7f6f26a33a A shulker lid's push sets the pushed entity's on-ground flag the same way
ShulkerBoxBlockEntity.moveCollidedEntities pushes through Entity.move too,
so it ends in the same setOnGroundWithMovement as a piston's push.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 23:30:46 +01:00
Nikita Maximilian Grimm 3d8924dcc8 Merge remote-tracking branch 'origin/main' into tnt-piston-ground 2026-11-07 23:15:42 +01:00
Nikita Maximilian GrimmandClaude Opus 5.5 8ad82f9ac0 A piston push sets an item's or primed TNT's on-ground flag as vanilla does
Entity.move(MoverType.PISTON) ends in setOnGroundWithMovement on a server,
for every push: a sideways push clears the flag, and so does a free push
down. The ArmorStand-only repair in move_pushed becomes a trait method that
items, primed TNT and armor stands share.

Fixes #12029

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 23:15:41 +01:00
Web-mc1 77290ac3c1 telemetry: match the user name exactly when lowercasing moves boundaries
`replace_word` lowercased the text to find the user's name in any case, and
used that copy whenever it had the same byte length as the text. Equal
lengths don't imply equal boundaries: `İ` (U+0130) grows from 2 bytes to 3
and `K` (U+212A) shrinks from 3 to 1, so a text holding both keeps its
length while its offsets move. A match then sliced the original text
mid-character and panicked.

Use the lowercased copy only when the two share their character boundaries.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <[email protected]>
2026-11-08 11:12:29 +13:00
Nikita Maximilian Grimm daa43de927 Merge remote-tracking branch 'origin/main' into boat-drop-motion 2026-11-07 23:10:52 +01:00
Nikita Maximilian Grimm 3fb98b187f Merge remote-tracking branch 'origin/main' into boat-fire-timer 2026-11-07 23:10:51 +01:00
Ludovic Marie 77af961663 Merge pull request #11444 from voidsipp/fix/round5-2-11340
🤖 Agent: A helmet is damaged by falling-block damage before the reduction, as LivingEntity.hurtHelmet does in damages_helmet.
2026-11-07 23:09:40 +01:00
martipopsandClaude Opus 5.5 76db917ded Controller settings: a Nintendo Buttons row (By Position / By Letter)
The Device page gets a row beside Button Glyphs that toggles
PadSettings::nintendo_letters, so players no longer edit
horizon-controller.txt to make A confirm and jump on a Nintendo pad.
Its description says what A and B do in each mode. Always shown, like
the DualSense rows. docs/controllers.md points at the row.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 17:09:01 -05:00
VaspyyyandClaude Opus 5.5 1ba320ca73 Vaults: list the players only when a vault looks for them
with_vault built the list of every player (lan_present: eye heights, effects,
omens) for every loaded vault every tick. VaultBlockEntity.Server.tick reads the
players only when the vault's state may change (stateUpdatingResumesAt, once a
second), so the list is now built at the first ask, from the same game state.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 23:01:12 +01:00
Horizon agent 2f60199b75 Merge remote-tracking branch 'origin/main' into fix/anticheat-effect-attributes 2026-11-07 22:49:45 +01:00
Horizon agentandClaude Opus 5.5 033b4a9e60 Test: the player's base speed is 0.1F
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 22:49:44 +01:00
Ludovic Marie 6a0b9e435b Merge pull request #11549 from horizon-rust/pvp-2/item-view
🤖 Agent: item components read by reference instead of cloned per frame (frame prepare 217→193 µs); earlier parity failures were harness bugs since fixed on main; parity-main covers it after merge.
2026-11-07 22:46:44 +01:00
Ludovic Marie 485226a6fd Merge pull request #11545 from horizon-rust/pvp-2/gui-glint-lookup
🤖 Agent: HUD glint sprite looked up once per item instead of per quad (HUD 84→72 µs); render-only, packet harness 16/16; parity-main covers it after merge.
2026-11-07 22:46:41 +01:00
Web-mc1 37b79d1b63 java_properties: write a NUL and a non-BMP character as Java does
saveConvert pushed one UTF-16 unit at a time and marked a surrogate half with
a NUL sentinel, then ran fix_surrogates over the whole value, which split every
NUL in the result. A \u0000 that is part of a value (legal in a .properties
file) was therefore taken for a marker: "k=\u0000\uD83D\uDE00x" came back from
store with the NUL and both halves of the pair replaced, and since
server.properties is rewritten on every start the value could not survive a
round trip. Walk chars() instead: a Rust str's UTF-16 units always pair up, so
Java's per-char loop needs no marker, and a character past U+FFFF is still
escaped as the two units Java escapes it as.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <[email protected]>
2026-11-08 10:44:43 +13:00
KyttuXDandClaude Haiku 5.5 702b0895bd Draw vanilla terrain flat on Metal at any section count
On Metal, vanilla terrain drew flat only past 1600 listed sections. Cave
culling can drop the listed count below that on a normal view, and the
per-section path then drops cutout leaves on Intel Metal: the canopy above
trunks vanished. Flat draws keep them, so Metal now always draws flat.

Co-Authored-By: Claude Haiku 5.5 <[email protected]>
2026-11-07 16:41:30 -05:00
martipopsandClaude Opus 5.5 4465238c9c Settings shell: build rows once, Down from search to the list, reveal clipped rows
Part of #11487.

- The shell keeps the rows it built and builds them again only when what
  they read changes: the screen's stamp of its settings, a press or slide,
  the search, the page, the size or the language. On Extras, a pad session
  of 727 frames built them twice instead of 751 times.
- On a screen with no page list, Down from the search field goes to the
  first row instead of Done, for the shell's keys and, through a declared
  focus link, for the pad and the keyboard's focus.
- A row the list hides part of scrolls fully into view when the pad or the
  keyboard focuses or presses it (AbstractSelectionList.setSelected), so A
  presses it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 16:35:56 -05:00
Horizon agentandClaude Opus 5.5 e4ce68e271 Client: take effect attribute modifiers from the server only (#12040)
A server's client applied its effects' attribute modifiers itself, so it
slowed down as soon as ClientboundRemoveMobEffectPacket arrived. Vanilla's
server removes a Speed that runs out in ServerPlayer.doTick, after the
tick's sendDirtyEntityData, so the movement speed without the effect's
modifier reaches the client a tick later and vanilla's client runs fast for
that tick. GrimAC's Simulation flagged the Horizon client on that tick
(0.051 off with Speed II, decaying over the next ticks).

LivingEntity.onEffectAdded/onEffectUpdated/onEffectsRemoved change
attributes on a server only; a server's client now keeps the modifiers
ClientboundUpdateAttributesPacket sends.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 22:35:44 +01:00
Ludovic Marie 151aa6c82b Merge pull request #11159 from Ragbie/fix/tnt-fuse-any-number
🤖 Agent: Minecart numeric NBT reads any numeric tag through NumericTag int/short/byte/double values.
2026-11-07 22:35:05 +01:00
Nikita Maximilian Grimm 43943df1ef Merge remote-tracking branch 'origin/main' into boat-drop-motion 2026-11-07 22:29:00 +01:00
KBroneZandClaude Opus 5.5 8906102eb9 chore(launcher): add the found list's string before each table's last entry
Inserting "Found on this computer" before the last entry of each language
table, instead of after it, adds one line per file and leaves the last line
alone, so other pull requests that append strings don't conflict with it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 23:23:19 +02:00
KBroneZandClaude Sonnet 5.5 0a51d6dc5c fix: shadow the TCP keepalive instead of mutating it, so Windows builds are lint-clean
`let mut keepalive` was only assigned under
`#[cfg(not(any(target_os = "windows", target_os = "openbsd")))]`, so on
Windows and OpenBSD clippy reported `unused_mut` and `-D warnings` failed.
Shadow the binding under the same cfg instead; behaviour is unchanged.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-07 23:22:22 +02:00
KBroneZandClaude Opus 5.5 21ca2fdd1d refactor(launcher): drop the found list's caption; check slugs against import
The "Found on this computer" heading says enough on its own, and the caption
cost a string in every language table. The test now checks that each found
instance's slug is the one importing it gives, so the Open button keeps
pointing at the pack an import made.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 23:15:51 +02:00
Nikita Maximilian Grimm 89a515a8d7 Merge remote-tracking branch 'origin/main' into boat-fire-timer 2026-11-07 22:14:37 +01:00
TheApple05andCodex GPT-6 b5406e647a Preserve legacy item normalization across upstream integration
Cover dropped components, pot decorations, instrument holders and nesting limits in the combined codecs; retain cross-platform keepalive settings without lint warnings.

Co-Authored-By: Codex GPT-6 <[email protected]>
2026-11-07 13:14:23 -08:00
KBroneZandClaude Sonnet 5.5 a0f9368edf feat: the launcher plays a profile that only has data packs
Imported.datapacks counts the packs in a profile's home/new-world-datapacks,
`can_play` and `apply_profile` accept a profile with only data packs, and
`outcome` and `modpack list` count them.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-07 23:13:52 +02:00
KBroneZandClaude Sonnet 5.5 1f86e14b9a feat: port data-only mod jars as data packs for a modpack's new worlds
A mod jar with no classes, a pack.mcmeta and a data/ folder is a data pack.
For a 26.2 or 26.3 modpack, `port` now upgrades such jars to 26.3 (the
upgrade-datapack code; 26.3 ones are copied) into the profile's
home/new-world-datapacks/ instead of carrying them as content mods, which
dropped their data/minecraft overrides. The game copies that folder into
each new world it creates.

Before its jar is read, a mod whose Modrinth project also ships as a data
pack counts as data tier, so `fetch` gets the jar; the jar then decides.
The coverage report lists such a jar as partial.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-07 23:13:51 +02:00
SenjinandClaude Opus 5.5 5ec521bb7b Shader packs: Horizon's dynamic lights as uniforms, horizonTier for terrain and entities
horizonDynamicLightCount and horizonDynamicLight0..15 give a pack Horizon
Extras' dynamic lights, nearest first (camera-relative position, light
level). horizonTier gives terrain, entity and hand programs a face's HDR
tier: 0 none, 1 to 4 low to very bright, plus 4 on terrain when the whole
face shines. Opt-in: a pack that declares none of them sees no change.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 22:13:03 +01:00
Nikita Maximilian Grimm 80bbced3fc Merge remote-tracking branch 'origin/main' into fix/diode-moving-piston-support 2026-11-07 22:12:23 +01:00
TheApple05andCodex GPT-6 2d72b0b260 Merge remote-tracking branch 'upstream/main' into codex/viahorizon-upstream
Co-Authored-By: Codex GPT-6 <[email protected]>
2026-11-07 13:12:23 -08:00
Nikita Maximilian GrimmandClaude Opus 5.5 c30b863128 Let water a boat drops into along its move put its fire out
Entity.collidedWithFluid sweeps the box along the whole move, so water reaches
the boat down to the lower of its start and end bottoms in every step.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 22:12:17 +01:00
TheApple05andCodex GPT-6 d79a36101f Merge upstream main and reconcile native protocol adapters
Preserve both older-server and older-client translation paths, consolidate protocol 766 tables, retain verified legacy codecs and resource-pack generations, and reject unsupported reverse protocols.

Co-Authored-By: Codex GPT-6 <[email protected]>
2026-11-07 13:12:16 -08:00
KBroneZandClaude Opus 5.5 690d434afa feat(launcher): list other launchers' modpacks on the Modpacks page
The Modpacks page now opens with "Found on this computer": the modpack
instances the CurseForge app, Prism Launcher, MultiMC, the Modrinth App,
ATLauncher and the official launcher keep in their usual folders, read with
`other_launchers::scan` on a thread. Each row shows the app, Minecraft
version and loader, and imports the instance with one click through the
same path as "Import an instance folder…" (a Prism instance from its own
folder, so its `mmc-pack.json` still names the version). A pack already
imported opens instead.

Only instances with a `mods` folder of their own are listed: the official
launcher's profiles that play in its own folder are the player's everyday
game, not a pack. The two new strings are in every language table.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 23:10:54 +02:00
Ludovic Marie 35f3dad207 Merge pull request #11409 from ilguerro/fix/w151
🤖 Agent: Setting a lead holder clears the saved, still unresolved holder, as LeashData.setLeashHolder does.
2026-11-07 22:09:23 +01:00
Nikita Maximilian GrimmandClaude Opus 5.5 f57811dd0d Drop a destroyed boat's item with an item entity's own motion
VehicleEntity.destroy spawns the boat item through spawnAtLocation, so it
leaves with ItemEntity's default motion (0.2 up, random sideways), not
straight up at 0.1. Chest contents keep their own scatter. Fixes #12026.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 22:05:45 +01:00
Nikita Maximilian GrimmandClaude Opus 5.5 73e054f827 Apply a boat's fire block effects in the order its move meets the blocks
Entity.checkInsideBlocks meets the blocks of the box a move starts in before
the ones it reaches, each once, and each step lights before it puts out: a
boat that leaves water for fire in one move ends up burning.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 22:01:24 +01:00
Ludovic Marie 5de5b6fd91 Merge pull request #12046 from horizon-rust/mods/stamp-chunk-tickets
🤖 Agent: stamps the existing chunk_tickets fragments (API 412/413, NATIVE_HOOKS_API 413); tests.test_stamp_native_hooks 10 OK, verify green, and this exact head built into the SMP engine on playhorizon (rtp2), whose /rtp proof passes with no slow tick. The SMP 0.35.1 PR in horizon-mods needs it.
2026-11-07 22:01:20 +01:00
Mik00000 e1434f2ee4 perf(game): reduce dimension swap overhead and cache raycast chunk lookups 2026-11-07 22:58:20 +02:00
Florian ERNSTandCodex 67a39ecaac Project ground after unoccupied server NoAI Mob piston movement
Co-Authored-By: Codex <[email protected]>
2026-11-07 21:56:55 +01:00
KBroneZandClaude Sonnet 5.5 199d9d74f8 fix: run tools/neoforge_oracle.py on Windows
Windows users cannot create symlinks without a privilege (WinError 1314), and NeoForge's
unix_args.txt gives "Illegal char <:>" there. Link the server's libraries folder with a
junction and the mod jars with a hard link, copying when that fails; use win_args.txt on
Windows. Linux and macOS keep the symlinks and unix_args.txt. Adds tests/test_neoforge_oracle.py.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-07 22:55:49 +02:00
Horizon agent ca17c0c5ba Merge remote-tracking branch 'origin/main' into vanilla-split/extras-feature
# Conflicts:
#	crates/horizon-client/src/game.rs
2026-11-07 21:55:27 +01:00
Ludovic Marie 23c3faedc1 Merge pull request #11947 from horizon-rust/docs/license-history-notices
🤖 Agent: notices list the three cases (Iris, OpenJDK math, OpenJDK HashMap); both clean-room rewrites (#11944, #12043) are merged, so the notice now describes main accurately.
2026-11-07 21:51:19 +01:00
TheMrZZandCodex 1005076a59 Route recipe-book placement through actual furnace menus
Reuse the existing machine menu cache, shared placement and authoritative
slot writeback for ordinary PlaceRecipe packets. Preserve inventory/table
placement and the separate Horizon recipe-transfer route.

Fixes #12009

Co-Authored-By: Codex (gpt-6.1-sol) <[email protected]>
2026-11-07 21:49:12 +01:00
Nikita Maximilian Grimm 8189734106 Merge remote-tracking branch 'origin/main' into fix/diode-moving-piston-support 2026-11-07 21:46:30 +01:00
KBroneZandClaude Sonnet 5.5 1f29c88a49 feat: copy a game folder's new-world-datapacks into every new world
A game folder can hold a `new-world-datapacks` folder. Create New World now
seeds its temporary data pack folder from it and selects those packs, and a
world opened without the Create screen (`--world`, the control socket) gets
a copy when it has no level.dat yet. Each world keeps its own copy. Without
the folder nothing changes.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-07 22:46:25 +02:00
Nikita Maximilian GrimmandClaude Opus 5.5 bf0f0dd811 Wear off a driven boat's damage before it burns, and let water it left put it out
AbstractBoat.tick decays the damage before Entity.baseTick burns the boat, also
for a boat its rider's client moves, and the block effects follow the whole
move, so water the boat started in still puts it out.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 21:44:44 +01:00
Ludovic Marie ba3c3feebf Merge pull request #11951 from horizon-rust/split/resource-tables
🤖 Agent: regenerated from main after #11871/#11940/#11961 landed; still a pure move of the three impl blocks (199 lines), structs stay in game.rs.
2026-11-07 21:29:31 +01:00
Ludovic Marie ec298576f6 Merge pull request #12043 from horizon-rust/fix/java-hash-map-clean-room
🤖 Agent: clean-room java_hash_map (CLRS tree, slab); java_hash_map and java_identity_map oracles identical over 1.18M lines plus 3,028 edge fixtures; 11/12 caller harnesses pass (structure_layout lacks oracle data on the desktop).
2026-11-07 21:29:28 +01:00
Ludovic Marie 97c9d1a14f Merge pull request #11944 from horizon-rust/fix/jmath-fdlibm-provenance
🤖 Agent: clean-room jmath; JVM-identical on ~2.5M inputs per function (log/log10/cbrt), 5.9M hypot pairs, 3.05M sin/cos, all 2^32 floats for round_f32; parity-main covers the ~420 dependent harnesses after merge.
2026-11-07 21:29:24 +01:00
Nikita Maximilian GrimmandClaude Opus 5.5 007458a627 A repeater or comparator on a retracting piston stays, as in vanilla
DiodeBlock.canSurviveOn asks isFaceSturdy(level, pos, UP, RIGID) of the
block below, in canSurvive and in updateShape with the neighbour's new
state. A retracting piston base is a moving_piston whose block entity
shape holds the extended base, so its top face is rigid. The diode checks
read the raw state's exported facts instead, saw no support, and broke the
repeater or comparator. Both paths now go through the contextual rigid
support, which takes the state a shape update hands over.

The redstone catalog harness gains fixed lever/piston/diode contraptions
for both piston and both diode kinds (#12019).

Fixes #12019

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 21:29:19 +01:00
Nikita Maximilian Grimm 9c30028167 Merge remote-tracking branch 'origin/main' into execute-block-nbt 2026-11-07 21:28:43 +01:00
Nikita Maximilian GrimmandClaude Opus 5.5 0d49b282c0 Read a block predicate's NBT with the argument's own SNBT grammar
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 21:28:40 +01:00
Florian ErnstandClaude Opus 5.5 099fb70e6a Pick up a sulfur cube with an empty bucket
SulfurCube.mobInteract ends with Bucketable.bucketMobPickup, and a sulfur
cube is picked up with an empty bucket (canBePickedUpWithBucket), not a
water bucket. Horizon's bucket pickup only knew the water bucket's mobs,
so a click with a bucket left the cube where it was.

After the cube's own baby, primed, shears and swallow branches, an empty
bucket now plays the sulfur fill sound, fills a sulfur_cube_bucket, fires
filled_bucket and player_interacted_with_entity, drops the leash, discards
the cube and gives the bucket through ItemUtils.createFilledResult, as the
other bucketable mobs do. The native bucket hook hears it.

The filled bucket carries SulfurCube.saveToBucketTag: the cube's age and
age_locked in bucket_entity_data, and its body block as
sulfur_cube_content when it has one. Other species keep their bucket data.

The command harness gets a mob_interact op: the connected player's real
Interact packet on a mob, on both sides.

Fixes #12044.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 21:26:20 +01:00
Nikita Maximilian GrimmandClaude Opus 5.5 c7aef39dd5 Burn boats: live fire timer, fire damage and both block-effect passes
A boat now keeps Entity.remainingFireTicks: Entity.baseTick burns it down a
tick at a time and hurts it every 20 ticks, and the two applyEffectsFromBlocks
of AbstractBoat.tick light it on fire blocks, hurt it once per fire block,
let water, powder snow and rain put it out and clear the cooldown. Saves,
/data, portals and the root vehicle write the live Fire; loads and /data
merge read it. Fixes #12025.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 21:24:20 +01:00
TheMrZZandCodex gpt-6.1-sol d7c3fefcfa Preserve Nautilus cooldown memory presence and lifetime
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-11-07 21:23:19 +01:00
Horizon agentandClaude Opus 5.5 9099175f8a Move the shared tables impl blocks to game/resource_tables.rs
Pure move (#11857). Private methods and items become pub(super), the same reach they had in game.rs (the game module and its children). Port map rows, source-reading tests and the docs tables follow the code.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 20:22:09 +00:00
Ludovic Marie e1e39361db Merge pull request #11961 from horizon-rust/split/loading-screen
🤖 Agent: pure move of 512 lines (Loading, App, run_event_loop, the start checks) to game/loading_screen.rs; the Windows-only hwnd stays in game.rs and stays visible to the moved code.
2026-11-07 21:20:15 +01:00
Ludovic Marie ef19179033 Merge pull request #11940 from horizon-rust/split/inventory-input
🤖 Agent: pure move of 506 lines (keys and clicks over a container screen) to game/inventory_input.rs; click_inventory stays because an open draft changes it.
2026-11-07 21:20:12 +01:00
Ludovic Marie b189c72fcc Merge pull request #11871 from horizon-rust/split/resource-load
🤖 Agent: pure move of the startup resource load (614 lines) to game/resource_load.rs; load_world_rest stays because #11574 changes it.
2026-11-07 21:20:08 +01:00
Florian ERNSTandCodex 8b48b0d74e test: checkpoint H255 piston ground regression before claim-cap decision
Qualified old-source failure only; no production repair or fixed-pass claim.

Co-Authored-By: Codex <[email protected]>
2026-11-07 21:19:53 +01:00
Horizon agentandClaude Opus 5.5 3a229da6b3 mod API: stamp chunk_tickets (412) and chunk_tickets_in_level (413)
python3 tools/stamp_native_hooks.py --only chunk_tickets chunk_tickets_in_level.
The SMP's /rtp moves from /forceload (which waits on the server thread
until its chunk is generated) to native_force_chunk_in, so it needs a
number to declare.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 21:19:46 +01:00
Horizon agentandClaude Opus 5.5 e22ccc17d2 Notices: add java_hash_map.rs, replaced in #12043
The same check for #6741 found that java_hash_map.rs was translated from
OpenJDK's HashMap.java. List it with the other two, and say so in
how-horizon-is-built.md.

Refs #6741

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 21:17:46 +01:00
Nikita Maximilian Grimm 5664a950e8 Merge remote-tracking branch 'origin/main' into execute-block-nbt 2026-11-07 21:17:10 +01:00
Nikita Maximilian GrimmandClaude Opus 5.5 538422f3c7 Match the NBT part of a block predicate against the block entity
execute if|unless block, fill ... replace and clone ... filtered tested only
the block state; BlockPredicateArgument also compares the {...} part with the
block entity's full saved data (NbtUtils.compareNbt), and a block without a
block entity fails it.

Fixes #12030

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 21:17:09 +01:00
Ludovic Marie 493dd75acf Merge pull request #12007 from horizon-rust/split/play-keys
🤖 Agent: pure move of key_pressed (290 lines) to game/play_keys.rs; AGENTS.md's Input row now names play_keys.rs for it.
2026-11-07 21:13:55 +01:00
Horizon agentandClaude Opus 5.5 f4481233b0 Move the loading screen and the pre-game App out of game.rs into game/loading_screen.rs
Pure move (#11857). Private methods and items become pub(super), the same reach they had in game.rs (the game module and its children). Port map rows, source-reading tests and the docs tables follow the code.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 20:13:07 +00:00
Ludovic Marie 63cbec701e Merge pull request #12036 from horizon-rust/split/cauldrons
🤖 Agent: pure move of cauldron_outcome and use_on_cauldron (96 lines) to game/cauldrons.rs; cauldron_at and cauldron_extinguish stay because an open draft changes them.
2026-11-07 21:06:30 +01:00
Ludovic Marie a597387a66 Merge pull request #12035 from horizon-rust/split/key-codes
🤖 Agent: pure move of 117 lines (GLFW key/button codes, follow_named_keymap, the key routing rules) to game/key_codes.rs; game.rs re-imports the names other game modules reach through super::.
2026-11-07 21:06:26 +01:00
Nikita Maximilian GrimmandClaude Opus 5.5 80e3532c4a docs: HDR design note: step 4 writes light a whole frame at a time
A blended draw over a surface of the other kind leaves one mark for both, so a frame that mixes
marked and written light would clip the written light and lift it again, or leave the marked part
unlifted. Step 4 now goes path by path: the frame-case rebuild first, then every marking shader on
the default path in one pull request, then Improved Transparency and the visibility buffer.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 21:06:12 +01:00
Horizon agentandClaude Opus 5.5 2d306dc52c Move container screen key and mouse input out of game.rs into game/inventory_input.rs
Pure move (#11857). Private methods and items become pub(super), the same reach they had in game.rs (the game module and its children). Port map rows, source-reading tests and the docs tables follow the code.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 20:01:15 +00:00
Florian ErnstandClaude Opus 5.5 b528247824 Merge origin/main into fix/h275-test-defs-world-packs
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 20:59:27 +01:00
Horizon agentandClaude Opus 5.5 4f6be12c2e Move the in-world key press handler out of game.rs into game/play_keys.rs
Pure move (#11857). Private methods and items become pub(super), the same reach they had in game.rs (the game module and its children). Port map rows, source-reading tests and the docs tables follow the code.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 19:54:16 +00:00
Ludovic Marie 8c93f81143 Merge pull request #12015 from horizon-rust/split/core-tests
🤖 Agent: moves 575 lines of game.rs's inline tests to game/core_tests.rs with unchanged bodies; the horizon-client test count stays 1159, and the tests open PRs touch stay put.
2026-11-07 20:51:01 +01:00
Ludovic Marie a20e9ce2d0 Merge pull request #12013 from horizon-rust/split/player-spawn
🤖 Agent: pure move of spawn_player (117 lines) to game/player_spawn.rs; test_world_input_capture's loading-completion check reads it from the new file and still sees the player set before the mouse capture.
2026-11-07 20:50:57 +01:00
Horizon agentandClaude Opus 5.5 aba1a90e6f java_hash_map: clean-room rewrite (#6741)
The previous implementation was translated from OpenJDK's
java/util/HashMap.java (GPL-2.0), which Horizon's MIT licence cannot
carry. This one was written in a two-person clean room: the implementer
worked from a behaviour spec, the Java SE API documentation, CLRS
chapter 13 (red-black trees) and runs on a real JVM, and never read
OpenJDK's source or the old file.

Entries live in a slab of nodes addressed by u32 index. A bucket holds
the first node of its bin's list and, for a tree bin, the root of a
red-black tree over the same nodes, whose links sit in a side table the
map allocates with its first tree bin. The public API is unchanged.

Unit tests pin iteration orders printed by java.util.HashMap on OpenJDK
25: a doubling from 16 to 32 buckets, a tree bin made at 64 buckets
after two doublings below 64, a tree-bin insertion after its tree
parent, movable and cursor removals from a tree bin, copies, and the
cursor's error order.

Refs #6741

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 20:50:56 +01:00
Ludovic Marie 73a3c0348d Merge pull request #11995 from horizon-rust/split/item-actions
🤖 Agent: pure move of 190 lines (startUseItem, interactOn, the placement/bucket cancel hooks) to game/item_actions.rs; the Player actions docs rows now point fn use_item and fn attack at their files.
2026-11-07 20:50:54 +01:00
MSedgeMC e8da3a8e21 A drowned stops the trident attack when it loses the trident
`DrownedTridentAttackGoal` overrides `canUse` with the trident test and
inherits `RangedAttackGoal.canContinueToUse`, which calls `canUse()` again
(and falls back to a target it is still walking towards). Horizon's
`DrownedTridentAttack::can_continue` asked only the base ranged goal, so a
drowned whose trident was gone kept the trident attack running once its
path was finished.
2026-11-07 19:42:47 +00:00
Horizon agentandClaude Opus 5.5 c95f24bc32 Move using an item on a cauldron out of game.rs into game/cauldrons.rs
Pure move (#11857). Private methods and items become pub(super), the same reach they had in game.rs (the game module and its children). Port map rows, source-reading tests and the docs tables follow the code.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 19:41:54 +00:00
Horizon agentandClaude Opus 5.5 ca0b945857 Move the GLFW key and button code helpers out of game.rs into game/key_codes.rs
Pure move (#11857). Private methods and items become pub(super), the same reach they had in game.rs (the game module and its children). Port map rows, source-reading tests and the docs tables follow the code.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 19:41:00 +00:00
Mateusz KorniakandClaude Opus 5.5 3004b64927 Fix speckled players with Improved Transparency on Metal
fs_oit_cull writes the nearest opaque see-through depth into the depth
buffer, and the later OIT stages test GreaterEqual against it, so an opaque
fragment is compared with its own depth. On Metal, position.z can differ from
the rasterizer's depth by an ulp, which dropped random pixels of players and
mannequins (horizontal dashes, text behind showing through). Write the depth
4 ulps farther: the fragment still passes, anything visibly behind is culled.

Checked on macOS (Metal, Apple M1) with a mannequin and a text display; not
yet on Linux (Vulkan).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 20:38:40 +01:00
Ludovic Marie 33be2a611a Merge pull request #12004 from horizon-rust/split/autosave
🤖 Agent: pure move of 90 lines (autosave interval, tick-rate change, player autosaves, level.dat metadata) to game/autosave.rs; save_game_with and save_level stay because open PRs change them.
2026-11-07 20:37:30 +01:00
Ludovic Marie 1199c117f2 Merge pull request #12003 from horizon-rust/split/render-settings
🤖 Agent: pure move of 109 lines (renderer_for, apply_settings, translucent layers, far terrain distances) to game/render_settings.rs; prepare_lod and all_sections_changed stay because an open draft changes them.
2026-11-07 20:37:27 +01:00
Ludovic Marie 2e749948f4 Merge pull request #11979 from horizon-rust/split/world-session
🤖 Agent: pure move of 234 lines (title actions, open_world, load_world, save_for_exit, close_world) to game/world_session.rs; finish_open_world and finish_close_world stay because an open draft changes them.
2026-11-07 20:37:24 +01:00
Horizon agent f5f96b1be4 Merge remote-tracking branch 'origin/main' into split/core-tests 2026-11-07 19:36:59 +00:00
VaspyyyandClaude Opus 5.5 abdf311e3b Native mod API: muffle the game's sounds for a while (native_muffle_sounds)
After a close blast a mod can now dull and quieten the world's sounds while
its own ringing plays clearly: native_muffle_sounds(amount, lowpass, ticks,
exempt) turns every sound down by `amount` and low-passes it by `lowpass`
(down to 300 Hz), both fading back over `ticks` client ticks and held while
the game is paused. The sound events in `exempt` play through untouched,
those already playing too.

The mixer mixes the muffled sounds into a bus of their own, runs two
one-pole low-pass stages and the duck over it, and adds it to the exempt
ones. The level glides over 20 ms so tick-sized steps don't click. While no
muffle is set or once it has faded, sounds mix straight into the output as
before. The sound engine marks a channel exempt as it starts, and marks the
playing ones again when the list changes.

Helper-only fragment muffle_sounds. Fixes #12006

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 20:34:54 +01:00
MSedgeMC f96e0fba0f A tamed cat stops being tempted
`CatTemptGoal` overrides `canUse` with `!cat.isTame()` but does not
override `canContinueToUse`; `TemptGoal.canContinueToUse` ends with
`return this.canUse()`, so taming a cat mid-tempt ends the goal. Horizon's
`CatTempt::can_continue` ran only the base `Tempt` continue, which never
asked whether the cat was tamed, so a cat tamed while following a fish
kept following it.
2026-11-07 19:32:24 +00:00
Horizon agentandClaude Opus 5.5 522634ca7f Move placing the local player in a world out of game.rs into game/player_spawn.rs
Pure move (#11857). Private methods and items become pub(super), the same reach they had in game.rs (the game module and its children). Port map rows, source-reading tests and the docs tables follow the code.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 19:31:48 +00:00
Horizon agentandClaude Opus 5.5 f7543ccffa Move using an item at nothing or at an entity out of game.rs into game/item_actions.rs
Pure move (#11857). Private methods and items become pub(super), the same reach they had in game.rs (the game module and its children). Port map rows, source-reading tests and the docs tables follow the code.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 19:30:54 +00:00
Horizon agentandClaude Opus 5.5 1fd2f02081 Move most of game.rs's inline tests into game/core_tests.rs
Pure move (#11857): the tests no open PR changes go to their own test module; the ones open PRs touch (the module header, the scaffolding fall test, the integrated save test) and the tests next to them stay. Test bodies are unchanged; rustfmt re-indented them one level.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 19:30:29 +00:00
VaspyyyandClaude Opus 5.5 653a4a99fe Native mods: a game horizon-mixins built has the mod image slot
The HUD atlas reserved `horizon/modimage` (native_mod_image, API 355) only
when the launcher's native-mods.json listed mods, so a bundle applied with
horizon-mixins by hand or by a mod's own script built a game whose
native_mod_image always returned false.

horizon-mixins leaves .horizon-mixins-stage.json at the root of every
workspace it changes, naming each injection's and module's mod. The build
script turns a record that names mods into HORIZON_MIXIN_MODS, and
built_with_native_mods counts it and installed trait mods as well as
native-mods.json. A release or Nightly has none of them and keeps no slot.

Fixes #12005

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 20:26:53 +01:00
Ludovic Marie 8d8c250f48 Merge pull request #11996 from horizon-rust/split/block-particles
🤖 Agent: pure move of 110 lines (block face and particle sprites, honey particles, destroy particles) to game/block_particles.rs; particle_sprite and crack_particle stay because open PRs change them.
2026-11-07 20:24:09 +01:00
Mik00000 65c35da0dc mobs: reuse pathfinding allocation buffers across queries 2026-11-07 21:21:07 +02:00
Ludovic Marie d21bc19b84 Merge pull request #11993 from horizon-rust/split/melee
🤖 Agent: pure move of 186 lines (attack strength, crits, sweeps, Player.attack) to game/melee.rs; melee_damage and attack_mob stay because open PRs change them.
2026-11-07 20:18:27 +01:00
Ludovic Marie 3afc51e740 Merge pull request #11972 from horizon-rust/split/window-handler
🤖 Agent: pure move of the game window's ApplicationHandler (295 lines) to game/window_handler.rs; the input tests read window_event/device_event from the new file first, since Loading and App have handlers with the same names.
2026-11-07 20:18:23 +01:00
Ludovic Marie 17778b9722 Merge pull request #11957 from horizon-rust/split/inventory-slots
🤖 Agent: pure move of 359 lines (slot layout, hit test, item and decoration drawing) to game/inventory_slots.rs; slots/slot_count/draw_inventory stay because open PRs change them.
2026-11-07 20:18:18 +01:00
Horizon agentandClaude Opus 5.5 abaa2a9f68 Put Horizon's extras behind a horizon-extras feature for forks
One Cargo feature, on by default, holds the entry points of Horizon's own
extras so a fork can build the game without them
(`--no-default-features --features audio,controllers`) or delete them:
cosmetics, emotes and the "Horizon player" presence (one gate,
game::cosmetics::enabled), the official and partner servers added to
servers.dat, the Options screen's Horizon Settings and Mods buttons, and
the existing discord, friends and heartbeat features it now turns on.

The default build resolves the same features as before and every gate is
cfg!(), so both sides compile in every build. A build without the extras
names the official build on Credits & Attribution. docs/forking.md lists
the extras, their files and their gates.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 20:18:11 +01:00
Horizon agentandClaude Opus 5.5 d1c1684fb5 Move opening and closing a local world out of game.rs into game/world_session.rs
Pure move (#11857). Private methods and items become pub(super), the same reach they had in game.rs (the game module and its children). Port map rows, source-reading tests and the docs tables follow the code.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 19:17:05 +00:00
MrCloudy2andClaude Opus 5.5 772088790c lan_e2e: sprint-hit-slowdown builds its sprint with a run-up
An absolute /tp zeroes the player's speed, as vanilla's does
(TeleportCommand's relatives carry no DELTA), so the check's teleport
right before the hit left the guest at walking speed and the check
failed on main. The guest now runs 19 ticks from 8 blocks out and
clicks in reach. The slowdown assertion is tightened: the first
post-hit sample must be under 0.75 of the sprint speed and then climb,
which fails with the client's 0.6 cut removed (the old 0.9 bound did not).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 20:14:01 +01:00
Ludovic Marie fdc87aecdb Merge pull request #11978 from horizon-rust/split/using-item
🤖 Agent: pure move of 237 lines (an item in use, tick by tick) to game/using_item.rs; apply_consume_effect and drop_bundle_content stay in game.rs because an open draft changes them.
2026-11-07 20:03:38 +01:00
Florian ErnstandClaude Opus 5.5 ad475b0573 Load a world's test instances from its data packs when it opens
/test only knew the test instances of the game's own data: read_test_def
read options.resources, and the selector ids were the exported built-in
ones, so a test a world's data pack defined was "No matches for
selector". Vanilla 26.3 loads Registries.TEST_INSTANCE from the packs the
world enables when it loads: later packs override earlier ones and the
built-in data, and /reload or /datapack do not change it.

When a world opens (the end of load_world_state), the built-in pack's and
the initially enabled packs' test_instance files are read once, in pack
order, through the existing test instance decoder. They become the
world's test definitions (test_defs) and its minecraft:test_instance ids,
replacing the previous world's. /reload and /datapack leave them as they
are. A game with no world opened reads the game's own data as before.

Fixes #11976

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 19:59:12 +01:00
Horizon agent 52343380b3 Merge remote-tracking branch 'origin/main' into split/melee 2026-11-07 18:58:14 +00:00
Florian ERNSTandCodex aa019cfaa4 Canonicalize two saved adventure predicate forms
Co-Authored-By: Codex <[email protected]>
2026-11-07 19:58:10 +01:00
Horizon agent dd0050e268 Merge remote-tracking branch 'origin/main' into cg65v-10559
# Conflicts:
#	crates/horizon-client/src/game.rs
2026-11-07 19:56:17 +01:00
Nikita Maximilian GrimmandClaude Opus 5.5 9e962f6350 HDR layered frame docs: say what a screenshot shows without promising the tone curve changes it
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 19:55:14 +01:00
Horizon agent e2680a5450 Merge remote-tracking branch 'origin/main' into split/window-handler 2026-11-07 18:53:48 +00:00
Nikita Maximilian GrimmandClaude Opus 5.5 78ea4b6d02 HDR layered frame: drop the coverage debug flag the display pass no longer reads
The coverage view is chosen once, when the shader is wrapped; nothing reads the field after.
Also lets a test fill the coverage with its own f64.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 19:51:26 +01:00
Florian ErnstandClaude Opus 5.5 56a578f0a6 Merge origin/main into fix/h170-place-feature-inline
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 19:50:54 +01:00
Horizon agentandClaude Opus 5.5 5e7d248148 Move the autosave timer and world metadata save out of game.rs into game/autosave.rs
Pure move (#11857). Private methods and items become pub(super), the same reach they had in game.rs (the game module and its children). Port map rows, source-reading tests and the docs tables follow the code.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 18:49:36 +00:00
Horizon agent 67f34e0fdd Merge remote-tracking branch 'origin/main' into split/inventory-slots 2026-11-07 18:49:36 +00:00
Horizon agentandClaude Opus 5.5 17a68d7b97 Move the renderer settings and far terrain distances out of game.rs into game/render_settings.rs
Pure move (#11857). Private methods and items become pub(super), the same reach they had in game.rs (the game module and its children). Port map rows, source-reading tests and the docs tables follow the code.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 18:48:42 +00:00
Horizon agentandClaude Opus 5.5 90b30ee17b Move block particle sprites and destroy particles out of game.rs into game/block_particles.rs
Pure move (#11857). Private methods and items become pub(super), the same reach they had in game.rs (the game module and its children). Port map rows, source-reading tests and the docs tables follow the code.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 18:47:47 +00:00
Ludovic Marie 5578753b58 Merge pull request #11981 from horizon-rust/perf/portal-ticking
🤖 Agent: requeued after merging main: the only conflict was the module list in game.rs (`pointer_capture` from main beside `portal_ticking_tests`), resolved by keeping both; no code change since the checks.
2026-11-07 19:46:09 +01:00
Horizon agentandClaude Opus 5.5 6faf3ef3f3 Move the player's melee attack out of game.rs into game/melee.rs
Pure move (#11857). Private methods and items become pub(super), the same reach they had in game.rs (the game module and its children). Port map rows, source-reading tests and the docs tables follow the code.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 18:45:59 +00:00
Horizon agentandClaude Opus 5.5 ab44611814 Move the game window's ApplicationHandler out of game.rs into game/window_handler.rs
Pure move (#11857). Private methods and items become pub(super), the same reach they had in game.rs (the game module and its children). Port map rows, source-reading tests and the docs tables follow the code.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 18:44:52 +00:00
Horizon agent 8c612bc687 Merge remote-tracking branch 'origin/main' into perf/portal-ticking
# Conflicts:
#	crates/horizon-client/src/game.rs
2026-11-07 19:44:39 +01:00
Ludovic Marie 9debdf26d4 Merge pull request #11982 from horizon-rust/perf/mod-hook-spans
🤖 Agent: queued: spans only, no behaviour change; pr_ready --quick green. Point: the guards wrap the mods' hooks outside every level span, so the SMP's 1-2 s ticks get a name in horizon-report.
2026-11-07 19:43:54 +01:00
Horizon agentandClaude Opus 5.5 27a2eb565e Move container slot layout and drawing out of game.rs into game/inventory_slots.rs
Pure move (#11857). Private methods and items become pub(super), the same reach they had in game.rs (the game module and its children). Port map rows, source-reading tests and the docs tables follow the code.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 18:43:45 +00:00
Florian ErnstandClaude Opus 5.5 596fda5618 Overlap note: check heads in one git call, diff in parallel
The workflow's clone is blobless, so `git cat-file -e` on a head it
does not hold fetches it from the server, one round trip per check.
On busy days the job ran into its 10-minute timeout. Check every head
in one `cat-file --batch-check` with lazy fetching off, and run the
diffs on 8 threads so their blob fetches overlap. A note for #11723
now takes 13 s instead of 170 s; `show` over every open pull request
29 s instead of 785 s, with the same notes.

Closes #11984

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 19:42:34 +01:00
TheMrZZandCodex gpt-6.1-sol 46b257b1d5 fix: honor supported Equippable overrides on item use
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-11-07 19:38:39 +01:00
Ludovic Marie d7ae75b441 Merge pull request #11967 from horizon-rust/split/play-input
🤖 Agent: pure move of 390 lines (toggled keys, mapped keys, hotbar wheel, play_mouse) to game/play_input.rs; the two hook tests that compile mapped_key_event and the wheel branch read the new file.
2026-11-07 19:36:04 +01:00
Ludovic Marie bc017eec9b Merge pull request #11946 from horizon-rust/split/screen-backgrounds
🤖 Agent: pure move of 170 lines (title panorama, menu blur, border wall, the screens' render-post hooks) to game/screen_backgrounds.rs; regenerated on main after #11939 since the blocks were adjacent.
2026-11-07 19:36:01 +01:00
Horizon agentandClaude Opus 5.5 693178dc36 Move the startup resource load out of game.rs into game/resource_load.rs
Pure move (#11857). Private methods and items become pub(super), the same reach they had in game.rs (the game module and its children). Port map rows, source-reading tests and the docs tables follow the code.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 18:34:57 +00:00
MSedgeMC 8b0c8e40a6 A trader stops trading when hurt or out of reach
`TradeWithPlayerGoal.canUse` refuses a trader that `wasHurtRecently` or
whose trading player is more than four blocks away, but Horizon's
`TradeWithPlayer::can_use` checked neither. A wandering trader that took a
hit, or whose customer walked off, stayed in the stand-still trade goal
where vanilla ends the trade.
2026-11-07 18:34:07 +00:00
MSedgeMC d980cd810d A happy ghast on a still timeout stops floating
`HappyGhast.HappyGhastFloatGoal` overrides `canUse` with
`!isOnStillTimeout() && super.canUse()`. `FloatGoal` does not override
`canContinueToUse`, so `Goal.canContinueToUse` asks that same overridden
`canUse` on every tick: a player standing on the ghast makes it stop
floating. Horizon's `HappyGhastFloat::can_continue` re-ran only
`Float.can_use`, so the goal kept hopping in water under a player who was
holding it still.
2026-11-07 18:31:37 +00:00
Nikita Maximilian GrimmandClaude Opus 5.5 196bea6f1a HDR marks: entities, the hand and translucent particles mark at the entity pipeline's depth bias
#7820 gave the entity pipeline a constant depth bias of 128 so entities win the tie with
coplanar terrain. HDR output marks what it draws by drawing the same quads again, alpha only,
GreaterEqual against the depth it left, but without the bias: every mark tested a hair behind
that depth and failed. Mobs lost their HDR lighting, light sources in the hand stopped shining,
and the empty hand only looked right by reading the ground's mark behind it.

The marks after entities, the hand (unmark and mark) and translucent particles now draw at the
same bias. Translucent items, additive draws and the OIT stage follow unbiased pipelines and
keep the unbiased mark.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 19:27:20 +01:00
Nikita Maximilian GrimmandClaude Opus 5.5 cdabdddb6b HDR float frame: the HUD and menus lay over the world's light
On the float HDR frame the last pass guessed which pixels still showed the world
(`shows_world`) and clipped everything else at white, so the world's light dropped to
SDR under the chat's see-through background, a menu's backdrop and the coloured camera
overlays. Where the device records HUD coverage (#11896) the frame is now drawn in
layers, as the HDR note's step 3 specifies:

- After the world and the hand, `Hdr::resolve` turns the frame's marks into light once.
  The camera overlays, post effects and entity outlines draw on light after it.
- `keep_world` becomes a pass: the world clipped to white, alpha 0, into a HUD target in
  the frame's format, which the HUD and menus draw on with their unchanged blends.
- The last pass lays the HUD over the world's light by its coverage, in gamma as the HUD
  blends (H = P - (1 - c) B, then H ui^(1/2.2) + (1 - c) W). UI Brightness scales the
  HUD's own part.
- The menu blur blurs that composite, light included, and makes it the world the menu
  draws over.

Until the tone curve, a screenshot is the HUD target and the world icon the world, both
clipped at white; the -hdr.png is the full picture. The legacy path, the 16-bit frame and
HDR off are unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 19:27:07 +01:00
abit-foggyandCodex GPT-6 e5fe17622c chore(studio): merge main into B2 branch
Co-Authored-By: Codex GPT-6 <[email protected]>
2026-11-07 08:20:11 -10:00
Horizon agentandClaude Opus 5.5 b19634003c Server tick: spans for the mods' tick hooks, the config poll and idle levels
A dedicated server's slow tick lines on the SMP show 1-2 s ticks whose
time is in no span: it is spent outside every level, where only the mods'
ServerTickStart/End hooks, the config poll and the idle-level close run.
Each gets a span, so a slow tick line and horizon-report name it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 18:17:09 +00:00
Florian ErnstandClaude Opus 5.5 78efabcfa7 Copy a clone's pending block ticks to its destination
/clone placed the blocks but not the block ticks pending in its source
box, so a strict clone of fresh sand never fell, while vanilla 26.3's
CloneCommands.clone ends with LevelTicks.copyAreaFrom over the whole
source box.

cmd_clone now calls the existing LevelTicks::copy_area with the source
box and the destination offset, after the placements and block
entities and before the empty-count failure, as vanilla does. The core
copy (the box filter, trigger times, priorities, sub-tick reordering
and dedup) is unchanged. Block ticks only, in the same level.

Fixes #11963

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 19:16:16 +01:00
MSedgeMC 8d783c0ff0 A cube mob keeps still while it is a passenger
`AbstractCubeMob.CubeMobKeepOnJumpingGoal.canUse` is
`!this.cubeMob.isPassenger()`, but Horizon's `CubeKeepJumping::can_use`
returned true unconditionally. A slime or magma cube riding a boat, a
minecart or another mob therefore kept hopping and steering its vehicle
every tick, where vanilla leaves the vehicle alone.
2026-11-07 18:14:53 +00:00
Nikita Maximilian GrimmandClaude Opus 5.5 cbe9ddfa3f HDR marks: entities, the hand and translucent particles mark at the entity pipeline's depth bias
#7820 gave the entity pipeline a constant depth bias of 128 so entities win the tie with
coplanar terrain. HDR output marks what it draws by drawing the same quads again, alpha only,
GreaterEqual against the depth it left, but without the bias: every mark tested a hair behind
that depth and failed. Mobs lost their HDR lighting, light sources in the hand stopped shining,
and the empty hand only looked right by reading the ground's mark behind it.

The marks after entities, the hand (unmark and mark) and translucent particles now draw at the
same bias. Translucent items, additive draws and the OIT stage follow unbiased pipelines and
keep the unbiased mark.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 19:14:43 +01:00
Stink-OandClaude Opus 5.5 749c5405f6 World hold: its import on a line of its own, apart from the shared mod_api list
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 18:13:53 +00:00
Horizon agent 8711aabd75 Merge remote-tracking branch 'origin/main' into split/play-input 2026-11-07 18:09:24 +00:00
Horizon agentandClaude Opus 5.5 76a5bd8a22 Move item use over time out of game.rs into game/using_item.rs
Pure move (#11857). Private methods and items become pub(super), the same reach they had in game.rs (the game module and its children). Port map rows, source-reading tests and the docs tables follow the code.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 18:08:23 +00:00
Horizon agentandClaude Opus 5.5 453d8b89ce Move the menu backgrounds out of game.rs into game/screen_backgrounds.rs
Pure move (#11857). Private methods and items become pub(super), the same reach they had in game.rs (the game module and its children). Port map rows, source-reading tests and the docs tables follow the code.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 18:05:14 +00:00
Ludovic Marie 845132a56b Merge pull request #11945 from horizon-rust/split/loading-window
🤖 Agent: pure move of 415 lines (first renderer, start_game) to game/loading_window.rs; the Windows-only hwnd stays in game.rs, so no cfg'd re-import is needed.
2026-11-07 19:03:20 +01:00
Mik00000 bc8912c13f bench: add stress_mob_ai scene for active mob AI and pathfinding 2026-11-07 20:02:57 +02:00
Horizon agentandClaude Opus 5.5 051e3dd546 Portals: entities past the simulation distance neither cross nor cool down
Entity.handlePortal runs in the entity's own tick, and ServerLevel.tick
ticks only the entities in entity-ticking chunks. handle_portals and
find_portals walked every loaded entity, so a mob in a nether portal past
the simulation distance went through it and its cooldown counted down.
They now skip entities outside entity-ticking chunks and keep the portal
state of the ones waiting there.

Fixes #11960

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 18:01:59 +00:00
Horizon agentandClaude Opus 5.5 ab5dfae985 Move play key and mouse mappings out of game.rs into game/play_input.rs
Pure move (#11857). Private methods and items become pub(super), the same reach they had in game.rs (the game module and its children). Port map rows, source-reading tests and the docs tables follow the code.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 18:01:11 +00:00
Ludovic Marie 19a6e9b81b Merge pull request #11943 from horizon-rust/split/world-entry
🤖 Agent: pure move of the world entry impl block (367 lines) to game/world_entry.rs; the world_items!/world_recipes! macros stay defined before the mod list, so the moved code still sees them.
2026-11-07 18:54:04 +01:00
Ludovic Marie de118feb4e Merge pull request #11948 from horizon-rust/perf/entity-inside-ticking
🤖 Agent: merging #11948 (smp-200). The one risk was the filter cutting block contact for entities that do tick: it keys on the same entity_ticking_area test the mob and item ticks use. The new test shows a zombie inside simulation distance 4 still presses its plate while one past it doesn't (and fails without the change). server_entities, mob_ai, cauldron, raid and dedicated_server harnesses pass locally; CI parity covers the rest.
2026-11-07 18:51:29 +01:00
Horizon agentandClaude Opus 5.5 e508e7919f rustfmt
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 17:49:45 +00:00
Ludovic Marie f222616f7e Merge pull request #11939 from horizon-rust/split/creative-screen
🤖 Agent: pure move of 440 lines (the creative screen) to game/creative_screen.rs; no open PR touches them, visibility stays within the game module.
2026-11-07 18:48:49 +01:00
Ludovic Marie 0f89fd7e00 Merge pull request #11937 from horizon-rust/split/pointer-capture
🤖 Agent: pure move of 324 lines to game/pointer_capture.rs; test_world_input_capture's method() now also finds pub(super) methods, so the capture fixture still compiles the real code.
2026-11-07 18:48:46 +01:00
Horizon agentandClaude Opus 5.5 6889d9366b Notices: point the OpenJDK entry at its replacement, #11944
Refs #6741

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 18:46:18 +01:00
Florian ErnstandClaude Opus 5.5 40ad3a91c9 Keep an Allay's liked_player memory time to live and expire it
An Allay loaded with a finite minecraft:liked_player memory kept it
forever and saved it without its ttl: the loader read only the memory's
presence and value, the save wrote the value, and nothing counted the
memory down.

- The memory's time to live is kept next to the existing liked-player
  state (None for a memory that never expires), read on load and
  written on save as MemorySlot.visit does.
- The Allay's AI step, which NoAI skips like customServerAiStep, ticks
  it as MemorySlot.tick does: it counts down, and is cleared on the
  tick after it reaches 0.
- Giving an item sets the memory without a time to live
  (Brain.setMemory), taking it back erases it, and the /data path that
  keeps the memory keeps its time to live too.

Fixes #11936

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 18:42:50 +01:00
abit-foggyandCodex GPT-6 114b09babe feat(studio): autocomplete studio commands
Co-Authored-By: Codex GPT-6 <[email protected]>
2026-11-07 07:37:39 -10:00
Horizon agent 0804e2219a Merge remote-tracking branch 'origin/main' into split/loading-window 2026-11-07 17:35:51 +00:00
Horizon agentandClaude Opus 5.5 1c6a22db1a java_hash_map: remove the implementation translated from OpenJDK
JavaHashMap reproduced java.util.HashMap iteration order by translating
OpenJDK's java/util/HashMap.java (GPL-2.0 with the Classpath Exception):
its tree-bin routines, their signatures and their quirks (#6741). This
keeps the public API and removes the body, which panics with todo!() until
the next commit writes it again from a behaviour description, the Javadoc
and the real JVM's results, by an agent that has not read either source.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 18:34:10 +01:00
LokiandAntigravity 0a85491338 client/render: omit block outline when an entity is targeted (fixes #11804)
Co-Authored-By: Antigravity <[email protected]>
2026-11-08 02:31:51 +09:00
Horizon agent 8a9a6d5e3f Merge remote-tracking branch 'origin/main' into split/world-entry 2026-11-07 17:28:44 +00:00
Ludovic Marie d17e8fa343 Merge pull request #10330 from MSedgeMC/fix/bullet
🤖 Agent: kacpherek's Dir point is fixed (from3DDataValue wrap, Steps unclamped, tested); on top of MSedgeMC's main merges I only moved the no-target doc comment back onto its test; shulker_space and the save harnesses pass on the union of this gate's PRs on main 1d63e0a5a (playhorizon: fmt, clippy -D warnings, client + client-lib tests, 26 harnesses all pass).
2026-11-07 18:28:07 +01:00
Ludovic Marie 6f374959b7 Merge pull request #10288 from MSedgeMC/port_map/seven-dead-citations
🤖 Agent: port_ledger map-check on the four changed TSVs reads 519 rows, no problems, and kacpherek's PackSelectionScreen.Watcher row is restored (rdbtCVS's fix); TSV-only, no game code.
2026-11-07 18:28:03 +01:00
Horizon agentandClaude Opus 5.5 15a8b2a77c jmath: rewrite the fdlibm functions from Sun's C sources (#6741)
jmath.rs held ports of OpenJDK code: fdlibm::{log, log10, cbrt, hypot}
from OpenJDK's FdLibm.java (GPLv2 with the Classpath Exception),
aarch64::{sin, cos} from HotSpot's AArch64 trig intrinsics (GPLv2), and
round_f32 from OpenJDK's Math.round(float). Horizon is MIT.

Rewritten in a two-person clean room from Sun's fdlibm 5.3 C sources, a
behaviour-only description of the AArch64 sin/cos, and the Java SE API
documentation:

- jmath/fdlibm.rs (new, Sun's notice at the top): log, log10, cbrt and
  hypot ported from e_log.c, e_log10.c, s_cbrt.c and e_hypot.c; sin_fma
  and cos_fma, fdlibm's sin and cos (s_sin.c, s_cos.c, k_sin.c, k_cos.c,
  e_rem_pio2.c) with fused multiply-adds, as Math.sin/cos on AArch64.
- jmath.rs: round_f32 from the Java SE specification of
  Math.round(float); the module doc says what each platform computes;
  jmath::fdlibm and jmath::aarch64::{sin, cos} keep their paths.
- geometry/trig.rs: Sun's notice above its fdlibm asin/atan/atan2 ports.
- THIRD_PARTY_NOTICES.md: an fdlibm section.

Bit for bit against JDK 25: StrictMath log, log10, cbrt and hypot on
2.5M, 2.5M and 2.7M inputs and 5.9M pairs; Math.sin and Math.cos on an
Apple M4 on 3.05M arguments, compiled and -Xint; Math.round(float) on
all 2^32 floats.

Refs #6741

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 18:24:45 +01:00
Horizon agent 1206818cde Merge remote-tracking branch 'origin/main' into split/creative-screen 2026-11-07 17:17:00 +00:00
Ludovic Marie 12157a2d67 Merge pull request #11901 from horizon-rust/feat/shop-stand-in
🤖 Agent: the stand-in is painted from the website's own palette and boxes (no Mojang texture); test pins its face pixels and transparent outer layers.
2026-11-07 18:12:33 +01:00
Horizon agentandClaude Opus 5.5 b029b9d04f Docs: list code removed for its license, correct "No Minecraft files"
THIRD_PARTY_NOTICES.md gets a "Code removed for its license" section for
anyone holding an older version: the shader pack code written from Iris's
source (LGPL-3.0, removed in #1561) and the math functions translated from
OpenJDK and HotSpot (GPL-2.0, #6741). In those versions that code is not
covered by Horizon's MIT license.

CONTRIBUTING.md says OpenJDK's own source is GPL too, so it is off limits
like any GPL project; Java's behaviour comes from its documentation, a JVM's
results, or a permissive original such as Sun's fdlibm.

docs/how-horizon-is-built.md no longer says Horizon holds no Minecraft data
and never downloads it: the datafix tables come from vanilla's code, and the
launcher downloads 26.3 from Mojang for a signed-in account that owns it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 18:11:12 +01:00
Horizon agent 9b89ff00ee Merge remote-tracking branch 'origin/main' into split/pointer-capture 2026-11-07 17:10:59 +00:00
Horizon agentandClaude Opus 5.5 2e13076a93 List game/loading_window.rs in the docs
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 17:10:21 +00:00
Horizon agentandClaude Opus 5.5 9974322832 Launcher Shop: dress Horizon's stand-in player when there is no skin
Without a skin the launcher can read (signed out, offline), the Shop's pictures
and its 3D view drew the plain grey figure. They now use the stand-in the
website's shop draws (its standInSkin, original art painted from rows, not
Mojang's Steve or Alex), so both shops show the same player.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 18:09:34 +01:00
Horizon agent b21f0c92f2 wip loading_window 2026-11-07 17:08:53 +00:00
Florian ErnstandClaude Opus 5.5 5f2e3cb14d Register a newly loaded chunk's tick queues before commands reach it
In a frozen level, water a command placed in newly forceloaded chunks
before the next world tick never flowed: the chunks' tick containers
were only registered by tick_blocks (a running world tick) or by
postprocessing, so the water's flow tick found no container and was
dropped. Vanilla registers them when the chunk becomes a full chunk
(LevelChunk.registerTickContainerInLevel), before any command sees it.

update_world now runs the existing sync_tick_chunks once the streamed
chunks are resident and the unloaded ones are saved with their ticks,
so registration comes before the tick's commands and world tick. Only
an authoritative game (not a server's client) registers.

Fixes #11873

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 18:06:05 +01:00
CocofireHDandopencode a61d272adc Block entities: shade by the face normal so packs don't darken them twice
A block entity's faces were baked with `minecraft_mix_light` and their
normal left zero. A shader pack's `gbuffers_block` (or its fallback)
receives the unshaded colour and the face normal from Iris, then applies
its own lighting, so Horizon's baked shade was applied a second time:
chests, shulkers, banners and the rest came out darker than the terrain
around them (#8390).

`draw_part` now emits the tint alone and the face's world normal
(`normal.w` = 127), and the entity shader applies `minecraft_mix_light`
from it, as it already does for mobs. Lights that are not the level's (an
item preview's `Lighting.Entry`, which the entity shader cannot know)
stay baked into the colour with `normal.w` 0, which is the path
`game/item_draw` gives block-entity items in inventories and hands.

The parity harness compares the tint and the face normal; the loom's GUI
preview keeps its baked `ITEMS_FLAT` colour. The chest cache test now
checks the nether flag reaches the shader instead of changing a baked
colour, so the cached geometry serves both dimensions.

Fixes #8390

Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
2026-11-07 18:05:25 +01:00
abit-foggyandCodex GPT-6 d607c2f137 feat(studio): add freecam and camera paths
Co-Authored-By: Codex GPT-6 <[email protected]>
2026-11-07 07:04:37 -10:00
Ludovic Marie 31545199d4 Merge pull request #11894 from horizon-rust/feat/shop-fortnite
🤖 Agent: pictures are drawn on background threads and cached by item hash and screen scale, so the Shop never renders on the UI thread; prices and the checkout call are the same rules as before (two live checkouts cancelled via Paddle).
2026-11-07 18:03:10 +01:00
Delete228andCodex e54dcc07cf Merge Horizon main into the stateful block entity PR
Co-authored-by: Codex <[email protected]>
2026-11-07 20:00:52 +03:00
Horizon agentandClaude Opus 5.5 9a8781468e rustfmt
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 16:56:54 +00:00
Horizon agentandClaude Opus 5.5 1117b98d15 rustfmt
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 16:56:19 +00:00
Horizon agentandClaude Opus 5.5 8fe0e40d68 Block contact only for entities in entity-ticking chunks
Entity.applyEffectsFromBlocks runs in the entity's own tick, and
ServerLevel.tick ticks only entities in entity-ticking chunks, so a mob or
item past the simulation distance presses no plate and is not hurt by a
cactus. tick_blocks swept every loaded entity: 2.5 ms a tick on the SMP,
where ~2,900 of 4,600 overworld mobs sit outside entity-ticking chunks.

Fixes #11881

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 16:55:32 +00:00
Florian ERNSTandCodex 6079903ffc Create missing live dungeon spawner block entities
Carry the already selected mob through an optional creation-only level hook. Preserve existing entries and the generation initializer and RNG order.

Co-Authored-By: Codex <[email protected]>
2026-11-07 17:54:52 +01:00
CocofireHDandopencode 3a7f276eab Fonts: read a pack font file the overlay lacks from the game
A pack that adds font providers over the game's own can leave the overlay without the vanilla font sheets its merged font/default.json names. Every glyph then became the missing box. Read a bitmap sheet, TrueType file or Unihex file the overlay lacks from the game's own root, as vanilla's resource stack keeps the game below every pack. Font definitions stay the overlay's, so a pack filter can still hide one.

Fixes #8875

Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
2026-11-07 17:54:37 +01:00
Horizon agentandClaude Opus 5.5 f68c335eb2 Point the port map and source-reading tests at game/pointer_capture.rs
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 16:53:05 +00:00
Nikita Maximilian GrimmandClaude Opus 5.5 beaad0e79d HUD coverage: drop a pack's colour outputs the HUD has no target for
A pack text shader may declare colour outputs past location 0. The HUD's
pipelines have one target, so they went nowhere, but the coverage wrapper kept
them, and one at location 1 collided with the coverage and dropped a shader
that works today.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 17:50:19 +01:00
Horizon agent 12dad67f8c Merge remote-tracking branch 'origin/main' into feat/shop-fortnite
# Conflicts:
#	launcher/lang/af.json
#	launcher/lang/ar.json
#	launcher/lang/az.json
#	launcher/lang/ba.json
#	launcher/lang/be.json
#	launcher/lang/bg.json
#	launcher/lang/br.json
#	launcher/lang/bs.json
#	launcher/lang/ca.json
#	launcher/lang/cs.json
#	launcher/lang/cv.json
#	launcher/lang/cy.json
#	launcher/lang/da.json
#	launcher/lang/de.json
#	launcher/lang/el.json
#	launcher/lang/eo.json
#	launcher/lang/es.json
#	launcher/lang/et.json
#	launcher/lang/eu.json
#	launcher/lang/fa.json
#	launcher/lang/fi.json
#	launcher/lang/fil.json
#	launcher/lang/fo.json
#	launcher/lang/fr.json
#	launcher/lang/fur.json
#	launcher/lang/fy.json
#	launcher/lang/ga.json
#	launcher/lang/gd.json
#	launcher/lang/gl.json
#	launcher/lang/haw.json
#	launcher/lang/he.json
#	launcher/lang/hi.json
#	launcher/lang/hr.json
#	launcher/lang/hu.json
#	launcher/lang/hy.json
#	launcher/lang/id.json
#	launcher/lang/ig.json
#	launcher/lang/is.json
#	launcher/lang/it.json
#	launcher/lang/ja.json
#	launcher/lang/ka.json
#	launcher/lang/kk.json
#	launcher/lang/kn.json
#	launcher/lang/ko.json
#	launcher/lang/ky.json
#	launcher/lang/la.json
#	launcher/lang/lb.json
#	launcher/lang/li.json
#	launcher/lang/lmo.json
#	launcher/lang/lo.json
#	launcher/lang/lt.json
#	launcher/lang/lv.json
#	launcher/lang/mk.json
#	launcher/lang/mn.json
#	launcher/lang/ms.json
#	launcher/lang/mt.json
#	launcher/lang/nl.json
#	launcher/lang/no.json
#	launcher/lang/oc.json
#	launcher/lang/pl.json
#	launcher/lang/pt-br.json
#	launcher/lang/pt.json
#	launcher/lang/ro.json
#	launcher/lang/ru.json
#	launcher/lang/sah.json
#	launcher/lang/se.json
#	launcher/lang/sk.json
#	launcher/lang/sl.json
#	launcher/lang/so.json
#	launcher/lang/sq.json
#	launcher/lang/sr.json
#	launcher/lang/sv.json
#	launcher/lang/szl.json
#	launcher/lang/ta.json
#	launcher/lang/th.json
#	launcher/lang/tr.json
#	launcher/lang/tt.json
#	launcher/lang/uk.json
#	launcher/lang/ur.json
#	launcher/lang/uz.json
#	launcher/lang/vec.json
#	launcher/lang/vi.json
#	launcher/lang/yi.json
#	launcher/lang/yo.json
#	launcher/lang/zh.json
#	launcher/lang/zu.json
2026-11-07 17:49:15 +01:00
Horizon agentandClaude Opus 5.5 922f00f52e Launcher Shop: a game-style item shop with drawn items
Ludovic (2026-11-07): "In the launcher you just don't see the items." The Shop
now opens on the season pack across the top (its looks drawn side by side, name,
item count, price, Buy), then rows of big tiles: Looks, each collection's items,
the founder set and the Supporter badge. A tile is the item drawn, its name and its
price; it lifts under the pointer. A tile opens it big on the account's own player
in 3D (an emote plays), with a look's items, Buy and Gift.

- Pictures: the Wardrobe's CPU renderer, framed on the item (preview::fitted_view),
  at device pixels supersampled 2x, on background threads, kept by item hashes.
- Buy and Gift are one checkout of that offer through the same web checkout;
  prices still come from the catalogue and the service's set rules.
- Cut: the page's line, the basket and its notes; three new strings (Buy, Gift,
  Looks) in all 86 tables, nine unused ones removed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 17:48:35 +01:00
Horizon agentandClaude Opus 5.5 ab37cfb5aa Merge origin/main into #11497
items.rs tests: keeps this branch's tool-rules test and main's command
loot zero-count test, which both landed at the same place.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 17:48:34 +01:00
Horizon agentandClaude Opus 5.5 92b2977e3b Merge origin/main into #11474
end.rs: keeps this branch's live gateway items; main's only change there
(item_entity_to_nbt_with_items) was inside the saved-item path this
branch removes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 17:46:53 +01:00
Ludovic Marie 1fe21fb861 Merge pull request #11885 from horizon-rust/feature/ms-account-picker
🤖 Agent: opens the code page in the default browser's private window (flag per browser, pure-function tests); no Azure change, Safari/unknown get Copy link + hint
2026-11-07 17:45:36 +01:00
Horizon agentandClaude Opus 5.5 68b03b5184 docs: how the launcher adds another Microsoft account (#3786)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 17:43:47 +01:00
Horizon agentandClaude Opus 5.5 200615a30a Move world entry and the world content tables out of game.rs into game/world_entry.rs
Pure move (#11857): defer_world_entry to join_world_rest (the deferred world entry, world creation and rest polling, the world's item/recipe/station tables). Private methods become pub(super), the same reach they had in game.rs.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 16:41:27 +00:00
Horizon agentandClaude Opus 5.5 1ba0db3c3c Move the creative inventory screen out of game.rs into game/creative_screen.rs
Pure move (#11857): the creative screen's tabs, pages, tooltips, trash, search and clicks. Private methods become pub(super), the same reach they had in game.rs.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 16:41:27 +00:00
Horizon agentandClaude Opus 5.5 0ce0e67282 Move the mouse look and pointer grab out of game.rs into game/pointer_capture.rs
Pure move (#11857): look_options to regrab_on_refocus (mouse look, pause when inactive, held keys, the pointer grab and cursor icon). Private methods become pub(super), the same reach they had in game.rs.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 16:41:27 +00:00
Horizon agentandClaude Opus 5.5 00d1e1a46e Launcher: clippy, and the Linux program table only where Linux reads it
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 17:35:45 +01:00
CocofireHDandopencode f178cbe003 HUD atlas: pack a bitmap font's sheet from any texture path
A bitmap provider's file may be any texture, and HUD packs keep sheets outside textures/font. HudAtlas already received the sheet from the font commit, but only if the texture was packed: WALKED_TEXTURE_FOLDERS did not include custom paths such as textures/lumiere/hud, so those sheets still fell back to per-glyph run-time copies. Collect every bitmap provider's file while building the atlas, deduplicated against the walked textures, so its glyphs use the sheet's atlas rect.

Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
2026-11-07 17:35:29 +01:00
CocofireHDandopencode 0478b4deec Fonts: use an atlas sheet outside textures/font for server HUDs
A bitmap provider may name any texture, and server HUD packs keep their glyph sheets under their own namespace or a path such as textures/lumiere/hud. The HUD atlas already holds those textures, but Font only looked up sheets under font/, so every glyph was decoded from disk and copied into the 512-row run-time region one by one. When that region filled, a glyph became the 6-pixel missing glyph, shifting the rest of the HUD line.

Look the sheet up by its atlas name for every namespace and keep every atlas sprite available to the font. Glyphs now use the sheet's atlas rect, so their advances come from the original texels and the run-time region is not involved.

Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
2026-11-07 17:35:29 +01:00
Nikita Maximilian GrimmandClaude Opus 5.5 b5a1d40dc6 HDR coverage view: magenta at paper white, not PQ's 10 000 nits
In PQ output the debug view mixed toward PQ (1, 0, 1), so any covered pixel
blew out. Mix toward BT.709 magenta at the paper white instead, encoded like
the picture.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 17:32:22 +01:00
Horizon agentandClaude Opus 5.5 c794b87841 Signatures for prebuilt native-mod builds (#11859)
horizon-release-sign gets `native_build`: a build's request (target, base commit, each mod's
id, version and registry file SHA-256, sorted) and its key, the release and asset names in
horizon-mods, and the signed message `horizon native build <key> <target>\n<sha256>  <asset>\n`,
which no release or Nightly signature can pass for. The keys are their own
(native-build-keys.txt): horizon-mods' `native-builds` environment holds the private one,
because the registry compiles other people's code there.

tools/ci/sign_native_build.sh signs with OpenSSL as sign_release.sh does, refuses a key whose
public half isn't listed, and checks its own signature; tests sign with it and verify as the
launcher will.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 17:19:02 +01:00
MrCloudy2andClaude Opus 5.5 a61825404a A vehicle's teleports leave its rider's look alone
ClientPacketListener.handleMovePlayer applies a position packet only when
the player is no passenger, and still accepts the teleport. Horizon's
client applied it to a rider too, look included: a map whose datapack
teleports the vehicle every tick (`tp @s ~ ~ ~ 0 0` on a strider) snapped
the rider's view back 20 times a second. A rider now keeps its own
position and look and answers the teleport with them.

lan_e2e gains `rider-look`: a tick function teleports the strider the
guest rides, and the guest's view stays where they turned it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 17:18:26 +01:00
Horizon agentandClaude Opus 5.5 1ee069b6ba Launcher: private-window strings in every language, rustfmt
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 17:18:12 +01:00
Horizon agentandClaude Opus 5.5 c30dfd55e5 Launcher: add another Microsoft account from a private browser window (#3786)
The device code page goes ahead with the account the browser is signed in to.
Open it in the default browser's private window (Windows ProgId, macOS
LaunchServices, Linux xdg-settings), or give the link to copy, or sign the
browser out of Microsoft first. No Azure app change.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 17:15:21 +01:00
Florian ErnstandClaude Opus 5.5 1a9c87059b Merge origin/main into fix/connected-guest-attribute-commands
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 17:15:12 +01:00
Florian ErnstandClaude Opus 5.5 4d8e70da2c Merge origin/main into fix/h297-potion-effect-icon
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 17:14:59 +01:00
Florian ErnstandClaude Opus 5.5 8efbe1c75e Merge origin/main into fix/h051-kill-vehicle-contents
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 17:14:58 +01:00
Florian ErnstandClaude Opus 5.5 df25b701c5 Merge origin/main into fix/loaded-sleeper-bed-position
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 17:14:54 +01:00
TheMrZZandCodex gpt-6.1-sol 3890e57c13 fix: select direct mob passengers for execute relations
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-11-07 17:14:06 +01:00
Florian ErnstandClaude Opus 5.5 2282116773 Place valid inline features with /place feature
/place feature refused every inline feature value with "An unexpected
error occurred", although vanilla 26.3 places it like a named one
(PlaceCommand.placeFeature over a ResourceOrHolderArgument value) and
reports commands.place.feature.success.inline.

The inline value is read with the existing SNBT-to-JSON pair, decoded
by the same feature codec as a named feature (feature_with_blocks,
uncached), and placed through the same Feature.place path with the
level random. A feature that does not place reports
commands.place.feature.failed; a value that does not decode keeps the
plain failure.

Fixes #11843

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 17:10:54 +01:00
Horizon agentandClaude Opus 5.5 0f8cf58043 Launcher: the Wardrobe preview can read a saved shop catalogue
HORIZON_COSMETICS_CATALOGUE=<file of GET /v1/catalogue> gives the development preview
real prices, sets and collections, so Shop screenshots show what players see.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 17:08:41 +01:00
c74ff0070b HDR float frame: every HUD and menu draw writes its coverage of the world
With HORIZON_HDR_FRAME=float, HDR on and no shader pack, on devices with independent blending, each
HUD and menu pipeline gets a variant with a second output into an R8 target: how much of the world
behind it the draw hides (docs/design/hdr-linear-light.md, The HUD and menus). The fragment shader
is wrapped as it is, so the primary colour, its blend and alpha are unchanged, and the coverage
follows the draw's final colour and blend: layering by its alpha, additive draws not at all,
darkening ones by how much they darken, inverting and replacing ones fully. HORIZON_HDR_DEBUG=coverage
tints it magenta in the last pass; nothing else reads it yet. A resource pack text shader without a
colour output is dropped as an unusable one is, instead of panicking.

Co-Authored-By: GPT 6.1 Sol <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 17:05:50 +01:00
JoansitohandClaude Sonnet 5.5 af13271b86 fix(launcher): wrap Home's tab strip on narrow windows instead of scrolling it sideways
Fixes #11849

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-07 16:58:59 +01:00
GIGABAIT93andClaude Opus 5.5 0df4792709 Ukrainian: the 445 strings uk_ua.json lacked (PvP & HUD, Studio, mods, cosmetics)
PvP & HUD, Replay Studio, the music page, Browse Mods, the resource pack
browser, cosmetics and emotes, moderation and tempban, the controller page,
graphics tooltips and recipe transfer. Placeholders, leading and trailing
spaces match en_us; durations use abbreviations (хв, год, дн.) so one form
reads right for every number.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 17:56:20 +02:00
Ludovic Marie 2ebf00a028 Merge pull request #10305 from martipops/controller/guide
🤖 Agent: requeued at 8cd24bc4e5: the merger stopped on a main conflict, and the author's main merge resolves it. verify is green on this head; the run-parity label reruns parity on it before the merge.
2026-11-07 16:56:15 +01:00
Evrim CicekandClaude Opus 5.5 51eae8d435 rustfmt
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 18:50:56 +03:00
CocofireHDandopencode 7142128a7a wgpu-hal GL: report the context's texture units per stage
The GL backend reported MAX_TEXTURE_SLOTS (16, WebGL2's minimum) as max_sampled_textures_per_shader_stage even on desktop GL, where drivers usually offer 32. A native shader pack's sky pipeline layout declares the terrain's 10 sampled textures and the frame's 9, so it was refused at 16 although the driver accepts it (#10095).

Query GL_MAX_VERTEX_TEXTURE_IMAGE_UNITS and GL_MAX_TEXTURE_IMAGE_UNITS, take the lower, and cap at the arrays' new 32 slots (MAX_SAMPLERS likewise). On WebGL2 the query still yields 16.

Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
2026-11-07 16:49:30 +01:00
CocofireHDandopencode 81df2a1864 Renderer: refuse a native shader pack instead of panicking
A layout or pipeline the backend cannot take (a stage's bindings past its sampled-texture limit, say) reaches the device's error scope. A native pack built its frame, stages and passes outside one, so wgpu's validation error hit the default handler and crashed the game (#10095). Build the pack in a scope and return the first error, which the shader pack screen already shows as a refusal.

The test asks for OpenGL's minimum 16 sampled textures per stage and builds a sky-only pack whose layout needs 19: the pack is an error, not a validation panic.

Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
2026-11-07 16:49:30 +01:00
SenjinandClaude Opus 5.5 edb550ac32 HDR and shader packs: glowing sign text shines
Glow ink text was full-bright in the lightmap only: HDR output and shader packs that light
with their own GI (and read the HDR tier, horizonTier) showed it as dark as plain text.
Its glyphs now carry the low HDR tier, like other things that glow.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 16:48:02 +01:00
yahya-nusseibehandClaude Opus 5.5 f2e081520f Resource packs: take the unpack budget without fetch_update
CI's stable Rust deprecates AtomicU64::fetch_update in favour of
try_update, which the workspace's minimum Rust (1.95) doesn't have, so
clippy -D warnings failed. A compare_exchange_weak loop does the same
on every version.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
2026-11-07 17:47:21 +02:00
Ludovic Marie 821426be75 Merge pull request #11812 from horizon-rust/cg61/flame-test
🤖 Agent: test-only: Flame is Ignite(constant(100)) seconds, so 2000 ticks; a_flame_crossbow_shoots_burning_arrows passes with prepared resources on the union of this gate's PRs on main 1d63e0a5a (playhorizon: fmt, clippy -D warnings, client + client-lib tests, 26 harnesses all pass).
2026-11-07 16:42:22 +01:00
Ludovic Marie 7325d8811b Merge pull request #10365 from MSedgeMC/fix/warden-anger
🤖 Agent: a re-angered suspect keeps its saved anger as increaseAnger does (test now expects 54); carries #10290 for the shared corpus; sculk, wolf_anger_writes and the save harnesses pass on the union of this gate's PRs on main 1d63e0a5a (playhorizon: fmt, clippy -D warnings, client + client-lib tests, 26 harnesses all pass).
2026-11-07 16:42:19 +01:00
Ludovic Marie 5bbfe0f311 Merge pull request #10290 from TheMrZZ/fix/h183-spectral-arrow-duration
🤖 Agent: added the hit's Glowing from the arrow's own Duration (SpectralArrow.doPostHurtEffects reads this.duration; Horizon used a fixed 200); carries #10344 since both extend the command corpus; spectral_duration and command_exec pass on the union of this gate's PRs on main 1d63e0a5a (playhorizon: fmt, clippy -D warnings, client + client-lib tests, 26 harnesses all pass).
2026-11-07 16:42:15 +01:00
Ludovic Marie f5c936af5e Merge pull request #10344 from TheMrZZ/fix/h053-item-health-short
🤖 Agent: merged main; the Health doc line now sits on item_entity_from_nbt next to main's numeric-UUID line instead of above the UUID helper; item_health_load and item_numeric_uuid pass on the union of this gate's PRs on main 1d63e0a5a (playhorizon: fmt, clippy -D warnings, client + client-lib tests, 26 harnesses all pass).
2026-11-07 16:42:11 +01:00
Ludovic Marie 0a898f21b0 Merge pull request #10291 from TheMrZZ/fix/saved-empty-entity-tags
🤖 Agent: merged main, keeping main's arrow_owner_tests next to this PR's saved_tag_tests in data.rs; saved_empty_tags, arrow_owner and the explicit-empty ignored test pass on the union of this gate's PRs on main 1d63e0a5a (playhorizon: fmt, clippy -D warnings, client + client-lib tests, 26 harnesses all pass).
2026-11-07 16:42:06 +01:00
Florian ErnstandClaude Opus 5.5 f1ea540f08 Remove a mob's Absorption effect once the absorption is used up
AbsorptionMobEffect.applyEffectTick, asked on every tick, answers false
when the absorption amount is 0, so MobEffectInstance.tickServer drops
the effect before its duration counts down, finite or infinite. A mob's
effect tick had no Absorption case: a used-up Absorption stayed until it
expired. It now ends through the existing removal path; other effects,
infinite ones included, are unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 16:34:19 +01:00
Stink-OandClaude Opus 5.5 aa2220fcf6 Native mod API: hold the terrain's picture while the world changes (native_world_hold)
A client mod asks once a frame (`hold.hold()`, or `hold.hold_within(x, z, radius)` for circles):
sections already on screen that are meshed again are uploaded as ever but wait beside the meshes
drawn, and letting go swaps them all in on one frame with nothing left to upload. Smaller circles
let go of what they leave. In single player, far terrain waits too for the columns the server
changes beyond the view inside the hold, and takes them in when it lets go of them. Bounded: 30 s,
1 GiB of waiting meshes, the arena running out; ended so, it stays off until no mod asks for a
frame. Clearing the sections (another world or dimension) and unloading columns drop what waits;
arena compaction moves waiting meshes too. Renderer::upload keeps its order on the normal path
(before_mesh / upload_mesh / after_mesh). Tests in gfx/hold.rs; docs in native-mods.md; fragment
world_hold.json.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 15:34:09 +00:00
Vinícius MoreiraandClaude Opus 5.5 12dd25d421 Polytone: legacy math expressions, color and distance functions
Older packs write expressions in the legacy syntax: uppercase variables
(AGE, DISTANCE_SQUARED, RAIN...), pi and e, and bare rand(), gaussian()
and noise(). Each variable now runs as the scripting call the wiki maps
it to, and a legacy expression keeps the old argument order of lerp,
smoothstep and color. The color functions (red, color, withAlpha,
lerpColor, hsb, lerpHsb...) follow vanilla's ARGB channel rules, and
dist/distSquare join the math table. All expressions of Visual
Effects+ now compile.

Part of #11486

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 12:25:21 -03:00
Ludovic Marie 6cc93d13ce Merge pull request #11587 from voidsipp/fix/11123-native-hud-mode
🤖 Agent: F1 stays the vanilla two-state toggle and resets the mod's hand override; set_hud_mode is a no-op on servers and during a guest's borrowed turn; hud_mode helper fragment passes tests.test_stamp_native_hooks with main merged in (union of 74 heads on main: fmt, clippy -D warnings, tests green).
2026-11-07 16:23:42 +01:00
Vinícius MoreiraandClaude Opus 5.5 c6c0a97e57 Polytone: the scripting expression language
Expressions such as `o.age() * 0.1`, ternaries, `&&`/`||`, strings,
local variables, `if` blocks and calls on context objects are parsed
once when the pack loads, with every name resolved, so a typo is a load
error. A Host answers the context objects (o, p, c, g, r, v), their
methods and the game's global functions; the math functions follow
Java's Math. 1461 of the 1520 distinct expressions of Visual Effects+
compile; the rest use the legacy uppercase names, which come next.

Part of #11486

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 12:22:21 -03:00
Vinícius MoreiraandClaude Opus 5.5 edab7b313f Polytone: leave keys beside the attribute groups alone
An attributes_modifiers object with a group (`base`...) no longer refuses
the file over a key that is none of the modifier's groups: it is left
alone, so a pack Polytone loads isn't rejected over an entry neither reads.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 12:21:34 -03:00
yahya-nusseibehandClaude Opus 5.5 d94fdae0b9 Agent control: no shared /tmp socket path, and bench refuses bad numbers
Without XDG_RUNTIME_DIR (containers, some WSL and minimal setups) the
control socket was /tmp/horizon-control.sock, a fixed name in a folder
every user shares. Another user who bound it first made --control fail
to start, and horizon-ctl then talked to their socket.

When the temporary directory is shared (world-writable, as /tmp), the
default endpoint is now horizon-<uid>/horizon-control.sock in it. The
game makes that folder 0700, and both the game and horizon-ctl refuse
it unless it is a real folder, owned by this user, that no one else may
enter. A temporary directory of the user's own (macOS's $TMPDIR) is
used as before, so nothing moves there. The launcher's copy of the
default endpoint (game_tools.rs) follows the same rule.

Separately, `bench -1` or `bench nan` panicked the game in
Duration::from_secs_f64; bench now takes 0 to 86400 seconds and refuses
anything else.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
2026-11-07 17:20:50 +02:00
Florian ErnstandClaude Opus 5.5 4924374fa5 Keep a mob's FallFlying flag and wear its glider as vanilla does
A mob loaded or summoned with FallFlying:1b kept the flag even when it
could not glide, and a gliding mob's elytra never wore. The flag was
only the raw tag's value: the loader did not read it, the save only
supplied a default, and the mob tick had no updateFallFlying step.

- mob_from_nbt reads FallFlying into the mob, and mob_to_nbt writes the
  mob's current flag (LivingEntity.read/addAdditionalSaveData).
- The mob's aiStep, with or without AI, runs updateFallFlying while the
  flag is set: canGlide (ground, vehicle, levitation, a glider whose
  next damage does not break it) clears the flag; every 20 glide ticks
  Util.getRandom picks a glider from the mob's own random.
- The game applies that wear with the existing durability path
  (Unbreaking from the level's random) to the mob's kept stack.
- LivingEntity.tick counts fallFlyTicks after aiStep.

Glide travel, checkFallDistanceAccumulation and the ELYTRA_GLIDE game
event are not part of this change.

Fixes #11809

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 16:14:52 +01:00
iseaprojectsandDeepSeek V4.1 Flash d1dd934be2 Spanish: finish the launcher and es_es strings
The launcher's es.json still held 91 English values: the mod-port status sentences, release notices, look-and-feel settings, the SSH hosting panel and several install/update results. Translate them, and fix Back from the body part (Espalda) to Atrás.

es_es.json was 20 keys short of en_us.json: every /studio replay message, Play in Background and the controller-focus note. Add them so Spanish screens no longer fall back to English.

Closes #11814.

Co-Authored-By: DeepSeek V4.1 Flash <[email protected]>
2026-11-07 11:08:01 -04:00
Horizon agentandClaude Opus 5.5 b86fa679b9 function_tags harness: run without --work-dir
tools/parity.py runs every harness with no arguments, so the harness's
required --work-dir made it fail on every CI run before reaching the
oracle. Without the flag it now captures into a fresh
.local/function-tags-parity; an explicit --work-dir must still be new.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 16:05:53 +01:00
Horizon agentandClaude Opus 5.5 9a292609eb Merge origin/main into #11587
hand_geometry: keeps this branch's ui.hands_drawn (native HUD modes) and
main's rule that a studio replay never draws the hands.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 16:04:18 +01:00
Horizon agentandClaude Opus 5.5 45da47c08b Merge origin/main into #11666
game.rs: keeps this branch's wax_on_sound (honeycomb) beside main's
transform_copper_chest_neighbour (axe/hoe/shovel transformers, #33ec06ab12).
test_use_item_on_parity.py: keeps both the honeycomb double-chest cases
and main's block transformer cases.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 16:03:22 +01:00
yahya-nusseibehandClaude Opus 5.5 04449d8b73 Multiplayer: a server's view distance and chunk centre can't overflow
view_distance and chunk_center come straight from Login,
SetChunkCacheRadius and SetChunkCacheCenter, any i32 a server sends.
remote_chunk_in_range added 3 to the radius and subtracted the centre,
and the streaming radius and sent_radius added 1: a server sending
i32::MAX or i32::MIN panicked a debug build, and in release (no
overflow checks) the wrapped range dropped every loaded chunk.

The storage range is now in_storage_range: the same ClientChunkCache
rule (radius at least 2, plus 3), with a saturating add and abs_diff,
so nothing changes for any radius a real server sends. The +1s
saturate too.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
2026-11-07 17:00:32 +02:00
Horizon agentandClaude Opus 5.5 da0b4899ff Merge origin/main into #11480
Keeps both imports in command_exec_corpus.groups(): this branch's
player_food_predicate_corpus and main's treasure_map_copies.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 16:00:14 +01:00
Nikita Maximilian GrimmandClaude Opus 5.5 50b1d6232e docs: HDR design note: marks turned into light once in step 3, tests for mixed light and TAA
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 15:52:07 +01:00
yahya-nusseibehandClaude Opus 5.5 706cf3b3fa Resource packs: delete overlays no selection has used lately
Each distinct pack selection gets its own overlay folder under
.horizon-cache/resourcepacks, holding a full unpacked copy of its zip
packs, and nothing removed old ones. Every server pack, and every new
version of one, left a permanent copy the player can't see: ten
servers with 100 MiB packs kept about a gigabyte.

resource_root now marks the overlay it returns as used by this process
(a shared lock on <overlay>.overlay-cache.use, held until the process
exits, whose time is the last use), then keeps the 8 most recently used
overlays in the folder and deletes the rest. An overlay another
process uses or is building is skipped, so two games sharing a game
folder never pull one out from under each other; the marker goes before
the folder, so a half-deleted overlay is never reused. The lock files
stay, as build() already requires.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
2026-11-07 16:45:03 +02:00
yahya-nusseibehandClaude Opus 5.5 439b609a94 Play with Friends: the guest's loopback tunnel takes only its own client
After the encrypted join, the guest offered the session on a random
127.0.0.1 port for 30 s and handed it to the first loopback connection.
Any other local user or process that found the port first got the
authenticated session to the host's server, and a web page aiming
fetch at local ports could send HTTP into the tunnel and make the
player's own join fail.

serve_loopback now returns a Loopback with the port, a random 16-byte
token and the thread. A connection must send the token within a second
of connecting (compared in constant time); anything else is dropped and
the port keeps waiting until its deadline. The client's join_friend
passes the token on, and run() writes it raw before the handshake.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
2026-11-07 16:42:17 +02:00
Nikita Maximilian GrimmandClaude Opus 5.5 537291b26d docs: HDR design note: the HUD's layer and the tone curve before light is written
Light written into the frame can't be taken back out from under the HUD, and without the tone curve it clips at
the peak, so step 3 now holds the HUD's own layer and the tone curve, and step 4 the shader groups. The look
target names the differences allowed (rounding, light behaving as light under glass, water and anti-aliasing)
and the artifacts that aren't.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 15:41:33 +01:00
MrCloudy2andClaude Opus 5.5 a95c0e022d lan_e2e shield: each player's own game shows the disabled shield's cooldown
The shield check now also asks each player's window what its hotbar
draws over the shield (a new `shield_cooldown_shown` in the control
state: ItemCooldowns' percent for minecraft:shield). Right after the
vindicator's axe disables it the cooldown shows, and after the 100 ticks
it is gone. On main both players' games showed none (#11805).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 15:41:24 +01:00
Horizon agentandClaude Opus 5.5 fd4d709dc2 Merge #10290 into #10365 (both edit the same lines)
Conflicts kept from both sides: crates/horizon-client/src/entity_nbt.rs

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 15:41:13 +01:00
Horizon agentandClaude Opus 5.5 c34114751a Put the no-target doc comment back on its own test
The Dir/Steps test had taken the doc comment of the no-target test; each test now has its own.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 15:41:12 +01:00
Horizon agentandClaude Opus 5.5 dd93b0164a Merge #10290 into #10330 (both edit the same lines)
Conflicts kept from both sides: crates/horizon-client/src/entity_nbt.rs

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 15:41:11 +01:00
Horizon agentandClaude Opus 5.5 ffef7b9c7e Merge #10344 into #10290 (both edit the same lines)
Conflicts kept from both sides: tools/command_exec_corpus.py tools/test_command_exec_parity.py

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 15:41:11 +01:00
Horizon agentandClaude Opus 5.5 7622c02706 Put the no-target doc comment back on its own test
The Dir/Steps test had taken the doc comment of the no-target test; each test now has its own.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 15:41:11 +01:00
Horizon agentandClaude Opus 5.5 744fcd7e01 A spectral arrow gives Glowing for its own Duration
SpectralArrow.doPostHurtEffects builds the effect from this.duration, which this PR now loads and saves; the hit still used a fixed 200.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 15:41:10 +01:00
Horizon agentandClaude Opus 5.5 e760415d7b Merge main into #10290
Conflicts kept from both sides: tools/command_exec_corpus.py tools/test_command_exec_parity.py

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 15:41:10 +01:00
Horizon agentandClaude Opus 5.5 95f1756a8d Merge main into #10344
Conflicts kept from both sides: crates/horizon-client/src/entity_nbt.rs tools/command_exec_corpus.py tools/test_command_exec_parity.py

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 15:41:08 +01:00
Horizon agentandClaude Opus 5.5 e8c56045c9 Merge main into #10291
Conflicts kept from both sides: crates/horizon-client/src/game/commands/data.rs

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 15:41:04 +01:00
Horizon agentandClaude Opus 5.5 5ee3b9da9a Run the NoAI nautilus harness without --binary, as parity.py does
parity.py and parity-main call tools/test_*_parity.py without --binary or --work-dir, so the harness failed in 0 s there. Use harness_args.ensure_work_dir, as a98ba0273 did for the dolphin and spell-ticks harnesses.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 15:41:01 +01:00
Horizon agentandClaude Opus 5.5 307d82bb63 Merge main into #10511
Conflicts kept from both sides: crates/horizon-client/src/mobs/ai.rs

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 15:41:01 +01:00
yahya-nusseibehandClaude Opus 5.5 1e38170d49 Play with Friends: refuse a port mapping granted no lifetime
In PCP and NAT-PMP a lifetime of 0 means the mapping is deleted, but a
success reply carrying 0 was taken as a mapping. Mapping::due() was
then always true, so the host renewed on every 100 ms wake-up of its
loop (a UDP exchange each time, under the reach mutex) while
advertising a direct address that no longer existed.

pcp_map and natpmp_map now refuse a zero lifetime when they asked for
one (a delete, which asks for zero, still expects zero), and due()
never fires sooner than 30 s after the last renewal, so a router that
grants a few seconds isn't asked again ten times a second either.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
2026-11-07 16:36:25 +02:00
yahya-nusseibehandClaude Opus 5.5 b0600cec3c Resource packs: cap what one overlay build may unpack
Applying packs builds an on-disk overlay: every file under assets/ in
every zip pack was inflated (up to 1 GiB each, whole in memory) and
written out, on every worker thread at once. A gigabyte of zeros
deflates to about a megabyte, so a server pack at the 250 MiB download
limit could write some 240 GiB to the player's disk and hold a 1 GiB
buffer per thread, once the player accepted the pack prompt. Vanilla
never unpacks a pack.

Zip::copy_to now streams an entry to its file 64 KiB at a time, taking
each piece from a byte budget shared by the whole build: 16 times the
zips' size on disk, at least 64 MiB, at most 8 GiB. Past it the build
fails with FileTooLarge (so the reload drops the server packs, as for
any failed build) and the half-written overlay folder is removed. An
unreadable entry still costs only that file. Memory per thread is now
the compressed entry plus one piece.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
2026-11-07 16:31:41 +02:00
Evrim CicekandClaude Opus 5.5 c8753a2ea4 Infested's throw in floats as JOML does; the unit test hurts through Mob::damage
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 17:31:04 +03:00
MrCloudy2andClaude Opus 5.5 2f3743d282 An axe disabling a shield sends the defender the shield's cooldown
BlocksAttacks.disable adds the item cooldown (ItemCooldowns.addCooldown
on the shield's group), which ServerItemCooldowns sends a player as
Cooldown(minecraft:shield, 100) and ends with 0. Horizon only set its own
shield counter, so a guest's client never heard of it: no cooldown on the
hotbar, and a held right click could show the shield raised while the
server had it down. The disable now also goes through the item cooldowns.

Fixes #11805

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 15:25:58 +01:00
Evrim CicekandClaude Opus 5.5 3fc5044868 Infested draws at the hurt, before an enderman's or a shulker's own roll
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 17:21:38 +03:00
Evrim CicekandClaude Opus 5.5 e44fc5af01 mob AI parity: an effect flag, an infested enderman in the rain and a poisoned shulker
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 17:21:38 +03:00
Ludovic Marie 1d63e0a5a3 Merge pull request #11640 from masa-masa-0314/feat/realms-service-fixes
🤖 Agent: Realms subscription URL (subscriptions/$WORLD_ID), JSON-bool settings, InviteCodeList array/result parsing and the JSON Content-Type now match the 26.3 RealmsClient; same Microsoft session, no other auth (Mojang rules OK); footer buttons wired to the selected code. (union of 74 heads on main: fmt, clippy -D warnings, tests green).
2026-11-07 15:20:47 +01:00
martipopsandClaude Opus 5.5 8cd24bc4e5 Merge main into controller/guide
Creative draws main's mod browser, then the pad's menu guide over it, as
the inventory does.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 09:15:10 -05:00
Teiwazik 1eb3b7f6c3 Merge remote-tracking branch 'origin/main' into tmp-seat/follow-plugin-seat-chains 2026-11-07 15:12:38 +01:00
TeiwazikandClaude Opus 5.5 43e106cece Upscaling Sharpness: English texts
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 15:08:56 +01:00
umcekoandClaude Opus 5.5 1c8113ce32 Merge origin/main into #11344
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 17:05:06 +03:00
TeiwazikandClaude Opus 5.5 4b5b667560 Upscaling Sharpness: German and Russian texts
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 14:54:09 +01:00
HiuoyandClaude Opus 5.5 f898655ae6 Port pack: RenderTooltipEvent.Color and AnvilRepairEvent under their 26.1 names
NeoForge 26.1 has neither. A tooltip is drawn from a style's textures, picked
in RenderTooltipEvent.Texture, and the anvil take is AnvilCraftEvent, whose
.Pre comes before the inputs are used and the anvil is damaged (no break
chance). `events` now looks the 1.21.1 names up under those, so ATM10's 6
Color and 4 AnvilRepair users are answered by the hooks that exist.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 17:54:03 +04:00
73fa431ac5 fix: widen entity air and fluid drags from vanilla floats
Vanilla Entity.getAirDrag is 0.98F. PrimedTnt, FallingBlockEntity and
ItemEntity scaled motion by the f64 0.98, so after four ticks a TNT's
Motion x was 0.092236816 instead of 0.09223682318…. Item underwater
and lava drags, LlamaSpit air drag, and hurting-projectile inertia
were the same class of float-widened constants.

Fixes #6814

Co-Authored-By: OpenCode <[email protected]>
Co-authored-by: monkeycode-ai <[email protected]>
Co-authored-by: monkeycode-ai <[email protected]>
2026-11-07 13:53:30 +00:00
Ludovic Marie c78187dfff Merge pull request #11763 from TiggerTakkyTTT/codex/launcher-latest-version
Launcher: make the latest Nightly obvious
2026-11-07 14:49:32 +01:00
TheMrZZandCodex gpt-6.1-sol e135804a52 fix: match exact local command rotation arithmetic
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-11-07 14:46:33 +01:00
MrCloudy2andClaude Opus 5.5 36add718c0 Spears stab other players: the charge and the jab reach players on a server
KineticWeapon.damageEntities and PiercingWeapon.attack hit every entity
along the view, players included (PiercingWeapon.canHitEntity:
canHarmPlayer, not the same vehicle). Horizon's stab targets were the
mobs only. A guest's spear now also reaches the other players of its
level; each stab is worked out as the attacker in their turn and lands
through the PvP path once the hub is back, with the spear's damage type,
the 0.4 push and the weapon's Knockback, the dismount, thorns and the
kill score, then the attacker's wear, stats, exhaustion and hit feedback.

The kill score and thorns of pvp_land move into pvp_aftermath, shared by
both. lan_e2e gains a `spear` check: a jab and a sprinting charge hurt
the host, and with the pvp game rule off neither does.

Fixes #11790

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 14:42:20 +01:00
MSedgeMC 077164ff2f port_map: the camel RandomSitting row no longer records the pose gap 2026-11-07 13:39:44 +00:00
MSedgeMC c953fe79cb Test that a sitting camel cannot stand under a two-block ceiling
The test builds a world with a stone ceiling cell inside the standing box
but above the sitting box, then asks RandomSitting, the arm the goal
selector calls: 200 asks must all be refused. A second camel with open sky
fires within the same 200, so the refusal is the pose check and not the
state setup. Dropping the can_change_pose conjunct fails the first loop.
2026-11-07 13:39:44 +00:00
MSedgeMC 92282c7476 A sitting camel stands up only when its standing box fits
`CamelAi.RandomSitting.checkExtraStartConditions` ends with
`body.canCamelChangePose()`, which is
`wouldNotSuffocateAtTargetPose(isCamelSitting() ? STANDING : SITTING)`.
RandomSitting did not ask, so a camel sitting under a ceiling that blocks
its 2.375 tall standing box (its sitting pose is 0.945 tall) stood up into
it. The `can_change_pose` helper already existed for the elastic leash pull.
2026-11-07 13:39:43 +00:00
TeiwazikandClaude Opus 5.5 6689a656a0 TAA: clip history to the neighbourhood spread, weigh new frames more under motion
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 14:39:08 +01:00
TeiwazikandClaude Opus 5.5 fc64f0ee27 Upscaling Sharpness: CAS sharpen after TAA upscaling
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 14:38:31 +01:00
Ludovic Marie fc2868b2b2 Merge pull request #11773 from horizon-rust/agent/recipes-11443-main-merge
🤖 Agent: #11443 by GabrielNichols with main merged (its fork refuses pushes): the Recipes panel keeps its editable search and query when a recipe returns to an empty result, and opens in Creative inventory. The only resolution is THIRD_PARTY_NOTICES, where the pinned JEI paragraph now names the GUI sprites under the MIT LICENSE.txt shipped beside them. Verify and launcher green; clippy -D warnings OK.
2026-11-07 14:37:29 +01:00
TeiwazikandClaude Sonnet 5.5 052c2eb962 Native shader packs: sections fade in toward the fog colour like vanilla terrain
The fade-in rides the high 16 bits of the flat hdr_code varying (no new
location); solid and translucent colours mix toward the fog colour after
the pack's material, alpha kept.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-07 14:35:56 +01:00
TeiwazikandClaude Sonnet 5.5 4a20af007c Framed maps: no slot taken from a map drawn this frame
The atlas holds 256 map pictures. Every framed map in range claimed one, seen or not, so past 256 the maps took slots from each other within one frame and showed each other's pictures. Now a frame's outline is culled before its map claims a slot, the nearest claim first, and a slot shown this frame is never given away: past the slots, the far maps go undrawn.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-07 14:33:28 +01:00
MSedgeMC aadec58092 port_map: the FrogAi.initCoreActivity panic gap is closed 2026-11-07 13:32:24 +00:00
MSedgeMC a90c5f8179 Test the panic speed a hurt frog's move control reads
Drives the real goal selector on a built test world: hurt the frog with a
PANIC_CAUSES damage, tick the goals once, and assert the panic goal started
and left vanilla's 2.0 in `Navigation.speed_modifier`, the value
`SmoothSwimmingMoveControl` multiplies. Reverting the constant fails it with
1.25 against 2.0.
2026-11-07 13:32:24 +00:00
MSedgeMC c1e649f61b Frogs panic at AnimalPanic's 2.0 speed multiplier
`FrogAi.initCoreActivity` builds the brain's core activity with
`AnimalPanic(2.0F)`, but the goal list used 1.25.

`PanicGoal.start` hands the multiplier to `PathNavigation.moveTo`, and
`SmoothSwimmingMoveControl.tick` computes
`speedModifier * getAttributeValue(MOVEMENT_SPEED)` and then multiplies by
0.1 on land or 0.02 in water. A frog's movement speed is 1.0, so a fleeing
frog should move 0.2 blocks a tick on land and 0.04 in water; at 1.25 it
moved 0.125 and 0.025, 60% slower.
2026-11-07 13:32:24 +00:00
TeiwazikandClaude Sonnet 5.5 fdc056c8ac Older servers: send sign_update with the side before the lines
26.3 ends sign_update with the side as a VarInt; 26.2 and older read it as a
boolean right after the position. Forwarding the packet unchanged made the
server misread it and kick the player on Done.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-07 14:23:33 +01:00
VaspyyyandClaude Opus 5.5 eff292b1e2 Minecarts: keep the rolled loot out of the copies a cart push makes
push_from_ticked_cart cloned the ticked cart into the list and back each tick,
and with it a chest cart's rolled loot: its whole Items list as NBT. The stored
cart already goes without the loot for the tick (tick_carts_now takes it), so
the copies do too and the loot stays with the ticked cart.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 14:20:35 +01:00
Horizon agent a6b863754b Merge origin/main into PR #11672
Co-Authored-By: Claude Opus 5.5 <[email protected]>

# Conflicts:
#	crates/horizon-client/src/game/commands/terrain_cmds.rs
#	crates/horizon-items/src/items.rs
2026-11-07 14:19:04 +01:00
Horizon agent e414a7a14b Merge origin/main into #11443's series branch
Co-Authored-By: Claude Opus 5.5 <[email protected]>

# Conflicts:
#	THIRD_PARTY_NOTICES.md
2026-11-07 14:17:59 +01:00
Horizon agent 04b8ed64ca Merge origin/main into PR #11625
Co-Authored-By: Claude Opus 5.5 <[email protected]>

# Conflicts:
#	crates/horizon-client/src/mobs/ai.rs
2026-11-07 14:17:57 +01:00
Horizon agent 1c81831b12 Merge origin/main into PR #11533
Co-Authored-By: Claude Opus 5.5 <[email protected]>

# Conflicts:
#	crates/horizon-client/src/game/commands/terrain_cmds.rs
2026-11-07 14:17:31 +01:00
Ludovic Marie 2b8d23af24 Merge pull request #11550 from socksprox/client/feature-gate-online
🤖 Agent: discord, friends and heartbeat become default-on Cargo features (compile gating only, no new network call), so the --no-default-features server build drops 14 crates; main's newer native_client_services invite code is gated too. (union of 74 heads on main: fmt, clippy -D warnings, tests green).
2026-11-07 14:16:40 +01:00
Horizon agent 547c7decbc Merge origin/main into PR #11508
Co-Authored-By: Claude Opus 5.5 <[email protected]>

# Conflicts:
#	crates/horizon-client/src/game/persist.rs
2026-11-07 14:16:30 +01:00
e8a5564059 Cache decoded saved palette entries in the block registry
A saved area resolves the same dozen block states in every section of every column, but decode_column built a format!-ed String key and parsed each entry through BlockState::from_nbt into a per-column HashMap. The registry now memoizes palette entries by their NBT in an FxHashMap keyed by the tag itself, so a repeated entry costs one hash and one comparison and allocates nothing; the per-entry resolution rules move with it unchanged. Loading the 529-column saved area drops Persistent::load from 1,467.8M to 1,284.3M instructions.

Part of #9996.

A first draft by DeepSeek V4.1 Flash, via pi, reviewed and reworked.

Co-Authored-By: DeepSeek V4.1 Flash <[email protected]>
Co-Authored-By: Codex (gpt-6.1-terra) <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 14:15:22 +01:00
70a0a29eec Check packed palette indices one word at a time
Loading a generated area spends 16% of Persistent::load (the worker's
chunk load) in PalettedContainer::from_packed, and nearly all of it is
check_indices: a scalar bounds check of every entry of every section
(~10.6 instructions per entry, 22M entries per 529-column area).

check_indices now rejects a whole word at once. Adding 2^bits - palette
to a word lifts every field by that much, so a field carries out of its
own bits exactly when its index is past the palette; the carry out of
each bit is computed for the whole word with (a & b) | ((a | b) & !sum).
A field can only appear bad where a genuinely bad field precedes it in
the same word (a carry adds one to it), which never turns an accepted
input into a rejected one, and the offending word is rescanned entry by
entry on the error path so the reported index is the same first
offending entry as before. The last word's padding fields are biased by
zero, so they cannot look bad, as the entry-by-entry scan ignored them.
The same set of inputs is accepted: the early return when the palette
holds exactly 2^bits entries was already there and stays.

Part of #9996.

A first draft by DeepSeek V4.1 Flash, via pi, reviewed and reworked.

Co-Authored-By: DeepSeek V4.1 Flash <[email protected]>
Co-Authored-By: Codex (gpt-6.1-terra) <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 14:15:22 +01:00
5cffcf1370 Load saved light directly from its layers instead of expanding and repacking
The saved sky and block nibble layers are already the resident Light::Sky and Light::Block layout. Choose the resident variant from the layers and copy the stored layer when only one kind of light varies, so only sections where both vary still expand their light into a full array. Missing layers still inherit the bottom row above and dimensions without sky light still ignore saved sky layers.

Part of #9996.

A first draft by DeepSeek V4.1 Flash, via pi, reviewed and reworked.

Co-Authored-By: DeepSeek V4.1 Flash <[email protected]>
Co-Authored-By: Codex (gpt-6.1-terra) <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 14:15:22 +01:00
Ludovic Marie 6560641ba5 Merge pull request #11731 from horizon-rust/fix/wool-vibration-drops
🤖 Agent: harness-only. Kills items and corpses between cases (the drop leak between the cow and item cases), and lists server_wool_vibration as known_flaky for the real-time player timing (1 of 4 on a loaded playhorizon).
2026-11-07 14:12:19 +01:00
Ludovic Marie 295a73028d Merge pull request #11771 from horizon-rust/fix/stab-move-hold
🤖 Agent: exempts only STAB (action 8) from #11223's move hold, so a release still shoots from the position before the move (#8423). lunge_motion: parent 3/3 pass, #11223 3/3 fail, this branch 3/3 pass on playhorizon.
2026-11-07 14:12:15 +01:00
Horizon agentandClaude Opus 5.5 a52b9e48f3 Merge #11366 (main merged) into #11390
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 14:10:37 +01:00
Horizon agentandClaude Opus 5.5 21a27ca73e Merge origin/main into #11433 (main builds again after #11767)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 14:10:29 +01:00
Horizon agentandClaude Opus 5.5 17e7655cdf Merge origin/main into #11366 (main builds again after #11767)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 14:10:29 +01:00
Horizon agentandClaude Opus 5.5 f8ab91d422 Merge origin/main into #11445 (main builds again after #11767)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 14:10:28 +01:00
Horizon agentandClaude Opus 5.5 6b72630759 Merge origin/main into #11116 (main builds again after #11767)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 14:10:28 +01:00
Horizon agentandClaude Opus 5.5 7cf67fa99d Merge origin/main into #11159 (main builds again after #11767)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 14:10:28 +01:00
Horizon agentandClaude Opus 5.5 d4515b62ec Merge origin/main into #11425 (main builds again after #11767)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 14:10:28 +01:00
TiggerTakkyTTTandCodex ad89bb3123 Launcher: clarify latest nightly version
Show the newest build with a Latest badge, present Nightly dates in a player-friendly title, and separate older builds on the Versions page.

Co-Authored-By: Codex <[email protected]>
2026-11-07 20:10:13 +07:00
MSedgeMC 8c3f88a194 Merge remote-tracking branch 'upstream/main' into HEAD 2026-11-07 13:09:35 +00:00
MSedgeMC ac02477732 Merge remote-tracking branch 'upstream/main' into HEAD 2026-11-07 13:09:30 +00:00
MSedgeMC 4583362634 Merge remote-tracking branch 'upstream/main' into HEAD 2026-11-07 13:09:26 +00:00
MSedgeMC d00cf662b1 Merge remote-tracking branch 'upstream/main' into HEAD 2026-11-07 13:09:20 +00:00
MSedgeMC 6deb10741a port_map: client-007's prepareQuadFlat row cites the renamed helper 2026-11-07 13:07:59 +00:00
Horizon agentandClaude Opus 5.5 7770124ea0 Merge #11366's fix into #11390
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 14:06:02 +01:00
Horizon agentandClaude Opus 5.5 78ab0c3b39 living.rs landing test: MobWorld's level and dimension_type in place of nether/end (main's #10445 test)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 14:06:02 +01:00
Ludovic Marie b893b6ae31 Merge pull request #11767 from horizon-rust/fix/native-level-play-sound-clash
Fix main build: two native_level_play_sound methods break the mod API generator
2026-11-07 14:05:02 +01:00
Horizon agentandClaude Opus 5.5 7c7d13c458 Merge origin/main into #11390 (through #11366's resolved main merge)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 14:01:51 +01:00
Horizon agentandClaude Opus 5.5 ed7114b4a0 Merge origin/main into #11366 (spawn_rules_fixture: main's spawn_selection imports, without the Dimension this PR removes)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 14:01:39 +01:00
Horizon agentandClaude Opus 5.5 acbb98aa5c Merge origin/main into #11433 (keep both data.rs test modules)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 14:00:23 +01:00
Horizon agentandClaude Opus 5.5 385877d1fc Merge origin/main into #11445 (keep both probe fixtures)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 14:00:13 +01:00
Horizon agentandClaude Opus 5.5 771ccd721f Merge origin/main into #11425 (keep both commands.rs test modules and command_exec imports)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 13:59:55 +01:00
Horizon agentandClaude Opus 5.5 b9e272b1ab Merge origin/main into #11116 (keep both native-mods.md paragraphs)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 13:59:18 +01:00
Horizon agentandClaude Opus 5.5 37cee5b4fd Merge origin/main into #11159 (keep both command_exec corpus groups)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 13:59:09 +01:00
Ludovic Marie 6416c415a7 Merge pull request #11500 from iseaprojects/i18n/es-es-894
🤖 Agent: es_es now covers all 898 en_us keys (four recipe-transfer strings added after main grew), placeholders checked key by key; the old red verify/launcher was the horizon-checks-1 runner out of disk. (union of 74 heads on main: fmt, clippy -D warnings, tests green).
2026-11-07 13:57:23 +01:00
bulkandClaude Opus 5.5 29804afe17 mod API: rename the host-bridge sound and particle helpers that clash with level effects
#7676 and #9222 each added Game::native_level_play_sound and
native_level_send_particles; the mod API generator needs one of each,
so every build on main panics in build.rs. The host-bridge pair becomes
native_play_sound_in_place / native_send_particles_in_place; the names
mods call (play_sound, send_particles) are unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 12:53:23 +00:00
MSedgeMC 8fe6762146 test: compare enchantment durability effects
Add a direct Unbreaking and Mending durability comparison over the real
enchantment registry: item_damage remove_binomial (small and large paths)
and repair_with_xp, with --no-mending reproducing the Unbreaking corpus.
2026-11-07 12:50:41 +00:00
rdbt 0628aa5870 Merge pull request #11403 from TheMrZZ/fix/h065-shrieker-local-capture
Keep the client-level shrieker effect out of shared capture
2026-11-07 12:45:18 +00:00
rdbt fb03dbf6e3 Merge pull request #11402 from ilguerro/fix/w030
Reset the impulse context after an ender pearl teleport
2026-11-07 12:45:06 +00:00
rdbt 915f58a8c5 Merge pull request #11392 from rison1337/perf/cache-bone-rotations
perf(mobs): cache exact bone rotations when TAA is off
2026-11-07 12:44:55 +00:00
rdbt 2557cb185b Merge pull request #11371 from TheMrZZ/fix/saved-max-stack-prototype
Omit prototype max-stack patches in saved ItemEntity stacks
2026-11-07 12:44:44 +00:00
rdbt db2bc09336 Merge pull request #11370 from MSedgeMC/test/brewing
brewing: every recipe's mix, slot filters and the stand tick
2026-11-07 12:44:33 +00:00
rdbt 23af2bfc62 Merge pull request #11361 from MSedgeMC/test/firework-edges
firework_tooltip: every flight duration, long explosion runs and both components
2026-11-07 12:44:22 +00:00
TeiwazikandClaude Sonnet 5.5 ff28824d42 Riding: boat riders keep their own seat
position_riders re-seated every mob whose vehicle is not a mob through
passenger_riding_position, which puts every boat rider in seat 0 from
the boat's position. seat_boat_mobs had already placed them, so remote
players on a moving boat jumped between the two seats each pass.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-07 13:44:21 +01:00
rdbt f6980a83e6 Merge pull request #11360 from AlfaRojo/fix/spectator-food-and-regen
A spectator in loaded chunks keeps its food and regeneration ticks
2026-11-07 12:44:11 +00:00
rdbt dd64878f38 Merge pull request #11296 from TheMrZZ/fix/mob-command-stack-retention
Preserve standard mob equipment stacks in item commands
2026-11-07 12:43:23 +00:00
TeiwazikandClaude Sonnet 5.5 45978aaf67 Seat test: import Vec3 and play packets
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-07 13:43:19 +01:00
rdbt f0e0d96b61 Merge pull request #11258 from TheMrZZ/fix/loot-empty-result-positions
Preserve empty loot result positions for command replacement
2026-11-07 12:43:11 +00:00
rdbt 45cc5a91e0 Merge pull request #11253 from TheMrZZ/fix/h189-data-literal-snbt
Preserve modern SNBT literals during data execution
2026-11-07 12:43:00 +00:00
rdbt 132aa6b682 Merge pull request #11133 from Ragbie/fix/enderman-hurt-teleport-now
An enderman hurt by the game layer teleports at the hurt, as vanilla's does
2026-11-07 12:42:42 +00:00
rdbt fcc57182ae Merge pull request #11122 from Delete228/mod-data-load-in-place-upstream
Preserve live mod block entities when loading command data
2026-11-07 12:42:31 +00:00
rdbt fcc6416b34 Merge pull request #11121 from TheMrZZ/fix/h090-tnt-fuse
Decode primed TNT fuse through numeric short conversion
2026-11-07 12:42:17 +00:00
rdbt c85011b04d Merge pull request #10570 from Detimes/perf-nbt-ascii-strings
NBT: read and write one-byte strings whole
2026-11-07 12:41:58 +00:00
rdbt d93bf565f4 Merge pull request #10531 from PzmuV1517/shaders/perf-parallel
Shader packs: a pack's pipelines compile together on several threads
2026-11-07 12:41:47 +00:00
rdbt 9c5a3734ea Merge pull request #10506 from voidsipp/fix/round3-1-wet-fire-order
Apply burning damage before extinguishing wet mobs
2026-11-07 12:41:36 +00:00
rdbt 2ff274b96c Merge pull request #10493 from voidsipp/fix/mob-water-breathing
Respect breathing effects and bubble columns for mobs
2026-11-07 12:41:25 +00:00
rdbt 13d03ec87f Merge pull request #10384 from voidsipp/fix/10044-forced-respawn
Validate forced Overworld respawn points
2026-11-07 12:41:14 +00:00
rdbt 4ea806a2db Merge pull request #10383 from voidsipp/fix/10369-map-x-offscreen
Keep treasure map targets when carrying undecorated copies
2026-11-07 12:41:08 +00:00
martipopsandClaude Opus 5.5 303255f2d8 Merge main into controller/guide
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 07:40:51 -05:00
martipopsandClaude Opus 5.5 a6339e67b5 Merge main into controller/guide
Brings in #11026 and #11534. docs/controllers.md keeps the guide's slider
hold beside main's server-list bumpers and Playing in the background.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 07:38:09 -05:00
rdbt 1cd80a0bcb Merge pull request #11757 from kacpherek/perf/render-api-tracking
an edit in game/ no longer rebuilds the renderer: dev rebuild 22.7 s -> 12.0 s
2026-11-07 12:37:35 +00:00
rdbt 4ebee0d01e Merge pull request #11218 from oxi-does-dev/feat/launcher-nord
Launcher: Nord theme and the theme picker in the Blocks look
2026-11-07 12:36:10 +00:00
rdbt 0ebe0d40de Merge pull request #11135 from SouceCalve/main
Adding support for installing from PrismLauncher-portable
2026-11-07 12:35:57 +00:00
rdbt 30ee97bbc7 Merge pull request #7690 from levzzz5154/modapi/entity-moves
Native mods: teleport, rotation, head yaw, facing, size and pushes through EntityRef (#7387)
2026-11-07 12:35:28 +00:00
rdbt 2a57a30ae2 Merge pull request #7685 from levzzz5154/modapi/entity-state
Native mods: an entity's state flags through EntityRef (#7387)
2026-11-07 12:35:16 +00:00
rdbt b3f9eddfd3 Merge pull request #7676 from levzzz5154/modapi/level-effects
Native mods: sounds and particles in a level through LevelRef (#7387)
2026-11-07 12:35:04 +00:00
rdbt f5a6eb9835 Merge pull request #7564 from levzzz5154/modapi/level-world
Native mods: a level's seed, difficulty, game rules and weather through LevelRef (#7387)
2026-11-07 12:34:52 +00:00
rdbt dd3f458ba1 Merge pull request #11303 from UMCEKO/dims/world-facts
Block behaviour reads the dimension type, not the dimension (#11282, 2/9)
2026-11-07 12:34:39 +00:00
rdbt d4006f0770 Merge pull request #11415 from UMCEKO/dims/dimension-facts
Dimension type facts read instead of matching the level (#11282, 1b/9)
2026-11-07 12:34:27 +00:00
rdbt bed953b207 Merge pull request #11291 from UMCEKO/dims/level-key
LevelKey: a level's key for any dimension, not three variants (#11282, 1a/9)
2026-11-07 12:34:11 +00:00
Horizon agentandClaude Opus 5.5 0e674b827c lan hub: a STAB keeps the move behind it in its tick
#11223 made every PlayerAction hold the move queued behind it for the next
tick. For a spear's STAB that moved the jump the move carries
(handlePlayerPositionChange's jumpFromGround) a tick after the Lunge's
impulse, and the server took two moves in the next tick: lunge_motion has
failed every jump phase on parity-main since. Releases, drops and digs keep
the hold.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 13:34:01 +01:00
rdbt 2f0b066f38 Merge pull request #11026 from martipops/fix/pad-read
Controllers: fixes from reviewing the pad reader, typing and joining
2026-11-07 12:34:00 +00:00
rdbt 7d8bb9e69b Merge pull request #11645 from vegetableovosh/fix/11214-chunk-refresh
Fix/11214 chunk refresh
2026-11-07 12:33:47 +00:00
rdbt dd3c1d17a2 Merge pull request #11756 from TheMrZZ/fix/item-display-default-transformation
Save item-display transformations from modeled state
2026-11-07 12:33:35 +00:00
rdbt d5f43e0967 Merge pull request #11256 from MSedgeMC/test/transformer-use
use_item_on: roots on the clicked face, and the copper chest's other half
2026-11-07 12:33:23 +00:00
rdbt d372562a73 Merge pull request #11744 from yahya-nusseibeh/fix/launcher-account-key-salt
Seal the launcher's secrets file with a per-install secret
2026-11-07 12:33:11 +00:00
rdbt 99ddbc629e Merge pull request #11739 from yahya-nusseibeh/fix/relay-per-address-limits
Relay: a cap on open connections per address, and a per-site join budget
2026-11-07 12:32:59 +00:00
rdbt 37f69234e7 Merge pull request #11707 from doodlyyousuf/251106-fix-mob-equip-vibration
fix: mob saddle and gear post equip vibrations at full volume
2026-11-07 12:32:47 +00:00
rdbt 8fd21e02d4 Merge pull request #11746 from NongTham/fix/advancement-parent-normalization
Advancements: normalize parent and lookup IDs with full resource locations (#11653)
2026-11-07 12:32:36 +00:00
rdbt 32191d5b6a Merge pull request #11745 from yahya-nusseibeh/fix/deps-tokio-lru
Update tokio to 1.53.3 and mysql to 28 (lru 0.18) for advisories
2026-11-07 12:32:24 +00:00
rdbt 8331bedf2b Merge pull request #11725 from MSedgeMC/test/spawn-obstruction
test: compare mob spawn obstruction
2026-11-07 12:32:13 +00:00
rdbt 37656cc775 Merge pull request #11738 from yahya-nusseibeh/fix/shader-pack-bounded-reads
Shader packs: bound what one zipped file may unpack to
2026-11-07 12:32:01 +00:00
rdbt ae0ccc199c Merge pull request #11737 from yahya-nusseibeh/fix/hshader-running-total
Shader packages: count the 8 MiB total while reading, not after
2026-11-07 12:31:50 +00:00
rdbt 4c1d1990e1 Merge pull request #11736 from yahya-nusseibeh/fix/report-gh-refs
Website bug reports: GH-123 cross-references nothing either
2026-11-07 12:31:38 +00:00
rdbt bc0ddfbff0 Merge pull request #11529 from horizon-rust/fix/followup-10571
Text displays: animated text is never cached, and a culled one keeps its glyphs
2026-11-07 12:31:27 +00:00
rdbt 330d3db811 Merge pull request #11089 from CocofireHD/docs/perf-comparison-readme
README: a Performance section from the clean Horizon vs vanilla runs
2026-11-07 12:31:22 +00:00
rdbt a7a2cf9305 Merge pull request #11749 from M0-kii/fix/windows-launcher-bin-tests
Run launcher binary tests in the Windows PR checker
2026-11-07 12:30:56 +00:00
rdbt 1087b5a6ff Merge pull request #11724 from horizon-rust/ci/runner-gc-busy-versioned-bin
ci: runner-gc sees jobs after a runner self-update
2026-11-07 12:30:51 +00:00
24fa04d753 fix: drop mob XP after the guest who hurt it has left
Vanilla LivingEntity.dropExperience and Mob.dropCustomDeathLoot key on
lastHurtByPlayerMemoryTime, not whether that player is still in the
world. Horizon folded both into Death.player, so a guest who left
within the 100-tick memory made drops::die skip experience and
equipment.

Give die a player_killed flag from the timer. LAST_DAMAGE_PLAYER stays
None when killer_uuid cannot resolve them.

Fixes #11674

Co-Authored-By: OpenCode <[email protected]>
Co-authored-by: monkeycode-ai <[email protected]>
2026-11-07 12:29:23 +00:00
martipopsandClaude Opus 5.5 fb3e73d3e2 Merge main into controller/guide
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 07:26:09 -05:00
Ludovic Marie 951fc0f0cb Merge pull request #11534 from martipops/settings/shell-api
🤖 Agent: Extras moves onto the new settings_shell (one row per setting, search, description strip), same items and saves; design-system.md now lists Extras with Graphics/Controller as Blocks-frame pages; size (+1748) accepted because ~860 lines are the shell the next pages reuse (#11487 plan). (union of 74 heads on main: fmt, clippy -D warnings, tests green).
2026-11-07 13:24:55 +01:00
Mayonesu0 1429b27098 Merge remote-tracking branch 'origin/main' into perf/launcher-write-data-file 2026-11-07 13:20:05 +01:00
Horizon agent 2f88719668 Merge remote-tracking branch 'origin/main' into fix/wool-vibration-drops 2026-11-07 13:19:20 +01:00
Ludovic Marie 25a1add0a2 Merge pull request #10208 from ilguerro/fix/entity-die-non-mobs
🤖 Agent: Requeued after merging main twice (lan_e2e.py keep-both: this PR's entity-die check beside main's new mob-thorns check; then #11735's clippy/lock fix); verify green. Breaking a painting, cushion, boat, minecart, frame or armor stand emits ENTITY_DIE like vanilla's kill path, and its sculk/vibration harnesses passed on the earlier head.
2026-11-07 13:16:57 +01:00
DetimesandClaude Opus 5.5 c78882c7da Older clients: attributes and effects in the client's numbering
1.21.2 dropped the generic./player./zombie. attribute prefixes, so the id tables of 1.21.1 and 1.20.6 (matched by
name) had no attribute runs and every update_attributes value was left out for those clients. They are regenerated
with the renames. Mob effect ids were never renumbered: update_mob_effect and remove_mob_effect now go through the
client's table, and an effect added after its release is not sent.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 12:16:51 +00:00
Horizon agentandClaude Opus 5.5 4e1c0d68be Merge main into #10384 (main's clippy and launcher lockfile fixes)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 13:16:21 +01:00
martipopsandClaude Opus 5.5 7753a08ca9 Merge main into controller/guide
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 07:15:00 -05:00
Florian ERNSTandCodex 34a9bfaa88 Save item-display transformations from modeled state
Co-Authored-By: Codex <[email protected]>
2026-11-07 13:11:42 +01:00
Horizon agentandClaude Opus 5.5 40dd7109bb Merge origin/main into random-source
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 13:09:37 +01:00
Horizon agentandClaude Opus 5.5 1db1b34dcb Merge origin/main into perf/11126-structure-starts-jigsaw
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 13:08:04 +01:00
Horizon agentandClaude Opus 5.5 5fdf9fb0ac Merge origin/main into fix/11214-chunk-refresh
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 13:07:40 +01:00
Horizon agentandClaude Opus 5.5 6e19f95786 Merge origin/main into feat/realms-service-fixes
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 13:07:15 +01:00
Horizon agentandClaude Opus 5.5 d30b514728 Merge origin/main into fix/old-stats-file
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 13:06:46 +01:00
Kacpherek cf2ab15de3 Build the mod API generator and syn optimized: its build script parses all of game/ on every gameplay edit 2026-11-07 13:06:14 +01:00
M0-kii 68ec463175 Run launcher binary tests in the Windows PR checker
Keep --bins in the Windows launcher test command so binary-owned tests
are checked alongside the library, matching the planned validation.

Fixes #11747
2026-11-07 15:35:53 +03:30
DetimesandClaude Opus 5.5 6993f213b6 step_1_21_1: drop the import the explode rewrite no longer uses
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 12:05:41 +00:00
Horizon agentandClaude Opus 5.5 35d60b45b5 Merge origin/main into client/feature-gate-online
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 13:04:55 +01:00
Horizon agentandClaude Opus 5.5 a4df22049c Merge origin/main into settings/shell-api
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 13:04:01 +01:00
Horizon agentandClaude Opus 5.5 4bf0560d40 Merge origin/main into i18n/es-es-894
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 13:03:34 +01:00
Horizon agentandClaude Opus 5.5 511771b9f9 Merge origin/main into fix/11317-block-crack-feedback
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 13:03:03 +01:00
Horizon agentandClaude Opus 5.5 344382682e Merge origin/main into fix/11314-player-food-predicate
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 13:02:36 +01:00
Ludovic Marie 18840db914 Merge pull request #11451 from MSedgeMC/fix/armadillo-state-save
🤖 Agent: An armadillo's state is saved always and restored through switchToState on load, as Armadillo.add/readAdditionalSaveData do.
2026-11-07 12:59:39 +01:00
Detimes bb22bf6570 Merge remote-tracking branch 'upstream/main' into fix-11722-old-pick-item 2026-11-07 11:58:27 +00:00
Detimes edef044a68 Merge remote-tracking branch 'upstream/main' into fix-11721-explode-radius 2026-11-07 11:58:27 +00:00
DetimesandClaude Opus 5.5 c7f54b7461 Older clients: a 1.21.3 client's pick-item swaps the slot into its hand
1.21.3 and older clients send pick_item with the inventory slot that holds the picked block's item; 26.3 has no
such packet and the adapter dropped it. It now becomes a container_click swap of that slot with the held hotbar
slot (tracked from set_carried_item and set_held_slot), state id 0 so the server resends the inventory.

Fixes #11722

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:58:27 +00:00
DetimesandClaude Opus 5.5 47fd58c339 Older clients: real explosion radius for 1.21.1, explosions no longer fail for 1.21.8 and older
The 1.21.8 and 1.21.1 explode steps re-read a payload the renumbering had already put in the client's ids with
the pinned codec, so the particle's layout was chosen by the wrong id and sending any explosion failed. They now
read and write through the client's ids. 1.21.1's step read the knockback as three doubles where 1.21.4's step
leaves a double and two floats. 1.21.1's strength is 26.3's radius instead of a fixed 3.0.

Fixes #11721

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:54:53 +00:00
Horizon agent 88a75f983b Merge remote-tracking branch 'origin/main' into cg65v-10210 2026-11-07 12:54:24 +01:00
Horizon agent 4c4728ead5 Merge remote-tracking branch 'origin/main' into cg65v-10208 2026-11-07 12:54:16 +01:00
Ludovic Marie ea55e8156c Merge pull request #11735 from MSedgeMC/fix/clippy-is-multiple-of
🤖 Agent: Unbreaks main: clippy 1.99's manual_is_multiple_of in horizon-core (from #10553) and the socket2 0.5.10 entry missing from both lockfiles under --locked; both workspaces' cargo metadata --locked and clippy -p horizon-core -D warnings pass on this head.
2026-11-07 12:53:19 +01:00
Horizon agentandClaude Opus 5.5 320d55a88c launcher: the socket2 0.5.10 entry its lock was missing
Same break as the workspace lock: #11464 and #11569 left "socket2 0.5.10"
referenced with no package entry, so main's launcher build fails under
--locked. Regenerated with cargo metadata; nothing else moves.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 12:51:04 +01:00
MSedgeMC 3b7dde0cd5 horizon-core: use is_multiple_of for the stride check
clippy::manual_is_multiple_of is in the stable toolchain now, and the workspace clippy step runs with -D warnings,
so main's own horizon-core fails verify before any PR's code is compiled.
2026-11-07 11:48:18 +00:00
NongThamandAntigravity <gemini> 4df3bd4db0 Advancements: normalize parent and lookup IDs with full resource locations (#11653)
Ensure advancement parent references and command lookups handle both bare
and namespaced identifiers, preventing orphaned advancement nodes and
matching vanilla's progression count on '/advancement grant @s everything'.

Co-Authored-By: Antigravity <gemini> <[email protected]>
2026-11-07 18:45:55 +07:00
Horizon agent dbd5775947 Merge remote-tracking branch 'origin/main' into cg65v-10210
# Conflicts:
#	crates/horizon-client/src/game/command_harness.rs
#	mods/horizon/src/test/java/dev/horizonlab/parity/VanillaCommandExecOracle.java
#	tools/test_command_exec_parity.py
2026-11-07 12:39:25 +01:00
Mayonesu0andClaude Opus 5.5 199e8193fd Launcher: write a prepared version's data file after installing it
A version's first start read its thousands of small files one by one, about
100 s under Windows Defender here, because only the game wrote
horizon-data.bin, and only after that slow start. The launcher now writes it
in the background once an install or import has its resources, reading on up
to 8 threads (43 s against 281 s on one, on a fresh copy); the start then
loads in 1.2 s. The game stays the fallback, on its one idle thread.

A reused version no longer copies the data file, which names its own root.
complete_assets removes the file when it adds something, as the file answers
missing for what it lacks. A write removes what a stopped writer left.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 12:38:08 +01:00
Horizon agent f16cc3d6d0 Merge remote-tracking branch 'origin/main' into cg65v-10208
# Conflicts:
#	tools/lan_e2e.py
2026-11-07 12:37:32 +01:00
Ludovic Marie 0000cd3e7d Merge pull request #10538 from arslanarm/shaderpacks/end-fog-sky-fan
🤖 Agent: Requeued at the author's main merge 552f2c1b4: same net patch as d8676fff0 (identical patch-id), whose parity failed only command_exec's 26 /say lines that #11687 has since fixed on main. The change: shader packs in the End draw the sky fan under the dragon fight's fog like the rest of the pack's sky.
2026-11-07 12:37:21 +01:00
Ludovic Marie 2303a445fe Merge pull request #11482 from DUzzL/fix/11298-elytra-glide-refused
🤖 Agent: a refused START_FALL_FLYING now flips the glide bit of sent_shared so a flags update goes out, as LivingEntity.stopFallFlying's set(true)+set(false) dirties the flag in vanilla (union of 74 heads on main: fmt, clippy -D warnings, tests green).
2026-11-07 12:34:18 +01:00
Ludovic Marie 6c53f16dbf Merge pull request #11591 from rdbtCVS/launcher-notes-in-versions
🤖 Agent: the Patch notes tab's three orphan strings are gone from the 86 lang files, so the launcher's i18n tests pass (union of 74 heads on main: fmt, clippy -D warnings, tests green).
2026-11-07 12:31:28 +01:00
martipopsandClaude Opus 5.5 9495790d0b Merge main into controller/guide
main hides the HUD in Replay Studio; the guide's guide_from stays.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 06:31:23 -05:00
Ludovic Marie ee8a79e80e Merge pull request #11706 from Bioneisme/fix/damage-type-command-ids
🤖 Agent: /damage ids now come from the world's damage catalog and are replaced on each world open, so a pack's type doesn't leak into the next world (union of 74 heads on main: fmt, clippy -D warnings, tests green).
2026-11-07 12:30:08 +01:00
Ludovic Marie f98c9b1f74 Merge pull request #11697 from mvanhorn/fix/6763-memory-first-view-chunk-batch
🤖 Agent: the single-player memory connection sends every ready view column in one batch before PlayerLoaded (about 0.5 s saved at render distance 12), then the 64-per-tick cap comes back; socket guests keep vanilla pacing. (union of 74 heads on main: fmt, clippy -D warnings, tests green).
2026-11-07 12:30:04 +01:00
Ludovic Marie 7524ec7664 Merge pull request #11667 from Cx188/fix/11212-chat-suggestion-callback
🤖 Agent: new client-only, read-only native hook chat_suggestion_applied fires after Tab or a click applies a suggestion, with the replaced range in UTF-16 units of the list's original input; fragment stamps cleanly on main. (union of 74 heads on main: fmt, clippy -D warnings, tests green).
2026-11-07 12:30:01 +01:00
Ludovic Marie 1bb63d1e2a Merge pull request #11647 from Hiuoy/feat/port-pack-events-data
🤖 Agent: port-pack events answers VillagerTradesEvent/WandererTradesEvent with the trade_set and villager_trade registries 26.x moved trades to. (union of 74 heads on main: fmt, clippy -D warnings, tests green).
2026-11-07 12:29:57 +01:00
Ludovic Marie b85e0f4f67 Merge pull request #11635 from MSedgeMC/port_map/dead-row-verdicts
🤖 Agent: Three ledger rows corrected (destroyAndAck's ack, RateKickingConnection via PacketRate::received, shouldRun as skip:other); checked PacketRate::received exists on main. (union of 74 heads on main: fmt, clippy -D warnings, tests green).
2026-11-07 12:29:53 +01:00
Ludovic Marie 35b963c3b8 Merge pull request #11547 from abit-foggy/codex/studio-b1-playback
🤖 Agent: Studio playback of .mcpr recordings through the normal client handlers (B1 slice of #9712, +2120), only active when a recording is opened; F1 now hides chat and tab list as vanilla Hud.extractRenderState does. (union of 74 heads on main: fmt, clippy -D warnings, tests green).
2026-11-07 12:29:50 +01:00
Ludovic Marie e43c075351 Merge pull request #11531 from vnsmoreira/packs/polytone-reader
🤖 Agent: new horizon-polytone crate reads Polytone targets and biome/dimension attribute layers from the public wiki only (Polytone is GPL, its source not read); no caller yet, first step of #11486. (union of 74 heads on main: fmt, clippy -D warnings, tests green).
2026-11-07 12:29:46 +01:00
Ludovic Marie da21244ed0 Merge pull request #11504 from voidsipp/fix/wind-burst-block-hook
🤖 Agent: the hook runs after the vanilla shuffle so RNG draws are unchanged and a no-mod dispatch is an empty loop; fragment wind_burst passes tests.test_stamp_native_hooks and --check --only on main merged in (union of 74 heads on main: fmt, clippy -D warnings, tests green).
2026-11-07 12:29:42 +01:00
Ludovic Marie 2617902218 Merge pull request #11499 from martipops/controller/studio-reach
🤖 Agent: Studio rows and buttons now register as focus targets and the wheel moves one whole 40 px row per notch, which also stops part-scrolled rows drawing over the title and makes the last recording reachable. (union of 74 heads on main: fmt, clippy -D warnings, tests green).
2026-11-07 12:29:39 +01:00
Ludovic Marie bd5dc3ac4a Merge pull request #11496 from RINNECODER/up/port-chain-models
🤖 Agent: only block/ and item/ paths equal to the old id are renamed, so a mod's own pulley:block/chain and chain_command_block_* stay, and files are rewritten only when something changed (union of 74 heads on main: fmt, clippy -D warnings, tests green).
2026-11-07 12:29:36 +01:00
Ludovic Marie 471cdcf2db Merge pull request #11495 from RINNECODER/up/recipe-suggest
🤖 Agent: recipe suggestions reuse json_ids_of_every_namespace like /loot, and the dead ids_under goes (union of 74 heads on main: fmt, clippy -D warnings, tests green).
2026-11-07 12:29:32 +01:00
Ludovic Marie 6ffbbfc44c Merge pull request #11479 from k3yh/log-unrunnable-features
🤖 Agent: The unsupported placed features are now named in the log (capped at 20), which is what a pack author needs to fix them. (union of 74 heads on main: fmt, clippy -D warnings, tests green).
2026-11-07 12:29:29 +01:00
Ludovic Marie c0e74b2b53 Merge pull request #11475 from rison1337/perf/cull-offscreen-flames-taa
🤖 Agent: under TAA the flame gate now uses a frustum widened by one rendered pixel per side (jitter is at most half a pixel: 2× margin), one extra frustum per frame, shader packs still bypass. (union of 74 heads on main: fmt, clippy -D warnings, tests green).
2026-11-07 12:29:25 +01:00
Ludovic Marie d7cc28f952 Merge pull request #11468 from rison1337/perf/lazy-dropped-item-bounds
🤖 Agent: with entity culling off the decision is Bypass before any bounds are read, so the posed-envelope walk over every model point of every copy is now skipped (TAA condition dropped), RNG order of cluster_poses pinned by a test. (union of 74 heads on main: fmt, clippy -D warnings, tests green).
2026-11-07 12:29:22 +01:00
Ludovic Marie 16b511ed65 Merge pull request #11464 from TaquitoSuabe/perf/net-keepalive-pooling
🤖 Agent: TCP keepalive (10 s + 3×3 s) now ends dead friends tunnels and relay bridges in ~19 s instead of holding them until the 120 s idle; the stack buffers replace one Vec per connection, not per read. (union of 74 heads on main: fmt, clippy -D warnings, tests green).
2026-11-07 12:29:18 +01:00
Horizon agentandClaude Opus 5.5 301765dc69 List server_wool_vibration as known flaky: its player cases are paced by the wall clock
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 12:28:35 +01:00
MSedgeMC 0748416e9e Merge remote-tracking branch 'upstream/main' into HEAD
# Conflicts:
#	crates/horizon-client/src/entity_nbt.rs
2026-11-07 11:22:23 +00:00
MSedgeMC f64b1b9d55 Add the who_lookup seam to living.rs's test fixture 2026-11-07 11:22:07 +00:00
Kacpherek 59dff021a1 Don't rebuild the renderer on every gameplay edit: its mod API reads only native_hooks.rs 2026-11-07 12:14:32 +01:00
MSedgeMC 1748b489eb Merge remote-tracking branch 'upstream/main' into HEAD
# Conflicts:
#	crates/horizon-client/src/entity_nbt.rs
2026-11-07 11:08:58 +00:00
Ludovic Marie 98b8827123 Merge pull request #10553 from ilguerro/perf/9996-sections
🤖 Agent: Chunk-section block writes now do one position lookup instead of two on the worldgen hot path; output unchanged, and CI parity on main+head (worldgen, structures, light and the save harnesses it selects) passes in full.
2026-11-07 12:07:04 +01:00
Horizon agentandClaude Opus 5.5 ba61774567 server_wool_vibration: let the killed cow's corpse go before the arena changes
A /kill-ed cow stays 20 ticks (LivingEntity.tickDeath) and still moves;
the fill that removes the carpet or slab under it drops it onto the floor
while the next case measures. item-slab read power 2 before its summon on
vanilla's side in a playhorizon run. Wait 25 server ticks after the kills.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 12:02:52 +01:00
vegetableandOpenAI Codex 5eeb4d5678 Keep interpolation compatible with the MSRV
Co-Authored-By: OpenAI Codex <[email protected]>
2026-11-07 16:01:42 +05:00
Horizon agentandClaude Opus 5.5 a5129be064 Studio playback: its Remote takes main's recipe_transfer (off)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:55:01 +01:00
Horizon agentandClaude Opus 5.5 7b03ce49c7 Merge origin/main into codex/studio-b1-playback
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:54:51 +01:00
Horizon agentandClaude Opus 5.5 5a65831d4a Merge #11483's head (main with the clippy as_chunks lint fix)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:53:46 +01:00
Horizon agentandClaude Opus 5.5 4f2f4078d1 ci: see runner jobs after a runner self-update; keep the Mac's disk free
GitHub's runner moved its binaries to bin.<version>/ after a self-update, so
`pgrep -f "$runner/bin/Runner.Worker"` never matched: runner-gc took busy
runners for idle and deleted the probe builds and old prepared resources of
two running parity jobs (2026-11-07). Match bin*/ in runner-gc,
checks-runners.sh and linux-runner.sh.

runner-gc also counts shared-local/parity-target (19-24 GB per runner),
recreates it empty after removing it (a dangling .local link failed every
harness), and drops harness folders over 2 GB untouched for 6 hours.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:53:10 +01:00
Horizon agentandClaude Opus 5.5 64f9bb1a61 Merge main (clippy as_chunks lint fix)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:53:08 +01:00
Horizon agentandClaude Opus 5.5 991983679c Merge main (clippy as_chunks lint fix)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:52:41 +01:00
Florian ERNSTandCodex c2b59b6f99 Dispatch ordinary arrow shoot events on their first tick
Co-Authored-By: Codex <[email protected]>
2026-11-07 11:52:23 +01:00
Horizon agentandClaude Opus 5.5 b844b69826 Merge main (clippy as_chunks lint fix)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:51:48 +01:00
Horizon agentandClaude Opus 5.5 c2386a4616 Merge main (clippy as_chunks lint fix)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:50:06 +01:00
Horizon agentandClaude Opus 5.5 c524b40874 Merge main (clippy as_chunks lint fix)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:49:40 +01:00
Horizon agentandClaude Opus 5.5 d53411a24e Merge main (clippy as_chunks lint fix)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:49:15 +01:00
Horizon agentandClaude Opus 5.5 db503217e3 Merge main (clippy as_chunks lint fix)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:48:22 +01:00
Horizon agentandClaude Opus 5.5 dddf61b000 Merge main (clippy as_chunks lint fix)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:47:28 +01:00
MSedgeMC 6ac83b92d7 test: compare mob spawn obstruction
Extend the spawn rule suite with Mob.checkSpawnObstruction: real entities
against water, lava, an obstructed level and Ocelot's special base, with
--no-obstruction reproducing the previous corpus.
2026-11-07 10:46:37 +00:00
Horizon agentandClaude Opus 5.5 48986d7fd8 Merge main (clippy as_chunks lint fix)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:46:34 +01:00
exampleandClaude Opus 5.5 7746c0ef78 Merge main: picks up #11716's lint fix for verify
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:45:49 +01:00
Horizon agentandClaude Opus 5.5 fbbe20dc9c Merge main (clippy as_chunks lint fix)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:45:40 +01:00
Horizon agentandClaude Opus 5.5 b9928751e4 Merge main (clippy as_chunks lint fix)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:45:12 +01:00
Horizon agentandClaude Opus 5.5 ed98ebc8a1 Merge main (clippy as_chunks lint fix)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:44:46 +01:00
TeiwazikandClaude Sonnet 5.5 55a54edebe Riding: follow plugin seat chains through invisible vehicles
Plugins seat players on text displays, interactions, armor stands or area
effect clouds that ride another player. Resolve the seat through the whole
chain of vehicles, keep the packet riding graph, move the local rider with a
moving seat, and use the Avatar attachment height for player vehicles.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-07 11:44:41 +01:00
Horizon agentandClaude Opus 5.5 c0ca0cce68 Merge main (clippy as_chunks lint fix)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:43:51 +01:00
Horizon agentandClaude Opus 5.5 984f2d9f06 Merge main (clippy as_chunks lint fix)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:43:23 +01:00
Horizon agentandClaude Opus 5.5 7fcc1c900a server_wool_vibration: clear the previous cow's drops between cases
The arena's /kill @e[type=!player] kills the previous case's cow, and the
cow drops beef and leather as it dies, after the selector was evaluated.
Those items fall onto the next arena and can hit the floor while the next
case measures, at a wall-clock moment: parity-main 42359055402 read power
2 before item-stone on vanilla's side, and playhorizon read 2 after
item-carpet in 1 of 3 runs at 67902962fd. Both cases follow a cow case.
A second /kill @e[type=item] removes the drops; setup lines aren't compared.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:43:17 +01:00
Horizon agentandClaude Opus 5.5 3ed97c7339 Merge main
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:39:07 +01:00
Riccardo GuerrieroandClaude Opus 5.5 3f1b851c07 Merge origin/main into #11400 (main's 1.95 lint fix for interpolation.rs)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:37:51 +01:00
Horizon agentandClaude Opus 5.5 aa7516532a Merge main; drop run logs and the unrun diagnostic
tools/test_warden_memory_full_diagnostic.py is not a test_*_parity.py harness, so nothing runs it, and it is 816/839 by design. The canonical owned-memories harness stays. Drops the docs/parity run logs.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:37:36 +01:00
Ludovic Marie fd06d1c265 Merge pull request #11716 from horizon-rust/agent/cg70-one-rust-version
🤖 Agent: fixes main's red clippy since #11561 (1.95 lints on interpolation.rs: is_multiple_of on non-zero unsigned divisors, as_chunks_mut::<4> quads); workspace clippy clean on main + this.
2026-11-07 11:34:53 +01:00
Riccardo GuerrieroandClaude Opus 5.5 c9f2efa210 A player's Thorns hurts the mob as the player's thorns hit
Thorns on the player answered a mob's hit with generic damage that had no
attacker, so the mob was not knocked back, did not turn on the player and
dropped no experience when it died of it. It now hurts the mob by
`minecraft:thorns` damage the player caused (`DamageEntity` with the
wearer as owner), rolled from the mob's own random, and the held weapon's
Breach applies as on any hit by the player.

The whole equipment counts, in `EquipmentSlot` order: the main hand, the
off hand, then feet up to head (Thorns' slots are `any`), so a Thorns
piece held in a hand answers too. Each piece rolls its chance, hurts the
attacker and wears in that order, also when the attacker is already dead.
A piece that takes no wear, or a creative player's, is left alone.

A mob hit by thorns damage plays `enchant.thorns.hit` where it stands
when the hit lands whole, silent or not (`playSecondaryHurtSound`).

Part of #9954.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:34:28 +01:00
Horizon agentandClaude Opus 5.5 43c987818b Drop the docs/parity run logs
Run logs are not docs; the PR body keeps the numbers.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:32:52 +01:00
Horizon agentandClaude Opus 5.5 a94924cd72 Merge origin/main into fix/loot-kill-sheep-state
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:32:52 +01:00
Horizon agentandClaude Opus 5.5 484a9d0ae8 Apply the 1.95 lints to main's new interpolation code
is_multiple_of on a usize cell size, and as_chunks_mut::<4> for the four
columns' quads (each quad is now a [f32; 4]).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:32:14 +01:00
Horizon agentandClaude Opus 5.5 fa9500a04d Stop pinning camel temper to variant3 in the pose test
The pose test asserted that Temper lands in the legacy variant3 byte. #11483/#11659 move every equine's temper to the signed equine_temper field; the assertion was outside this test's subject (LastPoseTick), so drop it rather than pin the storage.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:30:49 +01:00
Horizon agentandClaude Opus 5.5 81a719f832 Drop the docs/parity run logs
Run logs are not docs; the PR body keeps the numbers.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:30:20 +01:00
Horizon agentandClaude Opus 5.5 029358df0d Merge #11483 (signed equine temper) into fix/camel-inherited-saved-fields
Both PRs added a signed AbstractHorse temper: #11483 for the seven horse and llama species, this one for camels. Keep one field, equine_temper, for every equine: the loader reads Temper unclamped for all of them, temper()/modify_temper() use it for camels too, and the camel saver writes it. The camel_temper field and the camel byte-clamp path are gone; tests follow.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:30:16 +01:00
Horizon agentandClaude Opus 5.5 ec62d53993 Merge origin/main into fix/camel-inherited-saved-fields
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:28:36 +01:00
Horizon agentandClaude Opus 5.5 23b6a5da91 crack feedback tests: give the test guest main's new Joined.configured
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:25:26 +01:00
Horizon agentandClaude Opus 5.5 1e1e48831a Merge origin/main into fix/11317-block-crack-feedback
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:25:20 +01:00
Horizon agentandClaude Opus 5.5 8a38d8831f Drop the docs/parity run logs
Run logs are not docs; the PR body keeps the numbers.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:21:39 +01:00
Horizon agentandClaude Opus 5.5 4595cfca2f Merge origin/main into fix/loaded-sleeper-bed-position
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:21:38 +01:00
Horizon agentandClaude Opus 5.5 d7889ba66c Merge origin/main into fix/h290-pot-template-compounds
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:20:30 +01:00
Horizon agentandClaude Opus 5.5 78634140b3 Drop the docs/parity run logs
Run logs are not docs; the PR body keeps the numbers.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:19:26 +01:00
Horizon agentandClaude Opus 5.5 5ebfcbdf7f Merge origin/main into fix/world-pack-enchantment-definitions
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:19:26 +01:00
Ludovic Marie d3319194cc Merge pull request #11118 from Delete228/native-model-material-stack-upstream
🤖 Agent: Native mods can draw terrain models with an explicit material (shade, UV shift); the shading follows Flywheel's (MIT, credited). Main merged in after #11117 landed; its test now builds faces with #11356's BakedFace::new (clippy -D warnings passes).
2026-11-07 11:18:17 +01:00
Horizon agentandClaude Opus 5.5 c3083e75cb Fog test: clone the start state, FogState is no longer Copy
FogState now holds the torch flicker's LegacyRandom, so main's rain fog
test (settled_rain_fog_is_its_target_and_easing_is_unchanged) moved
`start` twice; verify failed on E0382.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:18:11 +01:00
Horizon agent 67728a8568 Merge remote-tracking branch 'origin/main' into HEAD 2026-11-07 11:17:34 +01:00
Horizon agentandClaude Opus 5.5 9a1da6539a Merge origin/main into fix/h145-required-function-tags
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:15:52 +01:00
Horizon agentandClaude Opus 5.5 6cbece0baa Chunk refresh: name the 26.3 class, LevelExtractor.allChanged
LevelExtractor.setupRender calls allChanged when the effective render
distance changes; LevelRenderer has no allChanged in 26.3.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:15:23 +01:00
Horizon agentandClaude Opus 5.5 99901a6359 Merge origin/main into feat/realms-service-fixes; wire the invite codes footer
Main's backup screens (#9982) came in beside the invite codes screen.
The footer's Copy/Delete/Edit used ids 180-182, which no arm handled (the
100..=200 arm read them as rows 16-17) and which main's Backups arm
(170..=189) now takes first: they carry the selected code's row id
(130 + 3 * row + Copy 0 / Edit 1 / Delete 2) instead. Request.Post's
Content-Type is application/json; charset=utf-8.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:14:16 +01:00
Horizon agentandClaude Opus 5.5 7cb47be9fe The terrain model test builds its face with main's BakedFace::new
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:13:43 +01:00
Horizon agentandClaude Opus 5.5 c9af605b1f Drop the docs/parity run logs
Run logs are not docs; the PR body keeps the numbers.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:13:33 +01:00
Ludovic Marie d14e14539b Merge pull request #11419 from voidsipp/fix/round5-4-11335
🤖 Agent: The brewing stand's fuel slot and quick-move both read the brewing_fuel component of the stack, as BrewingStandMenu.FuelSlot and quickMoveStack do.
2026-11-07 11:13:32 +01:00
Horizon agentandClaude Opus 5.5 b7772e4a84 Merge origin/main into fix/folder-datapack-metadata
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:13:32 +01:00
Horizon agentandClaude Opus 5.5 199eaad7da Merge #11631 into #11652: one RENAMES table
#11631 adds saved_piece_id (PiecesContainer.load's RENAMES) to the same file; read_jigsaw_start now asks it instead of its own copy of the four old ids. The upper-case id case is dropped from the test: Identifier.parse refuses upper case, so vanilla never reads 'MINECRAFT:JIGSAW' as a jigsaw piece. Land #11631 first.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:13:04 +01:00
Horizon agentandClaude Opus 5.5 2d05c9517f Merge origin/main into #11480
command_exec_corpus.groups: keep main's mob_name_predicate_corpus import beside this branch's player_food_predicate_corpus one.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:11:02 +01:00
Horizon agentandClaude Opus 5.5 b6ab7d72b5 Native client services: friend world ids behind the friends feature
Main added invite_public_part (horizon_friends::invite) after this branch
was cut; without the feature the server build's clippy
(--no-default-features --all-targets) could not resolve it. A friend's
world then shows no part of the invite, and the invite test needs the
feature.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:10:50 +01:00
Horizon agent 725f9bb17d Merge remote-tracking branch 'origin/main' into HEAD 2026-11-07 11:10:25 +01:00
Horizon agentandClaude Opus 5.5 e3e18fc523 Drop the docs/parity run logs
Run logs are not docs; the PR body keeps the numbers.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:10:09 +01:00
Horizon agentandClaude Opus 5.5 c082361b4d Merge origin/main into fix/function-command-admission
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:10:09 +01:00
Horizon agentandClaude Opus 5.5 2860d47ffd Merge origin/main into #11666
Main's #11661 added LevelAccess::container_open and its test for the open copper chest; this branch keeps that name and its test and drops its own container_has_viewers, folding the copper golems' chests into GameLevel::container_open. The recheck_container_openers doc comments #11661 left stranded above container_open are back on their function.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:10:07 +01:00
Horizon agentandClaude Opus 5.5 a1ec90f7fc Stats: a joining player's old name-based file moves too
load_guest_records read stats/<uuid>.json directly, and on the server thread (the default single player) and on LAN and dedicated servers every player joins through it, so locateStatsFile only reached the in-frame path. The guest's own name goes to locate_player_json.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:08:59 +01:00
Horizon agentandClaude Opus 5.5 b5d3de5ea4 Studio: log why a recording failed to open
Both entry points dropped open_studio_replay's error, so a bad archive did
nothing with no trace.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:08:54 +01:00
Horizon agentandClaude Opus 5.5 db673e9b76 Merge main into #10383: keep the craft-menu and map-file ops beside map_carried and treasure map copies
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:08:30 +01:00
Horizon agentandClaude Opus 5.5 4d9d2cee62 Saved pieces: read an id without its namespace; move tag_json's doc back
Identifier.parse reads 'nvi' as minecraft:nvi, so RENAMES applies to a bare id too. The new saved_piece_id had been inserted between tag_json and its doc comment.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:08:17 +01:00
Ludovic Marie d8ae358b69 Merge pull request #11699 from Detimes/feat-10404-older-clients-3
🤖 Agent: older clients 26.1→1.20.5 behind accept_older_clients (off by default); a 26.3 client never reaches ClientSession or the components_before_1_21_6 rewrites; per-step unit tests + lan_bot joins one bot per release; doc of older_client fixed.
2026-11-07 11:07:55 +01:00
Horizon agentandClaude Opus 5.5 5564c36339 Merge main into #10384
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:07:24 +01:00
Horizon agentandClaude Opus 5.5 ed53fe24ad Drop the docs/parity run logs
Run logs are not docs; the PR body keeps the numbers.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:06:33 +01:00
Horizon agentandClaude Opus 5.5 9fc3a4f0fc Merge origin/main into fix/loot-mine-executor-presence
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:06:32 +01:00
Horizon agentandClaude Opus 5.5 0eba976485 Spanish: the four recipe transfer strings main added since
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:05:21 +01:00
MSedgeMC c19b0817a0 test: compare falling block landings
Add a direct parity suite for the landing branch of
FallingBlockEntity.tick: real falling entities into air, solid blocks,
water, plants, slabs, cobwebs and moving pistons, with concrete powder's
water contact and the anvil landing event behind --no-contact.
2026-11-07 10:05:19 +00:00
Horizon agent 28cb958bb2 Merge remote-tracking branch 'origin/main' into HEAD 2026-11-07 11:05:09 +01:00
Horizon agentandClaude Opus 5.5 3cb902bb21 Drop the docs/parity run logs
Run logs are not docs; the PR body keeps the numbers.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:04:50 +01:00
Horizon agentandClaude Opus 5.5 9f8ce3033d Merge origin/main into fix/camel-loaded-pose-timestamp
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:04:49 +01:00
Ludovic Marie 2535dd9715 Merge pull request #11584 from rdbtCVS/agent/rdbtcvs/dead-code-review
🤖 Agent: attributes only, no code removed; the 30 kept allow(dead_code) now carry a reason (19 native mod API). Windows/macOS-only code is the risk, CI's other platforms cover it.
2026-11-07 11:04:25 +01:00
Horizon agentandClaude Opus 5.5 9fa7986397 design-system: Extras and Controller pages draw the Blocks frame too
Extras moves onto settings_shell, which draws with graphics_style, so the
doc names the three settings screens that take the Blocks palette.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:03:49 +01:00
Ludovic Marie e9bd8be99b Merge pull request #11569 from rdbtCVS/agent/rdbtcvs/fewer-duplicate-deps
🤖 Agent: socket2 0.6 keeps every call direct.rs makes (set_reuse_address, the unix-only set_reuse_port, set_only_v6, connect_timeout), so the friends socket code needs no change; lock loses 11 packages.
2026-11-07 11:03:33 +01:00
Horizon agentandClaude Opus 5.5 7cad6a50ff Drop the docs/parity run logs
Run logs are not docs; the PR body keeps the numbers.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:03:19 +01:00
Horizon agentandClaude Opus 5.5 9712ebdde7 Merge origin/main into fix/effective-item-component-validation
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:03:18 +01:00
Florian ERNSTandCodex 70cedfb11a Route attribute commands through the selected guest body
Co-Authored-By: Codex <[email protected]>
2026-11-07 11:02:58 +01:00
Ludovic Marie 4972ede23a Merge pull request #11561 from rdbtCVS/agent/rdbtcvs/one-rust-version
🤖 Agent: one workspace rust-version (1.95, which horizon-mods already needed); the second commit is clippy's let-chain/as_chunks/is_multiple_of rewrites, all on unsigned values with non-zero divisors, so no behaviour moves.
2026-11-07 11:02:43 +01:00
Ludovic Marie e86cb3276b Merge pull request #11680 from kacpherek/fix/near-chunks-no-xray
🤖 Agent: near sections no longer dither over far terrain (fade() is 0 or lod::HELD), and the replaced tile still stays behind for lod::FADE through previous, so no hole opens; the test bound lowered to 1,000 matches the half that now come in whole.
2026-11-07 11:01:47 +01:00
Ludovic Marie 3e80662fbb Merge pull request #11621 from horizon-rust/fix/harness-reuse-finished-world
🤖 Agent: respawn_world_point and disconnect_crafting reuse generate/world only after a generate/finished marker, written after server.stop(); an old half-made world regenerates once.
2026-11-07 11:01:04 +01:00
Ludovic Marie a9e99c3385 Merge pull request #11620 from horizon-rust/fix/lunge-jab-tick-diagnostic
🤖 Agent: LocatorBarClient.arc now flags a catch-up tick only before the jab (tick < stabTick); a catch-up right before it still changes the jab's motion, which the harness compares with vanilla, so lunge_motion keeps its signal.
2026-11-07 11:00:58 +01:00
Ludovic Marie 31908392d0 Merge pull request #11527 from rdbtCVS/agent/rdbtcvs/agents-md-no-counts
🤖 Agent: drops the harness/probe/field counts from AGENTS.md and CONTRIBUTING.md; the commands and paths around them are unchanged, so nothing a reader runs moves.
2026-11-07 11:00:53 +01:00
Ludovic Marie 35e72b8da9 Merge pull request #11239 from NikitaMGrimm/screenshot-sequence
🤖 Agent: horizon-ctl screenshot-sequence takes repeatable frames: each waits for the GPU (30 s at most) and for the pack's centre-depth copy, and F2 is refused while a sequence runs. Lands after #11238.
2026-11-07 11:00:45 +01:00
Horizon agentandClaude Opus 5.5 63d0b51776 Drop the docs/parity run log
Run logs are not docs; the PR body keeps the numbers.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:00:08 +01:00
Horizon agentandClaude Opus 5.5 8917e8a9cb Merge origin/main into fix/llama-saved-temper
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:00:07 +01:00
Horizon agentandClaude Opus 5.5 3787a6b53c older_client: its doc names every release it now lets join
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 10:59:36 +01:00
Horizon agentandClaude Opus 5.5 ee026a522e Replace the damage type ids instead of extending them
by_registry outlives a world, so a pack's type stayed accepted by /damage in
the next world opened. Same as sync_enchantment_registries.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 10:59:32 +01:00
Horizon agentandClaude Opus 5.5 3933ed160b Flame culling under TAA: keep the doc to one paragraph, drop the run samples
The flame culling doc now says TAA uses the widened frustum; the run
notes and samples stay in the PR body (run logs are not docs).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 10:58:34 +01:00
Horizon agentandClaude Opus 5.5 8a83859133 Launcher: drop the Patch notes page's three strings from every language
The page is gone, so i18n's every_language_has_every_string_the_code_asks_for
failed on them.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 10:58:02 +01:00
Horizon agentandClaude Opus 5.5 36d9983718 Merge origin/main into launcher-notes-in-versions
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 10:58:01 +01:00
Horizon agentandClaude Opus 5.5 354ba4a38a Merge origin/main into fix/h097-loaded-air-first-tick
Keep main's fall_step(entities) signature with the air guard first;
keep main's vibration_tests module beside the new test module.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 10:57:58 +01:00
Horizon agentandClaude Opus 5.5 d306a4c171 Dropped items: skip the posed envelope whenever entity culling is off
entity_cull_decision_in_bounds returns Bypass for EntityCullingMode::Off
before reading the bounds, so the TAA condition only kept an unused walk.
Drop the docs/performance run notes (run logs are not docs).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 10:57:19 +01:00
Horizon agentandClaude Opus 5.5 b06f5f36b9 Merge origin/main into #11118
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 10:56:15 +01:00
Ludovic Marie 9319ad4bf1 Merge pull request #11356 from ilguerro/perf/mesh-padded-index
🤖 Agent: The mesher bakes face data once and reuses padded cell indices; run-parity on meshing passed, so the output is the same.
2026-11-07 10:55:19 +01:00
AkirabaneandClaude Opus 5.5 8e0b157254 dedicated: a stop waits for a proxy to move the players out first
With [transfer] on and players on, a stop (/stop, a stop signal, the
supervisor's stop or restart) now waits stop_wait_seconds (10 by default,
0 to 60, 0 stops at once as before) so a proxy told server/stopping can
move the players out first. The log says "Stopping in 10 s: a proxy may
move the players out first", which horizon-server takes as the start of
the stop. Every login is refused as server_shutdown, and the server ticks
on and answers the proxy's requests.

The wait ends as soon as no player is placed, at its time when no
hand-over is frozen, and at the latest a freeze's longest time later, so a
hand-over started in time finishes; the whole wait is cut at 90 s, within
the supervisor's 120 s for a stop, so stop_wait_seconds and
max_freeze_seconds may add up to 90 at most when the stop waits. No freeze
starts that this limit would cut: a player its source saved after the
target placed it would be on both servers. A freeze lasts ticks, so its
length is reckoned at the tick length of the moment (the rate's, or longer
on a server slower than its rate). A second stop ends the wait. Then the
stop goes on as vanilla's.

Refs #7484

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 10:51:02 +01:00
Ludovic Marie 411d1ca42c Merge pull request #11515 from horizon-rust/fix/mob-entity-collisions-10445
🤖 Agent: mobs path-stop at shulkers and a held-still happy ghast; earlier parity failures were env + main's /say regression (#11687 fixed it); CI parity reruns with run-parity before landing.
2026-11-07 10:50:43 +01:00
Alysia Zappavigna 4bb27d7c90 Merge remote-tracking branch 'upstream/main' into pr-11701 2026-11-07 20:48:42 +11:00
Arslan Armanuly 552f2c1b4b Merge main into #10538 (brings #11687's command_exec /say fix for parity) 2026-11-07 14:46:42 +05:00
Horizon agentandClaude Opus 5.5 765f388607 Merge main into #10493; take main's harness drivers
#11532 and main since make loaded_pig_tags, loot_mine_pot_components, mob_discard
and command_exec run under a plain parity.py call, which is what ccb85ce45 and
aae27797d did here. Main's versions replace them, so this PR keeps only its own change.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 10:44:51 +01:00
Horizon agentandClaude Opus 5.5 0007d5c2a8 Merge main into #10506; take main's harness drivers
#11532 and main since make loaded_pig_tags, loot_mine_pot_components, mob_discard
and command_exec run under a plain parity.py call, which is what ccb85ce45 and
aae27797d did here. Main's versions replace them, so this PR keeps only its own change.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 10:44:46 +01:00
Ludovic Marie 59aa0e53df Merge pull request #11690 from horizon-rust/fix/raid-wave-ticking-11374
🤖 Agent: raid wave positions (new and cached) now need an entity-ticking chunk in entity ticking range (ServerLevel.isPositionEntityTicking), not just a loaded one; raid, raid_village and raider_celebration harnesses passed.
2026-11-07 10:38:47 +01:00
Ludovic Marie 6920ab1e45 Merge pull request #11688 from horizon-rust/fix/hidden-effect-save-11384
🤖 Agent: the player save now writes and reads the recursive hidden_effect chain (Poison I under Poison II resumes after a reload); SavedEffect became a struct, guest data uses the same path; active_effects, effect_sync, player_effect_amplifier and status_effect harnesses passed.
2026-11-07 10:38:44 +01:00
Ludovic Marie 9bd77676c8 Merge branch 'main' into fix/mob-entity-collisions-10445 2026-11-07 10:38:40 +01:00
Ludovic Marie c3135f0196 Merge pull request #11274 from NikitaMGrimm/still-weather-at-level
🤖 Agent: HORIZON_STILL now settles a frozen world's rain, thunder and rain fog at the level instead of easing them; it only runs with that dev variable set.
2026-11-07 10:33:20 +01:00
Ludovic Marie 44abc057eb Merge pull request #11238 from NikitaMGrimm/pack-history-reset
🤖 Agent: The shader pack reset (both kinds) lands the in-flight frame before replacing targets, so the next frame draws as a fresh pack's first; dev tooling, no change without the reset command. #11239 follows it.
2026-11-07 10:33:15 +01:00
Ludovic Marie 111ba4fd45 Merge pull request #11197 from arslanarm/shaderpacks/shadow-settings-from-fragment-shaders
🤖 Agent: Shader pack shadow settings are now read from the stage that declares them (fragment const settings), with main's noiseTextureResolution kept on the formats stage; only shaders/ packs see a change.
2026-11-07 10:33:11 +01:00
Ludovic Marie 8a50b63daa Merge pull request #11117 from Delete228/native-still-fluid-render-upstream
🤖 Agent: Native mods can draw a still-fluid volume in a block; the fluid shading follows Ponder's FluidRenderHelper (MIT, credited in THIRD_PARTY_NOTICES) and nothing changes without a mod calling it.
2026-11-07 10:33:08 +01:00
Horizon agentandClaude Opus 5.5 073e47caed Merge origin/main into pvp-2/item-view
Picks up #11532 and 88a372f808 (four harnesses that failed under parity.py's defaults).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 10:32:55 +01:00
Horizon agentandClaude Opus 5.5 e36bbcfc63 Merge origin/main into pvp-2/gui-glint-lookup
Picks up #11532 and 88a372f808 (four harnesses that failed under parity.py's defaults).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 10:32:55 +01:00
Ludovic Marie afdeb72891 Merge pull request #11689 from horizon-rust/fix/raid-command-11372
🤖 Agent: /raid check now finds a raid still counting down (getRaidAt has no started filter), and /raid sound plays the horn only for "local" at volume 2 with the level random's seed; raid and raid_village harnesses passed.
2026-11-07 10:28:42 +01:00
Horizon agentandClaude Opus 5.5 f58b34cd26 Merge origin/main into #11443
#10523's settings gear now takes its width from the Recipes search box of
this layout; both Escape paths (mod_browser_back, close_transferred_recipe)
kept; one JEI notice for the sprites and the icon.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 10:27:14 +01:00
bioneismeandClaude Opus 5.5 6cc37bdcef /damage takes content mods' and world packs' damage types
The commands' minecraft:damage_type ids came from the vanilla export
alone, so /damage refused a type from a content mod or a world's data
pack with argument.resource.not_found. Vanilla's ResourceArgument looks
the id up in the server's registries, packs included.

build_registries now adds the damage catalog's types to the registry's
ids after the export is applied, keeping the export's tags.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 14:24:35 +05:00
Ludovic Marie e7bc612275 Merge pull request #11442 from GabrielNichols/codex/recipes-viewer-transfer
🤖 Agent: The viewer's transfer is a separate server-side path: it only moves ingredients the player already has, leaves grid and inventory untouched when one is missing or the inventory is full, and never teaches the recipe; the vanilla recipe book keeps its own path. Second of the Recipes series, after #10523.
2026-11-07 10:24:08 +01:00
Ludovic Marie 7ee4860304 Merge pull request #11111 from voidsipp/fix/round4-1-focused-text-hook
🤖 Agent: The focused text field is exposed to native mods before the character is inserted, so a hook can veto it; the hook fragments stamp cleanly on main. Main merged in (its ChatScreen is_draft argument added to the test).
2026-11-07 10:21:27 +01:00
536b3316bd fix: mob saddle and gear post equip vibrations at full volume
Vanilla LivingEntity.onEquipItem posts GameEvent.EQUIP (or UNEQUIP) at
the mob and plays the equip sound at volume 1.0. Horizon only did that
for dispensers, copper golems and the player's armor, so saddling a pig
did not activate a sculk sensor and pig/strider/horse saddles played at
0.5.

Interact, inventory, pickup and /item paths now post the event from the
mob, and saddle/harness/armor sounds play at 1.0.

Fixes #9956

Co-Authored-By: OpenCode <[email protected]>
Co-authored-by: monkeycode-ai <[email protected]>
Co-authored-by: monkeycode-ai <[email protected]>
2026-11-07 09:19:20 +00:00
Matt Van Horn 5c7efbc427 Send the single-player first view faster than 64 chunks per tick
A memory connection that has not reported the view loaded now sends
every ready view column in one batch, and the recorded quota is raised
to cover that send so the following tick is capped again. A socket
guest, a player who has already loaded, and a respawn stay on the
64-column quota. Unlit columns stay withheld, the far ring still spends
only the leftover vanilla quota, and the gates that send nothing are
unchanged.

Fixes #6763
2026-11-07 01:18:24 -08:00
Ludovic MarieandClaude Opus 5.5 5e90d9f10b Merge origin/main into #11411; the credits test's Joined takes main's configured field
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 10:18:16 +01:00
Horizon agentandClaude Opus 5.5 faa8a4a52a Merge origin/main into #11413; the glide test's Joined and LanHost take main's new fields
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 10:17:40 +01:00
Ludovic Marie 6c0de3509d Merge remote-tracking branch 'origin/main' into w-11413 2026-11-07 10:17:26 +01:00
Horizon agentandClaude Opus 5.5 70ac0a01b8 Merge origin/main into #11457; the silent TNT test passes main's entity boxes (none)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 10:16:57 +01:00
Ludovic Marie 85a5c9f340 Merge remote-tracking branch 'origin/main' into w-11457 2026-11-07 10:16:57 +01:00
Ludovic Marie 67902962fd Merge pull request #11115 from Delete228/native-client-update-fields-upstream
🤖 Agent: Transient block entity fields from the update tag reach the native API without being saved; only mods that ask for them see anything new. Main merged in (its new Joined field added to the test).
2026-11-07 10:16:43 +01:00
Ludovic Marie 8aa99b4f64 Merge pull request #11692 from horizon-rust/perf/apple-pcore-workers
🤖 Agent: only the automatic count changes, and only where hw.perflevel0.physicalcpu answers (Apple silicon): M4 Pro 13 → 9 workers with no loading or pacing regression over 3 alternating rounds; off Apple the count is unchanged (tested on Linux), and a manual Worker threads setting still wins.
2026-11-07 10:16:39 +01:00
Ludovic Marie a93c0e1f11 Merge pull request #11687 from horizon-rust/fix/say-capture-broadcast
🤖 Agent: #10015's early return dropped /say from chat_capture, so command_exec lost 26 announcement lines; the broadcast now skips only the acting guest (lan_acting), whose copy stays in the capture in order. Console and command-block paths are unchanged. command_exec PASS fresh on the desktop; the control on main 004a39be0 FAILs 16320/16346.
2026-11-07 10:16:35 +01:00
Ludovic Marie 7615a429d3 Merge pull request #11684 from horizon-rust/fix/tnt-cart-impact-11380
🤖 Agent: a head-on stop on a straight rail no longer blows a TNT cart up (Entity.move zeroes the blocked axis and OldMinecartBehavior never re-applies it), and a glancing hit off its rails now does; tnt_saved_power, tnt_data_adoption and both vehicle harnesses passed.
2026-11-07 10:16:31 +01:00
Horizon agentandClaude Opus 5.5 e533502260 Merge origin/main into #11111; the test's ChatScreen takes main's is_draft
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 10:16:27 +01:00
Ludovic Marie b8206893b2 Merge remote-tracking branch 'origin/main' into w-11111 2026-11-07 10:16:27 +01:00
Detimes 81c1cd8b33 Merge remote-tracking branch 'upstream/main' into via-3 2026-11-07 09:15:38 +00:00
Ludovic Marie f4858e8a25 Merge pull request #11186 from flexeykinDev/install-ps1-signature
🤖 Agent: install.ps1 now verifies the release signature with the release-keys.txt key, the same check install.sh already does.
2026-11-07 10:12:13 +01:00
Ludovic Marie 261a343e75 Merge pull request #11428 from Akirabane/feat/transfer-refuse
🤖 Agent: The player-expect move now refuses a full server like a normal join would, and operators still bypass it as vanilla's PlayerList.canBypassPlayerLimit does.
2026-11-07 10:12:09 +01:00
Ludovic Marie 5798723345 Merge branch 'w-11238' into w-11239 2026-11-07 10:12:03 +01:00
Horizon agentandClaude Opus 5.5 37f3271472 Merge origin/main into #11238; the reset tests use main's NativeFrame api and NativePost tonemap arguments
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 10:11:37 +01:00
Ludovic Marie d887b1331d Merge pull request #11358 from NikitaMGrimm/hdr-step3-float-frame
🤖 Agent: The float HDR frame is only used with HORIZON_HDR_FRAME=float, so the default frame format and its cost are unchanged.
2026-11-07 10:11:24 +01:00
Ludovic Marie 2fcdb9f7af Merge remote-tracking branch 'origin/main' into w-11238 2026-11-07 10:11:21 +01:00
Horizon agentandClaude Opus 5.5 4aaf71dc39 Merge origin/main into #11411; main's spectator portal trips are not a credits respawn
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 10:10:54 +01:00
Ludovic Marie 6a3359d943 Merge remote-tracking branch 'origin/main' into w-11411 2026-11-07 10:10:44 +01:00
Horizon agentandClaude Opus 5.5 6397e50017 Merge origin/main into #11116; main's block_meets_player names its entity a Player
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 10:10:20 +01:00
Ludovic Marie fe36131256 Merge remote-tracking branch 'origin/main' into w-11116 2026-11-07 10:10:11 +01:00
Horizon agentandClaude Opus 5.5 99809b5a55 Merge origin/main into #11115; the new test's Joined takes main's configured field
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 10:09:44 +01:00
Ludovic Marie bc472639ed Merge remote-tracking branch 'origin/main' into w-11115 2026-11-07 10:09:36 +01:00
Ludovic Marie 7ebad255b0 Merge pull request #11107 from Detimes/feat-10404-older-clients-2
🤖 Agent: 26.2 packet layouts are only used inside a session that negotiated V26_2, and accept_older_clients stays off by default, so a 26.3 client's path is unchanged; the lan_bot tests cover both.
2026-11-07 10:08:01 +01:00
Ludovic Marie 1faa3281c2 Merge pull request #10523 from GabrielNichols/codex/recipes-inventory-settings
🤖 Agent: The Recipes settings button opens from the inventory browser; the JEI-derived icon is MIT and credited in THIRD_PARTY_NOTICES. First of the Recipes series (#11442 → #11453 follow).
2026-11-07 10:07:20 +01:00
TheMrZZandCodex gpt-6.1-sol 50467de40e fix: match typed custom data in mob equipment predicates
Project only typed custom-data snapshots from existing mob equipment
stacks and evaluate the admitted SNBT subset predicate. Preserve prior
view fields and unsupported unprojected-consumer behavior.

Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-11-07 10:05:55 +01:00
Horizon agentandClaude Opus 5.5 a7f5e08990 rustfmt the merge resolution
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 10:05:52 +01:00
Horizon agentandClaude Opus 5.5 9d1c979e91 Merge #11117 (with main) into #11118
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 10:00:02 +01:00
Florian ERNSTandCodex d4d2214cfb Bind loaded Warden memories to their runtime slots
Co-Authored-By: Codex <[email protected]>
2026-11-07 09:59:36 +01:00
Horizon agentandClaude Opus 5.5 b00d753c62 Merge origin/main into #11117
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 09:59:20 +01:00
Ludovic Marie 7ab92f9eb7 Merge pull request #11683 from horizon-rust/fix/command-oracle-light-wait
🤖 Agent: only reinforcement_room uses the oracle's light op, so the wait (ThreadedLevelLightEngine.waitForPendingTasks, ticking like settle in a frozen world) changes no other harness's records; command_reinforcements --fresh --repeat 3 PASS 3/3 at desktop load 40-48, where main failed 7 of 10.
2026-11-07 09:58:28 +01:00
Horizon agentandClaude Opus 5.5 3aaef65c5d Merge origin/main into #11121
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 09:58:25 +01:00
Horizon agentandClaude Opus 5.5 278ffc3b96 Merge #11238 (with main) into #11239
take_screenshot returns main's Option<PathBuf>; a running sequence still
refuses F2 (None).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 09:57:30 +01:00
Horizon agentandClaude Opus 5.5 7cfee48c36 Merge origin/main into #11238
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 09:56:45 +01:00
Horizon agentandClaude Opus 5.5 5c18aa614e Merge origin/main into #11253
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 09:56:12 +01:00
Horizon agentandClaude Opus 5.5 f7ba58871b Merge origin/main into #11256
Main's Clicker already posts the use-start vibration; take its version.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 09:55:32 +01:00
Horizon agentandClaude Opus 5.5 017aee7bcf Drop the run notes from the PR's docs
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 09:54:50 +01:00
Horizon agentandClaude Opus 5.5 09ec3f2452 Merge origin/main into #11258
Keep the loot position test module beside main's mine component tests.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 09:54:49 +01:00
Horizon agentandClaude Opus 5.5 8667fab8c9 Merge origin/main into #11274
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 09:54:04 +01:00
Horizon agentandClaude Opus 5.5 d89ca171f3 Drop the run notes from the PR's docs
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 09:53:31 +01:00
Horizon agentandClaude Opus 5.5 59f6b76699 Merge origin/main into #11296
The saddle slot (main) is read before the PR's equipment stacks; both test
modules kept.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 09:53:30 +01:00
Horizon agentandClaude Opus 5.5 5fe63f13e8 Merge origin/main into #11403
Keep the shriek capture test module beside main's command damage tests.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 09:52:38 +01:00
Horizon agentandClaude Opus 5.5 bbe57b6b19 Merge origin/main into #11419
Keep the brewing fuel test beside main's new merchant test.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 09:51:56 +01:00
Horizon agentandClaude Opus 5.5 8368732e4e Merge origin/main into #11425
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 09:50:55 +01:00
Horizon agentandClaude Opus 5.5 a26743d278 Merge origin/main into #11444
Main's thorns check counts events from before the hurt; the PR's helmet
path runs after that count like the plain one.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 09:50:13 +01:00
Horizon agentandClaude Opus 5.5 e8edf93a48 Merge origin/main into #11457
splash takes both the PR's silent flag and main's hit motion (#11388).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 09:49:08 +01:00
Ludovic Marie 0876de55cc Merge pull request #9974 from daniliammo/fix/launcher-prune-nightlies
🤖 Agent: code-owner approval given; pruning only touches marker-carrying Nightlies, manual/legacy ones are outside the quota (tests cover all four cases).
2026-11-07 09:47:56 +01:00
Ludovic Marie 6f01443e55 Merge pull request #11551 from horizon-rust/fix/11519-checkerboard-negative-scale
🤖 Agent: vanilla's CheckerboardColumnBiomeSource sets bitShift = size + 2 with no clamp, and Java's >> masks the distance; wrapping_add(2) as u32 with wrapping_shr does the same. biome_source PASS with fresh Java on the desktop (main + this head).
2026-11-07 09:39:35 +01:00
Horizon agentandClaude Opus 5.5 dce6093276 Merge origin/main into #11414
Keep the mining experience cases alongside main's survival-only switch.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 09:36:43 +01:00
Horizon agentandClaude Opus 5.5 2e41b259c2 Merge main into #11197 (noiseTextureResolution kept beside the stage-aware reads)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 09:33:55 +01:00
Ludovic Marie e255c1b2fa Merge pull request #9967 from TheMrZZ/fix/h324-craft-map-swap
🤖 Agent: a map crafted from the grid gets Item.onCraftedBy on the taken stack and the scaling recipe reads the level's maps; merged with main's merchant on_take; craft_map_take and merchant_crafted pass.
2026-11-07 09:33:52 +01:00
Horizon agentandClaude Opus 5.5 b8a6663a3a Drop the run report from docs/
The evidence stays in the PR description.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 09:31:02 +01:00
Horizon agentandClaude Opus 5.5 05a81f37b5 Drop the run report from docs/
The evidence stays in the PR description.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 09:30:32 +01:00
Horizon agentandClaude Opus 5.5 e8d2cf4ee2 Drop the run report from docs/
The evidence stays in the PR description.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 09:29:54 +01:00
Horizon agentandClaude Opus 5.5 199ef4ad95 Restore the docs the previous commit removed by mistake
Only the PR's own run notes were meant to go.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 09:29:19 +01:00
Horizon agentandClaude Opus 5.5 f2658dff92 Restore the docs the previous commit removed by mistake
Only the PR's own run notes were meant to go.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 09:28:54 +01:00
Horizon agentandClaude Opus 5.5 07b7935bae Drop the run notes from the PR's docs
Validation and benchmark logs belong in the PR description, not in docs/.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 09:28:15 +01:00
Horizon agentandClaude Opus 5.5 b9bc67509a Drop the run notes from the PR's docs
Validation and benchmark logs belong in the PR description, not in docs/.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 09:27:42 +01:00
Horizon agentandClaude Opus 5.5 f834560860 Drop the unreachable second return in minecart_numeric_fields
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 09:26:59 +01:00
Horizon agentandClaude Opus 5.5 70f9c6e829 Keep eye_columns' doc comment on eye_columns
The new touching_unloaded_chunk was inserted between eye_columns and its
doc comment, which then documented the wrong function.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 09:26:21 +01:00
Ludovic Marie 1e35443f6e Merge pull request #11532 from horizon-rust/fix/11520-harness-plain-run
🤖 Agent: dropping the --reference-only assertion makes a plain run go through runner.main()'s full Java/native comparison, and then the strict tags/pot report, so CI now compares instead of exiting in 0 s. mob_discard's default --resources is lab.RESOURCES (the prepared 26.3 folder) and it gets the missing import os. The two sound harnesses take main's #11555 version.
2026-11-07 09:23:03 +01:00
Ludovic Marie 5075231ffa Merge pull request #10031 from TheMrZZ/fix/h326-boat-live-leash
🤖 Agent: data get on a leashed boat now reads its leash from the same save path the world writes (write_boat_leash); merged main twice, boat_leash_view harness passes and the u/3 union builds with clippy -D warnings.
2026-11-07 09:22:59 +01:00
Ludovic Marie 004a39be07 Merge pull request #11670 from horizon-rust/fix/breeding-bottle-xp-11388
🤖 Agent: breeding now adds one fresh orb of the whole 1-7 roll (no award split, no merge), and XP bottle orbs start 0.25 along the hit face's normal with their motion turned away from it; orb_lava/orb_water/orb_saved_uuid/bed_explosion harnesses passed.
2026-11-07 09:17:36 +01:00
Ludovic Marie d4dc032645 Merge pull request #10024 from some-du6e/optifine/sky-draw
🤖 Agent: OptiFine custom skies draw their layers over the vanilla sky, only when a pack ships optifine/sky and the Horizon sky option is on (a frame without one only clears an empty list); merged main, clippy and the sky tests pass.
2026-11-07 09:17:32 +01:00
MSedgeMC 1bc23d7a17 test: compare nether portal frame search
The portal parity suite only sent entities through portals: the PortalShape
search that turns an obsidian frame into portal blocks was never exercised.
Add a shape family over real frames and a --no-shape switch that reproduces
the previous corpus byte for byte.
2026-11-07 08:16:39 +00:00
Horizon agentandClaude Opus 5.5 81261cc98d Apple silicon: size the chunk worker pool from the performance cores
The automatic pool size was every core but one. On Apple silicon the
efficiency cores count too, so a base M4 (4 performance + 6 efficiency
cores) ran 9 workers that kept the render and server threads waiting for a
performance core: fast flight's p99 frame went from 20 ms to 7 ms with 3
workers (#11294). The automatic count now leaves one of the performance
cores (hw.perflevel0.physicalcpu) instead; elsewhere it is unchanged, and
Graphics > Worker threads still overrides it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 09:14:33 +01:00
Ludovic Marie 36fc4fa187 Merge pull request #9991 from MSedgeMC/fix/can-be-a-baby
🤖 Agent: a negative Age makes a baby only where AgeableMob.canBeABaby allows (frog, parrot, zombie nautilus, camel husk, slime, magma cube stay adults: box, voice, loot); ageable_integer_load, entity_death_loot and baby_pig_attachment pass.
2026-11-07 09:13:51 +01:00
Ludovic Marie 9ab09b8ed4 Merge pull request #10032 from masa-masa-0314/feat/realms-reset-world
🤖 Agent: Realms reset-world, templates and upload, stacked on #9982 (land after it); upload goes only to the endpoint the service returns; realms tests pass.
2026-11-07 09:13:47 +01:00
Ludovic Marie 92b2dbfbc7 Merge pull request #9982 from masa-masa-0314/feat/realms-backups
🤖 Agent: Realms backup list and backup info screens on main's Realms code (only the backups commit kept; the carried settings/invite copies already landed); restore asks the service and re-reads the list; realms tests pass.
2026-11-07 09:13:43 +01:00
Ludovic Marie b3eee8adef Merge pull request #10130 from TheMrZZ/fix/guest-advancement-selectors
🤖 Agent: advancements= in a selector reads the selected player's own progress, a guest's included (EntitySelectorOptions on the ServerPlayer); guest_advancement_selector passes on the merge, now runnable by parity.py.
2026-11-07 09:10:49 +01:00
Horizon agentandClaude Opus 5.5 542f14f3bf Merge the rebuilt backups branch: reset, templates and upload on top
The two reset-world commits rebuilt on the backups branch (both PRs extend the
same configure screen): the scroll wheel covers the product page, template and
upload lists, and the worlds tab has Options and Backup side by side with
Reset below.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 09:10:28 +01:00
Horizon agentandClaude Opus 5.5 bdb927a7c4 Merge main: the backups commit rebuilt on main
This branch also carried early copies of the settings-tab and invite-code
commits, which landed on main as #9926 and #9968 and changed since. The tree
is main with only the backups commit on it, plus the configure screen's
tier fields that main added.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 09:10:27 +01:00
Alysia Zappavigna e297f7245b ambience tests: import SoundEngine 2026-11-07 19:09:01 +11:00
Horizon agentandClaude Opus 5.5 3788883346 /say in a captured command: the acting player gets it with the output
#10015 broadcast /say from cmd_say to every guest and dropped the line
from chat_capture. The acting guest still saw it, but the command
harness reads the capture, so command_exec lost every /say line
("[Player] relative dimension", the h154 function's "[Pig] H154
fallthrough") and failed parity-main at ebcfa2263a.

When a guest's command is captured, the broadcast now skips that guest
and the line stays in the capture, in order with the other output, which
is delivered to them as before. The console path is unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 09:09:00 +01:00
Ludovic Marie 62a480890c Merge pull request #11679 from horizon-rust/docs/roadmap-drop-alt-accounts
🤖 Agent: roadmap.toml is the source; ROADMAP.md and roadmap.json regenerated (build --check passes). older-servers kept on purpose (1.8 PvP servers), only the sign-in item goes.
2026-11-07 09:08:00 +01:00
Alysia Zappavigna 640dd0bcf5 ambience tests: import SoundEngine 2026-11-07 19:05:56 +11:00
Horizon agentandClaude Opus 5.5 c4a240cb63 Roadmap: drop alt-accounts (other sign-in providers)
Mojang's approval covers Microsoft sign-in only; other account providers
(Ely.by, Yggdrasil servers) are against the rules Horizon plays by, so the
item can't ship. #1270 stays closed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 09:05:10 +01:00
Horizon agentandClaude Opus 5.5 7ea951a07e Merge main: mob_local_sound and ominous_warning_sound already run plainly (#11555)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 09:04:53 +01:00
Horizon agentandClaude Opus 5.5 2f548e8b23 Run the raid command test with an operator's permission
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 09:04:27 +01:00
Horizon agentandClaude Opus 5.5 64d97f7db8 Command oracle: read light once the queued light updates ran
ThreadedLevelLightEngine runs the checkBlock tasks a /fill queues at most
1000 per update. command_reinforcements fills ~100k blocks around its room
and then requires block and sky light 0 there; on a loaded runner the
vanilla side still read the old light (7, 9, 10 at different positions),
which failed parity-main and 7 of its bisect steps back to 11-05 merges.
The light op now waits for waitForPendingTasks on that chunk, ticking as
settle does.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 09:03:33 +01:00
Ludovic Marie 39e13b9f70 Merge pull request #11556 from horizon-rust/fix/remote-server-errors-i18n
🤖 Agent: follow-up to #10166: the remote server's SSH errors go through tr/trf with named placeholders, English stand-ins in all 86 lang files.
2026-11-07 09:02:23 +01:00
Ludovic Marie 90bdc031fe Merge pull request #11557 from horizon-rust/fix/macos-titlebar-without-glass
🤖 Agent: follow-up to #10129: with glass off, macOS still gets its 32 px title bar (plain side colour), so content no longer sits under the traffic lights.
2026-11-07 09:02:20 +01:00
Ludovic Marie b66a4683f1 Merge pull request #11617 from horizon-rust/fix/fullscreen-unmaximize-windows-only
🤖 Agent: follow-up to #10177: the unmaximize-before-fullscreen step runs on Windows only, and apply() no longer re-requests fullscreen each call while the window reports maximized.
2026-11-07 09:02:17 +01:00
Horizon agent cdd4332ae4 Merge remote-tracking branch 'origin/main' into n/11556
# Conflicts:
#	launcher/lang/af.json
#	launcher/lang/ar.json
#	launcher/lang/az.json
#	launcher/lang/ba.json
#	launcher/lang/be.json
#	launcher/lang/bg.json
#	launcher/lang/br.json
#	launcher/lang/bs.json
#	launcher/lang/ca.json
#	launcher/lang/cs.json
#	launcher/lang/cv.json
#	launcher/lang/cy.json
#	launcher/lang/da.json
#	launcher/lang/de.json
#	launcher/lang/el.json
#	launcher/lang/eo.json
#	launcher/lang/es.json
#	launcher/lang/et.json
#	launcher/lang/eu.json
#	launcher/lang/fa.json
#	launcher/lang/fi.json
#	launcher/lang/fil.json
#	launcher/lang/fo.json
#	launcher/lang/fr.json
#	launcher/lang/fur.json
#	launcher/lang/fy.json
#	launcher/lang/ga.json
#	launcher/lang/gd.json
#	launcher/lang/gl.json
#	launcher/lang/haw.json
#	launcher/lang/he.json
#	launcher/lang/hi.json
#	launcher/lang/hr.json
#	launcher/lang/hu.json
#	launcher/lang/hy.json
#	launcher/lang/id.json
#	launcher/lang/ig.json
#	launcher/lang/is.json
#	launcher/lang/it.json
#	launcher/lang/ja.json
#	launcher/lang/ka.json
#	launcher/lang/kk.json
#	launcher/lang/kn.json
#	launcher/lang/ko.json
#	launcher/lang/ky.json
#	launcher/lang/la.json
#	launcher/lang/lb.json
#	launcher/lang/li.json
#	launcher/lang/lmo.json
#	launcher/lang/lo.json
#	launcher/lang/lt.json
#	launcher/lang/lv.json
#	launcher/lang/mk.json
#	launcher/lang/mn.json
#	launcher/lang/ms.json
#	launcher/lang/mt.json
#	launcher/lang/nl.json
#	launcher/lang/no.json
#	launcher/lang/oc.json
#	launcher/lang/pl.json
#	launcher/lang/pt-br.json
#	launcher/lang/pt.json
#	launcher/lang/ro.json
#	launcher/lang/ru.json
#	launcher/lang/sah.json
#	launcher/lang/se.json
#	launcher/lang/sk.json
#	launcher/lang/sl.json
#	launcher/lang/so.json
#	launcher/lang/sq.json
#	launcher/lang/sr.json
#	launcher/lang/sv.json
#	launcher/lang/szl.json
#	launcher/lang/ta.json
#	launcher/lang/th.json
#	launcher/lang/tr.json
#	launcher/lang/tt.json
#	launcher/lang/uk.json
#	launcher/lang/ur.json
#	launcher/lang/uz.json
#	launcher/lang/vec.json
#	launcher/lang/vi.json
#	launcher/lang/yi.json
#	launcher/lang/yo.json
#	launcher/lang/zh.json
#	launcher/lang/zu.json
2026-11-07 08:59:28 +01:00
Horizon agent 4835c74731 Merge remote-tracking branch 'origin/main' into n/9967
# Conflicts:
#	crates/horizon-client/src/menu.rs
#	tools/test_command_exec_parity.py
2026-11-07 08:58:08 +01:00
Horizon agent de976257da Merge remote-tracking branch 'origin/main' into n/10031
# Conflicts:
#	tools/test_command_exec_parity.py
2026-11-07 08:57:25 +01:00
Alysia Zappavigna 175fbe9e52 Merge commit '37d8dc0e5' into random-source
# Conflicts:
#	crates/horizon-client/src/game/lightning.rs
2026-11-07 18:56:19 +11:00
Horizon agentandClaude Opus 5.5 33f6584496 guest_advancement_selector harness: run both sides by default
parity.py runs a harness with no arguments; --side was required, so every
automatic selection failed it at once. The default now runs Java, then native
against Java's facts.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 08:56:10 +01:00
Alysia Zappavigna fde182860f random_source: repeat under HORIZON_STILL; tidy imports 2026-11-07 18:56:10 +11:00
Kacpherek 7386e3968f Draw near sections whole over far terrain: their dither showed the caves inside 2026-11-07 08:50:34 +01:00
Horizon agentandClaude Opus 5.5 9478067990 Spawn raid waves only where entities tick
Raid.findRandomSpawnPos and the cached wave position both ask
ServerLevel.isPositionEntityTicking: an entity-ticking chunk in entity
ticking range. Horizon accepted any loaded chunk, so a wave could spawn
past the simulation distance, where its raiders would never tick.

Part of #11374 (the spawn positions; the raid/spawner order stays open).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 08:48:52 +01:00
Ludovic Marie 37d8dc0e56 Merge pull request #11661 from horizon-rust/fix/copper-chest-viewers-11378
🤖 Agent: a copper chest someone has open now skips its weathering step (WeatheringCopperChestBlock.randomTick checks getEntitiesWithContainerOpen); random_tick and random_tick_sections harnesses passed.
2026-11-07 08:48:46 +01:00
Horizon agentandClaude Opus 5.5 d12e6637f0 Let the raid command test reach the raids
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 08:47:14 +01:00
Alysia Zappavigna 3fc0eb473e random_source: one RandomSource.create(); entity and level randoms as vanilla 2026-11-07 18:46:43 +11:00
Florian ERNSTandCodex f9eb279790 Bind actual mob target state in loot-kill predicates
Co-Authored-By: Codex <[email protected]>
2026-11-07 08:46:18 +01:00
Ludovic Marie 1f205c93df Merge pull request #11022 from ilguerro/fix/9776-block-vibrations
🤖 Agent: using items (fishing rod, ender eye…) now posts vanilla's item-use game events through causeUseVibration, so sculk sensors react as in Java; sculk, server_player_sculk, server_wool_vibration and use_item_on pass against the 26.3 oracle in the gate's harness run (242 selected; failures were host setup only plus the lunge_motion timing flake).
2026-11-07 08:42:05 +01:00
Ludovic Marie 66b8b9e69c Merge pull request #11017 from TheMrZZ/fix/h101-merchant-crafted-stat
🤖 Agent: crafting with a merchant result slot now fires vanilla's merchant crafted trigger and updates the trade, checked by the new --merchant-crafted-fixture in command_exec; merchant_crafted, command_exec and trading pass against the 26.3 oracle in the gate's harness run (242 selected; failures were host setup only plus the lunge_motion timing flake).
2026-11-07 08:42:02 +01:00
Ludovic Marie 17995c544f Merge pull request #10837 from TheMrZZ/fix/non-mob-saved-mob-fields
🤖 Agent: item entities, armor stands and boats now save the shared Entity flags (Invulnerable, CustomNameVisible, Silent, NoGravity, Glowing, HasVisualFire, Tags) as vanilla's saveWithoutId does. Its 242 selected harnesses ran on main + this head + #11017 + #11022: save_roundtrip, world_reopen, entity_nbt_boolean, the armor stand and boat ones pass; the only failures are setup (no sound/lang assets on the host, harness args) and the known lunge_motion tick-timing flake under software rendering.
2026-11-07 08:41:57 +01:00
Horizon agentandClaude Opus 5.5 31338299d6 Format the orb tests; give the orb merge fixture merge_new_orb
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 08:36:45 +01:00
Horizon agentandClaude Opus 5.5 e2d24c60a4 Detonate a TNT minecart from the speed left after its move
MinecartTNT.tick reads the delta after AbstractMinecart.tick. The move
zeroes the blocked axes and no track step puts them back, so a cart
stopped head-on by a wall on a straight rail does not blow up, while one
glancing off a wall keeps its free axis and does, on or off its rails.
Horizon kept the speed the cart asked for before the move on the rail
paths and none off them.

Part of #11380 (the impact detonation; the team push points stay open).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 08:28:43 +01:00
Ludovic Marie 648f935466 Merge pull request #11657 from horizon-rust/fix/bee-hive-11377
🤖 Agent: overflow honey now leaves through Player.drop (eye height − 0.3, thrown along the view, pickup delay 40) instead of popping at the feet; held_drop harness passed, plus Game tests for Peaceful anger and event 2011.
2026-11-07 08:26:33 +01:00
exampleandClaude Opus 5.5 69ca4f94b7 Waxing a double copper chest reaches both halves; an open one doesn't weather
HoneycombItem.useOn gives a double chest's other half its game event and
particles and plays a second sound from the clicked block, on the server and
in the client's prediction. The wax sound leaves out the acting player and
goes out in the blocks source.

WeatheringCopperChestBlock.randomTick skips the attempt, and its draw, while
getEntitiesWithContainerOpen has anyone: a player's menu, or a copper golem
(CopperGolem.hasContainerOpen), a double chest counting for both halves.

Fixes #11378

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 08:24:54 +01:00
Horizon agentandClaude Opus 5.5 23d08068aa Keep a player's hidden effects in the save
MobEffectInstance.Details nests the instance an effect hides under
hidden_effect, and the player save writes the whole chain. Horizon wrote
only the top instance, so a weaker, longer effect (Poison I under
Poison II) was gone after a reload instead of resuming. SavedEffect is
now a struct carrying that chain; guest player data goes through the
same code.

Part of #11384 (the player side; mobs still drop weaker instances).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 08:24:54 +01:00
Cx188andCodex 4f0045d2d0 Notify native mods when chat suggestions are applied
Let native mods keep shared-item text aligned when a player accepts or
cycles chat suggestions. Report the original input, UTF-16 replacement
range, selected suggestion and resulting text. Cover keyboard and mouse
handling, server suggestions, emoji and length limits.

Part of #11212.

Co-Authored-By: Codex (GPT-6) <[email protected]>
2026-11-07 10:24:13 +03:00
Ludovic Marie 6ab56ba9f1 Merge pull request #11600 from horizon-rust/fix/audio-probe-off-render-thread-11490
🤖 Agent: the device list is now read on a worker thread, so a slow Windows audio driver no longer stalls a frame; the render thread only swaps in the result. No vanilla behaviour changes, no harness covers it.
2026-11-07 08:23:53 +01:00
Horizon agentandClaude Opus 5.5 d193cb15ca Make /raid check find a raid counting down; horn only for "local"
RaidCommand.getRaid is getRaidAt, which returns a raid that has not
started its first wave yet. playSound plays the horn only for the text
"local", at volume 2 with the level random's seed. The status line
concatenates its floats the Java way (20.0, not 20).

Fixes #11372.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 08:21:25 +01:00
Ludovic Marie cc7f3b4c22 Merge pull request #11648 from horizon-rust/fix/effect-show-icon-11385
🤖 Agent: effects now carry vanilla's showIcon apart from the particles (5-arg constructor, update, save codec default, packet bit 4), so warden Darkness and /effect hideParticles no longer show a HUD icon; 8 effect/HUD/sculk harnesses pass.
2026-11-07 08:18:46 +01:00
Florian ERNSTandCodex b322275555 Save Camel inherited horse fields from modeled state
Co-Authored-By: Codex <[email protected]>
2026-11-07 08:11:46 +01:00
Horizon agentandClaude Opus 5.5 b1eaf67925 One fresh orb from breeding; bottle orbs start away from the hit
- Animal.finalizeSpawnChildFromBreeding adds one new ExperienceOrb of the
  parent's 1-7 roll: breeding no longer splits it (4 into 3 + 1) or merges.
- ThrownExperienceBottle.onHit awards with direction: away from the block
  face it hit, or back along its motion after an entity hit; the new orb
  starts 0.25 that way and a random motion against it is turned round
  (ExperienceOrb(level, pos, roughly, value)) (#11388).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 08:06:24 +01:00
TheMrZZandCodex gpt-6.1-sol b8d3a3b132 fix: keep vanilla crafter drops unnamed
Exclude actual crafters from automatic dropped-item name capture while
preserving chest/shulker names, contents and the other copy policies.
Cover the real destruction route against a frozen fresh Java reference.

Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-11-07 07:53:50 +01:00
Ludovic Marie ebcfa2263a Merge pull request #11637 from horizon-rust/fix/lightning-fire-radius-11376
🤖 Agent: lightning now places fire (and draws its extra-source offsets) only when canSpreadFireAround passes at the bolt, so fire_spread_radius_around_player holds for strikes far from every player.
2026-11-07 07:40:30 +01:00
Horizon agentandClaude Opus 5.5 185187e7e2 Don't weather a copper chest someone has open
WeatheringCopperChestBlock.randomTick runs changeOverTime only when the
chest's getEntitiesWithContainerOpen is empty, so an open chest neither
oxidizes nor draws its odds (part of #11378).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 07:30:45 +01:00
HiuoyandClaude Opus 5.5 383f733f9e Port pack: villager and wandering trader trade events are data in 26.x
`horizon-port-pack events` gives a fifth answer, data: NeoForge's
`VillagerTradesEvent` and `WandererTradesEvent` are gone since vanilla 26.1
made trades the `trade_set` and `villager_trade` registries (NeoForge's 26.1
primer), so a port writes trades as data instead of waiting for a hook.
On ATM10 that answers 33 uses (20 and 13 mods).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 10:29:37 +04:00
yahya-nusseibehandClaude Opus 5.5 b984398cbc Seal the launcher's secrets file with a per-install secret
Without a keychain, account-secrets.json was sealed under SHA-256 of a
constant and /etc/machine-id, which every local user can read; on a
system with none of the machine-id files the key was a constant built
into every launcher. Only the file's 0600 mode protected the Microsoft
refresh token inside, so a copy restored with wider permissions, or a
shared home, gave it away.

Format 2 (HZSECv2) adds a random 32-byte install secret to the key,
kept beside the file in account-secrets.key (0600, made at the first
write through a temporary file and a rename, under the file's lock).
The key is never the bare constant any more, and a v2 file whose
secret is missing or belongs to another install is refused like any
unreadable file, never written over.

A file sealed by an older launcher (HZSECv1) is still read with the old
key and sealed again under the new one at once.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
2026-11-07 08:20:48 +02:00
Ludovic Marie 7127fb8ed1 Merge pull request #10065 from ilguerro/fix/thorns-victim
🤖 Agent: merged main again (#10015's lan_e2e check landed beside this one's); the Thorns change itself is the head the mace harness passed on.
2026-11-07 07:19:55 +01:00
Horizon agent 9d35a0c0ad Merge remote-tracking branch 'origin/main' into n/10065
# Conflicts:
#	tools/lan_e2e.py
2026-11-07 07:18:08 +01:00
yahya-nusseibehandClaude Opus 5.5 d689881e68 Cap the packets a connection queues for the hub between ticks
A connection's network thread hands every play packet to the hub over
an unbounded channel. The hub's inbox caps (INBOX_LIMIT packets,
INBOX_BYTES bytes) apply only as it drains that channel at the start of
a tick, so while one tick runs long (an autosave, chunk generation) a
client can queue tiny packets without bound, each a boxed Serverbound
many times its size on the wire.

Each connection now counts what it handed the hub and the hub hasn't
taken yet (Undrained, shared between the network thread and the
guest's PlayerSender). Past twice the inbox caps the network thread
disconnects the client with the same message as the packet-rate kick
(disconnect.exceeded_packet_rate) and logs a warning; the hub
uncounts each packet as it takes it. A client within the inbox caps
never comes near.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
2026-11-07 08:06:12 +02:00
Ludovic Marie b6aa962829 Merge pull request #10035 from IsT3RiK/traduction-messages-mods
🤖 Agent: the Mods page's install and port messages go through tr/trf with named arguments, so they translate; lang files merged key by key onto main, i18n tests pass.
2026-11-07 07:05:28 +01:00
vegetableandOpenAI Codex 3baa4f56ae Cover chunk reload invariants
Co-Authored-By: OpenAI Codex <[email protected]>
2026-11-07 11:03:01 +05:00
Horizon agentandClaude Opus 5.5 a03c91e630 guest_advancement_tests: Joined gained configured on main
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 06:59:51 +01:00
masa-masa-0314andZCode 59bafaf8b6 Realms: the invite screen's selection reads as focused on main's draw
The invite screen's list is the screen's one focusable beside its
footer, so its extractSelection outline is white; main's draw has no
list_focused parameter to ask.

Co-Authored-By: ZCode <[email protected]>
2026-11-07 13:59:09 +08:00
yahya-nusseibehandClaude Opus 5.5 c76534d9f1 Update tokio to 1.53.3 and mysql to 28 (lru 0.18) for advisories
cargo audit flagged two crates in the locks:

- tokio 1.53.2 (RUSTSEC-2026-0386), reached through quinn in
  horizon-mod-deps and in the launcher's lock. Patched in 1.53.3, a
  patch release, so both locks move there.
- lru 0.16.4 (RUSTSEC-2026-0253), reached only through mysql 27 in
  horizon-mod-deps. No 0.16 release carries the fix; mysql 28 depends
  on lru 0.18, which does, so the crate mods may use moves to mysql 28
  (mysql_common 0.37, pem 4, base64 0.23 and arc-swap come with it).

`cargo check -p horizon-mod-deps --features mysql` builds, and cargo
audit no longer reports either advisory. A native mod that asks for
`mysql = "27"` now has to ask for 28, since the lock is the allow list;
docs/native-mods.md names the new version.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
2026-11-07 07:59:08 +02:00
Ludovic Marie c1102d9227 Merge pull request #11628 from horizon-rust/fix/absorption-instant-shift-11386
🤖 Agent: a mob's Absorption now refills on every accepted add (AbsorptionMobEffect.onEffectStarted), and heal/harm and effect intervals shift like Java's int << / >> (distance mod 32, wrapping) instead of clamping or panicking at amplifier 32+.
2026-11-07 06:58:08 +01:00
vegetableandOpenAI Codex 2b9cb6ba26 Fix chunk reload invalidation
Co-Authored-By: OpenAI Codex <[email protected]>
2026-11-07 10:56:42 +05:00
MSedgeMC a4951f7377 port_map: three rows the tree has moved past
Guest break refusal is the sequence ack every PlayerAction arm sends, the packet rate limit is PacketRate::received,
and MinecraftServer.shouldRun has no haveTime budget to defer against in Horizon's loop.
2026-11-07 05:54:17 +00:00
yahya-nusseibehandClaude Opus 5.5 bddd5ebf76 Website bug reports: GH-123 cross-references nothing either
neutralise breaks @mentions, #123 and github.com/ issue links in what
people write in the website's report form, so an anonymous report can't
put backlinks on the repository's issues. GitHub also links GH-123
(either case) to issue 123, which got through.

- At the start of a word, GH- followed by a digit gets a zero-width
  space after GH, the way @ and # already do: the text reads the same
  and links nothing. Other words (high-5, GH-pages, laugh-1) are left
  alone.
- Test: GH-12, gh-7 and (Gh-8) are neutralised; look-alikes aren't.

Found in yahya-nusseibeh's review of Horizon (L2, 7 Nov).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
2026-11-07 07:54:10 +02:00
yahya-nusseibehandClaude Opus 5.5 07ed5dac67 Relay: a cap on open connections per address, and a per-site join budget
Two ways one client could lock every user out of the Play with Friends
relay, with no account:

(a) The only connection cap was global (4,096), counted before anything
    is read, and each connection may stay silent for the 10 s first-line
    wait: about 410 idle connections a second from one address kept the
    relay full, so hosts couldn't register and guests couldn't join.
(b) Every join, known code or not, is refused while the global
    unknown-code budget (120 a minute) is empty. Each /64 may try 5
    unknown codes per 10 minutes, so ~240 /64s keep it empty, and one
    residential /56 has 256 of them.

- Limiter::open_connection / close_connection: connections open at
  once per address key (IPv6 /64; a /48 four times as many), 64 by
  default (RELAY_MAX_OPEN_PER_ADDRESS; a host keeps one connection per
  guest, hence the margin). Counted before anything is read, or right
  after the PROXY line behind the PROXY protocol; past the cap the
  connection is closed at once, not held for the first-line wait. An
  RAII OpenSlot gives the place back however the connection ends.
- Each site (IPv4 /24, IPv6 /48) has its own unknown-code budget, 24 a
  minute with a burst of 6, checked before the global one and spent on
  every unknown code, so emptying the global budget takes many sites,
  not one home.
- docs/play-with-friends.md: the new setting and the site budget.
- Tests: the per-address cap (IPv4, IPv6 /64 and /48, off without
  per-address limits), the per-site budget (shared within a /24 and a
  /48, refilling), and a running relay closing a fifth silent
  connection at once with a cap of 4, then letting new ones in once
  the held ones go.

Found in yahya-nusseibeh's review of Horizon (H2, 7 Nov).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
2026-11-07 07:51:55 +02:00
MSedgeMC 03c5e6b091 port_map: PlayerList.locateStatsFile is ported
The row said the name-based stats file is not moved to the uuid file. It names the function that
does it, and the segment rule that decides whether vanilla would look for it at all.
2026-11-07 05:49:55 +00:00
MSedgeMC f4cdc0873c test: pin the old stats file moving to the uuid name
The test works in a temporary world folder: nothing is written when there is nothing to move, the
`players/stats/<name>.json` file lands on the uuid name and is not left behind, a uuid file that
already exists wins and leaves the old file alone, and the segment check refuses capitals and
separators while accepting a lowercase name with dashes, dots and digits.
2026-11-07 05:49:55 +00:00
MSedgeMC f802d6cc2c fix: move an old name-based stats file to the player's uuid
PlayerList.locateStatsFile: the stats file is `stats/<uuid>.json`, but a version that wrote
`stats/<player name>.json` left its numbers under the old name. When the uuid file does not exist
yet and the name is one vanilla would write as a path segment
(FileUtil.isValidPathSegment: `[-._a-z0-9]`, and not `.` or `..` — the check is on
`<name>.json`, so a capital letter in the player's name refuses it), the old file is moved to the
uuid name and that is the file that is read; a failed move leaves the old one in place.

Horizon always resolved the uuid path, so a world opened after such a version started with empty
stats. The 26.3 advancements file has no name-based form (`PlayerList.getPlayerAdvancements`
resolves the uuid directory directly), so only stats move.
2026-11-07 05:49:55 +00:00
yahya-nusseibehandClaude Opus 5.5 bda6cb38ae Shader packs: bound what one zipped file may unpack to
Pack::read preallocated from a zip entry's declared size and inflated it
with no limit. Shader packs are zips players download, so a deflate bomb
(or a forged size field) in any file under shaders/ aborted the game
with an allocation failure as soon as shaders.properties was read: 512
MiB of spaces zip to 509 KiB and took peak RSS from 4 to 516 MiB; at
1.5 GiB the review measured 1,543 MiB.

- Nothing is allocated from the declared size; a file that declares or
  inflates to more than MAX_FILE (64 MiB, far above any .properties,
  .lang or GLSL file, with room for the largest textures) reads as
  missing, like a file the pack doesn't have. The same pack now stays
  at 4 MiB.
- Folder packs are unchanged (files on disk, not inflated).
- Test: a small zip whose shaders.properties unpacks past a 16 KiB cap
  reads as missing; the rest of the pack still reads.

Found in yahya-nusseibeh's review of Horizon (M1, 7 Nov).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
2026-11-07 07:48:23 +02:00
abit-foggyandCodex 3ec6d2426f fix(studio): restore title and hand state after replay
Co-Authored-By: Codex <[email protected]>
2026-11-06 19:47:05 -10:00
abit-foggyandCodex 34c4cd3322 fix(studio): wait for world resources before replay entry
Co-Authored-By: Codex <[email protected]>
2026-11-06 19:47:05 -10:00
abit-foggyandCodex 2c142a1d49 fix(studio): drop stale HUD height state
Co-Authored-By: Codex <[email protected]>
2026-11-06 19:47:05 -10:00
abit-foggyandCodex b83cbac5c4 chore(studio): merge latest main into playback
Co-Authored-By: Codex <[email protected]>
2026-11-06 19:47:05 -10:00
abit-foggyandCodex 49471d6449 fix(studio): initialize replay save folder
Co-Authored-By: Codex <[email protected]>
2026-11-06 19:46:53 -10:00
abit-foggyandCodex 2e57c30ab2 chore(studio): merge main into playback
Co-Authored-By: Codex <[email protected]>
2026-11-06 19:46:53 -10:00
abit-foggyandCodex 745dc168bc fix(studio): rebuild replay state on seek
Co-Authored-By: Codex <[email protected]>
2026-11-06 19:46:21 -10:00
abit-foggyandCodex 698b836e2e fix(studio): route world exit through replay cleanup
Send pause-menu and death-screen exits through close_studio_replay when playback is active. Clear the replay timer and restore the prior camera so the title screen and the next world use normal player and hand rendering.

Co-Authored-By: Codex <[email protected]>
2026-11-06 19:46:21 -10:00
abit-foggyandCodex c8388a50f6 fix(studio): recover failed replay opens and return to title
Open and validate a replay archive before replacing the current screen. Track standalone replay sessions so closing one disconnects its synthetic remote and restores the title screen. Start playback in first person after applying timestamp-zero world packets.

Co-Authored-By: Codex <[email protected]>
2026-11-06 19:46:20 -10:00
abit-foggyandCodex c8d6221624 feat(studio): add MCPR streaming reader and archive options
Inflate recording streams to a bounded temporary file, verify the stream CRC during the copy, and index a sparse set of seek anchors instead of retaining every packet in memory. Preserve whole-frame recovery and timestamp-group seeking.

Co-Authored-By: Codex <[email protected]>
2026-11-06 19:46:20 -10:00
abit-foggy 44a7fa4dda fix(studio): smooth replay limb animations with virtual tick pacing 2026-11-06 19:46:20 -10:00
abit-foggy 53304d1bb6 fix(studio): advance walk limb animations during replay playback 2026-11-06 19:46:20 -10:00
abit-foggy 4dfa1dc31f feat(studio): record local player swing and pose animations into stream 2026-11-06 19:46:20 -10:00
abit-foggy 06e83b2f80 feat(studio): hide first-person hand and player hotbar in studio mode 2026-11-06 19:46:19 -10:00
abit-foggy 17c146a7be feat(studio): record local player movement into replay stream 2026-11-06 19:46:19 -10:00
abit-foggy 438b47c86b fix(studio): dump initial world snapshot on record start and return to main menu on close
- Dump resident chunk columns, player pose, center chunk, and dimension login into .mcpr stream at timestamp 0ms on recording start
- Allow all chunks in remote_chunk_in_range during studio replay and freecam so distant chunks are never discarded
- Reset title mode to Mode::Main when closing studio replay so the user returns to the main menu instead of multiplayer
2026-11-06 19:46:19 -10:00
abit-foggy bd9883050e fix(studio): bypass remote loading overlay and panorama in studio replay 2026-11-06 19:46:19 -10:00
abit-foggy 0faacc6a85 fix(studio): launch 3D replay editor from studio browser and title screen
- Add interactive glass button for '▶ Edit' on recording cards in StudioUi
- Allow single-clicking '▶ Edit' on any card to immediately load and open the editor
- Allow clicking the bottom 'Edit Replay' button to open the selected or first replay
- Handle Event::OpenStudioReplay in Loading::menu_option_events by queueing PendingWorldEntry::StudioReplay
- Handle PendingWorldEntry::StudioReplay in Game::poll_pending_world_entry to enter replay
- Add Session::replay and init_remote_for_replay to initialize client state when entering replay from title screen
- Dismiss title screen and uncapture cursor so timeline and studio HUD controls are interactive
- Release mouse cursor on Escape key in studio replay mode before opening pause menu
2026-11-06 19:46:19 -10:00
abit-foggy 6a9f30fd40 feat(studio): packet recording and replay playback (.mcpr container format)
- Implement standard .mcpr container reader/writer (metaData.json + VarInt timestamped recording.tmcpr stream)
- Add asynchronous StudioRecorder worker to record clientbound packets with zero impact on gameplay FPS
- Add ReplayDriver with timeline seeking, variable speed control, pausing, and looping
- Wire zero-alloc packet recording tap into Session for singleplayer, LAN, and remote multiplayer
- Add in-game /studio chat commands and control socket commands for recording and replay management
2026-11-06 19:46:18 -10:00
Horizon agentandClaude Opus 5.5 097dd3a63f Bees: Peaceful ends player anger, crop sparks, thrown overflow honey
- NeutralMob.isValidPlayerTarget also fails on Peaceful: a bee's anger at
  the player ends there.
- BeeGrowCropGoal sends level event 2011 (data 15) before the crop grows;
  the client draws it like 2012 (spawnParticleInBlock, happy villager).
- A honey bottle that no longer fits is player.drop(stack, false): from
  below the eyes, thrown along the view, no thrower (#11377).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 06:45:56 +01:00
masa-masa-0314andZCode 0787a9e7ff Realms: the invite codes screen is vanilla's selection list
RealmsInviteCodesScreen lays out a footer of two rows (Copy/Delete/Edit
97 wide over Create/Back 150 wide, FOOTER_HEIGHT 64), titles itself
Invite Codes (n/5) once fetched with the gray subtitle over an empty
list, and shows the loading dots without a list behind them. Its rows
are 28 high with only the code's text, selected like every
ObjectSelectionList (the port's extractSelection); Copy answers the
clipboard and Delete/Edit act on the selection (ids 180-182), not on
per-row buttons.

Co-Authored-By: ZCode <[email protected]>
2026-11-07 13:44:56 +08:00
yahya-nusseibehandClaude Opus 5.5 22e1582558 Shader packages: count the 8 MiB total while reading, not after
Pack::read inflated each of up to 512 zip entries to 1 MiB + 1 and kept
them all, and only Pack::from_files then checked the pack's 8 MiB total.
511 entries of zeros take 561 KiB zipped, and the mod registry reads
every submitted .hshader (index::inspect, Kind::Shader) on its blocking
pool: one upload made it hold 1 GiB (measured: peak RSS 3 -> 1,026 MiB),
a few in parallel could get it OOM-killed.

- Each entry is read with room for no more than what is left of
  MAX_TOTAL (and its own 1 MiB cap), and refused as soon as it passes
  it: "the pack is over 8 MiB", or the file named when it alone is over.
  The same package now peaks at 6 MiB.
- A folder pack counts its total the same way.
- Tests: 64 MiB of zeros in a small zip, one file over its cap, and a
  folder over the total are all refused.

Found in yahya-nusseibeh's review of Horizon (H1, 7 Nov).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
2026-11-07 07:44:44 +02:00
k3yhandClaude Opus 5.5 29a511747d Selectors find cushions
A cushion is an ordinary entity in vanilla (a BlockAttachedEntity), but
Horizon had no command target for it: @e never matched one, so execute
if entity, tag, data get, tp and kill missed every cushion. Target::Cushion
joins the level's candidates like a painting; /kill removes it with no
drop and lets its rider go, /tp moves it with its block, data get writes
it through cushion_to_nbt.

Fixes #11626.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 00:44:33 -05:00
Horizon agent 057869bdad Merge remote-tracking branch 'origin/main' into n/10035
# Conflicts:
#	launcher/lang/af.json
#	launcher/lang/ar.json
#	launcher/lang/az.json
#	launcher/lang/ba.json
#	launcher/lang/be.json
#	launcher/lang/bg.json
#	launcher/lang/br.json
#	launcher/lang/bs.json
#	launcher/lang/ca.json
#	launcher/lang/cs.json
#	launcher/lang/cv.json
#	launcher/lang/cy.json
#	launcher/lang/da.json
#	launcher/lang/de.json
#	launcher/lang/el.json
#	launcher/lang/eo.json
#	launcher/lang/es.json
#	launcher/lang/et.json
#	launcher/lang/eu.json
#	launcher/lang/fa.json
#	launcher/lang/fi.json
#	launcher/lang/fil.json
#	launcher/lang/fo.json
#	launcher/lang/fr.json
#	launcher/lang/fur.json
#	launcher/lang/fy.json
#	launcher/lang/ga.json
#	launcher/lang/gd.json
#	launcher/lang/gl.json
#	launcher/lang/haw.json
#	launcher/lang/he.json
#	launcher/lang/hi.json
#	launcher/lang/hr.json
#	launcher/lang/hu.json
#	launcher/lang/hy.json
#	launcher/lang/id.json
#	launcher/lang/ig.json
#	launcher/lang/is.json
#	launcher/lang/it.json
#	launcher/lang/ja.json
#	launcher/lang/ka.json
#	launcher/lang/kk.json
#	launcher/lang/kn.json
#	launcher/lang/ko.json
#	launcher/lang/ky.json
#	launcher/lang/la.json
#	launcher/lang/lb.json
#	launcher/lang/li.json
#	launcher/lang/lmo.json
#	launcher/lang/lo.json
#	launcher/lang/lt.json
#	launcher/lang/lv.json
#	launcher/lang/mk.json
#	launcher/lang/mn.json
#	launcher/lang/ms.json
#	launcher/lang/mt.json
#	launcher/lang/nl.json
#	launcher/lang/no.json
#	launcher/lang/oc.json
#	launcher/lang/pl.json
#	launcher/lang/pt-br.json
#	launcher/lang/pt.json
#	launcher/lang/ro.json
#	launcher/lang/ru.json
#	launcher/lang/sah.json
#	launcher/lang/se.json
#	launcher/lang/sk.json
#	launcher/lang/sl.json
#	launcher/lang/so.json
#	launcher/lang/sq.json
#	launcher/lang/sr.json
#	launcher/lang/sv.json
#	launcher/lang/szl.json
#	launcher/lang/ta.json
#	launcher/lang/th.json
#	launcher/lang/tr.json
#	launcher/lang/tt.json
#	launcher/lang/uk.json
#	launcher/lang/ur.json
#	launcher/lang/uz.json
#	launcher/lang/vec.json
#	launcher/lang/vi.json
#	launcher/lang/yi.json
#	launcher/lang/yo.json
#	launcher/lang/zh.json
#	launcher/lang/zu.json
2026-11-07 06:43:57 +01:00
masa-masa-0314andZCode 9b524815b9 Realms: the invite-code list is an array or its result
InviteCodeList.parse reads a JSON array, or an object whose 'result' is
one, dropping entries without a code; the port waited for an
'inviteCodes' object the service never sends, so the codes screen
listed nothing to manage.

Co-Authored-By: ZCode <[email protected]>
2026-11-07 13:43:42 +08:00
masa-masa-0314andZCode 4b1561050d Realms: the detail is the screen's server, and POSTs say json
- RealmsConfigureWorldScreen.fetchServerData replaces its serverData with
  getOwnRealm's answer; the port kept the server list's copy, whose
  regionSelectionPreference is stale, so a saved region read back as
  automatic. The detail answer now becomes the screen's server.
- Request.post sends Content-Type: application/json; the frontend
  answered 415 to the invite codes' text/plain POST.
- RealmsPreferredRegionSelectionScreen draws each region's ServiceQuality
  icon (ServiceQuality.getIcon, 10x8 at the row's right).

Co-Authored-By: ZCode <[email protected]>
2026-11-07 13:42:26 +08:00
Horizon agent ef3856a408 Merge remote-tracking branch 'origin/main' into n/10024
# Conflicts:
#	crates/horizon-client/src/client_lib.rs
#	crates/horizon-client/src/game.rs
2026-11-07 06:42:20 +01:00
masa-masa-0314andZCode 8d50119388 Realms: the region screen carries the settings tab's data back
Done used to rebuild the Configure screen with detail and tier cleared,
so the save that leaving the tab triggers found no slot to send and the
chosen region was dropped. The screen now carries both back, and the
save runs on the data it had.

Co-Authored-By: ZCode <[email protected]>
2026-11-07 13:41:51 +08:00
masa-masa-0314andZCode 6f4a6a3865 Realms: the region rows answer a click
The click arm that handles the screens' drawn rows was guarded to
SubScreen::Configure, so the region preference's rows (and the create
screen's fields) never saw a press. The guard now covers the screens
whose rows it handles; the region selection draws extractSelection's
black entry with its white outline and the footer is vanilla's
150-wide Done/Cancel pair.

Co-Authored-By: ZCode <[email protected]>
2026-11-07 13:41:51 +08:00
MSedgeMC 3afa8e234b port_map: the two saved-start rows say which piece ids are read
PiecesContainer.load now names saved_piece_id, where the rename table lives,
and StructureStart.loadStaticStart spells out that a jigsaw start (the renamed
old ids included) comes back from its saved pieces while another structure's
start comes back from the generator's rebuild of its chunk.
2026-11-07 05:41:18 +00:00
MSedgeMC efe0b28747 test: the four old jigsaw piece ids are read, other piece types are not
A piece tag named minecraft:nvi, pcp, bastionremnant or runtime comes back as
jigsaw, as does minecraft:jigsaw itself; a monument, stronghold or jungle
temple piece (OMB, SHCC, TeJP) comes back as nothing so the start keeps the
generator's rebuild, and so does a tag with no id or one that is not a
compound. With the table narrowed to minecraft:jigsaw alone the first four
fail.
2026-11-07 05:41:18 +00:00
MSedgeMC c7d9d1aedd fix: a saved start's pieces use the ids an old save wrote for jigsaw
PiecesContainer.load maps four piece ids - minecraft:nvi, minecraft:pcp,
minecraft:bastionremnant and minecraft:runtime - to minecraft:jigsaw before it
looks the type up: a save from before those pieces were jigsaw wrote them under
the older names with the same pool-element tag. The saved-start reader only
took the literal minecraft:jigsaw id, so such a start fell through to the
generator's rebuild of its chunk, and where that rebuild makes a different
layout (a world whose structure data or version moved on) the start came back
with no pieces at all: nothing placed, nothing bearding the terrain.

saved_piece_id now holds that rename table for the reader; a piece of another
type (a monument's or a stronghold's, whose NBT is not a pool piece's) still
sends the start to the rebuild.
2026-11-07 05:41:18 +00:00
masa-masa-0314andZCode ffa3a4d413 Realms: a slot setting's value reads as Gson's getAsString
The service sends settings like {"name":"hardcore","value":false},
a JSON bool where the DTO says string; Gson's JsonPrimitive.getAsString
accepts it, serde did not, and the detail answer the Worlds and Players
tabs need failed to parse.

Co-Authored-By: ZCode <[email protected]>
2026-11-07 13:40:25 +08:00
masa-masa-0314andZCode aaccb9b51e Realms: the subscription is at subscriptions/{id}, not worlds/{id}/subscription
RealmsClient.subscriptionFor builds 'subscriptions/'; the port's
'worlds/{id}/subscription' answered 404, which the configure screen showed
as a fatal service error instead of its tabs.

Co-Authored-By: ZCode <[email protected]>
2026-11-07 13:40:25 +08:00
Horizon agent 06ca1059f8 Merge remote-tracking branch 'origin/main' into n/9991
# Conflicts:
#	crates/horizon-client/src/entity_nbt.rs
#	crates/horizon-client/src/mobs/living.rs
#	tools/port_map/game-004.tsv
2026-11-07 06:40:22 +01:00
Ludovic Marie 84ec8516c6 Merge pull request #11622 from horizon-rust/fix/frame-map-limit-11383
🤖 Agent: an empty frame now refuses a map tracking more than 256 counted markers (ItemFrame.interact's isTrackedCountOverLimit FAIL): the map stays held, no swing; exactly 256 still goes in.
2026-11-07 06:40:06 +01:00
Horizon agentandClaude Opus 5.5 4a951a3199 Drop the harness run log from docs/parity
The harness and its tests hold what the log recorded.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 06:38:27 +01:00
Horizon agent b09ef7084f Merge remote-tracking branch 'origin/main' into n/10130
# Conflicts:
#	crates/horizon-client/src/game/scoreboard.rs
2026-11-07 06:37:08 +01:00
Horizon agent b4c2f89660 Merge remote-tracking branch 'origin/main' into n/9967
# Conflicts:
#	tools/command_exec_corpus.py
2026-11-07 06:36:08 +01:00
Ludovic Marie 4500b9e72b Merge pull request #10015 from repositorykreml1n/codex/fix-issue-9377
🤖 Agent: /say from a command block reaches every player under the block's name, and a captured guest command no longer echoes it back as feedback (kacpherek's point); lan_e2e command-block-chat not run here.
2026-11-07 06:35:42 +01:00
Horizon agent b0ae048383 Merge remote-tracking branch 'origin/main' into n/10031
# Conflicts:
#	tools/test_command_exec_parity.py
2026-11-07 06:34:52 +01:00
GabrielNichols 4b753f5956 Report Recipes warnings at their visible tooltip bounds 2026-11-07 02:33:12 -03:00
Florian ERNSTandCodex 6c6efb8d74 Refresh loaded sleeper position from bed geometry on first tick
Co-Authored-By: Codex <[email protected]>
2026-11-07 06:31:19 +01:00
Horizon agentandClaude Opus 5.5 2710eae7cf Start a bolt's fire only near a player
LightningBolt.spawnFire places fire, and draws its extra sources, only when
ServerLevel.canSpreadFireAround passes at the bolt: the
fire_spread_radius_around_player rule now holds for lightning (#11376).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 06:30:22 +01:00
Ludovic Marie ac40181853 Merge pull request #11613 from horizon-rust/fix/omen-added-sound-11373
🤖 Agent: the omen sound now plays from the player's addEffect for any newly added omen (command, potion, mod) at the feet, as MobEffect.onEffectAdded does; the three special paths moved from the eye to the feet too.
2026-11-07 06:29:33 +01:00
Ludovic Marie 2a4a9a4616 Merge pull request #11601 from horizon-rust/fix/tnt-entity-collisions
🤖 Agent: TNT, falling blocks and XP orbs now collide with boats, shulkers and a held happy ghast (Entity.collide's canBeCollidedWith); sand landing on a boat over air breaks into an item like vanilla's wouldContinueFalling.
2026-11-07 06:29:29 +01:00
Horizon agent bec6e40518 Merge remote-tracking branch 'origin/main' into n/10065
# Conflicts:
#	tools/lan_e2e.py
2026-11-07 06:26:44 +01:00
Horizon agent 702adabb25 Merge remote-tracking branch 'origin/main' into n/10015
# Conflicts:
#	tools/lan_e2e.py
2026-11-07 06:24:27 +01:00
Ludovic Marie eb2d90fb10 Merge pull request #10040 from NikitaMGrimm/shader-api-02-serve
🤖 Agent: 0.2 shader packs get the float HDR frame only under HORIZON_HDR_FRAME=float; the default and 0.1 paths keep their formats (frame_format tests cover both), so the vanilla look is unchanged.
2026-11-07 06:18:07 +01:00
Horizon agentandClaude Opus 5.5 880f8c6b56 Keep an effect's show_icon apart from its particles
MobEffectInstance.showIcon was not modelled: every effect showed its HUD
icon and every UpdateMobEffect carried the icon flag. Warden Darkness
(no particles, no icon) showed an icon. The flag now follows vanilla's
constructors, update, saves (show_icon defaults to show_particles),
consume effects and the packet flags (#11385).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 06:16:04 +01:00
Ludovic Marie 5a83184c7e Merge pull request #10170 from TheMrZZ/fix/furnace-loaded-cooking-total
🤖 Agent: a loaded furnace keeps its saved cooking total until the item finishes, and a hopper or dropper filling the empty input slot restarts the cooking as setItem does (kacpherek's point); furnace_loaded_total and furnace_output_components pass.
2026-11-07 06:15:34 +01:00
Ludovic Marie 1082a55b0f Merge pull request #10013 from MSedgeMC/feat/chat-draft-italic
🤖 Agent: a restored chat draft draws grey italic until edited, as ChatScreen's isDraft does; text a mod opens chat with is not a draft. Merge with main builds and the chat tests pass.
2026-11-07 06:15:30 +01:00
Horizon agentandClaude Opus 5.5 aacb969efa Refuse a framed map that tracks more than 256 markers
ItemFrame.interact returns FAIL when MapItemSavedData.isTrackedCountOverLimit(256):
the map stays in the hand and the frame stays empty (part of #11383).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 06:15:02 +01:00
TheMrZZandCodex gpt-6.1-sol fc711c3f6d fix: save pot sherd string templates as item compounds
Encode bare item templates using the primary record form while preserving
compound counts and components. Cover actual canonical load/save and the
real command route against a frozen fresh Java reference.

Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-11-07 06:14:22 +01:00
Horizon agentandClaude Opus 5.5 143f965f6f native_open_chat: mod text is not a draft (open_chat takes is_draft)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 06:12:51 +01:00
Ludovic Marie 318e263959 Merge pull request #10119 from DUzzL/fix/10049-frost-walker-equip
🤖 Agent: Frost Walker's speed modifier now follows a boots change made by a guest too (the guest's armor is swapped in before the comparison); armor_changes copies only changed slots. armor_stand_frost passes on a union with main.
2026-11-07 06:12:42 +01:00
rdbtandClaude Opus 5.5 d2b41ed400 Parity harnesses: a generate step that died is not reused
respawn_world_point and disconnect_crafting reused generate/world whenever the
folder existed, so one crash left them failing until someone deleted it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 05:09:02 +00:00
rdbtandClaude Opus 5.5 7c5466e763 lunge_motion: the tick after a jab is not a catch-up before it
Since #11223 the server takes what follows a jab on the next tick, so the
harness's time query comes back a tick later and every jump failed as a
catch-up. The motions are identical to vanilla's and stay compared.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 05:06:46 +00:00
Horizon agent 955cd0b289 Merge remote-tracking branch 'origin/main' into n/10013 2026-11-07 05:59:01 +01:00
Ludovic Marie 29082ca5d1 Merge pull request #10009 from DUzzL/fix/9959-spectator-portals
🤖 Agent: a spectator using a portal block travels to its destination, never builds a Nether exit, and gets spectator.cannot_teleport without one (ServerPlayerGameMode.useItemOn); nether_portal harness passes on a union with main.
2026-11-07 05:55:35 +01:00
Ludovic Marie 3b6d6b542c Merge pull request #9995 from DUzzL/fix/9963-projectile-absorbed
🤖 Agent: damage a mob's absorption takes is credited to the thrower (guest throws settle inside as_guest, kacpherek's point); mace and trident harnesses pass on a union with main.
2026-11-07 05:55:32 +01:00
rdbtandClaude Opus 5.5 f670d71c41 Parity harnesses: mob_discard, mob_local_sound and ominous_warning_sound run on a plain run
tools/parity.py runs a harness with no arguments, and these three required
--binary, --work-dir or --resources (mob_discard also used os without importing it).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 04:52:13 +00:00
martipopsandClaude Opus 5.5 1edfd2f7ea Merge main into controller/guide
main removed the HUD's bar_height; the guide's guide_from stays.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 23:46:42 -05:00
Ludovic Marie 7a2f824043 Merge pull request #10701 from Detimes/perf-reuse-packet-deflater
🤖 Agent: region saves reuse one deflater per writer instead of building one per chunk; the test checks the compressed bytes are identical to a fresh deflater's. Union check (main + the gate's 9 heads): fmt, clippy -D warnings, 5666 tests green; verify green after the runner's full-disk rerun.
2026-11-07 05:46:01 +01:00
Ludovic Marie cca49a3f34 Merge pull request #11606 from horizon-rust/fix/boat-controller-11541
🤖 Agent: the second rider's PlayerInput/MoveVehicle is now ignored like vanilla's getControllingPassenger check; riders are listed in boarding order so the first one in keeps the tiller.
2026-11-07 05:43:37 +01:00
k3yhandClaude Opus 5.5 945f40e8ff /ride takes armor stands, displays, interactions and markers as vehicles
RideCommand.mount forces the ride (startRiding(vehicle, true, true)), so
any entity takes a rider. Horizon only took boats, carts and mobs. A rider
now sits at the vehicle's default passenger attachment (its box height,
less the rider's own vehicle attachment) and gets off on sneak on top of
it, as Entity does for a vehicle that overrides neither.

Fixes #11603.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 23:40:02 -05:00
Ludovic Marie b03633436a Merge pull request #11597 from horizon-rust/fix/swing-echo-9887
🤖 Agent: only SUCCESS_SERVER block uses (beds, light, a charged anchor, signs) swing back to their own player; item and entity uses still echo, as the body says. use_item_on, flat_place and dedicated_server pass locally.
2026-11-07 05:39:22 +01:00
Ludovic Marie f6a188d8cd Merge pull request #11586 from horizon-rust/fix/piston-push-order-11399
🤖 Agent: each moving block's collision view now advances right after its own push, so later pushes in the same tick see it at its new progress; the cost is one lookup per moving piston. piston_push and piston_movement pass locally.
2026-11-07 05:39:19 +01:00
GabrielNichols 6fbbfdde1c Include the Recipes transfer translations 2026-11-07 01:38:12 -03:00
Horizon agentandClaude Opus 5.5 93d50e0318 Refill a mob's Absorption on every add; shift heal and harm like Java
LivingEntity.addEffect runs onEffectStarted for every effect it accepts,
so Absorption given again refills a mob's shield to 4 per level of the
effect given. Horizon refilled it only for a new or stronger effect.
HealOrHarmMobEffect and the periodic effects use Java's shifts, which take
the distance modulo 32 and wrap: amplifier 32 is amplifier 0 again, and a
ticked heal is at least 0. Horizon clamped the distance to 30, and an
amplifier of 32 or more panicked on Regeneration's interval in a debug
build (#11386).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 05:36:50 +01:00
Florian ERNSTandCodex 4aeb14b3ba Load active world-pack enchantment definitions
Co-Authored-By: Codex <[email protected]>
2026-11-07 05:35:26 +01:00
levzzzandCodex 1301820f0d Run movement parity through the gameplay library tests
Select the library artifact that owns Game, retaining the exact one-test assertion so a thin executable with no tests cannot count as a pass.

Co-Authored-By: Codex <[email protected]>
2026-11-07 09:29:40 +05:00
voidsip b888cebdf6 Merge maintainer parity import correction 2026-11-06 22:24:07 -06:00
voidsip 337c5bbd4d Merge maintainer parity import correction 2026-11-06 22:24:05 -06:00
Horizon agentandClaude Opus 5.5 aef2dc6528 tests: load the parity drivers as tools.* in the CLI admission test
Under `unittest discover -s tests`, the bare name test_rust_parity is
tests/test_rust_parity.py, so the drivers' fallback import took the wrong
module and four tests failed (no attribute verify_artifacts) in verify.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 05:22:44 +01:00
Horizon agentandClaude Opus 5.5 65c57efffe tests: load the parity drivers as tools.* in the CLI admission test
Under `unittest discover -s tests`, the bare name test_rust_parity is
tests/test_rust_parity.py, so the drivers' fallback import took the wrong
module and four tests failed (no attribute verify_artifacts) in verify.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 05:22:20 +01:00
voidsip 446e36c1a6 Merge commit 'aae27797d94680928d6767419a51b1838b427e73' into fix/round3-1-wet-fire-order 2026-11-06 22:17:51 -06:00
voidsip 4efc3f6bb1 Merge commit 'aae27797d94680928d6767419a51b1838b427e73' into fix/mob-water-breathing 2026-11-06 22:17:50 -06:00
voidsip aae27797d9 Isolate parity helper imports from unittest discovery names 2026-11-06 22:17:17 -06:00
k3yhandClaude Opus 5.5 b9854b1d82 /ride seats and unseats any server player, whoever runs it
RideCommand.mount and dismount work on any ServerPlayer in vanilla. Horizon's
/ride reached only the player in place, so the console, a function, a command
block or another player could not seat a guest (every player of a server or a
LAN world, and in single player too): the guest's turn now runs through
for_player, as native_mount does. A guest as the vehicle answers
cant_ride_players, as any player does.

Fixes #11602.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 23:16:44 -05:00
Ludovic Marie 507e0ae6f4 Merge pull request #10180 from lightcodegit/fix/brush-horn-use-vibration
🤖 Agent: horn and brush uses end with stopUsingItem (the interaction vibration), and a mob in front of the brushed block stops the brush out to the block reach; item_use_catalog harness passes on a union with main.
2026-11-07 05:16:14 +01:00
Horizon agentandClaude Opus 5.5 1d0b591927 Play an omen's sound when it is added to an entity
MobEffect.onEffectAdded plays Bad Omen's, Trial Omen's and Raid Omen's
sound (withSoundOnAdded) whenever LivingEntity.addEffect adds one that
is new, at the entity's feet. Horizon played them only on the raid, bottle
and trial spawner paths, at the eye, so /effect give, a potion's custom
contents or a mod adding one was silent (#11373). The generic add now
plays it for players and, from /effect, for mobs; the three special paths
play at the feet.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 05:16:06 +01:00
Ludovic Marie 348e296ffa Merge pull request #10039 from MSedgeMC/fix/hud-text-height
🤖 Agent: the item name and the action bar take fixed heights above the hotbar (Hud.extractSelectedItemName 59, extractOverlayMessage 68) instead of following the bars; hud harness passes on a union with main.
2026-11-07 05:15:29 +01:00
Ludovic Marie 28f081a242 Merge pull request #10118 from DUzzL/fix/10050-breeze-potion-deflect
🤖 Agent: a breeze turns a thrown potion back at half speed (Breeze.deflection) once in a row, stopping at the hit point that tick; the REVERSE draw uses the potion's own random.
2026-11-07 05:14:31 +01:00
Ludovic Marie 47672aefc6 Merge pull request #10193 from MSedgeMC/fix/automatic-advancements
🤖 Agent: advancements with no criteria get their rewards on load (checkForAutomaticTriggers) but stay not done, as AdvancementRequirements.test answers false; checked on a union with main.
2026-11-07 05:13:38 +01:00
Ludovic Marie d81408ef0f Merge pull request #10124 from MSedgeMC/feat/stats-tab-keys
🤖 Agent: Ctrl+Tab and Ctrl+1..0 switch the statistics tabs from the raw key, as TabNavigationBar.keyPressed does; checked on a union with main.
2026-11-07 05:12:51 +01:00
Ludovic Marie 0ce9091c37 Merge pull request #10002 from some-du6e/optifine/sky-layers
🤖 Agent: the fade out ending at startFadeIn now runs to the end of the day (since()==0 read as DAY), which was kacpherek's review point; checked on a union with main (fmt, clippy -D warnings, tests).
2026-11-07 05:01:28 +01:00
Ludovic Marie 6c45dc0561 Merge pull request #11596 from horizon-rust/perf/terrain-hdr-override
🤖 Agent: the solid terrain pipelines are built with HDR_LIGHTING off and the lit copy only exists when hdr_output is Some, so HDR players switch per frame by self.hdr; frame-identity label makes CI check HDR off, offscreen HDR and HDR toggled off in a running game. −16 to −22% GPU on M4 Pro, image identical in-process.
2026-11-07 04:57:22 +01:00
voidsip c59cb034a2 Merge commit 'ccb85ce454b9ea064dc4c5a99f95bdbea836ee35' into fix/round3-1-wet-fire-order 2026-11-06 21:49:42 -06:00
voidsip fa733b3efa Merge commit 'ccb85ce454b9ea064dc4c5a99f95bdbea836ee35' into fix/mob-water-breathing 2026-11-06 21:49:41 -06:00
voidsip ccb85ce454 Keep specialized parity harnesses compatible with normal CI invocation 2026-11-06 21:49:02 -06:00
levzzzandCodex 3cb56882d9 Match vanilla movement bounds and facing at numeric limits
Reuse the native summon spawn-bounds predicate so negative endpoints stay valid, and use Java's integer width for the yaw direction lookup. Record 26 independent outcomes from both pinned vanilla jars and add a differential harness for the actual native calculations.

Co-Authored-By: Codex <[email protected]>
2026-11-07 08:44:37 +05:00
bulkandClaude Sonnet 5.5 596a11fb08 Launcher: show patch notes in a dropdown under each version, drop the Patch notes tab
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-07 03:42:56 +00:00
Ludovic Marie 7775a14526 Merge pull request #11562 from horizon-rust/perf/11024-parallel-world-convert
🤖 Agent: merging. Empty region files (13 303 of 13 620 in the test world) are no longer rewritten and synced on world open, and region files convert on all cores but one with byte-identical output; datafix harness 6/6.
2026-11-07 04:40:56 +01:00
TheMrZZandCodex gpt-6.1-sol f3fcb0ca25 fix: reject function tags with unresolved required references
Propagate required dependency failures before expanding function tags so
invalid tags cannot run partial members. Skip whole invalid optional children
while preserving the existing ordered, deduplicated expansion.

Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-11-07 04:38:01 +01:00
Horizon agentandClaude Opus 5.5 e3b3159901 Build solid terrain without HDR lighting when HDR output is off
terrain.wgsl's solid shaders carry HDR output's lighting behind a runtime
`hdr_light.z` test. The branch is never taken without HDR, yet on Apple GPUs its
code alone costs ~15% of the solid pass (register pressure). A pipeline
constant `HDR_LIGHTING` now guards it: the solid pipelines (per-section, pulled
and flat) are built without it, and a copy with it only where the display
offers HDR output, picked per frame by whether HDR is on.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 04:35:44 +01:00
Ludovic Marie 77936bf4be Merge pull request #10196 from IanWorley/native-api/session-state
🤖 Agent: the session state a client HUD reads (world id, screen kind) comes from fields the window already keeps; merged with #10195 and checked with clippy, the native_client tests and the hook stamp test.
2026-11-07 04:33:57 +01:00
Horizon agent f47ac2cbad Merge remote-tracking branch 'origin/main' into conv-par 2026-11-07 04:24:38 +01:00
levzzzandCodex eba31f178c Keep native rotation on the rotation-only command path
Reuse the current forceSetRotation path so turning an entity preserves its mount, position and velocity. Extend the existing resource-backed rider regression.

Co-Authored-By: Codex <[email protected]>
2026-11-07 08:23:11 +05:00
levzzzandCodex e539cd328f Merge current main into native entity movement
Preserve the current teleport passenger handling and supply its rotation policy from the native movement methods.

Co-Authored-By: Codex <[email protected]>
2026-11-07 08:18:25 +05:00
Horizon agentandClaude Opus 5.5 7f0778e4da Merge origin/main into #10196
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 04:12:16 +01:00
Ludovic Marie b0a8cbf6fb Merge pull request #11579 from horizon-rust/mods/client-combat-hooks
🤖 Agent: client_hurt reports the packet's causing entity (the shooter, not the arrow) and treats an untracked entity as not a player, so a mod never gets a false PvP signal. Main merged in (test conflict with #11512's open-chat test, both kept); verify green on this head.
2026-11-07 04:12:11 +01:00
voidsip f71a05a062 Keep the HUD mode export separate from client snapshots 2026-11-06 21:11:23 -06:00
Horizon agentandClaude Opus 5.5 6bcbf52250 Merge origin/main into mods/client-combat-hooks
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 03:59:52 +01:00
voidsip 48886daeb6 Merge commit '1647c1b4664f4221b224e81ec800df4331f81bcc' into fix/11123-native-hud-mode
# Conflicts:
#	crates/horizon-client/src/game/native_hooks.rs
2026-11-06 20:57:05 -06:00
voidsip f423aeea61 Keep HUD mode accessors in the native helper section 2026-11-06 20:56:39 -06:00
Horizon agentandClaude Opus 5.5 745b95ace9 Look at the audio devices off the render thread
Audio::should_change_device listed the output devices on the render
thread once a second. Windows has no cheap fingerprint to skip the list
(Linux reads /proc/asound/cards, macOS names the devices), so every
second the render thread waited ~90-120 ms on WASAPI's endpoints, even in
the main menu (#11490). The look now runs on a short-lived thread every 20
ticks; a later tick applies what it found with the same rules.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 03:54:49 +01:00
Horizon agentandClaude Opus 5.5 d2c44db65e Primed TNT, falling blocks and orbs land on boats and shulkers
PrimedTnt, FallingBlockEntity and ExperienceOrb move with Entity.move,
whose Entity.collide includes EntityGetter.getEntityCollisions: a boat,
a living shulker and a happy ghast held still stop them, as they stop a
dropped item. Horizon resolved their moves against blocks and the world
border only (#11518). The ordered tick gathers those boxes once per run
(again after an explosion) and passes the nearby ones to each move.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 03:54:41 +01:00
Horizon agentandClaude Opus 5.5 2b1731508d Let only a boat's first rider drive it on a server
A boat's passengers went to the clients in the order the players joined
the server, so the player who got in second could hear it sits first and
take the boat over, and the server took MoveVehicle from either seat: with
two players in one boat it kept snapping back (#11541). The passengers now
go in the order they boarded, and handleMoveVehicle ignores a player who
isn't the vehicle's controlling passenger, as vanilla does.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 03:54:07 +01:00
Ludovic Marie 026f014ff6 Merge pull request #11538 from horizon-rust/mods/chat-input-helpers
🤖 Agent: native_set_chat_input filters text to what typing allows (no control chars, DEL or §) and clamps the cursor, so a mod can't inject formatting codes into the chat box.
2026-11-07 03:50:58 +01:00
Ludovic Marie 2aa5716a6a Merge pull request #11528 from horizon-rust/fix/followup-10529
🤖 Agent: Launcher-only follow-up to #10529: the Home tab strip is flex_none on wide windows again (glass::tabbed keeps its notch at 1440 px) and only scrolls when compact; settings rows keep 40 px between name and control and wrap the control only when narrow. No new strings; fmt/clippy/tests green on the union of this wave's heads.
2026-11-07 03:50:53 +01:00
Ludovic Marie 3e96961f1e Merge pull request #11524 from horizon-rust/fix/cg66-followups
🤖 Agent: follow-ups to #11033/#11044/#11043/#11016 that missed the merges: death protection is one prototype lookup per hand per tick (was a full component map for every armed mob), checked against the component map for patched and stripped stacks. Union check: fmt, clippy -D warnings, 5666 tests green.
2026-11-07 03:50:48 +01:00
Ludovic Marie ed38625015 Merge pull request #11036 from ilguerro/perf/9996-interp2
🤖 Agent: the interpolation change is checked bit for bit against the old formula over the test's whole range, so no position moves; one multiply fewer per entity per frame. Union check (main + the gate's 9 heads): fmt, clippy -D warnings, 5666 tests green.
2026-11-07 03:50:43 +01:00
Ludovic Marie 31d0888da7 Merge pull request #10703 from Detimes/perf-region-inflate-in-place
🤖 Agent: inflates each region chunk in place instead of through a second buffer; the differential test decodes the same chunks with the old block decoder and compares every section. Union check (main + the gate's 9 heads): fmt, clippy -D warnings, 5666 tests green.
2026-11-07 03:50:39 +01:00
Ludovic Marie a5ea46ce3e Merge pull request #11049 from DUzzL/fix/11024-parallel-world-upgrade
🤖 Agent: Optimize World now upgrades region files in parallel with one sync per file instead of one per chunk (F_FULLFSYNC on macOS was the cost); the 1-vs-4-thread test checks the counts against the fixture's own layout, and a canceled recreate leaves every file untouched. Union check: fmt, clippy -D warnings, 5666 tests green.
2026-11-07 03:46:43 +01:00
Ludovic Marie 5088011241 Merge pull request #9566 from hildlabs/panel/glass-look
🤖 Agent: the server panel moves its dialogs into the page and follows the Glass look; launcher only, strings through tr().
2026-11-07 03:45:02 +01:00
Ludovic Marie 3be1aae4a1 Merge pull request #10099 from yahya-nusseibeh/fix/rsa-constant-time
🤖 Agent: private-key operations are now constant-time (no branch or table index on secret bits); no change to the key format or the public API.
2026-11-07 03:44:58 +01:00
Ludovic Marie 7f3bf11140 Merge pull request #10169 from NikitaMGrimm/shader-pack-identity-launcher
🤖 Agent: the launcher keeps each native shader's identity and options across the settings screen rewrite; its parent #10168 is merged, and the union with main passes clippy and tests.
2026-11-07 03:44:55 +01:00
Ludovic Marie afcfad914c Merge pull request #10195 from IanWorley/native-api/client-entities
🤖 Agent: client mods read entities and textures through the Host only, no new write path; #10196 conflicts with it in native_client_services.rs and gets main merged after this lands.
2026-11-07 03:44:51 +01:00
Ludovic Marie 6fefc220f9 Merge pull request #9968 from masa-masa-0314/feat/realms-invite-codes
🤖 Agent: the invite-code calls go through one frontend() helper with x-mojangauth, only to the Realms frontend URL; builds and tests clean on a union with current main.
2026-11-07 03:44:47 +01:00
Horizon agentandClaude Opus 5.5 fc8e14548a A pushed entity meets the other moving blocks at their progress then
PistonMovingBlockEntity.tick pushes entities from each block entity's own
tick, and getCollisionShape reads its progress: the moving blocks that
ticked before it have advanced, the ones after it have not. Horizon
pushed every entity against last tick's progress of all of them, so a
TNT duper's TNT was pushed through a block that was about to move
(#11399). Each piston's collision view now advances right after its own
push.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 03:42:55 +01:00
Horizon agentandClaude Opus 5.5 ae8bfba8c6 Fill the swing test's Joined like main's, rustfmt
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 03:40:33 +01:00
Horizon agent 8703ad614c Merge remote-tracking branch 'origin/main' into fix/swing-echo-9887 2026-11-07 03:39:58 +01:00
Ludovic Marie fce784f752 Merge pull request #11575 from horizon-rust/mods/screenshot-helper
🤖 Agent: native_take_screenshot refuses while another screenshot waits for the frame, so a mod's call can't overwrite F2's or the world icon's pending grab.
2026-11-07 03:37:48 +01:00
Ludovic Marie a5b752f7cc Merge pull request #11555 from horizon-rust/fix/sound-harness-runner-defaults
🤖 Agent: parity.py starts harnesses with no arguments; these two required --binary/--work-dir and failed parity-main in 0 s. On the desktop both now reach their servers (the Java sides ran); the full runs there timed out only because the desktop's swap was full.
2026-11-07 03:37:45 +01:00
bulkandClaude Opus 5.5 b9e891bf3e Remove the dead_code allows nothing needs, give the rest a reason
Removing each one and asking the compiler (the workspace's targets and the
server build): most guarded nothing, since `pub` methods of `pub` types already
count as used. The ones native mods need stay, now saying so, as do the fixtures
that share another fixture's source and code whose caller has not landed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 02:37:35 +00:00
Florian ERNSTandCodex 84f6959921 fix: require folder data-pack metadata sections
Reject folders without parseable metadata, an object pack section and a non-null description before exposing their contents. Preserve existing parser and format behavior.

Co-Authored-By: Codex <[email protected]>
2026-11-07 03:35:13 +01:00
Horizon agentandClaude Opus 5.5 9834143df1 Fix clippy: is_player needs no mut; keep player_voice_pitch's doc on it
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 03:34:42 +01:00
Ludovic Marie 4774d07dd5 Merge pull request #11513 from horizon-rust/fix/items-fall-through-minecarts-11091
🤖 Agent: removes only the minecart boxes from the item's collision list; boats, shulkers and a held happy ghast still hold items, as AbstractMinecart doesn't override canBeCollidedWith. entity_collision, held_drop, shulker_space, boat_item_hit pass locally.
2026-11-07 03:22:31 +01:00
Horizon agent b9693b5716 Merge remote-tracking branch 'origin/main' into fix/swing-echo-9887 2026-11-07 03:20:04 +01:00
Ludovic Marie 1647c1b466 Merge pull request #11028 from Detimes/hooks-b-all
🤖 Agent: merged after #11019 and main (hook and doc blocks kept whole, quinn kept beside the new optional crates). Without a mod, PlayerSender::send pays one relaxed WATCHED load and sends the bytes as they were, and worldgen, LOD columns and redstone keep vanilla's values.
2026-11-07 03:19:45 +01:00
Vinícius MoreiraandClaude Opus 5.5 f2c9af4827 Environment attributes: the registry and modifiers, checked against vanilla
Ports vanilla 26.3's environment attribute registry (EnvironmentAttributes:
types, defaults, ranges) and one EnvironmentAttributeMap entry: a value or
a {modifier, argument} object, parsed as its codec does (hex, list and int
colors, FloatWithAlpha, BlendToGray) and applied as AttributeModifier.apply
with float math in Java's order (ARGB.alphaBlend, addRgb, subtractRgb,
multiply, greyscale, scaleRGB, srgbLerp; Math.min/max). The layer stack
that sums these comes next; nothing uses the module yet.

The new harness environment_attribute_modifier compares 8,835 cases with
the pinned client, the registry included.

Part of #11486

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 23:07:36 -03:00
Horizon agentandClaude Opus 5.5 e494a9d1f5 rustfmt the boat Ride condition
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 03:06:44 +01:00
Horizon agentandClaude Opus 5.5 ac31802b58 Give #11019's guest test a Joined with its configuration
The zero-health test that #11019 added builds a Joined, which this branch
gave a configured field.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 03:01:53 +01:00
Horizon agentandClaude Opus 5.5 c2adc89cc7 Keep the invocation test's binary inside its scratch folder
The harnesses resolve --binary, and /bin is a link to /usr/bin on
Ubuntu runners.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 02:59:37 +01:00
bulkandClaude Opus 5.5 898d7a0b4a Drop the duplicate dependencies an upgrade can remove
arboard 3.7 moves to windows-sys 0.61 (removing 0.60, windows-targets 0.53 and
its eight platform crates). socket2 0.6 and if-addrs 0.15 in horizon-friends
remove socket2 0.5; the launcher's lock also loses windows-sys 0.59.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 01:55:58 +00:00
Horizon agent e82a08ca4d Merge remote-tracking branch 'origin/main' into cg68/hooks-b-all
# Conflicts:
#	crates/horizon-mod-deps/Cargo.toml
#	docs/native-mods.md
2026-11-07 02:55:11 +01:00
Florian ERNSTandCodex 0de6eeb024 fix: reject functions containing invalid ordinary commands
Validate candidate functions at actual compilation permission before exposing them, using refreshed world content and candidate registry IDs.

Co-Authored-By: Codex <[email protected]>
2026-11-07 02:55:05 +01:00
Horizon agentandClaude Opus 5.5 3fc2ed796f Controllers: the guide's boat Ride drops the boarding cooldown main removed
#11158 took the player's boarding cooldown out (Player.removeVehicle zeroes it), and
may_board with it, so this branch no longer built once merged with main.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 02:51:40 +01:00
Ludovic Marie 41944ed914 Merge pull request #11553 from horizon-rust/pvp-2/legacy-pvp-check
🤖 Agent: the check, not the 1.8 rules, was broken: the switch row sat half under the list's edge so the click missed it; verified 7/7 legacy, 9/3.6 modern on the Mac.
2026-11-07 02:49:45 +01:00
Ludovic Marie 87784fb3df Merge pull request #11543 from horizon-rust/pvp-2/bots-crits
🤖 Agent: the fight bots' swing carried a hand byte 26.3's swing packet doesn't have, so every server kicked them on the first hit; it now sends the same Punch as --attack.
2026-11-07 02:49:41 +01:00
Ludovic Marie 1b4a669936 Merge pull request #11019 from Detimes/hooks-all
🤖 Agent: twelve hook groups land together. Without a mod, the move, teleport and trample hooks are fn pointers over an empty mods() slice, so a player's move costs one indirect call and no allocation; a mod command's permission node falls back to its own level when no provider answers.
2026-11-07 02:49:38 +01:00
DetimesandClaude Opus 5.5 fa9d06a48f Older clients and servers: 1.21.4, 1.21.3, 1.21.1 and 1.20.6 (#10404)
Id tables from the vanilla data reports (1.20.6's packet list rebuilt from
the archived wiki page, which names no resources, via 1.21.1's report), with
the renames game_profile, set_carried_item and horse_screen_open. Packet
layouts from minecraft.wiki's archived pages for protocols 766 to 769 and the
release notes: 1.21.5's chunk format and per-entity data, hashed container
clicks, 1.21.2's window ids, teleports, movement flags, input and sea level,
1.21's particles, explosion sound and trades, 1.20.5's strict error handling.
1.21.5 item components (attribute modifier display, equippable shearing)
behind a quirk. A 1.21.1 client gets no recipe book.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 01:46:59 +00:00
Horizon agent 230d414d62 Merge commit '97e3a74179cf3e6137908ea2c8ce36a489605a84' into cg68/hooks-b-all
# Conflicts:
#	crates/horizon-client/src/game.rs
#	crates/horizon-client/src/game/lan_actions.rs
#	crates/horizon-client/src/game/native_hooks.rs
#	crates/horizon-client/src/game/native_net.rs
#	docs/native-mods.md
2026-11-07 02:45:45 +01:00
Kevin RamirezandClaude Fable 5.1 3aadba6aba CI: run verify again after a wall-clock test flaked on the slow runner
Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-11-06 19:43:57 -06:00
Horizon agentandClaude Opus 5.5 0da0d2b260 Merge main into #10383: keep the macro-type and mob-name corpus groups beside treasure map copies
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 02:40:25 +01:00
voidsip 8c991c7472 Isolate trade parity runs and format modifiers 2026-11-06 19:39:51 -06:00
Ludovic Marie 0ad4f59af2 Merge pull request #11537 from horizon-rust/multiplayer/drag-servers
🤖 Agent: a press on the already-selected server still joins without holding it, and servers.dat is written only on a release after a real move.
2026-11-07 02:37:14 +01:00
Ludovic Marie 3e6e75d8f2 Merge pull request #11511 from horizon-rust/settings/finer-lod-steps
🤖 Agent: only the slider's step table changes; saved values between steps snap to the nearest step (test covers 3000 → 13/23), so existing options.txt files still load.
2026-11-07 02:37:10 +01:00
Ludovic Marie cef80f4fd5 Merge pull request #11510 from horizon-rust/mods/quinn-dep
🤖 Agent: quinn is optional behind the quic feature with default features off (runtime-tokio + rustls-ring only), so no extra TLS backend lands in Cargo.lock beyond the 7 packages.
2026-11-07 02:37:06 +01:00
Ludovic Marie 52a7c28daf Merge pull request #11512 from horizon-rust/mods/open-chat-helper
🤖 Agent: native_open_chat refuses unless the screen is Playing, so a mod can't push the chat over an open container or menu.
2026-11-07 02:37:02 +01:00
socksproxandClaude Opus 5.5 be8d3d59d9 horizon-client: discord, friends and heartbeat features
Discord Rich Presence, Play with Friends (horizon-friends) and the
anonymous "I'm playing" ping (horizon-telemetry/live) were built
unconditionally, though a server never runs them. Each is now a
default-on feature of horizon-client and horizon-client-lib, so the
normal build is unchanged and the server build (--no-default-features,
horizon-engine) leaves them out: horizon-friends and 13 crypto and
network crates drop from its dependency tree.

Without `friends` the game keeps working: game/friends_off.rs stubs the
host side (no Friends button in the pause menu, as on another server),
no code reads as an invite, and a friend: address is refused as an
invalid address.

Fixes #11539

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 02:34:02 +01:00
Horizon agent 97e3a74179 Merge remote-tracking branch 'origin/main' into cg68/hooks-all 2026-11-07 02:33:34 +01:00
Horizon agentandClaude Opus 5.5 2ba9bcf90d Fix the build: the path goes to mods as a string
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 02:30:55 +01:00
Horizon agentandClaude Opus 5.5 dd338cd642 Fix the build: FnMut lookup for the attacker
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 02:29:11 +01:00
bulkandClaude Opus 5.5 a2727a069e Apply the lints the raised rust-version turns on
Clippy follows rust-version, so crates that move from 1.85 now get its
collapsible let chains, as_chunks and is_multiple_of suggestions. Almost all
from `cargo clippy --fix`, none changes what the code does.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 01:29:01 +00:00
Horizon agentandClaude Opus 5.5 17ee8c5f13 lan_e2e legacy-pvp: flip the switch once it is whole in view, and swap across ticks
The check failed on main with 1.8 rules that work: the switch row now sits
under the PvP page's HUD rows, the scroll stopped as soon as the row showed
at the list's lower edge, and the click at its centre landed outside the
list, so the rules never turned on (legacy hit 9, not 7). It now scrolls
until the row stops moving and asserts the click flipped it.

The swap and back went out in one tick: since the client sends a slot
change from its next tick, as vanilla does (#10084), that sends nothing,
and the modern hit kept its full charge. The stick is now held 3 ticks.

Mac mini: legacy (7, 7), modern (9, 3.6).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 02:27:08 +01:00
rdbtandClaude Opus 5.5 ac3436923a CheckerboardBiomeSource: a negative scale reaches the shift unclamped, as in vanilla
#11226 clamped scale to 0 before the + 2. Vanilla has no clamp and Java masks the shift
distance, so tools/test_biome_source_parity.py failed at scale -1 and below.

Fixes #11519

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 01:25:45 +00:00
Ludovic Marie 77db38ec8c Merge pull request #11523 from horizon-rust/platforms/ci-arm64-launcher-version
🤖 Agent: .github only; the by-hand ARM64 job's launcher --version step now runs from bash as launcher.yml does, and passes on the ARM64 runner (run 42320852405), where the pwsh version failed on any code.
2026-11-07 02:23:08 +01:00
Florian ERNSTandCodex 593e9e05cc Carry actual loot-mine executor presence into block drops
Co-Authored-By: Codex <[email protected]>
2026-11-07 02:20:33 +01:00
Horizon agentandClaude Opus 5.5 6858867f2a Convert older worlds on all cores and skip empty region files
Opening a world saved by an older version runs the converter in
datafix/files.rs (upgrade_world), which rewrote every region file on one
thread. Now:

- upgrade_regions spreads a folder's region files over worker_threads()
  workers (all cores but one). Each file is still read, fixed and written
  whole by one worker through the same staging folder, so the bytes do not
  depend on the thread count. A cancel or an error stops every worker
  before its next file; the error returned is the first file's in order.
- A region file without chunks is left as it is instead of being rewritten
  as an empty 8 KiB file and synced. The game leaves many of them: 13 303
  of the 13 620 overworld files of a 1.21.1 ATM10 test world.

Tests: 4 workers give byte-identical files to 1 and every chunk at the
current version, an empty file stays empty, and a cancel from the first
chunk converts at most one file per worker and leaves the rest old.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 02:17:31 +01:00
voidsip e03f69d607 fix: spill contents of items destroyed by explosions 2026-11-06 19:16:27 -06:00
rdbtandClaude Opus 5.5 a3c17924ca Parity harnesses: loaded_pig_tags and loot_mine_pot_components run on a plain run
Both asserted --reference-only when no --reference-dir was given, so tools/parity.py,
which passes no arguments, failed them before they ran anything.

Fixes #11520

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 01:15:56 +00:00
bulkandClaude Opus 5.5 fab81be9ac Build every workspace crate against one rust-version
Members declared 1.85 to 1.95, or nothing. horizon-mods needs 1.95 and sits
under horizon-client-lib, cargo-horizon-mod and the launcher, so 1.95 is the
version the game really needs; the lower numbers promised builds that could
not happen. Set it once in [workspace.package] and inherit it everywhere.

The separately built workspaces (launcher, telemetry server, mod registry,
third_party) cannot inherit it and keep their own.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 01:14:44 +00:00
Ludovic Marie 00f6358e7a Merge pull request #11081 from horizon-rust/platforms/docs-intel-arm64
🤖 Agent: one line in docs/hosting.md (Intel Macs have had server builds since the 10-31 Nightly; install.sh already maps Darwin-x86_64); the README half is already on main via bf4657da26.
2026-11-07 02:10:26 +01:00
Ludovic Marie b33917f89b Merge pull request #11080 from horizon-rust/platforms/arm64-emulated-dx12
🤖 Agent: Auto's backend order now reads the machine's arch (IsWow64Process2), so the x64 build on a Snapdragon PC starts on DX12 like the ARM64 one; the ARM64 runner built and started the game on this head. Unverified on real Adreno hardware (asked #7513's reporter).
2026-11-07 02:10:21 +01:00
Ludovic Marie 3fd9db1568 Merge pull request #10501 from arslanarm/shaderpacks/same-size-buffer-declarations
🤖 Agent: merged main in again (only the test module list in pack_post.rs collided, with #11196's sky_disc_blend_tests). The last parity failure on 4fa3bf699 was command_exec's '/advancement grant @s everything' count (116 vs 115), which fails identically without this PR; this head reruns parity. sizes_differ is a few array reads per pass per frame.
2026-11-07 02:10:17 +01:00
Horizon agentandClaude Opus 5.5 9ffa81a748 Items: read a stack's components in place instead of copying its item's defaults
ItemModels::components cloned the item's whole default component map (an
enchanted sword's: a few dozen JSON values and strings) for every stack
with a patch, and the model resolver did it on every draw: the hotbar,
each held item of every player in view, the first-person hand twice. A
PvP fight with enchanted gear paid it a dozen times a frame.

ComponentsView reads the patch, then the defaults, with no copy; the
resolver, the swap animation scale, attack range, use duration and the
use animation read through it. components() stays for what needs the
merged map (bundle fullness, still built only for bundles).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 02:09:17 +01:00
Horizon agentandClaude Opus 5.5 7ce65988b2 Items: look an enchanted GUI item's glint sprite up once per sprite, not per quad
submit_quads hashed the quad's sprite name into the glint atlas for every
quad of an enchanted item drawn in the GUI (a generated sword: hundreds of
side quads, one sprite). It now keeps the last sprite's glint rect, as it
already does for the item atlas rect: the same uvs, one lookup a sprite.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 02:08:35 +01:00
Horizon agentandClaude Opus 5.5 f31f4d179a horizon-bots: fight bots swing as 26.3 does, and --crits for PvP benches
The fight behaviour's swing went out as a raw Punch with a hand byte
26.3's ServerboundSwingPacket doesn't have: every server kicked the bots
on their first hit ("packet 46 was larger than expected: 1 extra bytes").
It now sends the same Punch as --attack.

--crits makes fight bots swing at a sword's full strength (13 ticks) and
jump into every other swing, landing it on the way down: a critical hit,
then a sweep from the ground. tools/perf PvP scenes get crit and sweep
particles from real hits.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 02:08:33 +01:00
Horizon agentandClaude Opus 5.5 88a372f808 Let mob_local_sound and ominous_warning_sound run with parity.py's defaults
tools/parity.py starts every harness with no arguments, and both scripts
required --binary and --work-dir, so parity-main failed them in 0 s
(argparse usage error) since #10524 and #10266. Like
hostile_sound_categories, they now build the game through net.sides and
pick a work folder under .local/ when none is given.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 02:06:56 +01:00
bulkandClaude Opus 5.5 71d809e40c AGENTS.md: drop the counts that go stale
The harness, probe, Game field, impl file and module counts had drifted
(324 harnesses against 502 on disk). Nothing regenerates prose, so name the
things and let the tree give the numbers. CONTRIBUTING.md's "about 290"
harnesses goes with them.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 01:05:27 +00:00
Horizon agentandClaude Opus 5.5 1f7515f5ee Item move comment: a minecart does not stop an item
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 02:04:23 +01:00
Horizon agentandClaude Opus 5.5 818c598db9 Text displays: animated text is never cached, and a culled one keeps its glyphs
Follow-up to #10571 (Skr1zzzz). The cache checked obfuscation on the raw
spans, so a plugin hologram sending §k in literal JSON stopped
animating, and an inline head whose skin arrives later stayed stale: the
animated test now runs once on the resolved text, as drawing reads it.
The key also holds lang::bidirectional() as the name tag cache does, and
a display out of view keeps its cached quads instead of dropping them
before the view check.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 02:01:34 +01:00
Horizon agentandClaude Opus 5.5 84f77ee9a5 Launcher: keep the Home tab notch and the settings rows on wide windows
Follow-up to #10529 (atsauban's responsive wrapping). The Home tab strip
took the full width at every size, so glass::tabbed drew one plate and
lost its notch even at 1440 px: it is full width and scrolls only when
compact. setting_row went from 40 px to 16 px with justify_between for
every row; it keeps 40 px between name and control and wraps the
control under the name (12 px) when the window is too narrow. The
breakpoints are named constants beside WIDE.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 02:01:34 +01:00
Horizon agent eadca0ce30 Merge main into #10242
# Conflicts:
#	tools/command_exec_corpus.py
#	tools/test_command_exec_parity.py
2026-11-07 02:01:07 +01:00
Horizon agentandClaude Opus 5.5 213cfce453 Launcher: the macOS title bar keeps its height without glass
#10129 made the native title bar transparent on macOS but drew the bar only with
glass on, so with glass off the page started under the traffic lights. The bar
is always there: glass, or the side colour as on Windows and Linux.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 02:00:38 +01:00
Horizon agentandClaude Opus 5.5 8b7f726609 Fullscreen: unmaximize first on Windows only, in one helper
#10177 unmaximized before every fullscreen request on every system, and apply()
asked for fullscreen again on each call while the window still reported
maximized. enter_fullscreen keeps the #9984 fix where it applies (winit keeps a
maximized window inside the work area on Windows); the resumed() checks share
one helper.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 02:00:24 +01:00
Horizon agent 3e88f5909b Merge main into #10210
# Conflicts:
#	tools/test_command_exec_parity.py
2026-11-07 02:00:23 +01:00
Horizon agentandClaude Opus 5.5 d59814a196 Shader packs: a pack build no longer wanted stops (follow-up to #10532)
#10532 builds a pack on a thread of its own, but dropping PackLoading (a
pack switch, packs off, another dimension) only detached that thread,
which went on for 5 to 50 s; switching back and forth stacked builds.
PackLoading now holds a cancel flag set when it is dropped: PackPost::new
checks it per program and compile() stops handing out jobs, and the
refusal it returns (is_cancelled) is not reported as the pack failing.
A thread that cannot be spawned now turns the pack off for the
dimension instead of retranslating it every frame.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 01:59:32 +01:00
Horizon agentandClaude Opus 5.5 18309e4967 Remote servers: translate the SSH errors
#10166 landed with its messages as raw strings. Every message a player reads
goes through tr/trf with named placeholders, and every language file gets the
English stand-ins.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 01:59:25 +01:00
Horizon agentandClaude Opus 5.5 955c1ba290 Merge main into #10531; the sky parts are pipelines of the compile too
Main gave a sky program two more pipelines (its vanilla blends for the
disc and void, and for the stars). Here they are RenderJobs made with the
others, in their own slots, and the draw reads them with get().

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 01:59:15 +01:00
Horizon agentandClaude Opus 5.5 8ce49872e7 Follow-ups to #11033, #11044, #11043 and #11016
- Death protection: one prototype lookup per hand instead of a component
  map rebuilt from the stack's patch. update_totem_ready runs for both of
  the player's hands and sync_mob_death_protection for every armed mob,
  every tick; only a stack that patches the component takes the old path.
- Remote projectiles: the swept bubble-column cells are deduplicated
  without a HashSet per projectile per tick.
- Ident::read hands a namespaced identifier back without a second String.
- parked_worlds keeps its doc comment (#11016 put parked_border under it).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 01:59:14 +01:00
Horizon agentandClaude Opus 5.5 280ef828d6 A placed block's swing is no longer sent back to the player
handleUseItemOn sends the use's swing to the player's own client only when
the result is not SwingSource.PREDICTED. Horizon sent every swing back, so
on a slow connection the echo of a placement's swing arrived after the first
half of the client's own swing and swung the arm again (#9887). A placement
and a block's own SUCCESS now count as predicted; the beds, light block,
respawn anchor and sign stay SUCCESS_SERVER and are still sent back.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 01:59:07 +01:00
Horizon agentandClaude Opus 5.5 3a6410b63b Location enchantments: copy only the armor slots that changed
The check runs every tick for every player; cloning all four stacks each
time allocated their components for nothing.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 01:57:52 +01:00
Horizon agentandClaude Opus 5.5 482cc7707c Merge origin/main into #10119
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 01:57:49 +01:00
Horizon agent d8676fff04 Merge main into #10538
# Conflicts:
#	crates/horizon-client-render/src/gfx/pack_post.rs
2026-11-07 01:57:46 +01:00
Horizon agentandClaude Opus 5.5 e409b37078 Brush: a mob blocks the brush out to the block interaction range
BrushItem.calculateHitResult picks entities with blockInteractionRange, not
the entity range: a mob 3 to 4.5 blocks away in front of the block stops it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 01:57:43 +01:00
Horizon agentandClaude Opus 5.5 64c68064f6 Merge origin/main into #10180
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 01:57:37 +01:00
Horizon agent 34d078e57b Merge main into #10208
# Conflicts:
#	tools/lan_e2e.py
2026-11-07 01:56:19 +01:00
Ludovic Marie 80947472c1 Merge pull request #11087 from horizon-rust/hosting/amp-template
🤖 Agent: the AMP update stage runs the egg's own install script byte for byte (test pins it), so both panels install and SHA-256-check the same way; not imported into a live AMP.
2026-11-07 01:55:30 +01:00
Horizon agentandClaude Opus 5.5 1ddc0b8a4d Flame crossbow test: a Flame arrow burns for 2000 ticks
a_flame_crossbow_shoots_burning_arrows (#10488) expected 100 fire ticks and failed with prepared resources. Enchantments.FLAME is Ignite(LevelBasedValue.constant(100.0F)), and Ignite calls igniteForSeconds: 100 seconds, 2000 ticks, as the bow path already gives. The code was right; only the expectation in the test was off.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 01:54:10 +01:00
Horizon agent 868f88a2ce Merge main into #10570 2026-11-07 01:54:00 +01:00
Horizon agent d64be662fe Merge main into #10559 2026-11-07 01:53:52 +01:00
Horizon agent 1d63e80fc1 Merge main into #10553 2026-11-07 01:53:49 +01:00
Horizon agent 3b3f6aa00f Merge main into #10552 2026-11-07 01:53:46 +01:00
Kevin RamirezandClaude Fable 5.1 69ceb53897 CI: run the checks again after the runner ran out of disk
Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-11-06 18:53:43 -06:00
Horizon agent e2268eb854 Merge main into #10201 2026-11-07 01:53:35 +01:00
Horizon agent cf93b66d14 Merge remote-tracking branch 'origin/main' into fix/items-fall-through-minecarts-11091 2026-11-07 01:53:29 +01:00
Horizon agent 5f2f0e689a Merge remote-tracking branch 'origin/main' into fix/mob-entity-collisions-10445 2026-11-07 01:53:25 +01:00
Horizon agentandClaude Opus 5.5 1c42629d64 mob_ai probe: fill MobWorld's others_solid
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 01:52:08 +01:00
Horizon agentandClaude Opus 5.5 486dd3f058 rustfmt
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 01:52:03 +01:00
Horizon agentandClaude Opus 5.5 53d0ea1320 Native mods: read and write the open chat box (native_chat_input, native_set_chat_input)
A client mod that converts text as it's typed (Emoji Type's :code: to emoji)
reads the chat box's text and cursor and writes the result back as an edit,
so completions refresh as for typing. Only what typing lets in reaches the
box. Stacked on native_open_chat.

Part of #10107

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 01:52:03 +01:00
Horizon agentandClaude Opus 5.5 403ecdbadb CI: the by-hand Windows ARM64 job reads the launcher's --version from bash
Its first runs failed at "horizon-launcher --version failed": pwsh read no
output from the GUI-subsystem launcher, while launcher.yml's same check, from
bash on the same runner, passes on every main push.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 01:51:38 +01:00
Horizon agentandClaude Opus 5.5 5d91256713 rustfmt
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 01:50:47 +01:00
Horizon agent a892d2d1eb Merge remote-tracking branch 'origin/main' into fix-11022
# Conflicts:
#	crates/horizon-client/src/game/sculk.rs
2026-11-07 01:50:41 +01:00
Horizon agent 74643df024 Merge remote-tracking branch 'origin/main' into fix-11017
# Conflicts:
#	tools/test_command_exec_parity.py
2026-11-07 01:50:40 +01:00
Horizon agent 07c0d4e8fb Merge remote-tracking branch 'origin/main' into fix-10837
# Conflicts:
#	crates/horizon-client/src/entity_nbt.rs
2026-11-07 01:50:40 +01:00
Horizon agentandClaude Opus 5.5 5a24802d6a rustfmt
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 01:50:16 +01:00
martipopsandClaude Opus 5.5 ecd3764d93 Settings shell: leaving search gives the keys back, and fixes from reviewing it
Once the search field had the focus it kept the keyboard, so Enter did nothing and letters filtered
the list with the ring on a row. Moving the focus away now ends typing.

A key that moves nothing shows no ring, a row greyed out while focused does nothing, the wheel stops
behind a resource error as `scroll_at` does, and a description too long for the strip ends in an
ellipsis, with room for one more line on Extras.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 19:50:14 -05:00
Horizon agentandClaude Opus 5.5 ac8a09ef0a rustfmt
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 01:49:41 +01:00
Horizon agentandClaude Opus 5.5 0a639d598e Merge main into #10501 (test module list in pack_post.rs)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 01:49:38 +01:00
Florian ERNSTandCodex 8b9e8417af Preserve loaded Camel pose timestamp boundaries
Co-Authored-By: Codex <[email protected]>
2026-11-07 01:42:37 +01:00
TaquitoSuabe 8bfcf86e6d net: configure TCP keepalive and zero-alloc stream pumps 2026-11-06 17:34:49 -07:00
Kevin RamirezandClaude Fable 5.1 1426129516 CI: run the checks again after the runner ran out of disk
Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-11-06 18:33:12 -06:00
voidsip a932bc6c86 Create recipe directory for isolated smithing menu fixture 2026-11-06 18:28:12 -06:00
Kevin RamirezandClaude Fable 5.1 747b958bd9 CI: run the checks again after the runner ran out of disk
Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-11-06 18:23:06 -06:00
martipopsandClaude Opus 5.5 7f1eef46b2 Settings shell: a screen describes its rows, and Horizon Extras moves onto it
A screen now implements settings_shell::Screen, describing its pages, groups,
rows and values, and Shell does the drawing, clicks, scrolling, the controller's
focus and declared buttons, keys and each row's description. OptionsUi reaches
it through one accessor, so the wheel and a stick's scrolling can't part ways.

Horizon Extras is the first screen on it. Its settings do what they did and are
saved the same way. It gains the shell's look and search, and its descriptions
move from tooltips to the strip, which grows for them.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 19:21:17 -05:00
voidsip 001f3d008d Clarify the wind hook preserves shuffle random draws 2026-11-06 18:19:07 -06:00
Vinícius MoreiraandClaude Opus 5.5 c80eac9506 Polytone: read targets and biome/dimension attribute modifiers
A new crate, horizon-polytone, reads the Polytone resource pack format
clean-room from the public wiki: modifier targets (implicit path id, ids,
tags, regex, required) and the attributes_modifiers layers of biome and
dimension modifiers (groups, REMOVE, values, modifier objects, colors,
colormap references, expressions kept as source). Nothing uses it yet.

Part of #11486

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 21:17:25 -03:00
DetimesandClaude Opus 5.5 edeee4d656 Older clients and servers: 1.21.8, 1.21.6 and 1.21.5 (#10404)
Packet and registry id tables generated from the vanilla data reports of the
three releases; packet layouts from the public protocol documentation
(minecraft.wiki's archived packet pages for protocols 770, 771 and 772).
Each step has a reader (older server) and a writer (older client) with round
trips; entity data serializers without 1.21.9's additions. The end-to-end
bot test now joins as every release down to 1.21.5.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 00:15:18 +00:00
iseaprojectsandOpenCode a0d291e2bb i18n(es_es): complete the Spanish strings after en_us grew to 894 keys
es_es.json held 271 of the 894 keys en_us.json now ships, so 623 strings
(70% of the file) fell back to English on Spanish screens: the whole PvP
and HUD page, splitscreen setup and profiles, Horizon extras, the mod
browser, the resource pack browser, friends, the on-screen keyboard,
Replay Studio, the music page, cosmetics and the shader and graphics
descriptions.

This fills the table: 894/894 keys, no missing or extra ones, every
placeholder kept. The four values that were still English (Particle
Culling and its three descriptions) are translated, and the shader page
name now says "shaders" like the rest of the file.

lang.rs also serves es_ar, es_cl, es_ec, es_mx, es_uy and es_ve from this
file.

Co-Authored-By: OpenCode (DeepSeek V4.1 Flash) <[email protected]>
2026-11-06 20:08:07 -04:00
Florian ERNSTandCodex 2aa9308e47 Reject stackable damageable item command inputs
Co-Authored-By: Codex <[email protected]>
2026-11-07 01:04:42 +01:00
Kevin RamirezandClaude Fable 5.1 23c572b374 CI: run the checks again after the runner ran out of disk
Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-11-06 18:03:09 -06:00
RINNECODERandClaude Opus 5.5 4ed6650b88 Port pack: carried models name 26.3's iron chain
A 1.21.1 model's minecraft:block/chain or item/chain (parent or texture) became
iron_chain in 26.3, so a mod block drawing vanilla's chain baked with the missing
texture. Carried models and block states get the RENAMED_IDS rename data files
already had.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 18:02:48 -06:00
RINNECODERandClaude Opus 5.5 a124aba31a /recipe suggests content mods' recipes
The recipe index reads every content namespace since 31756ebfe, but /recipe's
suggestions still listed only minecraft: ids. List data/<ns>/recipe of every
namespace, as /loot's loot tables already are.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 18:02:48 -06:00
voidsip 8158bcae73 Use the item codec crate in the mining parity probe 2026-11-06 17:45:50 -06:00
voidsip 7397241fb6 Give debug wind fixtures an explicit stack budget 2026-11-06 17:43:29 -06:00
Kevin RamirezandClaude Fable 5.1 733d3abd7e CI: run the checks again after the runner ran out of disk
Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-11-06 17:43:05 -06:00
voidsip 62724ffccf Record scoped mining parity source and artifact hashes 2026-11-06 17:41:16 -06:00
martipopsandClaude Opus 5.5 f8a7a59085 Controllers: Replay Studio's recordings and buttons take the focus
Its rows and buttons only painted, so a pad reached them through the crosshair alone. The D-pad now
walks the recordings to the buttons, the right stick scrolls them, and Done declares its button.

A notch is now one whole row, since nothing clips a row that's partly scrolled off, and the scroll
limit counts the rows that fit, so the last recording can be reached with the mouse too.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 18:39:17 -05:00
DUzzL 52105d2c9b Adventure mode: gate placed entities and firework on can_place_on 2026-11-07 00:38:11 +01:00
voidsip 9782763e82 Cover mining component admission and explicit break slots 2026-11-06 17:35:52 -06:00
voidsip 2eb6044dfc Keep remote mining fixture compatible with speed cache 2026-11-06 17:34:56 -06:00
Florian ERNSTandCodex dd47001112 Record signed equine temper proof and scope
Co-Authored-By: Codex <[email protected]>
2026-11-07 00:32:23 +01:00
DUzzL 330c59d954 Resend the glide flag when the server refuses a glide start 2026-11-07 00:30:33 +01:00
Evrim Cicek 02e7620924 Merge remote-tracking branch 'upstream/main' into fix/tnt-fuse-any-number
# Conflicts:
#	tools/command_exec_corpus.py
2026-11-07 02:30:18 +03:00
Evrim Cicek 71b527b7c2 Merge remote-tracking branch 'upstream/main' into fix/enderman-hurt-teleport-now
# Conflicts:
#	tools/command_exec_corpus.py
2026-11-07 02:30:09 +03:00
DetimesandClaude Opus 5.5 9d4cecee14 Older clients: serve 26.1, 1.21.11 and 1.21.10 clients too (#10404)
Mirror translate's per-release readers as writers (client_session/step_*),
keep sent entities for entity data and 26.1's spectate by UUID, drop the
world_clock registry and split 1.21.11's interact into interact and attack.
End-to-end test joins a bot of each release through the adapter's Session.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 23:29:51 +00:00
Kevin RamirezandClaude Fable 5.1 6564896c45 CI: run the checks again after the runner ran out of disk
Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-11-06 17:23:11 -06:00
k3yhandClaude Opus 5.5 a456a1476c Worldgen: name the placed features that cannot run
The warning only gave their count, so a data pack or content mod author
could not tell which features were skipped or why. Log each one with its
reason, up to 20.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 18:22:02 -05:00
MSedgeMC 85a97ed37c Re-run CI: the previous run failed in setup with the self-hosted runner out of disk 2026-11-06 23:13:26 +00:00
Florian ERNSTandCodex b775c66123 Preserve signed equine temper independently of gameplay limits
Co-Authored-By: Codex <[email protected]>
2026-11-07 00:10:40 +01:00
voidsip e3295a9974 Trim gateway fixture trailing blank line 2026-11-06 17:10:23 -06:00
rison1337andCodex d15d01fbc2 perf(render): keep offscreen flame culling active with TAA
Use a conservative input-pixel guard for the billboard camera frustum
without changing the ordinary model frustum, flame envelope or shader
pack fallback. Keep this PR independent of experience-orb culling.

Retain the separate flame measurements and image controls, with their
original binary provenance and an explicit note about the PR split.

Co-Authored-By: Codex (GPT-6) <[email protected]>
2026-11-07 04:04:56 +05:00
Kevin RamirezandClaude Fable 5.1 c14c063507 CI: run the checks again after the runner ran out of disk
Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-11-06 17:03:06 -06:00
voidsip 8554155932 Remove obsolete saved gateway arrival helper 2026-11-06 17:00:00 -06:00
rison1337andCodex 539c847e65 perf(items): avoid unused dropped-item bounds work
Reuse resolved local model depth when placing dropped-item stack copies.
Skip the unused posed occlusion envelope with entity culling and TAA off,
and retain the original preparation under TAA after measured tail regressions.

Keep the existing cluster API and random sequence, with a regression test,
paired production measurements and explicit rendering-validation limits.

Co-Authored-By: Codex (GPT-6) <[email protected]>
2026-11-07 03:58:08 +05:00
MSedgeMC d114d2ef89 test: a stub with inline data or no .mcc still reads the .mcc
Without the change the first reads `Err(InvalidData, "external chunk stub has
inline data")` and the second `Err(Os { code: 2, kind: NotFound })` instead of
the 4000 bytes in `c.-32.0.mcc` and an absent chunk.
2026-11-06 22:56:48 +00:00
MSedgeMC 30a3b019ff fix: read a chunk's external stream the way RegionFile reads it
`RegionFile.getChunkDataInputStream` never trusts the stub's length once the
external flag is set: `streamLength != 0` only logs "Chunk has both internal and
external streams" and the stream comes from the `.mcc`. When the `.mcc` is not a
regular file, `createExternalChunkInputStream` answers `null`, so the chunk is
absent. Horizon refused both: a stub that also declared inline data failed with
"external chunk stub has inline data", and a stub whose `.mcc` was gone failed
with a `NotFound` io error, which aborts a reader where vanilla skips the chunk.
2026-11-06 22:56:47 +00:00
voidsip 4807f13288 Format world border regression assertions 2026-11-06 16:56:09 -06:00
GabrielNichols fe28bf9738 Route Recipes shortcuts in Creative inventory 2026-11-06 19:54:08 -03:00
TheMrZZandCodex gpt-6.1-sol 1d72cf2ed8 Discard loaded falling air before age and physics
Use the existing air predicate and Gone result at the start of the actual falling-block tick. Cover admitted command actors with a real Game tick regression and frozen Java parity controls.

Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-11-06 23:53:32 +01:00
brasgaandClaude Opus 5.5 5b9fe567ec Player predicates test food level and saturation (#11314)
PlayerPredicate.matches calls FoodPredicate.matches on the player's
FoodData: the food level in `level`, then the saturation, a float
compared as a double, in `saturation`. The player facts carried no food
and the matcher never read `food`, so any food bounds passed.

PlayerFacts now holds the food, always filled (two numbers), for the
player in place and for a server's other players, and player_facts tests
it after the level, as vanilla does.

The command_exec harness gets a group that fills and drains the player's
food in player loops and tests named food predicates, a non-player, and
a tick advancement whose player condition asks only for food.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 19:53:29 -03:00
DUzzL 9558ecf848 Keep the copper golem's name on its statue 2026-11-06 23:49:15 +01:00
Kevin RamirezandClaude Fable 5.1 d6802414fa CI: run the checks again after the runner ran out of disk
Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-11-06 16:43:06 -06:00
DUzzL a7d2edbfb9 Beacon: select players by bounding-box overlap with the effect box 2026-11-06 23:37:57 +01:00
GabrielNichols 99a593a6bf Match the item browser header and Creative transfer affordance 2026-11-06 19:37:54 -03:00
martipopsandClaude Opus 5.5 c752fee870 Controllers: Play in Background, rumble in splitscreen, the keyboard after a new renderer
Play in Background, off by default and per player, lets the pad keep driving the game while its
window has no focus, and a game the pad is playing doesn't pause on losing focus.

While several play, the router now plays each player's rumble on that player's pad. Rumble no
longer stops when the window loses focus: nothing asked for that, and it's useful to an AFK player.

Starting or leaving splitscreen builds a new renderer, whose atlas has none of what was drawn into
it at run time. Those two paths only forgot the skins, so the on-screen keyboard lost its letters
and rounded corners, and maps were exposed the same way. All renderer swaps now forget the same
things.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 17:37:00 -05:00
Nikita Maximilian GrimmandClaude Opus 5.5 5d44519d5b Frame identity: keep the player on a block while the scenes are built
Left in the air at y 163, the player fell into the sea below while prepare() ran, which unlocked the boat
recipes: their toast showed over the first shots at a different moment each run, so main against itself
failed in the top right corner.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 23:27:38 +01:00
MSedgeMC 3e484d6adf port_map: Armadillo's state is saved and read
`tools/port_map/animal-1.tsv` had no answer for
`Armadillo.addAdditionalSaveData` or `Armadillo.readAdditionalSaveData`. Both
now name `entity_nbt::mob_to_nbt` / `mob_from_nbt`: the state written as its
`ArmadilloState` name beside `scute_time`, and read back through
`switchToState`, idle when the save names none.
2026-11-06 22:27:24 +00:00
MSedgeMC 3ba815a17a test: an armadillo's rolled-up state through the chunk save
`mob_from_nbt` and `mob_to_nbt` are the pair `Game::load_mob` and
`Game::mob_tree_nbt` go through. An armadillo whose save names
`state: rolling` loads rolling and takes 4.5 of a hit for 10; six ticks into
the roll its shell is out; a save that names no state, or a name that is no
`ArmadilloState`, loads idle and takes the whole 10. Saving a rolling armadillo
writes `rolling` even when the compound it came in with said `idle`, and the
reloaded one has its time in the state restarted, so the shell is not out until
it has been rolling for six ticks again.

Without the state in the pair the loaded armadillo is idle (left: 0, right: 1)
and takes 10 for a 10 hit where it took 4.5 rolled up.
2026-11-06 22:27:24 +00:00
MSedgeMC d2d4e99101 fix: keep an armadillo's rolled-up state in its chunk save
`Armadillo.addAdditionalSaveData` writes the state the armadillo is in beside
its scute timer, and `readAdditionalSaveData` switches to it:

    output.store("state", Armadillo.ArmadilloState.CODEC, this.getState());
    output.putInt("scute_time", this.scuteTime);
    ...
    this.switchToState(input.<Armadillo.ArmadilloState>read("state", Armadillo.ArmadilloState.CODEC)
       .orElse(Armadillo.ArmadilloState.IDLE));

`ArmadilloState.CODEC` is a `StringRepresentable.fromEnum`, so the key holds
`idle`, `rolling`, `scared` or `unrolling`; `switchToState` sets the synched
state, and `onSyncedDataUpdated` restarts `inStateTicks` when it changes.

Horizon models the same state (`extra.armadillo.state`, the value
`Armadillo.getState` is synched with) and wrote only `scute_time`, so an
armadillo that unloaded while rolled up came back idle: a hit for 10 went from
4.5 back to 10.0 (`Armadillo.hurtServer`: a ball takes half of the damage past
the first point), `hiding_in_shell` went false so the model lost its shell, and a
brush or a food click was answered by an idle armadillo instead of a ball.

`state` is now written from the live state - `idle` for one that is not rolled
up, as vanilla writes it unconditionally - and read back through `set_state`,
which is `switchToState` plus `onSyncedDataUpdated`: a save that names no state
(or names one that is not an `ArmadilloState`) loads as idle, and the time in
the state starts again at 0.
2026-11-06 22:27:24 +00:00
GabrielNichols a92071933a Bring Recipes search and its skin to Creative inventory 2026-11-06 19:27:01 -03:00
voidsip 7914cfe741 Keep saved-creeper radius test compatible with silence metadata 2026-11-06 16:23:43 -06:00
Kevin RamirezandClaude Fable 5.1 786bc6d8ca CI: run the checks again after the runner ran out of disk
Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-11-06 16:23:11 -06:00
MSedgeMC c3326aed5e test: direct furnace block-entity tick parity
The command-execution furnace family could not give a checkable gap control
on this host, so this is a direct AbstractFurnaceBlockEntity.serverTick
harness instead: a real RecipeManager and a furnace, blast furnace or smoker
in a block-update level, compared with FurnaceCooking::tick. It pins the
cooking_fuel burn table for every fuel item, the 200/100/100 cook times and
speed multiplier, fuel consumption and LIT state, RecipesUsed experience, a
full output slot, timer decay, and an input changed mid-cook. --no-change
reproduces the corpus before that family.

757/757 cases match the pinned client.
2026-11-06 22:20:04 +00:00
voidsip eab98fdd88 Allow slow debug fixtures and retain wind hook evidence 2026-11-06 16:18:35 -06:00
voidsip 3cdf31772f Fix falling damage helmet durability order 2026-11-06 16:14:24 -06:00
GabrielNichols 254d2c8e10 Keep the Recipes footer through empty search transitions 2026-11-06 19:13:52 -03:00
voidsip 088b9e37e5 Merge commit '9d11d0c710705ee0901c2186d78641048a6660b0' into fix/11330-explosion-item-health 2026-11-06 16:13:47 -06:00
voidsip 98cfa7ef55 Merge current upstream main into stack tool mining fix 2026-11-06 16:13:44 -06:00
voidsip a79aa4f977 test: add headless dropped-item explosion regression 2026-11-06 16:13:38 -06:00
voidsip 55d7ad61f1 Merge commit '9d11d0c710705ee0901c2186d78641048a6660b0' into fix/11328-trade-modifiers 2026-11-06 16:13:33 -06:00
voidsip 2b839bce3d Keep smithing regression compatible with baseline APIs 2026-11-06 16:13:32 -06:00
voidsip 929ad64e2a Match trade enchantment modifiers and provider costs 2026-11-06 16:13:26 -06:00
voidsip 5345f12b9d Merge commit '9d11d0c710705ee0901c2186d78641048a6660b0' into fix/smithing-transform-11307 2026-11-06 16:12:56 -06:00
voidsip 5a6a6e4e77 Merge commit '9d11d0c710705ee0901c2186d78641048a6660b0' into fix/11123-native-hud-mode
# Conflicts:
#	crates/horizon-client/src/game/native_hooks.rs
2026-11-06 16:12:55 -06:00
voidsip b1e79509ac Fix smithing transform optional inputs and result templates 2026-11-06 16:12:45 -06:00
voidsip bf40027865 Fix stack tool mining rules and immersion checks 2026-11-06 16:12:44 -06:00
voidsip 07b8984a5b Merge commit '9d11d0c710705ee0901c2186d78641048a6660b0' into fix/wind-burst-block-hook 2026-11-06 16:12:38 -06:00
voidsip 476bb33916 Expose native HUD mode with independent hand visibility 2026-11-06 16:12:32 -06:00
voidsip cf69ad16ef Add a native wind burst block filter hook 2026-11-06 16:12:23 -06:00
voidsip 0289686b53 Merge commit '9d11d0c710705ee0901c2186d78641048a6660b0' into fix/11317-block-crack-feedback 2026-11-06 16:12:21 -06:00
voidsip eeb3c04215 Match server crack stages and observer range 2026-11-06 16:12:13 -06:00
voidsip 9af63f81d3 Merge commit '9d11d0c710705ee0901c2186d78641048a6660b0' into fix/11319-gateway-item-motion 2026-11-06 16:12:06 -06:00
voidsip 4fd7c5c271 Keep dropped items live through End gateways 2026-11-06 16:11:57 -06:00
GabrielNichols c7d27a84a1 Fill Recipes grids without teaching the recipe 2026-11-06 19:10:19 -03:00
rdbt 13f70ad482 Merge pull request #11250 from TheMrZZ/fix/h252-loaded-pig-tags
Normalize Mob Tags when adopting NBT
2026-11-06 22:09:57 +00:00
rdbt 6590656a9a Merge pull request #11393 from ilguerro/fix/10548-villager-memories
Save villager brain memories like vanilla
2026-11-06 22:09:51 +00:00
rdbt f2e7c1865f Merge pull request #11405 from ilguerro/fix/w076
Count collidable entities in auto-jump like vanilla
2026-11-06 22:09:45 +00:00
rdbt 876c817f7d Merge pull request #11397 from ilguerro/fix/w029
Keep wandering trader homes like vanilla
2026-11-06 22:09:40 +00:00
voidsip c0aef4ac41 Merge commit '9d11d0c710705ee0901c2186d78641048a6660b0' into fix/round5-5-11321 2026-11-06 16:09:25 -06:00
voidsip 9e11182d53 Respect world borders for pistons and natural spawning 2026-11-06 16:09:24 -06:00
rdbt b465f24309 Merge pull request #11395 from ilguerro/fix/w024
Match vanilla hanging entity coexistence checks
2026-11-06 22:09:04 +00:00
voidsip 854e12bf62 Merge upstream main for source-silence validation 2026-11-06 16:08:31 -06:00
MrCloudy2andClaude Opus 5.5 858582b279 Droppers insert into chest boats and container minecarts
DropperBlock.dispenseFrom finds its target with HopperBlockEntity.getContainerAt:
the block's container, else a container entity (chest boat or raft, chest or
hopper minecart) whose box meets the space in front, picked at random. Horizon's
dropper only asked for a block container, so items fell out next to a chest boat
instead of going in. It now falls back to the entity lookup hoppers already use.

The dispenser harness gains 48 dropper cases with chest boats, rafts and chest
and hopper minecarts centred in front, reaching in from beyond, just past the
space, two at once, behind a chest block and non-container vehicles. The oracle
and the fixture print the container entity's items (Tinv lines).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 23:06:16 +01:00
voidsip c06d1d81c4 Respect Silent on explosions and potion impacts 2026-11-06 16:06:09 -06:00
Florian ERNSTandCodex a44cdec48e Adopt live falling-block data into the resident entity
Co-Authored-By: Codex <[email protected]>
2026-11-06 23:03:23 +01:00
Kevin RamirezandClaude Fable 5.1 03e72bc0b0 CI: run the checks again after the runner ran out of disk
Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-11-06 16:03:08 -06:00
MSedgeMC 197ad9fcc5 port_map: Phantom's anchor_pos is read and written
`tools/port_map/game2-002.tsv` recorded `gap: minor: anchor_pos is not read` and
`not written` for `Phantom.readAdditionalSaveData` and
`Phantom.addAdditionalSaveData`. Both now carry the anchor and say how: the key
only for a phantom that has an anchor, absent or unreadable as no anchor.
2026-11-06 22:00:10 +00:00
MSedgeMC 58feda2a09 test: a phantom's circling anchor through the chunk save
`mob_from_nbt` and `mob_to_nbt` are the pair `Game::load_mob` and
`Game::mob_tree_nbt` go through, so the test loads and saves a phantom by its
compound. A phantom at (0.5, 75, 0.5) whose save names `anchor_pos: [100, 64,
-40]` loads with that anchor, circles to (109.66, 62, -37.41) - the anchor,
`distance*cos(angle)` across and `-4 + height` up - and a phantom that moved its
anchor to (9, 70, -3) saves that position, not the one its compound came in
with. A phantom with no anchor loads with none and saves no `anchor_pos`.

Without reading the anchor the load gives the phantom (0, 80, 0), its own
position five blocks up, and the circle point (9.66, 78, 2.59); without writing
it the save keeps the stale (100, 64, -40).
2026-11-06 22:00:10 +00:00
MSedgeMC c02c215c62 fix: save a phantom's circling anchor with its chunk
`Phantom.addAdditionalSaveData` writes the anchor the phantom circles around
beside its size, and `readAdditionalSaveData` takes it back:

    protected void readAdditionalSaveData(final ValueInput input) {
       super.readAdditionalSaveData(input);
       this.anchorPoint = input.<BlockPos>read("anchor_pos", BlockPos.CODEC).orElse(null);
       this.setPhantomSize(input.getIntOr("size", 0));
    }

    protected void addAdditionalSaveData(final ValueOutput output) {
       super.addAdditionalSaveData(output);
       output.storeNullable("anchor_pos", BlockPos.CODEC, this.anchorPoint);
       output.putInt("size", this.getPhantomSize());
    }

Horizon models that anchor in `extra.home`, which
`PhantomCircleAroundAnchorGoal.selectNext` centres the circle on
(`anchor + (distance*cos(angle), -4 + height, distance*sin(angle))`), but
`mob_to_nbt` wrote only `size` and `mob_from_nbt` read only `size`. A phantom
saved and reloaded therefore kept the anchor `register` gives every new phantom
- its own position five blocks up, vanilla's `finalizeSpawn` value - instead of
the one in its save: a phantom at (0.5, 75, 0.5) loaded with
`anchor_pos: [100, 64, -40]` came back anchored at (0, 80, 0), and its first
circle point was (9.66, 78, 2.59) rather than (109.66, 62, -37.41).

`TagValueOutput.storeNullable` writes the key only for a non-null value, and
`read(...).orElse(null)` gives null for a save without one, so a phantom with no
anchor now writes no `anchor_pos` and loads back with none - the circle then
fills it with its own `blockPosition()`, as vanilla's does.
2026-11-06 22:00:10 +00:00
MSedgeMC 73737f9e6c test: brewing stand tick, brew time and fuel uses
The brewing suite stopped at recipe matching, assemble and the slot filters;
nothing advanced the block entity. The tick family now runs the real
BrewingStandBlockEntity.serverTick and BrewingStand::tick for 0..8001 ticks
and compares slots, brew time, total brew time, fuel, total fuel, speed,
bottle bits, ingredient and the brew-event count. It covers the 400-tick brew
divided by speed_multiplier and its ceiling (2, 3, 0.3 and 0), blaze powder's
twenty uses and speed 1, fuel loaded with nothing to brew, splash and
lingering outputs, ingredient shrink and mismatch, finishing without fuel and
twenty brews from one blaze powder. --no-tick leaves the tick corpus out.

2860/2860 cases match the pinned client.
2026-11-06 21:54:20 +00:00
GabrielNichols 7351b11d1a Sync Recipes settings with main 2026-11-06 18:52:17 -03:00
Delete228andCodex 8c13c07e40 Merge the fluid renderer fixture update
Co-authored-by: Codex <[email protected]>
2026-11-07 00:49:10 +03:00
Delete228andCodex a661a2262d Pass empty motion data in the fluid renderer fixture
Co-authored-by: Codex <[email protected]>
2026-11-07 00:48:50 +03:00
Delete228andCodex 00a77c1e1f Merge the updated fluid renderer into the model renderer
Co-authored-by: Codex <[email protected]>
2026-11-07 00:46:25 +03:00
martipops 37b174ada3 Merge remote-tracking branch 'upstream/main' into fix/pad-read
# Conflicts:
#	crates/horizon-client/src/game.rs
2026-11-06 16:46:14 -05:00
TheMrZZandCodex gpt-6.1-sol 008aba983e Spill explicit vehicle container contents on kill removal
Write back live menu slots before spilling chest boats, chest rafts and
container carts. Reuse the container split/motion algorithm through its
actual double-origin path, preserving integer callers and empty-slot draws.
Keep ordinary hurt, hull, lead and pending-loot policies separate.

Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-11-06 22:45:56 +01:00
Delete228andCodex 0c66644118 Merge Horizon main into the PR branch
Co-authored-by: Codex <[email protected]>
2026-11-07 00:45:54 +03:00
Delete228andCodex 129e00b6b1 Merge Horizon main into the PR branch
Co-authored-by: Codex <[email protected]>
2026-11-07 00:45:54 +03:00
Delete228andCodex 39f7f1bf42 Merge Horizon main into the PR branch
Co-authored-by: Codex <[email protected]>
2026-11-07 00:45:54 +03:00
Delete228andCodex 63ac8a4adf Merge Horizon main into the PR branch
Co-authored-by: Codex <[email protected]>
2026-11-07 00:45:54 +03:00
Kevin RamirezandClaude Fable 5.1 75a078b0ce CI: run the checks again after the runner ran out of disk
Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-11-06 15:43:56 -06:00
Kevin RamirezandClaude Fable 5.1 fdc26c707e CI: run the checks again after the runner ran out of disk
Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-11-06 15:43:30 -06:00
Riccardo Guerriero 01b3be1502 Merge remote-tracking branch 'origin/main' into perf/11126-structure-starts-jigsaw 2026-11-06 22:42:17 +01:00
voidsip 9c292718aa Persist comparator output and hopper transfer cooldown 2026-11-06 15:40:57 -06:00
DetimesandClaude Opus 5.5 611dfc244f Merge upstream/main into hooks-all
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 21:38:48 +00:00
Nikita Maximilian Grimm 3b64b39c5b Merge branch 'shader-api-02-serve' into hdr-step3-float-frame
# Conflicts:
#	crates/horizon-client/src/game/shader_packs.rs
2026-11-06 22:38:21 +01:00
Kevin RamirezandClaude Fable 5.1 58859564a8 CI: run the checks again after the runner ran out of disk
Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-11-06 15:36:34 -06:00
Florian ERNSTandCodex 7b53924423 Use modern falling-block state decoding and sand fallback
Co-Authored-By: Codex <[email protected]>
2026-11-06 22:36:12 +01:00
Kevin RamirezandClaude Fable 5.1 d4984f2ca0 CI: run the checks again after the runner ran out of disk
Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-11-06 15:36:08 -06:00
DetimesandClaude Opus 5.5 2e01586b5e Merge upstream/main into hooks-b-all
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 21:35:45 +00:00
voidsip 113cd40efb Respect per-stack brewing fuel components 2026-11-06 15:33:46 -06:00
Nikita Maximilian GrimmandClaude Opus 5.5 d36a2c9e21 fix(shaders): give the scripted-clock test its shader API line
main's test predates NativeFrame::new's api argument; it runs a 0.1 frame, as the other tests do.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 22:33:28 +01:00
DUzzL dd2d21f0f9 Fix natural spawn height bound off by one
NaturalSpawner.getRandomPosWithin samples up to chunk.getHeight(WORLD_SURFACE)+1,
and ChunkAccess.getHeight is firstAvailable-1, so the bound is firstAvailable.
Horizon already stored firstAvailable and added one more. Also correct the
oracle's fake LevelChunk.getHeight.
2026-11-06 22:30:06 +01:00
Nikita Maximilian Grimm 1ade310792 Merge remote-tracking branch 'origin/main' into shader-api-02-serve
# Conflicts:
#	crates/horizon-client/src/game/native_shaders.rs
#	crates/horizon-client/src/game/shader_packs.rs
2026-11-06 22:29:20 +01:00
rdbt 9d11d0c710 Merge pull request #11369 from Skr1zzzz/ride-any-tracked-entity
Multiplayer: the player rides any tracked entity, sitting where vanilla seats it
2026-11-06 21:27:13 +00:00
umcekoandClaude Sonnet 5.5 5fa72fe13a Gameplay reads the dimension type for lava, water, beds and anchors (#11282, 3/9)
Fast lava, water evaporation, bed and anchor rules, the raid-bright check, the
day timeline, ambient light and the default clock of /time read the level's
dimension type facts instead of matching on the level, as vanilla reads
DimensionType and its attributes. No behaviour change for the three built-in
levels.

Part of #11282

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-07 00:27:09 +03:00
umcekoandClaude Sonnet 5.5 0b23212f49 The dragon fight follows has_ender_dragon_fight (#11282, 5/9)
The fight tick, end crystal fire and respawn, the boss bar, the fight dragon
check, the exit portal and the DRAGON chunk ticket read the dimension type's
has_ender_dragon_fight, and the End flashes read its End skybox. No behaviour
change for the three built-in levels.

Part of #11282

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-07 00:27:09 +03:00
umcekoandClaude Sonnet 5.5 5db025666c Mobs and spawn rules read the dimension type (#11282, 4/9)
MobWorld carries the level key and its dimension type facts instead of nether
and end flags, and the spawn light test samples the type's
monster_spawn_light_level as vanilla's DimensionType.MonsterSettings does.
Fast lava, snow golem melting, piglin zombification, fixed-time and day
timeline checks read the type's facts. No behaviour change for the three
built-in levels; random draws are unchanged.

Part of #11282

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-07 00:27:09 +03:00
umcekoandClaude Sonnet 5.5 b37e409c9c Block behaviour reads the dimension type, not the dimension (#11282, 2/9)
Vanilla reads each level's DimensionType and its attributes (fast lava, water
evaporates, the day timeline, infiniburn, sky light) rather than naming the
Overworld, Nether and End, so a datapack dimension using the Nether's type
behaves as the Nether. horizon-world block behaviour now does the same through
LevelAccess::dimension_type(); sites vanilla keys on the level compare LevelKey.
No behaviour change for the three built-in levels.

Part of #11282

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-07 00:27:09 +03:00
rdbt c90689fd74 Merge pull request #10168 from NikitaMGrimm/shader-pack-identity
Keep native shader selections and settings across updates
2026-11-06 21:27:07 +00:00
rdbt 22628d794c Merge pull request #10194 from IanWorley/native-api/block-appearance
Native mods: block state facts and the client's resource files, for maps and radars
2026-11-06 21:27:01 +00:00
umcekoandClaude Sonnet 5.5 3874099e52 Dimension type facts read instead of matching the level (#11282, 1b/9)
Code that matches on the Overworld, the Nether or the End should read the
level's dimension type, as vanilla does. Add DimensionFacts: the
DimensionType fields and gameplay attributes Horizon reads, the four
vanilla types built in code, and from_json for packs. A test checks the
built-in table against the prepared 26.3 dimension_type and timeline tag
files.

No callers yet; nothing changes for players, saves or packets.

Part of #11282.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-07 00:26:56 +03:00
umcekoandClaude Sonnet 5.5 dfbfc81aa9 LevelKey: a level's key for any dimension, not three variants (#11282, 1a/9)
Vanilla makes one level per entry of the dimension registry and keys each
by its ResourceKey<Level>. Add LevelKey: the three built-in levels as
constants, any other id validated as Identifier.parse does and interned,
plus builtin_or_overworld for today's lossy lookups from saves, guests
and packets.

No callers yet; nothing changes for players, saves or packets.

Part of #11282.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-07 00:26:56 +03:00
rdbt a94b615c99 Merge pull request #11014 from senjinthedragon/entity-at-velocity
Shader packs: at_velocity for entities
2026-11-06 21:26:55 +00:00
rdbt 63c8c5bce2 Merge pull request #11181 from Slavik-85/f3-overlay-light
F3 overlay: paint at 20 Hz, no per-frame OS calls, less GPU timing
2026-11-06 21:26:49 +00:00
rdbt 13be1a9e4f Merge pull request #10529 from atsauban/fix/launcher-narrow-layout-6362
fix(launcher): support responsive wrapping on narrow windows
2026-11-06 21:26:42 +00:00
rdbt 6a978d3bef Merge pull request #11051 from Delete228/native-dropped-item-contact
Native mods: expose dropped-item block contact
2026-11-06 21:26:37 +00:00
rdbt a8055893a2 Merge pull request #11033 from DUzzL/fix/10587-death-protection-item
Death protection: use the death_protection item and its effects, for players and mobs
2026-11-06 21:26:31 +00:00
rdbt ef78519011 Merge pull request #9952 from Ragbie/fix/iron-golem-toss
Iron golems toss by knockback resistance and keep their anger like vanilla
2026-11-06 21:26:25 +00:00
rdbt c30775263f Merge pull request #10197 from IanWorley/native-api/client-world-snapshots
Native mods: client world snapshots a mod reads on its worker threads
2026-11-06 21:26:18 +00:00
rdbt 04176c2939 Merge pull request #10133 from Ragbie/fix/mounted-attack-box
A mounted mob reaches from its vehicle's width, and a rider is hit from its seat up
2026-11-06 21:26:10 +00:00
rdbt 96505f501d Merge pull request #11113 from Detimes/hooks-bots
Native mods: helpers for server-side player bots (one movement tick, block collision boxes)
2026-11-06 21:26:04 +00:00
rdbt c932d4bec1 Merge pull request #10166 from hildlabs/remote-servers-ssh
Launcher: a server on another computer, run over SSH
2026-11-06 21:25:59 +00:00
rdbt c87e44bf63 Merge pull request #11062 from voidsipp/fix/round4-7-10178
Add local particle spawning for native client mods
2026-11-06 21:25:53 +00:00
rdbt d8d0d5e832 Merge pull request #10554 from ilguerro/perf/9996-startup
Parse entity-models.json once at startup
2026-11-06 21:25:47 +00:00
rdbt 79d2b975f5 Merge pull request #11264 from senjinthedragon/hand-translucent
Shader packs: gbuffers_hand_water for translucent held items
2026-11-06 21:25:41 +00:00
rdbt 0b9487e6e9 Merge pull request #11090 from Delete228/native-fluid-provider-upstream
Native mods: let existing block fluid helpers reach native-owned tanks
2026-11-06 21:25:36 +00:00
rdbt e32b03ea27 Merge pull request #11064 from horizon-rust/roadmap/proposal-issues
Roadmap: attach 22 proposal issues to their items, two new Later items
2026-11-06 21:25:29 +00:00
Kevin RamirezandClaude Fable 5.1 df010b6204 CI: run the checks again after the runner ran out of disk
Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-11-06 15:24:27 -06:00
AkirabaneandClaude Opus 5.5 8e033a0480 dedicated: no move onto a server that is full or stopping
player-expect is refused as "the server is full" once the places the
player limit counts and those of the other expected players reach
max-players: each expectation holds a place, so two proxies sending
players at once cannot fill a server past its limit. As at login, an op
who bypasses the limit is never refused, and a renewal holds no second
place.

Once a stop has begun (/stop from the console, a player or the control
endpoint, or a stop signal), player-expect, player-import and
player-confirm are refused as "the server is stopping": a player must not
arrive on a server that is about to close. A retry after the commit is
still answered, and export, release and drop stay open, since they are how
players leave. transfer-status says whether the server is stopping.

Refs #7484

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 22:23:35 +01:00
Nikita Maximilian Grimm 89bfc9ab79 Merge remote-tracking branch 'fork/shader-api-02-serve' into hdr-step3-float-frame 2026-11-06 22:21:44 +01:00
rdbt c4d8c79e2b Merge pull request #11076 from voidsipp/fix/round4-5-10444
Add native client sound parameter interception
2026-11-06 21:21:31 +00:00
rdbt 8cc05f114c Merge pull request #11196 from arslanarm/shaderpacks/sky-part-blends
Shader packs: the sky's parts blend as vanilla draws them
2026-11-06 21:21:25 +00:00
rdbt b80e816a76 Merge pull request #10532 from PzmuV1517/shaders/perf-background
Shader packs: a pack builds on a thread of its own while the game draws
2026-11-06 21:21:20 +00:00
rdbt 9b8b755e4c Merge pull request #11183 from rison1337/perf/dynamic-light-selection
Stream dynamic-light sources and bound nearest-light selection
2026-11-06 21:21:14 +00:00
rdbt 5692713279 Merge pull request #10556 from ilguerro/perf/9996-light
Light a section from its palette's light info, not its state ids
2026-11-06 21:21:08 +00:00
rdbt ff31439c92 Merge pull request #9993 from MSedgeMC/fix/prediction-snap
A refused block change that comes back inside the player snaps it out
2026-11-06 21:20:30 +00:00
rdbt 02641599e8 Merge pull request #11221 from AlfaRojo/fix/player-boarding-cooldown
A player boards a vehicle again right after leaving it
2026-11-06 21:20:24 +00:00
rdbt 12030689f8 Merge pull request #11237 from NikitaMGrimm/pack-sequence-clock
Shader packs: a set frame clock for repeatable captures (screenshot-sequence 1/3)
2026-11-06 21:20:19 +00:00
rdbt ce68709f1f Merge pull request #11281 from ilguerro/perf/mesh-row-masks
Reduce repeated work in section meshing
2026-11-06 21:20:13 +00:00
rdbt 3e5b2ef194 Merge pull request #11268 from DUzzL/fix/11242-hanging-entity-data
Apply item custom data and entity data to placed frames, paintings and armor stands
2026-11-06 21:20:07 +00:00
rdbt 14bb1416d1 Merge pull request #11016 from DUzzL/fix/10549-per-level-border
Keep one world border per level
2026-11-06 21:20:02 +00:00
rdbt 1f666cfec4 Merge pull request #11044 from Scholiboi/fix/remote-pearl-bubble-column
Pearls on a server's client hover in bubble columns as vanilla's do
2026-11-06 21:19:56 +00:00
rdbt 8b578746ea Merge pull request #11205 from arslanarm/shaderpacks/float-into-integer-buffers
Shader packs: a float output into an integer buffer no longer refuses its program
2026-11-06 21:19:50 +00:00
rdbt af55aad580 Merge pull request #11204 from arslanarm/shaderpacks/r11f-images
Shader packs: an R11F_G11F_B10F image is stored into and read back
2026-11-06 21:19:44 +00:00
rdbt b11e8a54b6 Merge pull request #10571 from Skr1zzzz/text-display-cull
Text displays: keep their glyphs, and skip the ones whose text is off screen
2026-11-06 21:19:38 +00:00
rdbt 0ef93c96b9 Merge pull request #11092 from rison1337/perf/cull-behind-camera-falling-blocks
Skip falling-block mesh preparation behind the camera
2026-11-06 21:19:33 +00:00
voidsip c558b29549 Honor block drops and RNG order for mining XP 2026-11-06 15:19:27 -06:00
rdbt 5fca014076 Merge pull request #11227 from daMpoff/fix/launcher-instance-menu-width
Fix overflow in translated launcher instance menus
2026-11-06 21:19:27 +00:00
rdbt c1c985ce5b Merge pull request #10606 from Hiuoy/feat/entity-interact-specific
Native hooks: PlayerInteractEvent.EntityInteractSpecific, the point a player right-clicks on an entity
2026-11-06 21:19:22 +00:00
rdbt af24ac05bb Merge pull request #10564 from Alysia42/ghast-fireball-pickable
Arrows and ender pearls now redirect ghast fireballs instead of passing through them
2026-11-06 21:19:16 +00:00
rdbt 1a92fe7767 Merge pull request #11042 from Vaspyyy/launcher-updates-in-updates
Launcher: update status and Check now move from About to Updates
2026-11-06 21:19:10 +00:00
rdbt 079ed1a010 Merge pull request #11276 from apeirs/fix/mob-block-layers-cull
Mob block layers cull back faces like BlockModelRenderState
2026-11-06 21:19:05 +00:00
rdbt e55180e7dd Merge pull request #10018 from ilguerro/fix/player-death-stats
A player's death makes a vibration and counts toward Killed By
2026-11-06 21:18:59 +00:00
rdbt da4fb17df7 Merge pull request #11099 from Delete228/native-living-item-use-upstream
Native mods: item use after a mob passes interaction
2026-11-06 21:18:53 +00:00
rdbt 114e9920ac Merge pull request #11275 from TheMrZZ/fix/command-enchantment-map-validation
Validate command enchantment map entries before execution
2026-11-06 21:18:47 +00:00
rdbt ad2c0d2ecd Merge pull request #11355 from TheMrZZ/fix/vehicle-damage-command
Route generic damage commands to boats and minecarts
2026-11-06 21:18:41 +00:00
rdbt 976afe97e2 Merge pull request #11065 from voidsipp/fix/11038-server-property-numbers
Match Java numeric server properties
2026-11-06 21:18:34 +00:00
rdbt 7c58203441 Merge pull request #9668 from GabrielNichols/codex/recipes-hmod-server-fill
Keep .hmod Recipes searchable and placeable on the server
2026-11-06 21:18:28 +00:00
rdbt cb201c59e4 Merge pull request #10527 from Akirabane/feat/transfer-status
dedicated: transfer-status, the counts a proxy reads before it sends players
2026-11-06 21:18:22 +00:00
rdbt d22ecb0b52 Merge pull request #11299 from pehe1/fix/3109-airborne-knockback
Apply a guest jump before knockback at takeoff
2026-11-06 21:18:16 +00:00
rdbt 62075a5b32 Merge pull request #11070 from Delete228/native-block-clone-stack-upstream
Native mods: select the stack returned by Pick Block
2026-11-06 21:18:10 +00:00
rdbt aea2bdadb1 Merge pull request #11114 from Delete228/content-item-block-alias-upstream
Native content: place a block through an item alias
2026-11-06 21:18:04 +00:00
rdbt f59035948c Merge pull request #10177 from anthra123x/fix/ultrawide-fullscreen-taskbar
fix(client): prevent taskbar overlap and window shift in borderless fullscreen
2026-11-06 21:17:58 +00:00
rdbt d05f889fe1 Merge pull request #10129 from xFedeT/xfedet-launcher-transparent-topbar
Make launcher titlebar match Glass UI
2026-11-06 21:17:19 +00:00
rdbt c96af77eb6 Merge pull request #11144 from xxaxio/fix/11082-linux-audio-buffer
audio: support HORIZON_AUDIO_BUFFER override for Linux buffer underruns
2026-11-06 21:17:13 +00:00
rdbt d4df5dbf59 Merge pull request #10572 from GIGABAIT93/feat/launch-api-content
Content API: --launch-api says what content a build loads, and docs/api/content.md (#10562)
2026-11-06 21:16:35 +00:00
rdbt 66e6b23fb8 Merge pull request #11243 from AlfaRojo/fix/lead-hold-old-rotation
The lead held in first person turns with the mouse, without jitter
2026-11-06 21:16:29 +00:00
rdbt 918a4c8434 Merge pull request #11247 from Joansitoh/fix/11178-wither-skull-save
Keep wither skulls in flight through a chunk save
2026-11-06 21:16:23 +00:00
rdbt a3c540e8c0 Merge pull request #11255 from AlfaRojo/fix/screen-over-playing-releases-keys
A book, sign editor or prompt over the world releases the held buttons
2026-11-06 21:16:17 +00:00
rdbt e243cbac06 Merge pull request #11046 from voidsipp/fix/round4-2-10408
Honor remapped function keys from XKB
2026-11-06 21:16:11 +00:00
rdbt 4c5f37de8d Merge pull request #11199 from arslanarm/shaderpacks/noise-texture-resolution
Shader packs: noiseTextureResolution sizes the default noisetex
2026-11-06 21:16:05 +00:00
rdbt ba7cdd4d8c Merge pull request #11188 from TheMrZZ/fix/entity-scores-predicate
Evaluate named entity_scores predicates for bound holders
2026-11-06 21:15:58 +00:00
rdbt b61394f396 Merge pull request #11206 from arslanarm/shaderpacks/runtime-array-length
Shader packs: the length of a buffer block's runtime array reads back
2026-11-06 21:15:53 +00:00
rdbt a13866fff3 Merge pull request #10537 from arslanarm/shaderpacks/end-boss-fog-sky
The dragon fight's world fog leaves out the sky, as vanilla does
2026-11-06 21:15:46 +00:00
rdbt f9302c0736 Merge pull request #11023 from ilguerro/fix/9776-item-vibrations
Casting and reeling a fishing rod and throwing an eye of ender make the vibrations vanilla's do
2026-11-06 21:15:40 +00:00
rdbt 44e4124142 Merge pull request #10005 from UMCEKO/fix/projectile-owner-any-entity
Projectiles owned by any non-player entity need mob_griefing
2026-11-06 21:15:34 +00:00
rdbt dc040a9dd7 Merge pull request #11127 from Delete228/discarded-mob-lookup-upstream
Hide discarded mobs from immediate lookup and passenger saves
2026-11-06 21:15:29 +00:00
rdbt d54d736546 Merge pull request #11030 from ilguerro/fix/illager-target-memory
Merge remote-tracking branch 'origin/main' into fix/illager-target-memory
2026-11-06 21:15:23 +00:00
rdbt e4928503ac Merge pull request #11224 from TheMrZZ/fix/text-floating-zero
Canonicalize floating zero when parsing text tags
2026-11-06 21:15:17 +00:00
DUzzL 37f8ebb618 Read scaffolding as its stable shape in fluid wall tests 2026-11-06 22:15:14 +01:00
rdbt 2a83a524fc Merge pull request #11343 from TheMrZZ/fix/tnt-live-data-adoption
Adopt live TNT command data without replacing its identity
2026-11-06 21:15:11 +00:00
rdbt cfd68c41cf Merge pull request #11155 from TheMrZZ/fix/h125-mob-name-predicates
Fix live mob custom-name component predicates
2026-11-06 21:15:04 +00:00
rdbt 2c149ab9e6 Merge pull request #10096 from yahya-nusseibeh/fix/rcon-one-guess
RCON: one password guess per connection
2026-11-06 21:14:59 +00:00
rdbt aebabfb5c6 Merge pull request #11139 from TheMrZZ/fix/pillager-trader-saved-inventory
Preserve Pillager and WanderingTrader saved inventories
2026-11-06 21:14:53 +00:00
Kevin RamirezandClaude Fable 5.1 83d3c8afa5 CI: run the checks again after the runner ran out of disk
Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-11-06 15:14:48 -06:00
rdbt 90a09f5b25 Merge pull request #11346 from AlfaRojo/fix/creeper-explosion-radius
A creeper blows with its ExplosionRadius
2026-11-06 21:14:48 +00:00
Nikita Maximilian Grimm 1d1f36972c Merge remote-tracking branch 'origin/main' into shader-api-02-serve
# Conflicts:
#	docs/api/shaders.md
2026-11-06 22:14:42 +01:00
rdbt ea2986d57b Merge pull request #11223 from AlfaRojo/fix/release-use-before-move
An arrow leaves from where the player was when the bow was released
2026-11-06 21:14:42 +00:00
rdbt 7093191256 Merge pull request #11043 from voidsipp/fix/11034-protocol-identifiers
Validate resource identifiers when reading packets
2026-11-06 21:14:36 +00:00
rdbt d774839f1f Merge pull request #11202 from arslanarm/shaderpacks/compute-shadow-reads
Shader packs: a compute stage reading a shadow buffer turns the shadow pass on
2026-11-06 21:14:30 +00:00
rdbt 6cfb046532 Merge pull request #11203 from arslanarm/shaderpacks/celestial-alpha-blend
Shader packs: the sun and moon leave their own alpha, as vanilla's celestial blend
2026-11-06 21:14:24 +00:00
Kevin RamirezandClaude Fable 5.1 ab98869192 CI: run the checks again after the runner ran out of disk
Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-11-06 15:14:22 -06:00
rdbt fb0866ef3f Merge pull request #11278 from TheMrZZ/fix/h035-loot-mine-pot-components
Preserve pot decorations in loot mine results
2026-11-06 21:14:18 +00:00
rdbt eec2eb3b32 Merge pull request #11067 from voidsipp/fix/11039-nbt-path-reader
Match NBT path quoted escapes and index tokens
2026-11-06 21:14:12 +00:00
rdbt 4e8ed32e62 Merge pull request #10173 from MSedgeMC/fix/10071-auto-update-enabled
launcher: an automatic update keeps a disabled mod off
2026-11-06 21:14:06 +00:00
rdbt 8f55a6c245 Merge pull request #10542 from TheMrZZ/fix/player-default-effect-amplifier
Omit the default amplifier from saved player effects
2026-11-06 21:14:01 +00:00
rdbt 13664f3799 Merge pull request #11157 from TheMrZZ/fix/terminal-execute-fork-errors
Honor fork state for terminal execute condition errors
2026-11-06 21:13:55 +00:00
rdbt 1fb80a5d79 Merge pull request #11354 from MSedgeMC/fix/explosion-item-damage
fix: a blast takes its damage off a dropped item's health
2026-11-06 21:13:49 +00:00
rdbt 3d8521b429 Merge pull request #11209 from TheMrZZ/fix/inline-slot-range-admission
Reject unknown inline slot-range names before execution
2026-11-06 21:13:42 +00:00
rdbt 6181c89ed6 Merge pull request #11246 from TheMrZZ/fix/item-reference-uuid-codec
Decode Item Owner and Thrower with numeric UUID streams
2026-11-06 21:13:37 +00:00
rdbt 2018af984c Merge pull request #11284 from TheMrZZ/fix/mob-command-equipment-slots
Respect mob body-slot admission and saddle commands
2026-11-06 21:13:31 +00:00
rdbt f31d16a2c6 Merge pull request #11201 from TheMrZZ/fix/h154-unless-function-fallthrough
Fix unless-function continuation after fallthrough
2026-11-06 21:13:25 +00:00
rdbt c7d5881fdc Merge pull request #10996 from ilguerro/fix/sleeping-mob-push
A sleeping villager, fox or player isn't pushed by the mobs that walk into it
2026-11-06 21:13:19 +00:00
rdbt c815a0f81c Merge pull request #11162 from TheMrZZ/fix/modern-sign-literal-quotes
Preserve literal quotation characters in modern sign messages
2026-11-06 21:13:13 +00:00
rdbt 08c3b12345 Merge pull request #11032 from ilguerro/fix/piglin-admired-drop
Merge remote-tracking branch 'origin/main' into fix/piglin-admired-drop
2026-11-06 21:13:07 +00:00
rdbt 0925060e6f Merge pull request #11079 from horizon-rust/platforms/intel-mac-native-mods
Native mods on Intel Macs: the launcher brings Rust for x86_64 macOS
2026-11-06 21:13:01 +00:00
rdbt 08f1c4f6c6 Merge pull request #11244 from Gabbotampo/fix/11222-negative-holder-ids
Item components: refuse negative holder ids and holder set sizes
2026-11-06 21:12:56 +00:00
rdbt 838375d4c2 Merge pull request #11363 from Skr1zzzz/older-servers-unknown-types
Older servers: an entity or block entity type the pinned version lacks is dropped, not renumbered
2026-11-06 21:12:50 +00:00
rdbt 8bb2525e31 Merge pull request #11009 from ilguerro/fix/trade-display-drop-chance
A villager's held-up trade item never drops when it dies
2026-11-06 21:12:44 +00:00
rdbt ef6be13a1c Merge pull request #11056 from TheMrZZ/fix/interaction-saved-defaults
Save default Interaction dimensions and response
2026-11-06 21:12:39 +00:00
rdbt 014ad04d1c Merge pull request #11179 from TheMrZZ/fix/h113-command-damage-vibration
Emit mob command damage vibrations before returning
2026-11-06 21:12:33 +00:00
rdbt f2ec4e9c01 Merge pull request #11171 from TheMrZZ/fix/arrow-owner-command-nbt
Expose loaded arrow Owner references in command NBT
2026-11-06 21:12:27 +00:00
rdbt 0d840d54e3 Merge pull request #10219 from TheMrZZ/fix/selector-unknown-criterion
Reject missing advancement selector criteria
2026-11-06 21:12:21 +00:00
rdbt 43528d198c Merge pull request #10476 from TheMrZZ/fix/h220-macro-source-type
fix: report actual macro argument NBT type
2026-11-06 21:12:15 +00:00
rdbt 3d92e52a83 Merge pull request #10565 from TheMrZZ/fix/entity-nbt-boolean-byte
Match entity NBT boolean loads to numeric byte conversion
2026-11-06 21:12:09 +00:00
rdbt 0ef3df34d6 Merge pull request #10373 from xxaxio/fix/10317-narrate-incompatible-server
multiplayer: narrate incompatible server in server list
2026-11-06 21:12:02 +00:00
rdbt e4872aeba7 Merge pull request #11071 from TheMrZZ/fix/tnt-saved-default-power
Omit stale default power from saved TNT
2026-11-06 21:11:56 +00:00
rdbt d08f9ce9d5 Merge pull request #11108 from TheMrZZ/fix/animal-stale-love-cause
Remove stale Animal LoveCause when no reference is modeled
2026-11-06 21:11:51 +00:00
rdbt 162dd10147 Merge pull request #11305 from ilguerro/test/widen-harness-inputs
Widen four parity harnesses' inputs
2026-11-06 21:11:45 +00:00
rdbt 5919e28e0a Merge pull request #11341 from MSedgeMC/test/explosion-edge-ring
test: pin the bed explosion harness's blast edge to the item's own age
2026-11-06 21:11:08 +00:00
rdbt 8985543344 Merge pull request #10042 from NikitaMGrimm/hdr-note-serve-0-1-until-step-5
HDR design note: serve shader API 0.1 until 0.2 is on by default
2026-11-06 21:11:02 +00:00
rdbt 29f5c3504f Merge pull request #10540 from M0-kii/fix/changed-file-paths
Keep exact changed paths and rename sources in tooling checks
2026-11-06 21:10:57 +00:00
rdbt 06c3ba2173 Merge pull request #10019 from IsT3RiK/traduction-fr
Launcher French: translate the strings left in English, vous throughout
2026-11-06 21:10:51 +00:00
rdbt 57dcec2e2d Merge pull request #11173 from SamsVT/thai-translation
Launcher: rewrite the Thai translation (th.json)
2026-11-06 21:10:46 +00:00
rdbt a3b0891fc2 Merge pull request #11254 from MSedgeMC/fix/ghast-still-timeout
A happy ghast keeps its still timeout across a save
2026-11-06 21:10:40 +00:00
DUzzL bedf076c56 Read stack components in anvil and grindstone results 2026-11-06 22:10:39 +01:00
rdbt 68d5e2ec6c Merge pull request #11057 from DUzzL/fix/11018-marker-common-nbt
Save common Entity fields for registered Markers
2026-11-06 21:10:33 +00:00
rdbt 909a8123d0 Merge pull request #10546 from TheMrZZ/fix/h074-happy-ghast-ambient-position
Fix HappyGhast ambient sound position
2026-11-06 21:10:27 +00:00
rdbt da3a5aff7e Merge pull request #11265 from NikitaMGrimm/still-crystal-phase
HORIZON_STILL: end crystals start their spin from their position
2026-11-06 21:10:21 +00:00
rdbt e1d2186ee1 Merge pull request #11142 from voidsipp/fix/ci-shared-parity-invocation
Let dolphin and spell parity harnesses use runner defaults
2026-11-06 21:10:16 +00:00
rdbt e500989785 Merge pull request #11262 from MSedgeMC/test/enchant-gate
enchanting: the stack's damage and existing enchantments at the table
2026-11-06 21:10:10 +00:00
rdbt 57ad8718c3 Merge pull request #11198 from MSedgeMC/test/creative-removal
test: a creative player's break leaves the block, but no drops or statistic
2026-11-06 21:10:03 +00:00
rdbt c12fe63d33 Merge pull request #11109 from Detimes/native-refresh-commands
Native mods: native_refresh_commands rebuilds the players' command trees at run time
2026-11-06 21:09:57 +00:00
rdbt 71e18e8c0a Merge pull request #11054 from TaquitoSuabe/fix/haswell-vulkan-fallback
gfx: skip broken Haswell Vulkan driver (hasvk) and fall back to OpenGL
2026-11-06 21:09:51 +00:00
rdbt 880b690d6c Merge pull request #10086 from horizon-rust/pvp-safe/sneak-input
Multiplayer: sneaking for a use is the input of the last tick, as vanilla's isShiftKeyDown
2026-11-06 21:09:45 +00:00
rdbt d55b7f8594 Merge pull request #10551 from ilguerro/fix/9759-golem-peaceful
Peaceful drops a mob's player target and a golem's anger
2026-11-06 21:09:39 +00:00
rdbt b849bc3dab Merge pull request #11007 from ilguerro/fix/use-swing-attack-reset
A use that swings the hand resets the attack charge
2026-11-06 21:09:33 +00:00
rdbt 85d5ce37f0 Merge pull request #10908 from DUzzL/fix/10550-trident-hurts-players
A thrown trident hurts other players on a served world
2026-11-06 21:09:26 +00:00
rdbt 5395de1ab4 Merge pull request #10967 from ilguerro/fix/mob-block-fall-sound
A mob's damaging fall plays the fall sound of the block it lands on
2026-11-06 21:09:19 +00:00
rdbt 24459551e6 Merge pull request #11100 from Delete228/native-held-break-slot-upstream
Native mods: choose the break slot for held-item durability
2026-11-06 21:09:12 +00:00
rdbt 848ffed94d Merge pull request #11136 from DUzzL/fix/11088-slime-split-delay
Split slimes, magma cubes and sulfur cubes when the body is removed
2026-11-06 21:09:06 +00:00
rdbt 66ee01b737 Merge pull request #11143 from Delete228/frozen-sprint-tick-boundary-upstream
Restore the frozen tick decision before ending a sprint
2026-11-06 21:09:01 +00:00
rdbt 7227485ca2 Merge pull request #11211 from AlfaRojo/fix/elytra-glide-saved
A player saved while gliding comes back gliding
2026-11-06 21:08:55 +00:00
rdbt d914334acc Merge pull request #11226 from Horizon-is-vibe-coded/main
fix(checkerboard): clamp negative scale before shift to avoid pathological shift distance
2026-11-06 21:08:49 +00:00
rdbt 07b8f97138 Merge pull request #11229 from AlfaRojo/fix/own-model-freeze-shiver
The player's own third-person model shivers when fully frozen
2026-11-06 21:08:44 +00:00
TheMrZZandCodex gpt-6.1-sol a3d9d8f2c3 Keep client shrieker playback outside shared capture
Use the existing unshared audio path for level event 3007 and cover actual event capture, local playback, and random continuation.

Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-11-06 22:08:41 +01:00
rdbt 5e8d557b17 Merge pull request #11292 from Ragbie/fix/snbt-trailing-comma
SNBT: a compound, list or array may end with a comma, as vanilla's grammar allows
2026-11-06 21:08:38 +00:00
rdbt 07a162cb30 Merge pull request #11272 from echo-ilovech3ss/fix/tp-passenger-followups
game: cover passenger rotation through /tp in rider test
2026-11-06 21:08:32 +00:00
rdbt 8146a586da Merge pull request #11279 from DUzzL/fix/11271-smithing-trim
Draw trims on the smithing preview armour
2026-11-06 21:08:27 +00:00
rdbt dc8aba8bee Merge pull request #11259 from DUzzL/fix/11252-autosprint-sign
Keep a latched sprint toggle out of the text-field Ctrl shortcuts
2026-11-06 21:08:21 +00:00
rdbt f1f352044f Merge pull request #11348 from d3xc0/native-default-state
Native mods: a block default state by name (native_default_state)
2026-11-06 21:08:15 +00:00
rdbt 83feefe860 Merge pull request #11368 from Vaspyyy/docs/claimable-issues-query
AGENTS.md: one query for the issues free to claim
2026-11-06 21:08:09 +00:00
rdbt 2132e6126f Merge pull request #11313 from DUzzL/fix/11290-beacon-beam-view
Cull back faces of beacon beams
2026-11-06 21:08:03 +00:00
rdbt 97b36f8e1b Merge pull request #11362 from AlfaRojo/fix/mob-protection-order
A mob's Protection runs after Resistance and not against a sonic boom
2026-11-06 21:07:58 +00:00
rison1337andCodex 332f312b38 perf(mobs): cache exact bone rotations when TAA is off
Reuse bounded exact local geometry and normal rotations for GPU mob posing.
Keep the original walk and transform helpers when TAA is enabled, with the
policy refreshed every frame from the existing anti-aliasing setting.

Add byte-exact churn and bypass-switching coverage and measured crowd results.

Co-Authored-By: Codex (GPT-6) <[email protected]>
2026-11-07 02:07:56 +05:00
rdbt 521c26d6ca Merge pull request #11357 from AlfaRojo/fix/bone-meal-spreader-particles
Bone meal on a spreading block sparks over the block above it
2026-11-06 21:07:52 +00:00
rdbt 218624479d Merge pull request #11352 from AlfaRojo/fix/damage-taken-stat-threshold
Damage Taken is not awarded for a huge hit
2026-11-06 21:07:46 +00:00
rdbt 4dc1282f65 Merge pull request #9953 from Hiuoy/feat/9921-port-pack-events
Port pack: `events`, a mod's NeoForge listeners against the hooks
2026-11-06 21:04:33 +00:00
rdbt b351455eae Merge pull request #10245 from Hiuoy/feat/anvil-update
Native hooks: AnvilUpdateEvent, a mod's say in an anvil's result
2026-11-06 21:04:28 +00:00
rdbt 217e87e8f3 Merge pull request #10240 from MSedgeMC/fix/item-cull-frame
Dropped items: cull at the box the frame draws, not the tick's
2026-11-06 21:04:22 +00:00
rdbt e3356386d0 Merge pull request #11153 from DUzzL/fix/11055-cushion-sit-remote
Seat other players on a cushion they sit on
2026-11-06 21:04:15 +00:00
rdbt bf17581664 Merge pull request #11152 from DUzzL/fix/11074-dismount-hint
Show the dismount hint when the player starts riding
2026-11-06 21:04:09 +00:00
rdbt 02e62fbedf Merge pull request #11182 from DUzzL/fix/11137-golem-target-mounts
Keep golems off zombie horses, camel husks and zombie nautilus
2026-11-06 21:04:03 +00:00
rdbt e0f9934254 Merge pull request #11240 from DUzzL/fix/10287-shulker-piston-align
Snap piston-pushed shulkers to the block centre
2026-11-06 21:03:57 +00:00
rdbt 76bda62793 Merge pull request #11169 from DUzzL/fix/11130-mod-worldgen-feature-dir
Read mod features from worldgen/feature/, 26.3's folder
2026-11-06 21:03:51 +00:00
rdbt b12c89bf04 Merge pull request #11129 from Delete228/native-container-provisioning-upstream
Avoid inventory notifications when provisioning empty native slots
2026-11-06 21:03:45 +00:00
rdbt 345e6efdc9 Merge pull request #11151 from voidsipp/fix/gateway-passenger-player-ticks
Run the mounted player tick in the gateway parity scene
2026-11-06 21:03:38 +00:00
rdbt 77ec563892 Merge pull request #11112 from ilguerro/tooling/use-item-on-interact-start
use_item_on's oracle posts item_interact_start when a use starts
2026-11-06 21:03:32 +00:00
rdbt 5bd038ed99 Merge pull request #11156 from xxaxio/docs/11154-index-design-notes
docs/README.md: index live mod reloading design note
2026-11-06 21:03:26 +00:00
rdbt 06e2041d43 Merge pull request #11149 from xxaxio/docs/11145-platform-scope
docs/native-client: update stale platform scope note
2026-11-06 21:03:19 +00:00
rdbt 2643b0e246 Merge pull request #11134 from xxaxio/docs/stale-notes-cleanup
docs/server-levels: remove stale End credits for server players note
2026-11-06 21:03:13 +00:00
rdbt f235e95518 Merge pull request #11131 from xxaxio/fix/11120-test-harness-args-windows
tests/test_harness_args: match system path separator for default work dir
2026-11-06 21:03:07 +00:00
rdbt 1e68c9db3d Merge pull request #11010 from Hiuoy/docs/neoforge-labels-tooltip
Native hook catalog: tooltip_render_pre is RenderTooltipEvent.Pre; the Next hooks have all landed
2026-11-06 21:03:00 +00:00
Kevin RamirezandClaude Fable 5.1 4ee0e8dd33 CI: run the checks again after the runner ran out of disk
Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-11-06 14:57:41 -06:00
Kevin RamirezandClaude Fable 5.1 7416b4a5f6 CI: run the checks again after the runner ran out of disk
Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-11-06 14:57:15 -06:00
Florian ERNSTandCodex 9634732468 Omit prototype max-stack patches in saved ItemEntity stacks
Co-Authored-By: Codex <[email protected]>
2026-11-06 21:54:00 +01:00
MSedgeMC bb24c87d5d test: every brewing recipe's mix and the three slot filters
No parity suite reached the brewing recipes: brewing.rs's Brewing::find,
PotionIngredient and the BrewingStandMenu slot filters had no differential
corpus. A new suite loads the vanilla data pack's real RecipeManager and
compares every bundled BrewingRecipe's assemble with Horizon's find output,
the brewing_input and brewing_reagent property sets against is_bottle and
is_reagent, and the fuel slot's BrewingFuel against brewing_fuel. Cases cover
every recipe, every potion item and potion id against every reagent, the whole
item registry as a slot candidate, and non-matching pairs. --baseline keeps
only the exact recipe pairs and reproduces the corpus before the cross-product.

2811/2811 cases match the pinned client. The brewing-stand tick, brew time,
fuel use and remainders remain pending.
2026-11-06 20:51:50 +00:00
26210e2da4 Read saved jigsaw structure starts before regenerating them
Part of #11126.

Load complete jigsaw starts from the owning chunk's structures.starts into
its shared generator cache, retaining regeneration for unsupported or
undecodable starts. Read pending saves in every dimension and preserve
reference counts alongside the original piece layout.

Cover resource-free round trips, generated layouts across two seeds and
pending-record loading from a different referencing chunk.

Co-Authored-By: Codex (gpt-6.1-sol) <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WZzq8KX3vDMZy53y1vXqeT
2026-11-06 21:50:30 +01:00
VaspyyyandClaude Opus 5.5 ee88e8c870 AGENTS.md: one query for the issues free to claim
Agents had to open issues one by one to see if they were taken, and the rule
did not mention Discord claims (Kira's `in progress` label). Give them the
ready-made search and name both claim signals.

Fixes #11367

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 21:48:25 +01:00
martipopsandClaude Opus 5.5 197eb01adf Controllers: the guide says Open only for what opens, and nothing for a click a block keeps
Portals and an empty lectern showed Open, and a block that keeps the click and does nothing
(an empty flower pot or bookshelf slot, a pressed button) showed Use, or the held item's own
row. An ender chest said Use and now says Open.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 15:40:39 -05:00
rdbtCVSandClaude Opus 5.5 18eb428ffb Drop the agent marker from a code comment
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 20:35:38 +00:00
rdbtCVSandClaude Opus 5.5 cd99e67d1c Native client doc: macOS is a target too
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 20:35:11 +00:00
Kevin RamirezandClaude Fable 5.1 71622aabb3 A mob's Protection runs after Resistance and not against a sonic boom (#11308)
LivingEntity.getDamageAfterMagicAbsorb applies Resistance, returns 0 when
nothing is left, returns the damage as is for #bypasses_enchantments (the
warden's sonic_boom), and only then the enchantment protection. Horizon's
Mob::hurt_server ran the protection first and never read the tag, so a
zombie in a Protection IV chestplate kept 1.6 of a 10-point sonic boom.

DamageKind::bypasses_enchantments (SonicBoom, or a NamedDamage with the
new BYPASSES_ENCHANTMENTS tag) gates the protection, which now follows
Resistance as vanilla orders them.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-11-06 14:33:17 -06:00
rdbtCVSandClaude Opus 5.5 e70f5347ba Realms: finish the merge of main (frontend helper, tier fields)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 20:33:13 +00:00
Skr1zzzzandClaude Opus 5.5 ad8a713d95 Multiplayer: the player rides any tracked entity, sitting where vanilla seats it
A server seating the player on a block display (sit mods, NPC plugins) took the vehicle for a minecart; none was
found, so riding was dropped and the player fell into the slab under the seat (y -61.0 where the server keeps
-60.1). The seat is the vehicle's riding position less the player's vehicle attachment (0.6, scaled).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 23:31:46 +03:00
rdbtCVSandClaude Opus 5.5 f17d86aa79 Keep #[cfg(test)] on the test module the merge of main took it from
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 20:31:36 +00:00
rdbtCVSandClaude Opus 5.5 59ef682c9c Keep #[cfg(test)] on the test module the merge of main took it from
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 20:31:10 +00:00
MSedgeMC 176caeb5fd test: every firework flight duration and long explosion runs
The firework tooltip corpus sampled flight durations 0-3, 7 and 255 and runs
of at most six equal explosions, so Fireworks.addToTooltip's run count above
six and the rest of the unsigned-byte flight range were never compared. A
deterministic edge corpus now adds all 256 flight durations, runs of 0..256
equal explosions in every shape, colour and fade lists up to 32 entries
(custom, negative and duplicate colours), equality-sensitive runs (same shape
with different colours), both components on one stack, firework components on
non-rocket/non-star items, and every combination of hidden components.
--no-edges writes the random corpus exactly as before.

3331/3331 cases match the pinned client.
2026-11-06 20:30:47 +00:00
rdbtCVSandClaude Opus 5.5 9884fc3af1 Keep #[cfg(test)] on the test module the merge of main took it from
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 20:30:45 +00:00
rdbtCVSandClaude Opus 5.5 b25dbdab4e Restore the #[test] the merge of main took from this test
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 20:29:53 +00:00
rdbtCVSandClaude Opus 5.5 1e7af3a2c5 Restore the #[test] the merge of main took from this test
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 20:29:27 +00:00
Kevin RamirezandClaude Fable 5.1 b97af5d3da CI: run verify again after the runner ran out of disk
Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-11-06 14:28:11 -06:00
rdbtCVSandClaude Opus 5.5 dc175fe838 Hoppers restart a furnace's cooking when they fill its empty input
AbstractFurnaceBlockEntity.setItem resets the time; without the early reset a hopper-fed furnace would smelt its first item in one tick. Review feedback on #10170.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 20:23:55 +00:00
Kevin RamirezandClaude Fable 5.1 9dac406ddc A spectator in loaded chunks keeps its food and regeneration ticks (#11312)
ServerPlayer.doTick runs super.tick() (Player.aiStep's tickRegeneration)
and foodData.tick for every player but a spectator touching an unloaded
chunk. Horizon's guest tick returned for a spectator before the damage
clock, peaceful_regeneration and food_tick, so health, saturation and
exhaustion froze while watching and were still there on the way back to
survival.

A spectator whose box, inflated by one block, lies in loaded chunks
(Entity.touchingUnloadedChunk) now runs those three; hazards, travel
and movement statistics stay skipped. food_tick already counts a
spectator as invulnerable, so starvation hurts none.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-11-06 14:23:40 -06:00
rdbtCVSandClaude Opus 5.5 d447274d79 Merge origin/main into PR #10170
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 20:22:15 +00:00
rdbtCVSandClaude Opus 5.5 c99811b0a7 /say: no captured copy of the broadcast line
A guest's /execute run say and the dedicated console saw it twice; review feedback on #10015.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 20:21:48 +00:00
rdbtCVSandClaude Opus 5.5 883d53f647 Merge origin/main into PR #10015
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 20:21:48 +00:00
rdbtCVSandClaude Opus 5.5 1e137ff7f3 Launcher: DRAWN is only read off Linux now
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 20:19:06 +00:00
rdbtCVSandClaude Opus 5.5 dcc21109ac Say what the automatic grant does for an advancement with no criteria
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 20:18:41 +00:00
rdbtCVSandClaude Opus 5.5 daf2f4debc Statistics tabs read the raw key, as Screen.keyPressed does
Review feedback on #10124.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 20:18:16 +00:00
rdbtCVSandClaude Opus 5.5 c612f19826 Merge origin/main into PR #10124
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 20:17:59 +00:00
rdbtCVSandClaude Opus 5.5 40ffd36b11 TNT fuse test: skip without prepared resources, as its neighbours do
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 20:17:33 +00:00
rdbtCVSandClaude Opus 5.5 5c05eacc6d Let the remote motion handler reset a trident's stuck timer
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 20:17:07 +00:00
rdbtCVSandClaude Opus 5.5 0fe1d0e15d Port map: keep PackSelectionScreen.Watcher on folder_stamp, which exists
Review feedback on #10288.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 20:16:42 +00:00
rdbtCVSandClaude Opus 5.5 3f38b2d0bd Merge origin/main into PR #10288
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 20:16:42 +00:00
rdbtCVSandClaude Opus 5.5 8c820a5994 Chat: clearing a draft runs onEdited, so its old command info goes too
EditBox.setValue fires the responder; review feedback on #10013.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 20:16:17 +00:00
rdbtCVSandClaude Opus 5.5 377650f3ee Merge origin/main into PR #10013
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 20:16:16 +00:00
rdbtCVSandClaude Opus 5.5 7fbe88924e Keep AdvancementProgress.isDone false for an advancement with no criteria
AdvancementRequirements.test returns false when its list is empty; review feedback on #10193.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 20:15:51 +00:00
MSedgeMC bae2b17a0d fix: a blast takes its damage off a dropped item's health
`ServerExplosion.hurtEntities` damages every entity in reach, and
`ItemEntity.hurtServer` writes the result back:

    this.health = (int)((float)this.health - amount);
    if (this.health <= 0) { this.getItem().onDestroyed(this); this.discard(); }

Horizon's item branch only asked whether the damage reached 5 and destroyed the
item, so an item that lived through a blast kept all 5 points of health: a
power-5 bed blast that leaves vanilla's item at 1 left Horizon's at 5.

The damage is now subtracted in f32 and truncated after the subtraction, as the
cast does, and the item is destroyed at zero health as before. The nether star
stays immune through `ItemStack.canBeHurtBy`'s explosion exception, and a blast
whose source is an entity with `mobGriefing` off never reaches the branch
(`items_ignore`).
2026-11-06 20:15:40 +00:00
MSedgeMC 6f5be3bbf8 test: the blast edge ring reads the survivor's health too
A dropped item starts with 5 health and `ItemEntity.hurtServer` takes the
blast's damage off it, so a survivor's `Health` says how much of the blast it
took. The pinned compound the ring already reads carries it, so the compared
line grows one fact:

  data push -0.03 -0.00 -0.05 health 1 keep 0.9800

Red on main: Horizon reports `health 5` where vanilla reports `1`.
2026-11-06 20:15:40 +00:00
rdbtCVSandClaude Opus 5.5 42948e3f17 Merge origin/main into PR #10193
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 20:15:26 +00:00
Florian ERNSTandCodex 627617ee6b Route damage commands to actual boat and minecart damage
Co-Authored-By: Codex <[email protected]>
2026-11-06 21:15:20 +01:00
Kevin RamirezandClaude Fable 5.1 9343b3c357 Bone meal on a spreading block sparks over the block above it (#11310)
BoneMealItem.addGrowthParticles places a NEIGHBOR_SPREADER's happy
villager sparks at BonemealableBlock.getParticlePos, which is the block
above for grass, nylium, netherrack and moss; a GROWER keeps the block
itself, rooted dirt and mangrove leaves the one below. Horizon used the
bone-mealed block for spreaders too, a block lower than vanilla.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-11-06 14:13:58 -06:00
rdbtCVSandClaude Opus 5.5 b85a07d423 Merge origin/main into PR #10129
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 20:11:11 +00:00
rdbtCVSandClaude Opus 5.5 1088c588e4 Merge origin/main into PR #11121
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 20:11:11 +00:00
rdbtCVSandClaude Opus 5.5 ed8eaed845 Merge origin/main into PR #10201
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 20:11:11 +00:00
rdbtCVSandClaude Opus 5.5 8761658a18 Merge origin/main into PR #10180
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 20:11:10 +00:00
rdbtCVSandClaude Opus 5.5 fe0a3ac905 Merge origin/main into PR #10538
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 20:11:09 +00:00
rdbtCVSandClaude Opus 5.5 9b788418c3 Merge origin/main into PR #10219
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 20:11:08 +00:00
rdbtCVSandClaude Opus 5.5 06a0f118e2 Merge origin/main into PR #10119
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 20:11:07 +00:00
rdbtCVSandClaude Opus 5.5 19eff5eb79 Merge origin/main into PR #10532
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 20:11:07 +00:00
rdbtCVSandClaude Opus 5.5 6218257d19 Merge origin/main into PR #10118
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 20:11:06 +00:00
rdbtCVSandClaude Opus 5.5 41d3a0cbb7 Merge origin/main into PR #10009
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 20:11:05 +00:00
rdbtCVSandClaude Opus 5.5 5393956e8a Merge origin/main into PR #9995
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 20:11:04 +00:00
rdbtCVSandClaude Opus 5.5 439c074655 rustfmt the test modules after merging main
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 20:10:38 +00:00
rdbtCVSandClaude Opus 5.5 af5731d305 Merge origin/main into PR #9993
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 20:10:13 +00:00
Kevin RamirezandClaude Fable 5.1 53c13c0cd9 combat: no JDK method named in the damage_taken docs (the ledger reads them as citations)
Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-11-06 14:09:37 -06:00
rdbtCVSandClaude Opus 5.5 0a6d5eb0a7 Merge origin/main into PR #10511
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 20:09:33 +00:00
rdbtCVSandClaude Opus 5.5 57a7b16d5a Merge origin/main into PR #9968
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 20:08:41 +00:00
rdbtCVSandClaude Opus 5.5 03340f679c Merge origin/main into PR #10130
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 20:08:28 +00:00
Nikita Maximilian GrimmandClaude Opus 5.5 005530f4de style: rustfmt
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 21:08:18 +01:00
rdbtCVSandClaude Opus 5.5 76c095290f Custom skies: a fade out ending at the fade in's start ramps down over the rest of the day
Review feedback on #10002.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 20:07:41 +00:00
rdbtCVSandClaude Opus 5.5 7581d1fbe3 Merge origin/main into PR #10002
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 20:06:52 +00:00
rdbtCVSandClaude Opus 5.5 33cd3992bf Merge origin/main into PR #10019
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 20:06:12 +00:00
Kevin RamirezandClaude Fable 5.1 a419cc37c7 Damage Taken is not awarded for a huge hit (#11339)
Player.actuallyHurt awards DAMAGE_TAKEN as Math.round(dmg * 10.0F) only
while dmg < 3.4028235E37F. Horizon awarded every hurt with Rust's round,
so /damage @s 3.4e38 added a saturated i32::MAX where vanilla adds
nothing. A damage_taken_award helper applies the threshold and
jmath::round_f32 (Math.round(float): ties up, saturating).

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-11-06 14:06:03 -06:00
Skr1zzzzandClaude Opus 5.5 ec534edc6c Older servers: an entity or block entity type the pinned version lacks is dropped, not renumbered
A modded 1.21.11 server's own entity type (Sit's sit:entity_sit is 157) kept its number, which in 26.3 is
the zombie villager: every sitting player showed one under them. Chunks already dropped such block entities;
their updates now go too.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 23:05:51 +03:00
rdbtCVSandClaude Opus 5.5 c25f5a1f49 Merge origin/main into PR #11081
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 20:05:45 +00:00
Nikita Maximilian GrimmandClaude Opus 5.5 295d815ec2 fix(render): give unmarked light no HDR-lighting knee in the HDR pass
The knee eases HDR lighting's gain into the peak; light written as light (alpha 1) has no
gain, and on a float frame the knee would compress it before the clamp to the peak. On a
unorm frame such pixels never reached the knee, so nothing changes there.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 21:04:18 +01:00
rdbtCVS 1b1f240ab1 Merge origin/main into PR #9967
Co-Authored-By: Claude Opus 5.5 <[email protected]>

# Conflicts:
#	tools/command_exec_corpus.py
#	tools/test_command_exec_parity.py
2026-11-06 20:00:54 +00:00
rdbtCVS c6735e2d8e Merge origin/main into PR #10290
Co-Authored-By: Claude Opus 5.5 <[email protected]>

# Conflicts:
#	crates/horizon-client/src/game/command_harness.rs
#	mods/horizon/src/test/java/dev/horizonlab/parity/VanillaCommandExecOracle.java
#	tools/command_exec_corpus.py
#	tools/test_command_exec_parity.py
2026-11-06 20:00:43 +00:00
rdbtCVS 8c5025d3d9 Merge origin/main into PR #10476
Co-Authored-By: Claude Opus 5.5 <[email protected]>

# Conflicts:
#	tools/command_exec_corpus.py
#	tools/test_command_exec_parity.py
2026-11-06 20:00:41 +00:00
rdbtCVS 98afd549d4 Merge origin/main into PR #10344
Co-Authored-By: Claude Opus 5.5 <[email protected]>

# Conflicts:
#	tools/command_exec_corpus.py
#	tools/test_command_exec_parity.py
2026-11-06 20:00:39 +00:00
Nikita Maximilian GrimmandClaude Opus 5.5 44753cefe0 fix(render): keep today's HDR look on the float frame, and draw world text in the frame's format
Marked pixels (light sources, the sun, HDR lighting) clip at white before their lift, as on
a unorm frame; only unmarked ones show light over 1, which is how shaders will write light.
Without that, the float frame tinted the sun and its halo instead of lifting them white.

The native world-text pipeline targeted the swapchain's 8-bit format, which a 16-bit or
float frame's pass refuses.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 21:00:16 +01:00
Kevin RamirezandClaude Fable 5.1 ef6f7a2170 Sky darkening truncates the light level, as updateSkyBrightness does (#11323)
Level.updateSkyBrightness is `(int)(15.0F - skyLightLevel)`: the float
truncated. Horizon's environment::sky_darken (and the End copy in
Game::sky_darken) rounded, stepping the darkening up half a level early
at dusk and late at dawn, which moves bed eligibility and the light-
dependent spawns with it.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-11-06 14:00:13 -06:00
rdbtCVS 7b21af9519 Merge origin/main into PR #10196
Co-Authored-By: Claude Opus 5.5 <[email protected]>

# Conflicts:
#	crates/horizon-client/src/game/native_client_services.rs
2026-11-06 19:59:43 +00:00
rdbtCVS b9526c300c Merge origin/main into PR #10291
Co-Authored-By: Claude Opus 5.5 <[email protected]>

# Conflicts:
#	crates/horizon-client/src/entity_nbt.rs
2026-11-06 19:59:42 +00:00
rdbtCVS 67fa3d1bb3 Merge origin/main into PR #10065
Co-Authored-By: Claude Opus 5.5 <[email protected]>

# Conflicts:
#	tools/lan_e2e.py
2026-11-06 19:59:37 +00:00
rdbtCVS d6113ce7e5 Merge origin/main into PR #10018
Co-Authored-By: Claude Opus 5.5 <[email protected]>

# Conflicts:
#	tools/lan_e2e.py
2026-11-06 19:59:36 +00:00
rdbtCVS 1c389c3758 Merge origin/main into PR #10606
Co-Authored-By: Claude Opus 5.5 <[email protected]>

# Conflicts:
#	crates/horizon-client/tests/mod_api_hooks.rs
2026-11-06 19:59:35 +00:00
rdbtCVS 8d5e529b46 Merge origin/main into PR #10005
Co-Authored-By: Claude Opus 5.5 <[email protected]>

# Conflicts:
#	crates/horizon-client/src/game/creatures.rs
2026-11-06 19:59:01 +00:00
rdbtCVS 6f82bf74b1 Merge origin/main into PR #10173
Co-Authored-By: Claude Opus 5.5 <[email protected]>

# Conflicts:
#	launcher/src/browser/horizon/auto/tests.rs
2026-11-06 19:59:00 +00:00
Kevin RamirezandClaude Fable 5.1 e7c082ee6f A creeper blows with its ExplosionRadius (#11332)
Creeper.readAdditionalSaveData reads ExplosionRadius as a byte (default
3), addAdditionalSaveData writes it back, and explodeCreeper blows with
it, twice it when powered. Horizon used a fixed 3 or 6 and never read the
key, so a summoned creeper with ExplosionRadius:12b blew with 3.

Extra.explosion_radius holds it (3 from Mob::new), entity_nbt reads any
numeric tag narrowed to a byte and writes the byte, and creeper_tick
scales it by the powered multiplier.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-11-06 13:56:10 -06:00
Florian ERNSTandCodex 5efccc59f0 Adopt live TNT command data without replacing its identity
Co-Authored-By: Codex <[email protected]>
2026-11-06 20:53:04 +01:00
MSedgeMC cdf61801bb test: pin the bed explosion harness's blast edge to the item's own age
The survivor the harness gained at the edge of the blast is read through the
item's own tick counter. A push on an item decays every tick
(`Entity.getAirDrag`, 0.98), and the two servers do not advance the same number
of game ticks between the same commands: at three script points of one case the
vanilla survivor was 31, 33 and 48 ticks old and Horizon's 38, 41 and 75, so a
`Motion` read there measures the pacing rather than the blast.

The edge item's `Age` is now read before the blast, and its whole NBT (`Age`,
`Pos` and `Motion` from one snapshot) at two pinned ages after it. The case
compares the push carried back over the ticks since the blast, to two decimals
because a tick of slack between the blast and the read is worth 2%, and the
decay the two snapshots show per tick, to four, which is `getAirDrag`'s 0.98.
Both agree with vanilla's in all twelve cases.
2026-11-06 19:50:58 +00:00
6e09e5035b mesher: bake face data and reuse padded cell indices
Cache each baked face's shape for both collision-fullness states and pack
its own UVs once, preserving the original floating-point operation order.
Address neighboring blocks, AO samples and fluid samples from padded cell
indices instead of recomputing coordinate indices for each lookup.

Add asset-independent checks against coordinate lookups for padded steps
and all AO sample tables, plus literal shape bounds and packed UV values.

Co-Authored-By: pi coding agent (DeepSeek-V4.1-Flash) <[email protected]>
Co-Authored-By: Codex (gpt-6.1-sol) <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WZzq8KX3vDMZy53y1vXqeT
2026-11-06 20:49:08 +01:00
Riccardo Guerriero 425b65a99f Merge remote-tracking branch 'origin/main' into fix/w149 2026-11-06 20:45:56 +01:00
a222a7e47a End a server player's glide on a climbable like vanilla
LivingEntity.travelFallFlying stops the glide on a climbable, and a
server simulates its players' travel (AUTHORITATIVE_SIDE_AND_SERVER).
Horizon's server travel cleared the flag only on the guest's temporary
body; as_guest now copies it back, so the glide ends on the server and
the client, which takes the flag from the server, stops gliding too.

Co-Authored-By: Codex (gpt-6.1-terra) <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 20:45:15 +01:00
c4dcf2d2c8 Wait for the client's load after the End credits respawn
handleClientCommand(PERFORM_RESPAWN) for a player who won the game
calls restartClientLoadTimerAfterRespawn, as the death respawn does: the
player's movement and actions wait, and isInvulnerableTo spares it, until
its client reports the level loaded or 60 ticks pass. Horizon restarted
the timer only after a death.

Co-Authored-By: Codex (gpt-6.1-terra) <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 20:41:08 +01:00
Nikita Maximilian GrimmandClaude Opus 5.5 b376a835f6 feat(render): draw the frame in float with HDR on, under HORIZON_HDR_FRAME=float
HDR design step 3, the frame and the display pass (docs/design/hdr-linear-light.md):
with the switch, HDR on and no shader pack, the renderer draws into Rgba16Float where
the surface offers HDR output and the device draws it, whatever Smooth Gradients is.
0.1 and shaders/ packs keep today's frame; HDR off is untouched.

The HDR pass keeps light over 1 where the world shows, up to the peak, and clips it at
white under the HUD and menus and with HDR Lighting off, as a unorm frame does. On
today's frames, which hold nothing over 1, the pass computes the same values.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 20:40:17 +01:00
effe6ea423 Forget a mob's unresolved saved lead holder when a player leads it
Leashable.setLeashedTo goes through LeashData.setLeashHolder, which
clears the delayed holder a save named. Horizon kept it, so 100 ticks
after loading the forgotten holder still dropped a lead item next to a
mob the player was leading, or a holder that loaded late took the mob
from the player. Mobs and boats both.

Co-Authored-By: Codex (gpt-6.1-terra) <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 20:33:37 +01:00
f7190cff67 Widen four parity harnesses' inputs
Cover all block states, registered enchantments, lore styles and
representative Lunge tiers using the existing vanilla oracles.

Co-Authored-By: pi coding agent (DeepSeek-V4.1-Flash) <[email protected]>
Co-Authored-By: Codex (gpt-6.1-sol) <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>

Claude-Session: https://claude.ai/code/session_01WZzq8KX3vDMZy53y1vXqeT
2026-11-06 20:32:02 +01:00
DUzzL 02896862e8 Cull back faces of beacon beams 2026-11-06 20:29:32 +01:00
Nikita Maximilian Grimm 1f31d3d23a Merge remote-tracking branch 'origin/main' into hdr-step3-float-frame 2026-11-06 20:18:33 +01:00
be7591c70b Drop a converting piglin's inventory like vanilla
Piglin.finishConversion cancels the admire, then drops every non-empty
stack of its inventory with spawnAtLocation (whole stacks, components
kept), then converts; the converted sound comes before all of it. Horizon
dropped only the admired off-hand item, before the sound, and the
inventory was lost with the piglin.

Co-Authored-By: Codex (gpt-6.1-terra) <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 20:16:57 +01:00
Florian ERNSTandCodex 399171f0e0 Preserve standard mob equipment stacks in item commands
Co-Authored-By: Codex <[email protected]>
2026-11-06 20:16:54 +01:00
Pedro CavalcanteandCodex 87761cd5b0 Apply a guest jump before subsequent combat packets
Co-Authored-By: Codex <[email protected]>
2026-11-06 16:14:13 -03:00
Arthur Black ff80bb806c Native mods: a block default state by name (native_default_state) 2026-11-06 11:08:00 -08:00
Evrim CicekandClaude Opus 5.5 d41641f85d SNBT: a compound, list or array may end with a comma
SnbtGrammar accepts one trailing comma before the closing brace or
bracket; a comma alone and two in a row stay errors. Both readers take it:
horizon-world's parse_snbt (NBT arguments, item components) and the text
component reader (tellraw and other component arguments).

Fixes #11285

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 22:06:09 +03:00
ed0780a03a Count collidable entities in auto-jump like vanilla
LocalPlayer.updateAutoJump reads Level.getCollisions, which returns the
collidable entities' boxes (EntityGetter.getEntityCollisions, query
inflated by 1e-7) before the block shapes. Horizon's auto-jump only saw
blocks, so walking into a boat, a shulker or a happy ghast never
auto-jumped onto it.

Co-Authored-By: Codex (gpt-6.1-terra) <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 19:48:41 +01:00
Florian ERNSTandCodex 9bc728f303 Respect mob body-slot admission and saddle commands
Co-Authored-By: Codex <[email protected]>
2026-11-06 19:46:24 +01:00
1a43ca3439 Reset the impulse context after an ender pearl teleport
ThrownEnderpearl.onHit calls resetCurrentImpulseContext after the fall
distance reset, so a wind charge's or mace smash's impact height no
longer limits the next fall.

Co-Authored-By: Codex (gpt-6.1-terra) <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 19:26:03 +01:00
TheMrZZandCodex gpt-6.1-sol 58fc786742 Preserve pot decorations in loot mine results
Record the actual block entity or its existing pure save projection in a local Pending, then attach existing component copies to the unchanged block drops. Keep source blocks and loot replacement policy unchanged.

Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-11-06 19:22:40 +01:00
Ian WorleyandClaude Opus 5.5 cd86e8f45d docs(native-mods): entities a client sees
Document native_client_entities with its new fields,
native_client_items, native_client_texture and skins by UUID, which
entities the list leaves out, and an example loop.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 12:15:15 -06:00
Ian WorleyandClaude Opus 5.5 eecfdf2f7b feat(native-mods): list the client's entities and dropped items
Client mods such as radars, name tags and spectator ESP need the
entities the client shows, with what they look like, without reaching
into Game's internals. API 411's native_client_entities had ids,
positions and player profiles, but no type, babyhood or texture, and no
dropped items.

- NativeClientEntity gains kind, yaw, baby, spectator and the texture
  its body draws with (mobs::render::body), plus position_at, the
  renderer's interpolation. The struct is non_exhaustive, so existing
  mods keep building.
- native_client_items lists the dropped items the client shows.
  native_item_entities stays server-only, so server logic never acts on
  a client's copies.
- native_client_texture returns TexturePixels by the name texture and
  NativeClientProfile::skin give: a downloaded skin from memory, else
  native_client_resources().texture(name).

Pending fragment client_entity_list. Tests: position_at between two
ticks, and a client fed the server's play packets reading a zombie, a
calf, a spectating player with its default skin and a dropped item
through Host, plus texture pixels and a refused path out of the packs.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 12:15:15 -06:00
Ian WorleyandClaude Opus 5.5 6d9ed21235 feat(native-api): read the client's resource files and texture pixels
Add the `native_client_resources()` helper. It returns `ClientResources`
over the resource tree the game loaded, which is vanilla's with the
resource packs turned on and the content mods' files over it. A client
mod fetches it on the game thread and can then use it on any thread.

`read(path)` returns a file's bytes for a plain path under `assets/`,
such as `assets/minecraft/textures/entity/cow/cow_temperate.png`.
`texture(name)` decodes a texture by name (`block/stone`,
`entity/cow/cow_temperate`) into `TexturePixels`. Paths outside
`assets/` return none, so Horizon's own prepared tables stay private.

Mods stop reading `options.resources` with `atlas_sources::texture_path`
and `prepared_files::read`. The tests build a folder resource pack over
a fake vanilla tree and check that the pack's texture wins, and that
paths outside `assets/` are refused.

Co-Authored-By: Claude Opus 5.5 <[邮箱]>
2026-11-06 12:15:01 -06:00
Ian WorleyandClaude Opus 5.5 908903a58e feat(native-api): block states' facts and looks for client mods
Add the `native_block_states()` helper. It returns `BlockStates`, a copy
of every block state keyed by its `BlockStateId`. A client mod fetches it
on the game thread and can then use it on any thread.

Each state's `BlockStateFacts` holds the block name, air, invisible,
`FluidState`, waterlogged, light emission, outline boxes, map colour and
whether the state can collide at all. `sprites(id)` returns what the
state's model draws on each side, as `native_block_sprites` does for a
block name. `texture(sprite)` names the texture behind a sprite, the
inverse of `native_texture_sprites`. `mod_api` now also exports
`FluidKind`, the type `FluidState::kind` returns.

The table copies the game's internal facts into stable public types.
Client mods that draw blocks their own way, such as minimaps, world maps
and radars, no longer need `res.facts()`, `res.collision()` or
`block_sprites::Lookup`, which change from commit to commit.

Tests run on real prepared resources (ignored without
HORIZON_TEST_RESOURCES): grass block facts, air and light, slab
outlines, and a grass block's face texture names.

Co-Authored-By: Claude Opus 5.5 <[邮箱]>
2026-11-06 12:15:00 -06:00
Florian ERNSTandCodex 3d2be9b1bd Validate command enchantment map entries against active definitions
Co-Authored-By: Codex <[email protected]>
2026-11-06 19:12:28 +01:00
DUzzL 258dee9c8d Generate the trim texture of the smithing preview's result 2026-11-06 19:12:15 +01:00
apeirs 24036af2fb Mob block layers cull back faces like BlockModelRenderState
A mooshroom's mushrooms, an iron golem's poppy, a snow golem's pumpkin and a
copper golem's antenna block are submitted through BlockModelRenderState.submit,
whose block-item sheets (ITEM_CUTOUT, ITEM_TRANSLUCENT) cull back faces. Only the
enderman's carried block culled, so the coplanar faces of the other layers' cross
models z-fought as the mob moved (#5301).

The cull now goes by each quad's winding, as the GPU's does, rather than the baked
facing, which is the nearest axis and 45 degrees off for a cross model's planes.

Fixes #5301
2026-11-06 19:02:50 +01:00
65ee18174a Reduce repeated work in section meshing
Precompute row and direction masks, speed adjacent palette reads, and bypass
random model selection for a single unweighted variant.

Co-Authored-By: Codex (gpt-6.1-terra) <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WZzq8KX3vDMZy53y1vXqeT
2026-11-06 18:53:25 +01:00
DUzzL 10e24b4f32 Apply an item's custom data and entity data to a placed frame, painting or armor stand 2026-11-06 18:43:23 +01:00
Nikita Maximilian GrimmandClaude Opus 5.5 1e95d86f56 HORIZON_STILL: a frozen world's rain fog sits at its level too
Without a shader pack the rain also pulls the fog in
(AtmosphericFogEnvironment's rain fog), which eases toward the rain only in
tick_particles, and a frozen client skips that. With the rain at its level,
a frozen scene showed full rain but no rain fog. Under HORIZON_STILL a frozen
client now puts the rain fog at its target, from the same inputs the eased
path reads; the easing itself is unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 18:39:22 +01:00
echo-ilovech3ss 5303bf09af game: cover passenger rotation through /tp in rider test
Root cause: #9335 follow-ups from #8733 left passenger rotation after /tp covered only by parity corpora (tools/passenger_teleport_corpus.py), which cannot run without vanilla data. The Game-level rider test exercised offset carry (sub-item 1) but not the rider keeping its angle to the vehicle through absolute and relative turns (sub-item 2).

What changed: extend teleport_rider_tests::a_vehicle_carries_its_riders_and_a_rider_leaves_its_vehicle with a second horse/villager pair: absolute turn 0->40 leaves rider at 70 (30-degree offset kept), relative turn to 50 shifts rider 70->80, and facing (turn_riders=false) leaves rider at 80. No runtime change; the teleport/passenger writers already implement Entity.teleportSameDimension/calculatePassengerTransition via vehicle_teleport::passenger_rotation.

Vanilla reference: 26.3 Entity.calculatePassengerTransition, PositionMoveRotation.calculateAbsolute, Entity.setXRot (pitch clamp).

Refs #9335
2026-11-06 23:08:34 +05:30
MSedgeMC e524b6a114 test: an enchanted or damaged stack at the enchanting table
tools/test_enchanting_parity.py always built a fresh ItemStack for its offer
corpus, so EnchantmentMenu.slotsChanged's ItemStack.isEnchantable gate (the
enchantable component and an empty minecraft:enchantments) was never
exercised, and the oracle skipped the gate entirely. Fixture lines now carry
damage and a pre-existing enchantments entry; the oracle applies the gate and
the probe builds the same stack. The default corpus is unchanged and
--no-components reproduces it.

68992/68992 enchanting cases match.
2026-11-06 17:22:55 +00:00
Florian ERNSTandCodex 070a8019e0 Preserve empty loot result positions for command replacement
Co-Authored-By: Codex <[email protected]>
2026-11-06 18:12:15 +01:00
DUzzL 533dd084ae Keep a latched sprint or sneak toggle out of the edit shortcuts' modifiers 2026-11-06 18:11:35 +01:00
Nikita Maximilian GrimmandClaude Opus 5.5 f1fac7cc6f HORIZON_STILL: end crystals start their spin from their position
A crystal's spin and bob start from a random draw. Which crystal gets which
draw, and a server's entity ids that seed a client's copies, follow the order
chunks load in, so crystals stood at different angles in each launch of a
frozen scene. Under HORIZON_STILL the start now comes from the position; the
draw is still made, and normal play is unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 18:05:51 +01:00
Nikita Maximilian GrimmandClaude Opus 5.5 c50b3ef699 HORIZON_STILL: a frozen world's rain and thunder sit at their levels
The rain and thunder levels fade 0.01 a tick and stood still in a frozen
world, so a scene that wanted rain had to step the world about a hundred
ticks, and its mobs walked differently in each launch. Under HORIZON_STILL a
frozen world now holds both levels at their targets, as STILL already does
for a shader pack's smoothed values: /weather rain shows full rain with no
step. A running or stepping world, and normal play, fade them as before.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 18:05:51 +01:00
SenjinandClaude Opus 5.5 7b3be8d0b9 Shader packs: gbuffers_hand_water for translucent held items
The hand's quads from a translucent sheet (stained glass, ice...) go after the opaque ones in
its buffer and draw after deferred with gbuffers_hand_water (or its fallbacks: gbuffers_hand,
textured_lit, textured, basic), blended, as translucent terrain does; through gbuffers_hand
they came out solid.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 18:04:31 +01:00
Kevin RamirezandClaude Fable 5.1 408f38d62a tests: the key-release fixture stubs screen_over_playing, which mapped_key_event asks now
Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-11-06 10:52:52 -06:00
TheMrZZandCodex gpt-6.1-sol daefbd84e7 Preserve modern SNBT literals during data execution
Reuse the admitted modern SNBT decode contract for merge and literal modify.
Add actual Game storage and command-oracle regressions for typed values and
no-change copy callbacks.

Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-11-06 17:51:01 +01:00
MSedgeMC 33ec06ab12 fix: roots on the clicked face, and the chest beside a copper one
tools/test_use_item_on_parity.py never held an axe, hoe or shovel against a
block the minecraft:block_transformer registry names, so game.rs's
use_block_transformer was never exercised. It now compares log stripping,
copper scraping/waxing, copper doors and trapdoors, double copper chests and
soil tilling, which found two differences from vanilla:

- rooted dirt's hanging roots must use Block.popResourceFromFace, on the
  clicked face with the face's own motion, and drop before the block changes;
- scraping or waxing a double copper chest must send a second block_change
  game event and the same particle at the chest's other half.

The VanillaDispenserOracle stand-in player also swallowed the item use start
vibration LivingEntity.startUsingItem posts; it now posts it too.

5200/5200 cases match on the new corpus.
2026-11-06 16:49:46 +00:00
Kevin RamirezandClaude Fable 5.1 e68a0653a0 A book, sign editor or prompt over the world releases the held buttons (#9729)
Gui.setScreen runs KeyMapping.releaseAll for every screen and
Minecraft.tick skips handleKeybinds while one is up, books, the sign
editor, dialogs and prompts included. Horizon keeps Screen::Playing under
those, and its release-all (track_toggle_screens) and the client input
gate (remote_handle_input) only knew the server dialog, while the book's
and sign's mouse arms eat both edges of a click: a dig held into a book
went on after the book closed, until the next click.

gameplay_has_keyboard now takes "a screen is over the world"
(screen_over_playing) in place of the dialog alone, and remote_handle_input
is not ready while one is up.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-11-06 10:46:37 -06:00
TheMrZZandCodex gpt-6.1-sol 686d897f59 Normalize Mob Tags when adopting NBT
Adopt the bounded String input before duplicate removal in the two Mob NBT load paths. Keep the canonical retained state visible to real tag commands and saves, and pin cap, duplicate, scalar and live rewrite behavior against Java.

Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-11-06 17:38:11 +01:00
Florian ERNSTandCodex a8201731a1 Decode Item Owner and Thrower through numeric UUID streams
Co-Authored-By: Codex <[email protected]>
2026-11-06 17:27:10 +01:00
Kevin RamirezandClaude Fable 5.1 ff4fad66c9 The lead held in first person turns with the mouse, no jitter (#10276)
LocalPlayer.getRopeHoldPosition eases the lead's hand end from the view
to yRotO/xRotO by half the partial tick. Entity.turn moves yRotO and xRotO
by the mouse's delta too, and AbstractBoat.clampRotation moves yRotO with
the clamp, so only what the tick turned (a boat's deltaRotation) eases in.
Horizon's rope_rot_old was a once-a-tick snapshot the turn never moved:
while the mouse moved, the hand end lagged by half the turn since the
tick began and snapped back at the next tick, 20 times a second.

turn_player now moves rope_rot_old through the same Rotation::turn as the
view, and both boat clamps (the turn's and the tick's) move it with the
view. A test pins that a mouse turn leaves no gap, a boat turn eases, and
xRotO clamps at 90.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-11-06 10:22:24 -06:00
MSedgeMC a13fa8c13c port_map: HappyGhast's two save methods
`HappyGhast.addAdditionalSaveData` writes `still_timeout`, the timer
`still_tick` keeps in `counter`; `readAdditionalSaveData` reads it back and
a save without it is 0.
2026-11-06 16:17:03 +00:00
MSedgeMC 5c86d98a9b test: a happy ghast's still timeout survives the save
0, 7 and 10 through `mob_to_nbt` and `mob_from_nbt`: the Int vanilla writes,
the timer back on the mob, and `on_still_timeout` following it. Without the
writer the saved tag has no `still_timeout` (the test fails on `left: None,
right: Some(0)`).
2026-11-06 16:17:03 +00:00
MSedgeMC 9004c6c8d6 fix: a happy ghast keeps its still timeout in the world save
`HappyGhast.addAdditionalSaveData` writes `still_timeout`, the
`serverStillTimeout` behind `staysStill`/`isOnStillTimeout`, and
`readAdditionalSaveData` reads it back:

    protected void addAdditionalSaveData(ValueOutput out) {
        super.addAdditionalSaveData(out);
        out.putInt("still_timeout", this.serverStillTimeout);
    }

Horizon keeps that timer in `Mob.extra.counter` (`happy_ghast::still_tick`
counts it down after the first 60 ticks, `player_above` refreshes it to 10)
but neither `mob_to_nbt` nor `mob_from_nbt` touched it, so a ghast saved
while it was held still came back with 0. For the ticks vanilla restores, a
dropped item resting on its back had no box to rest on
(`EntityGetter.getEntityCollisions` takes a happy ghast only while
`isOnStillTimeout`), and a rider had no control (`getControllingPassenger`
asks `isOnStillTimeout`).
2026-11-06 16:17:03 +00:00
GabbotampoandClaude Opus 5.5 5fe1090401 Item components: refuse negative holder ids and holder set sizes
ByteBufCodecs.holder in vanilla 26.3 reads a VarInt and, when it isn't 0,
looks the entry up with `byIdOrThrow(id - 1)`, which no negative id passes.
skip.rs's `holder` accepted any nonzero id, so a slot whose kinetic_weapon
sound holder was negative was kept as a raw component, and hashing it for an
inventory click computed `id - 1` on i32::MIN: a panic with overflow checks
on, a wrapped id otherwise.

ByteBufCodecs.holderSet reads the size plus one and builds
`new ArrayList(size)`, which refuses a negative size. `holder_set` computed
`n - 1` before checking the sign, the same overflow for i32::MIN.

Both readers now refuse a negative value with a protocol error, so every
component that reads a holder or a holder set gets the same boundary.
Valid direct and registry holders read as before (their vanilla hashes in
hash/raw.rs's tests are unchanged).

Fixes #11222

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 17:07:33 +01:00
daMpoffandOpenAI Codex 0ab59f64d4 Fix overflow in translated launcher instance menus
Co-Authored-By: OpenAI Codex <[email protected]>
2026-11-06 19:02:25 +03:00
Kevin RamirezandClaude Fable 5.1 b0d94ed2c0 The player's own third-person model shivers when fully frozen (#11217)
LivingEntityRenderer.isShaking is isFullyFrozen (DATA_TICKS_FROZEN at or
over 140), and setupRotations wobbles the body while it is. Mobs and
other players already shiver, but the window's own model, rebuilt each
tick by tick_player_model, never got the player's frozen ticks, and the
window ignored the DATA_TICKS_FROZEN its server sends it, so a player in
powder snow stood still in third person.

The own model takes ticks_frozen from the player's body, and own entity
data index 7 sets it on the window's player as LocalPlayer reads it.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-11-06 10:01:55 -06:00
Horizon-is-vibe-coded a4c8dc296d fix(checkerboard): clamp negative scale before shift
Guard against negative scale values producing excessive shift
distances in CheckerboardBiomeSource::noise_biome by clamping
scale to a non-negative lower bound before adding the +2 offset.

This keeps the shift distance within a sane range and avoids
surprising behavior for out-of-range runtime scale inputs.
2026-11-06 17:00:42 +01:00
Florian ERNSTandCodex 1d800a7655 Canonicalize floating zero when parsing text tags
Co-Authored-By: Codex <[email protected]>
2026-11-06 16:57:32 +01:00
DUzzL 776bb4f4d5 Snap pistons-pushed shulkers to the block centre (Shulker.setPos) 2026-11-06 16:53:06 +01:00
Kevin RamirezandClaude Fable 5.1 9fb818013c An arrow leaves from where the player was when the bow was released (#8423)
ServerGamePacketListenerImpl handles a client's packets in order: the
RELEASE_USE_ITEM a client sends from handleKeybinds comes before the move
LocalPlayer.tick sends in the same client tick, so the server shoots from
the position of the move it handled the tick before, one tick of strafe
behind the camera. Hub::process_inbox applies moves at once and hands a
PlayerAction to the game for after the pass; it held a queued move behind
a dig, UseItem, UseItemOn and Interact, not behind the release, the drops
or the stab. The move ran first, and the arrow left a tick ahead of the
camera: strafing left, it seemed to come from the left instead of the
right.

Every PlayerAction now holds the queued move for the next tick. A test
drains a release followed by a move and checks the game gets the release
alone with the guest's position and known movement unchanged.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-11-06 09:53:03 -06:00
Kevin RamirezandClaude Fable 5.1 efc682f297 A player boards a vehicle again right after leaving it (#11158)
Entity.removePassenger sets the leaving passenger's boardingCooldown to
60, but Player.removeVehicle overrides Entity.removeVehicle and zeroes it
right after super.removeVehicle(), so a player never waits to board again;
only mobs do. #8733 (from #8586, which missed that override) gave the host
and every guest the 60 ticks, checked by may_board before boarding a boat,
cart, cushion, ghast, llama, mount or nautilus: the delay the issue
reports.

The player's cooldown goes: the boarding_cooldown and riding_at_tick
fields of Game and Guest, their swap for a guest's turn, the per-tick
count-down and may_board at its eight call sites. Each interaction still
refuses a sneaking player, the other half of Entity.canRide. Mobs keep
extra.boarding_cooldown.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-11-06 09:44:13 -06:00
oxi-does-dev 147c6bceef Launcher: keep the partner card's corners in the glass look 2026-11-06 18:39:41 +03:00
Nikita Maximilian GrimmandClaude Opus 5.5 94a66c81d4 screenshot-sequence: STEP_MS in decimal milliseconds
16.666667 steps a 60 fps clock. STEP_MS is a finite number more than 0,
and the reply gives it back as given.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 16:33:15 +01:00
Nikita Maximilian GrimmandClaude Opus 5.5 8650e51ddb screenshot-sequence: no pack reload or loading screen during a sequence
A Horizon pack's hot reload could rebuild the pack under the same name
mid-sequence, unseen by the scene check: it now waits for the reply. A
sequence doesn't start, or ends, while the level-loading screen shows,
which a new world keeps a moment after it is ready. The 30 s limit is on
the frames before the last: the last frame's PNG waits as screenshot does.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 16:33:15 +01:00
Nikita Maximilian GrimmandClaude Opus 5.5 0be2751890 horizon-ctl screenshot-sequence: a shader pack's first frames on a set clock
screenshot-sequence FRAMES STEP_MS PATH, under HORIZON_STILL with the world
frozen, makes the running pack start over and draws FRAMES frames on a
scripted clock: frame n is n frames and (n - 1) x STEP_MS milliseconds in,
STEP_MS after the frame before (0 on frame 1). The last is written as
screenshot PATH writes it, its -hdr.png too under HDR output, and the reply
names the pack and the last frame's clock. The same build, GPU and driver
write the same bytes every run, for shaders/ packs, Horizon's own and none.

Sequence frames are submitted inline and waited for on the GPU, so each
reads the centre depth the one before left, or the sequence fails; a frame
the surface refused is drawn again. It refuses to start with far terrain,
TAA, frame generation, chunk fade or the shadow cache on, refuses commands
that would change the scene while it runs, and ends with an error when the
camera, world, settings, pack or window size change. cancel stops it, quit
still quits. Documented in agent-control.md's STILL section, shaders.md and
api/shaders.md.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 16:33:15 +01:00
Nikita Maximilian GrimmandClaude Opus 5.5 bb8be7d516 Shader packs: the reset forgets the world's frames for smooth() too
Since smooth() keeps going through teleports, a pack remembers whether it
has drawn a frame of the world. A reset pack now forgets that too, as a
pack just loaded has seen none.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 16:33:14 +01:00
Nikita Maximilian GrimmandClaude Opus 5.5 4d025d5e93 Shader packs: the reset also blanks a Horizon pack's own post targets
A pass that reads one of the pack's targets ([[post]] output) before the
pass that writes it got last frame's picture after a reset; a pack just
made reads it blank. The reset now drops the targets and their bind
groups, made again blank on the next frame; the pipelines stay.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 16:28:14 +01:00
Nikita Maximilian GrimmandClaude Opus 5.5 8744511d74 Shader packs: a reset that makes the running pack start over
Renderer::reset_shader_pack_history makes the next frame draw as a freshly
loaded pack's first, for either kind. A shaders/ pack's buffers, images,
storage buffers and shadow maps are made again when next drawn, its passes
get their declared uniform values back, and the frame uniforms, custom
uniforms and centre depth forget earlier frames; its programs stay. A
Horizon pack's post history goes black and its eased values, wetness and
frame count restart. Without a pack it does nothing. Nothing calls it yet.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 16:28:14 +01:00
Nikita Maximilian GrimmandClaude Opus 5.5 0bd8f4c89c Shader packs: the scripted clock's step in decimal milliseconds
A 60 fps clock steps 16.666667 ms a frame, which a whole number of
milliseconds can't give. FrameTiming::nth takes the step as f64.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 16:28:13 +01:00
Nikita Maximilian GrimmandClaude Opus 5.5 e173055d5d Shader packs: a scripted clock that comes before STILL's ticks
PackInputs::frame_timing gives the frame number, the seconds since frame 1
and the seconds since the frame before. Iris-format packs take it for
frameCounter, frameTimeCounter and frameTime and smooth wetness,
eyeBrightnessSmooth and centerDepthSmooth by its delta; custom uniforms
start over on its frame 1 and carry their state after. Horizon packs take
it for time, frame_index and delta_time, ease by it, and do not treat a
slow frame as a gap. Nothing sets it yet.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 16:28:13 +01:00
Florian ERNSTandCodex 5b44880bfb Reject unknown inline slot-range names before execution
Co-Authored-By: Codex <[email protected]>
2026-11-06 16:26:05 +01:00
Kevin RamirezandClaude Fable 5.1 b2a6c25a6e A player saved while gliding comes back gliding (#11193)
LivingEntity.addAdditionalSaveData writes FallFlying as isFallFlying(), and
readAdditionalSaveData sets shared flag 7 from it, so a player who saves
mid-glide resumes the glide on the next load. Horizon wrote a fixed
FallFlying of 0 and never read it back: the player came back standing in
mid-air.

Level now keeps fall_flying, read from any numeric FallFlying tag (as
getBooleanOr does) and written from the guest's glide; playerdata's store
and apply carry it between the Guest and the file. The server's next
sync_player_state sends the 0x80 shared flag to the client, and
updateFallFlying's port ends the glide if it can't go on.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-11-06 09:24:52 -06:00
TheMrZZandCodex gpt-6.1-sol 942e212ca7 Fix unless-function continuation after fallthrough
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-11-06 16:20:21 +01:00
JoansitohandGrok <grok-4.7> e380e88edb Keep wither skulls in flight through a chunk save
Entity.shouldBeSaved is true for every projectile in 26.3, and
WitherSkull adds `dangerous` on top of the hurting projectile's
fields. Horizon listed WitherSkull as not saved, so a skull still in
the air when its chunk unloaded was gone when the chunk came back,
and a blue skull came back as nothing.

Write and read the skull the same way as the fireballs: `dangerous`
as a byte (absent means a black skull), and no `Fire` since
WitherSkull.shouldBurn is false. The fire rule now keys on the
fireballs proper (is_item_fireball) instead of "not blast-immune",
which would have lit a saved skull.

Co-Authored-By: Grok <grok-4.7> <[email protected]>
2026-11-06 16:18:52 +01:00
oxi-does-dev e35010eb7d Merge remote-tracking branch 'origin/main' into feat/launcher-nord 2026-11-06 18:16:00 +03:00
oxi-does-dev 014f25363a Launcher: Nord theme, square solid look, sky without resize bakes
Adds the Nord palette (polar night, snow storm, frost) as a launcher theme with its own dark and light tokens and sky art. Palette themes draw the square Blocks look: no glass, no rounded corners. The night tokens are square everywhere, and the Blocks status dots and controls follow. Blocks and the light glass look draw the baked sky, so resizing no longer rebakes the scene per frame and stays responsive. Settings gains the launcher-theme picker in the Blocks look. Nord now shows the sky instead of the scene's pale halo.
2026-11-06 18:11:28 +03:00
martipopsandClaude Opus 5.5 b0286fb598 Controllers: the bumpers move the selected server
Its arrows are hit-tested inside the icon, where a pad's click on the row never
lands.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 10:10:52 -05:00
martipopsandClaude Opus 5.5 cf73e1d25f Merge main
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 10:05:53 -05:00
Florian ERNSTandCodex 5201202a30 Evaluate named entity_scores predicates for bound holders
Co-Authored-By: Codex <[email protected]>
2026-11-06 15:59:55 +01:00
flexeykinDevandClaude Opus 5.5 4ac75c281a install.ps1: verify the release signature when OpenSSL 3 is found
The first install on Windows now checks SHA256SUMS.sig (a Nightly's
SHA256SUMS.v2.sig, for the version its title names) against the release
key, then the archive against SHA256SUMS, as install.sh does. OpenSSL is
looked for on the PATH and in Git for Windows. Without OpenSSL 3 it says
so and keeps the GitHub SHA-256 check.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 15:53:25 +01:00
rison1337andCodex GPT-6 f5c1c04245 Stream dynamic light sources and bound nearest-light selection
Avoid owning held-item names and intermediate lists on every frame.
Keep stable nearest-16 lighting output and document FPS and image checks.

Co-Authored-By: Codex GPT-6 <[email protected]>
2026-11-06 19:48:30 +05:00
TheMrZZandCodex gpt-6.1-sol fcb378d2dd Emit mob command damage vibrations before returning
Post the existing damage event for accepted health-changing mob command hits so a same-tick teleport cannot move its origin. Preserve the existing duplicate marker and reinforcement handling; pin the real command/listener route against Java.

Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-11-06 15:42:35 +01:00
MSedgeMC 4062a09766 test: mine a block in creative too, where nothing drops
tools/test_block_removal_parity.py only ever mined as a survival player:
every mine case ran ServerPlayerGameMode.destroyBlock in SURVIVAL, so the
creative branch (Player.preventsBlockDrops, no mined statistic, no tool
wear) was never compared even though game.rs::break_block_as_player splits
on it. Half the mine cases now run in creative, with a tool that can destroy
blocks in creative (a sword and a mace cannot, which the creative-mining
predicate corpus already owns).

VanillaDispenserOracle's Miner gains a creative flag (isCreative and
hasInfiniteMaterials) and the real CREATIVE game mode; the game's dispense
fixture sets the same mode for the case. --survival-only reproduces the
corpus as it was before this change.

4000/4000 cases match on the new corpus.
2026-11-06 14:29:14 +00:00
Slavik-85andClaude Opus 5.5 791e721843 F3 overlay: paint at 20 Hz, no per-frame OS calls, less GPU timing
The overlay formatted its lines and laid out its glyphs every frame. It now
paints at most 20 times a second and copies those vertices in between, and
repaints at once for another window size, GUI scale or font epoch.

- The refresh rate is read at most once a second: on Windows, winit calls
  EnumDisplaySettingsExW each time. The Window line uses the surface size.
- Passes are timed only while the overlay lists their times, at most 10 frames
  a second. gpu-passes and HORIZON_SHADER_PACK_TIMING keep every sample.
- The CPU name is read in the background when the window opens: on Windows it
  starts reg.exe, which the first F3 frame waited for.
- DebugEntries::any_enabled and status no longer build a String per entry; the
  game asks every frame, F3 closed or not.
- Closing the overlay stops the server thread computing its lines each tick.
- The Mem, Allocation rate and Allocated lines now show on Windows.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 16:28:44 +02:00
Florian ERNSTandCodex 48791d642e Expose loaded arrow Owner references in command NBT
Co-Authored-By: Codex <[email protected]>
2026-11-06 15:26:21 +01:00
DUzzL 7f87be741b Keep golems off zombie horses, camel husks and zombie nautilus 2026-11-06 15:22:15 +01:00
Arslan ArmanulyandClaude Opus 5.5 401693ba65 Shader packs: shadow settings come from the fragment shaders, as Iris reads them
Horizon took shadowDistance, sunPathRotation, shadowIntervalSize,
shadowNearPlane and shadowFarPlane from a program's vertex shader too, and
read nothing of shadow.fsh but its formats when the pack has no shadow
pass. Iris 1.11.7, with probe packs of ours, ignored those five declared
only in final.vsh, gbuffers_textured.vsh or shadow.vsh (its shadow
projection, shadow camera and sun stayed at the defaults), and applied
shadowDistance and sunPathRotation declared only in shadow.fsh of a pack
whose shadow buffers nothing read.

Part of #10490

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 19:20:46 +05:00
Arslan ArmanulyandClaude Opus 5.5 5a44d6dd7f Shader packs: the sky's parts blend as vanilla draws them
gbuffers_skybasic drew every part of the sky with the translucent blend.
Vanilla 26.3 draws the sky disc and the dark disc with RenderPipelines.SKY
(ColorTargetState.DEFAULT, no blending), the sunset fan translucent and
the stars with RenderPipelines.STARS (BlendFunction.OVERLAY: colour
SRC_ALPHA ONE, alpha ONE ZERO). Iris 1.11.7 drew them so with probe packs
of ours: the sky disc kept exactly what the program wrote, (0.3, 0.6,
0.9, 0.25) in buffers of four formats, and the stars added 0.25 x 0.5 to
the disc's 1.0 and left their own alpha 0.5. Without a
blend.gbuffers_skybasic of the pack's own, the disc and the void, and
the stars, now draw with pipelines of the same program that blend so,
keeping a blend.gbuffers_skybasic.<buffer> the pack sets (Iris blended
the disc into that buffer only).

Part of #10490

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 19:20:28 +05:00
Arslan ArmanulyandClaude Opus 5.5 54c4a07e61 Shader packs: a float output into an integer buffer no longer refuses its program
A program writing a float vec4 into an integer colortex (RGBA32UI...) was
refused: wgpu has no float output into an integer target, so the whole
program, and every buffer it writes, was lost. Iris 1.11.7 drew such a
program with a probe pack of ours: its other buffers kept what it wrote,
and the integer buffer read the floats' bits back (GL leaves that value
undefined; these are NVIDIA 615.78.08's). The output now writes the
float's bits as the buffer's integer type.

Part of #10490

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 19:19:41 +05:00
Arslan ArmanulyandClaude Opus 5.5 1fd49b9082 Shader packs: noiseTextureResolution sizes the default noisetex
Horizon always made its default noise texture 256 wide. Iris 1.11.7
made it the size a pack's `const int noiseTextureResolution` gives, when
declared in a fragment shader (composite, deferred, gbuffers, final, or
shadow.fsh with no shadow pass), and ignored it in a vertex shader, in
probe packs of ours. The setting is now read like a colortex format.

Part of #10490

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 19:19:10 +05:00
Arslan ArmanulyandClaude Opus 5.5 892f24b15d Shader packs: the length of a buffer block's runtime array reads back
`.length()` of a runtime-sized array in a named buffer block made naga's
validator refuse the program: its GLSL frontend loads the array and takes
the length of the loaded value. After parsing, the length is now taken of
the array's pointer. Iris 1.11.7 ran such a program in a probe pack of
ours and read the buffer's length (bufferObject size / stride).

Part of #10490

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 19:19:00 +05:00
Arslan ArmanulyandClaude Opus 5.5 dde8c502ca Shader packs: a compute stage reading a shadow buffer turns the shadow pass on
Horizon decided whether to run the shadow pass from the samplers of the
programs' vertex and fragment stages only. A pack whose only reader of
shadowtex0 or shadowcolor0 was a compute stage (prepare.csh alone, or a
composite.csh beside a composite.fsh that reads none) got an empty map
(1.0), while Iris 1.11.7 drew it for probe packs of ours (0.3956 depth,
0.251 shadowcolor0 at our noon scene). Compute stages now count as the
other stages do.

Part of #10490

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 19:18:41 +05:00
Arslan ArmanulyandClaude Opus 5.5 b31e63adc8 Shader packs: an R11F_G11F_B10F image is stored into and read back
Horizon made an R11F_G11F_B10F custom image as RGBA16F, then refused
every program that declared it layout(r11f_g11f_b10f), since the shader's
storage format no longer matched the texture. Iris 1.11.7 stored into such
an image and read it back, with a probe pack of ours: (0.25, 1.5, 3.0)
read back as written, alpha 1, also filtered between texels. The image is
now Rg11b10Ufloat where the device stores into that format; elsewhere it
stays the RGBA16F stand-in, and the shader's image declares that format.

Part of #10490

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 19:18:33 +05:00
Arslan ArmanulyandClaude Opus 5.5 e4bb7bf7ae Shader packs: the sun and moon leave their own alpha, as vanilla's celestial blend
The default blend of gbuffers_skytextured added the sun's alpha over the
sky's (One, OneMinusSrcAlpha). Vanilla 26.3 draws the sun and moon with
RenderPipelines.CELESTIAL, BlendFunction.OVERLAY: colour (SRC_ALPHA, ONE),
alpha (ONE, ZERO). Iris 1.11.7 left the sun's own alpha (2.0) in an
RGBA32F buffer the sky had written 1.0 to, with a probe pack of ours;
Horizon left 1.0.

Part of #10490

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 19:18:12 +05:00
DUzzL f7b204a231 Read mod features from worldgen/feature/, 26.3's folder 2026-11-06 15:17:45 +01:00
SamSu 6635e7a46a Launcher: rewrite the Thai translation (th.json)
Thank you for adding Thai to the launcher in #8306. We were really
happy to see it. When we used it, though, many lines were hard to
understand for us as Thai players. The translation looks
machine-made, and many words have their everyday meaning instead of
their computer meaning. We rewrote it so that Thai players can use the
launcher comfortably. 731 of the 1225 strings changed. The others
were already good, so we kept them as they were.

Below is each word we changed, what it is now, and how a Thai reader
understands the old word. We hope this makes our reasons clear, even
if you don't read Thai:

- build: อาคาร / โครงสร้าง -> บิลด์
  อาคาร means a building (a house, an office). โครงสร้าง means the
  structure of something. "Native build" read as "the original
  building". Thai developers and players say บิลด์, written as it
  sounds.
- instance: ตัวอย่าง -> อินสแตนซ์
  ตัวอย่าง means "example" or "sample", so "Instance" looked like a
  demo copy of the game, not a separate game setup. อินสแตนซ์ is the
  English word written in Thai, as Thai players say it.
- native (mod, build): ดั้งเดิม -> เนทีฟ
  ดั้งเดิม means traditional or original. "Native mod" read as "the
  original mod", which says nothing about compiled code. เนทีฟ is the
  usual Thai programming word.
- launcher: ตัวเรียกใช้งาน / ตัวเปิด / (left in English) -> ลันเชอร์
  The same thing had three names, so a player could not tell they
  were one program. ลันเชอร์ is what Thai players call it.
- jar: ขวด / โหล -> ไฟล์ jar
  ขวด is a bottle and โหล a glass jar for food. "Port gets the jars"
  read as "Port gets the bottles". It is a file type, so ไฟล์ jar.
- Loader: รถตักดิน -> ตัวโหลด
  รถตักดิน is a wheel loader, the construction vehicle.
- Client: ลูกค้า -> ไคลเอนต์
  ลูกค้า is a shop customer. Here it means the game client.
- Running / run: วิ่ง -> กำลังทำงาน / รัน
  วิ่ง is running on foot. "Running" read as "jogging". A program
  that runs is ทำงาน (working) or รัน.
- Proxy: หนังสือมอบฉันทะ -> พร็อกซี
  หนังสือมอบฉันทะ is a legal power-of-attorney letter.
- port: ท่าเรือ -> พอร์ต
  ท่าเรือ is a harbour for ships. A network port is พอร์ต.
- cape: แหลม -> ผ้าคลุม
  แหลม is a headland on a coast (or "sharp"). A Minecraft cape is a
  cloak, ผ้าคลุม.
- cosmetics: เครื่องสำอาง -> คอสเมติก
  เครื่องสำอาง is make-up (lipstick, powder). Thai gamers call
  in-game items to wear คอสเมติก.
- seed: เมล็ดพันธุ์ -> seed
  เมล็ดพันธุ์ is a plant seed for farming. Thai Minecraft players use
  the English word for a world seed.
- Home (page): บ้าน -> หน้าแรก
  บ้าน is a house. The Home page is the first page, หน้าแรก.
- Ban: บ้าน -> แบน
  Ban was also translated as บ้าน ("house"), so the ban button said
  "house". Thai players say แบน.
- founder (set): ผู้ก่อตั้ง -> ผู้เล่นรุ่นแรก
  ผู้ก่อตั้ง is the person who founded a company, so "Free for
  founders" read as "free for the people who started Horizon". The
  shop page says the set is for "the players who were here first", so
  it is ผู้เล่นรุ่นแรก, "early players". It is not "sponsors" either:
  the set is free.
- player counts: คน -> ผู้เล่น
  "{online} of {max} players" used คน (person). In a server list Thai
  players expect ผู้เล่น (player), as the game itself says.
- Java Edition: รุ่นจาวา -> Java Edition
  A product name, like Minecraft and Microsoft, which stay in
  English. รุ่นจาวา ("the Java version") reads as a Java release.
- 78 strings still in English -> translated
  They showed English in the middle of a Thai screen.
- Polite particle ครับ removed
  ครับ is the spoken "sir" that ends a sentence when a man talks. In
  UI text it sounds like a person talking, not a program.

We kept every {placeholder} and every middle dot of the English, and
we used one Thai word for each English term across the whole file.

The two of us, both native Thai speakers, read every changed line
(SamsVT and NotNiceKun). If anything still reads wrong,
please tell us and we will gladly fix it. Thank you for Horizon!
2026-11-06 21:11:40 +07:00
Florian ERNSTandCodex b127089760 Preserve literal modern sign string messages
Co-Authored-By: Codex <[email protected]>
2026-11-06 15:04:48 +01:00
Delete228andCodex be3fbc2d2b Supply entity kinds in collision probe fixtures
Co-authored-by: Codex <[email protected]>
2026-11-06 16:58:15 +03:00
Delete228andCodex 7af3e1ec44 Test stateful mod block entity lookup in the tooling fixture
Co-authored-by: Codex <[email protected]>
2026-11-06 16:58:15 +03:00
Delete228andCodex e575a22cb2 Use the isolated game environment for mob discard parity
Co-authored-by: Codex <[email protected]>
2026-11-06 16:58:15 +03:00
AxxioandAntigravity 4e6b887535 multiplayer: narrate incompatible server version and motd
Narrate incompatible server list entries matching vanilla narration order: select -> incompatible -> version -> motd. Also remove the note about incompatible server status from docs/narrator.md known differences.

Closes #10317

Co-Authored-By: Antigravity <[email protected]>
2026-11-06 14:49:21 +01:00
Florian ERNSTandCodex 4d3f9944eb Honor fork state for terminal execute condition errors
Co-Authored-By: Codex <[email protected]>
2026-11-06 14:44:04 +01:00
Delete228andCodex 79fe067a7e Declare the state factory with the GameLevel hook points
Co-authored-by: Codex <[email protected]>
2026-11-06 16:41:39 +03:00
Evrim CicekandClaude Opus 5.5 c40c92e0bc Minecarts read their numeric NBT from any numeric tag, as vanilla's ValueInput does
AbstractMinecart, MinecartTNT, MinecartFurnace and MinecartHopper read
their fields through ValueInput's numeric getters, which take any
NumericTag: getIntOr (fuse, DisplayOffset), getShortOr (Fuel, the low 16
bits), getDoubleOr (PushX, PushZ) and getBooleanOr (FlippedRotation,
HasTicked, Enabled: byteValue() != 0). Cart::load took only byte, short
and int, and only double for the pushes, so {fuse:20L} left a TNT
minecart unprimed and {PushX:1} gave a furnace minecart no push.

Command harness group "minecart numeric fields": 3/18 -> 18/18.

Fixes #11146

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 16:40:54 +03:00
AxxioandAntigravity 4e36ae1e4e docs/README.md: index live mod reloading design note
Link docs/design/mod-hotload.md under the Design notes section.

Fixes #11154.

Co-Authored-By: Antigravity <[email protected]>
2026-11-06 14:39:41 +01:00
TheMrZZandCodex gpt-6.1-sol 253a694871 Fix live mob custom-name component predicates
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-11-06 14:37:37 +01:00
AxxioandAntigravity bfdff7afb6 docs/native-client: update stale platform scope note
macOS and Windows are supported platforms with dedicated documentation under docs/platform/. Replace the stale 'macOS and Windows are out of scope' sentence with a reference to docs/platform/.

Fixes #11145.

Co-Authored-By: Antigravity <[email protected]>
2026-11-06 14:36:19 +01:00
cb62d074db Search neighboring columns for dolphin breathing like vanilla
Match vanilla's column order and fallback when choosing a breathing destination.

Co-Authored-By: Codex (gpt-6.1-terra) <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 14:35:37 +01:00
60742e9c49 Save villager brain memories like vanilla
A villager's Brain.memories now keep golem_detected_recently, last_woken
and potential_job_site, and each memory's remaining ttl, through a save
and load (Brain.pack, MemoryMap.CODEC, ExpirableValue.codec). The brain
built at the first tick starts from the loaded memories.
last_worked_at_poi stays unsaved: the villager brain does not register it.

Co-Authored-By: Codex (gpt-6.1-terra) <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 14:35:37 +01:00
0e0fa342b8 Keep wandering trader homes like vanilla
Use the shared mob home for spawn, movement restrictions and persistence.

Co-Authored-By: Codex (gpt-6.1-terra) <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 14:35:37 +01:00
fa76ed4d94 Match vanilla hanging entity coexistence checks
Reject conflicting overlaps between paintings and item frames.

Co-Authored-By: Codex (gpt-6.1-terra) <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 14:35:36 +01:00
voidsip ff416a1889 Tick the gateway passenger player in the command oracle scene 2026-11-06 07:33:50 -06:00
martipopsandClaude Opus 5.5 62298d7c36 Controllers: the guide's rows promise what the button does, from reviewing it
- The pad's own click hid the guide, and so did Alt or Super on their own. The
  inventory rows now hide for the keyboard and mouse too.
- Rows that promised something the game refuses: Equip on what can't be swapped,
  Throw on a Riptide trident out of water or about to break, Use on an empty or
  fixed item frame, Trade on a baby villager.
- Slots: Put rows on a slot that refuses the carried stack or has no room, Put Down
  for stacks with different components, Take over a result slot with another stack
  in hand, and any rows on a crafter's output or Put rows on a furnace's.
- Rows drew over a panel that fills a small view.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 08:28:21 -05:00
AxxioandAntigravity 89dc5664de audio: support HORIZON_AUDIO_BUFFER override for Linux buffer underruns
On Linux, Horizon asks ALSA for a 1024-frame fixed buffer to keep sound effects low-latency. When PipeWire is configured with min-quantum >= 1024 (common for Bluetooth audio or in VMs), the 1024-frame buffer repeatedly underruns with POLLERR and produces no sound.

Allow setting HORIZON_AUDIO_BUFFER=default or HORIZON_AUDIO_BUFFER=<frames> (e.g. 2048) to override the fixed buffer request and use a larger or system-default buffer. Document the environment variable in native-client.md.

Fixes #11082.

Co-Authored-By: Antigravity <[email protected]>
2026-11-06 14:21:51 +01:00
voidsip a98ba02730 Allow parity runner defaults in dolphin and spell harnesses 2026-11-06 07:19:21 -06:00
Delete228andCodex 36232d8820 Restore the frozen tick decision before ending a sprint
Co-authored-by: Codex <[email protected]>
2026-11-06 16:17:27 +03:00
Florian ERNSTandCodex 85b299df9d Preserve Pillager and WanderingTrader saved inventories
Co-Authored-By: Codex <[email protected]>
2026-11-06 14:13:19 +01:00
Delete228andCodex a3b1e9dd08 Keep native compatibility checks runnable in the upstream test harness
Co-authored-by: Codex <[email protected]>
2026-11-06 16:11:31 +03:00
Delete228andCodex d705f96e06 Keep native compatibility checks runnable in the upstream test harness
Co-authored-by: Codex <[email protected]>
2026-11-06 16:11:31 +03:00
Delete228andCodex c968fb0d7d Keep native compatibility checks runnable in the upstream test harness
Co-authored-by: Codex <[email protected]>
2026-11-06 16:11:31 +03:00
Delete228andCodex 1bb6912a4b Keep native compatibility checks runnable in the upstream test harness
Co-authored-by: Codex <[email protected]>
2026-11-06 16:11:31 +03:00
DUzzL 7a46ba1f27 Show the dismount hint when the player starts riding 2026-11-06 14:09:41 +01:00
DUzzL f3d842d846 Seat other players on a cushion they sit on 2026-11-06 14:08:47 +01:00
Delete228andCodex ea750ddf7b Format the native compatibility changes
Co-authored-by: Codex <[email protected]>
2026-11-06 16:05:49 +03:00
Delete228andCodex 5bea1f4226 Format the native compatibility changes
Co-authored-by: Codex <[email protected]>
2026-11-06 16:05:48 +03:00
Delete228andCodex 8f73091827 Format the native compatibility changes
Co-authored-by: Codex <[email protected]>
2026-11-06 16:05:48 +03:00
homayk228 266040ff8e Merge branch 'main' of https://github.com/SouceCalve/horizon 2026-11-06 19:52:48 +07:00
homayk228 a123c03b16 Added support for installing from PrismLauncher-portable
Now installer supports native to PrismLauncher json and correctly finds json, client.jar and other sources from minecraft, allowing it to install from portable installations without additional downloading from internet
2026-11-06 19:52:43 +07:00
AxxioandAntigravity 03d9b39fc4 tests/test_harness_args: match system path separator for default work dir
DEFAULT is a Path object that formats with backslashes on Windows, while harness_args.py appends str(default). Pinning against str(DEFAULT) instead of the hardcoded POSIX string lets the test pass across all platforms.

Co-Authored-By: Antigravity <[email protected]>
2026-11-06 13:52:31 +01:00
Delete228 a5fa6f0d09 Keep empty native container provisioning out of inventory notifications 2026-11-06 15:51:42 +03:00
Delete228andCodex 04448803c8 Hide discarded mobs from immediate lookup and passenger saves
Keep removed mob entries out of command enumeration and UUID/ID lookup before
the normal sweep, and prevent a discarded passenger from saving inside its
vehicle. Preserve existing death-time and health rules.

Validate frozen Game commands/API37/save assembly against real26.3 discard
and entity-manager behavior with both client and server JARs.

Co-Authored-By: Codex <[email protected]>
2026-11-06 15:51:39 +03:00
AxxioandAntigravity 8f39c8afd2 docs: remove stale server End credits note
Co-Authored-By: Antigravity <[email protected]>
2026-11-06 13:48:49 +01:00
Delete228andCodex 90b9df2899 Load same-type mod block entity command data in place
Replace saved NBT and item slots through the existing entity while preserving its live identity and runtime menu data. Keep normal decoding, dirty marking and update logging; verify real data commands, typed opaque fields, inventory removals and fresh different-type/chunk-loaded identities.

Co-Authored-By: Codex <[email protected]>
2026-11-06 15:47:54 +03:00
Delete228 3ba55cc9fa Supply the standard LAN join expectation in the fanout test 2026-11-06 15:46:01 +03:00
Evrim CicekandClaude Opus 5.5 0b0a58af3e Settle an enderman's teleport inside explosions, falling blocks and conduits; its hurt first
Review: hurts dealt after the creature tick (ordered TNT, beds and anchors,
falling blocks, conduits) still waited for the next tick. Explosions and
falling blocks now settle right after each hurt, before the push, and the
conduit pass after it runs. And the hurt's own events (its sound,
InfestedMobEffect.onMobHurt) are handled before the teleport, where the
enderman was hurt and from its random's next draws, as
LivingEntity.hurtServer runs them before Enderman.hurtServer teleports.

Tests: an_explosion_teleports_endermen_before_it_returns (11/12 left
pending without the explosion settle), an_infested_endermans_silverfish_
come_out_where_it_was_hurt (no silverfish when the outbox only joined the
teleport's events).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 15:42:29 +03:00
TheMrZZandCodex gpt-6.1-sol afd49ac9c2 Fix primed TNT numeric fuse loading
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-11-06 13:40:09 +01:00
Delete228 c8cdfd117a Draw native terrain models with explicit materials and lighting 2026-11-06 15:39:28 +03:00
Delete228 218449af0c Render native still fluid volumes with an explicit vanilla material 2026-11-06 15:39:28 +03:00
Horizon agentandClaude Opus 5.5 34af7009f5 Multiplayer: drag a saved server by its row to move it in the list
Reordering a long server list took many clicks on the icon's small arrows or
Shift+arrow presses. The press that selects a saved server now also holds it:
moving the pointer puts it on the saved server's row underneath, the others
close up, and the release saves the order. A press on the selected server
still joins, as in vanilla, and holds nothing.

Fixes #10060

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 13:38:53 +01:00
DUzzL 0f992292b7 Split slimes, magma cubes and sulfur cubes when the body is removed
Horizon split a dying cube at the moment of death. Vanilla
AbstractCubeMob.remove runs the split when the entity is removed, which
LivingEntity.tickDeath does after the death animation (deathTime 20),
so the smaller variants appear about one second later.
2026-11-06 13:37:46 +01:00
voidsip 87386f72d9 Expose focused text fields to native character hooks 2026-11-06 06:35:13 -06:00
Delete228andCodex 21d66471d4 Bind content item aliases to ordinary block placement
Compile place_block declarations into the existing BlockItem binding table and load secondary item-to-block mappings for client prediction and server placement. Preserve alias item identity, component defaults and existing primary block-item mappings.

Validation: 49 compiler, 38 declaration and 63 item tests pass. Explicit prepared-resource tests verify original placement context IDs, real survival consumption, adventure refusal and dry client prediction. Strict clippy, formatting and public-tree checks pass.

Co-Authored-By: Codex <[email protected]>
2026-11-06 15:35:03 +03:00
Florian ERNSTandCodex 6e302fab48 Remove stale Animal LoveCause when no reference is modeled
Co-Authored-By: Codex <[email protected]>
2026-11-06 13:34:29 +01:00
Delete228andCodex b55e8616c1 Keep native entity data through creative Pick Block
Co-Authored-By: Codex <[email protected]>
2026-11-06 15:32:57 +03:00
Delete228andCodex 796db06bf8 Resolve mod block shapes from entity context
Co-Authored-By: Codex <[email protected]>
2026-11-06 15:32:57 +03:00
Delete228andCodex e1604cc9c0 Support transient fields consumed at mod client update construction
Mod block entities can replace, clear and atomically take an owner-controlled batch of client update fields without saving it or changing retained contents and revisions. The lazy queue is empty for new, decoded and cloned entities; normal loaded-chunk and block broadcast paths remain unchanged.

Validated with 3 real entity lifecycle/concurrency/poison tests and 3 reusable save/chunk/two-viewer Hub tests. Strict all-target Clippy passes for horizon-client-render and horizon-client-lib. No new dependency, third-party implementation or resource files.

Co-authored-by: Codex <[email protected]>
2026-11-06 15:32:56 +03:00
Horizon agentandClaude Opus 5.5 3365d72927 Native mods: client_attack and client_hurt hooks for the local player's combat
A client mod (battle music, a combat HUD) couldn't hear its own player's
fights on a remote server: the attack and damage hooks run where Horizon is
the server. native_client_attack(target, player) fires when the attack packet
leaves; native_client_hurt(attacker, player) when the server's damage event
for this player arrives, with the causing entity it names and whether this
client knows it as a player.

Fixes #10064

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 13:31:31 +01:00
Riccardo GuerrieroandClaude Opus 5.5 f7af3f420f use_item_on's oracle posts item_interact_start when a use starts
The oracle's stand-in player overrode LivingEntity.startUsingItem and only recorded the item, so
the real vanilla vibration never ran: ItemStack.causeUseVibration posts ITEM_INTERACT_START when
the stack's use effects ask for interaction vibrations, which every item's UseEffects.DEFAULT
does (DataComponents.COMMON_ITEM_COMPONENTS). Since #10272 Horizon posts it, so the harness
reported 284 false mismatches (4916/5200). It now calls causeUseVibration under the same guard
as vanilla: 5200/5200.

A first draft by DeepSeek V4.1 Flash, via pi, reviewed by Claude.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 13:29:30 +01:00
Horizon agentandClaude Opus 5.5 ee55bb1e75 Dropped items fall through minecarts, as in vanilla
An item's move takes getEntityCollisions: the entities whose
canBeCollidedWith(item) holds. AbstractMinecart overrides only
canCollideWith (for its own moves), so a cart keeps Entity's false and
an item dropped on it falls through; the item list chained the carts in.
Boats, living shulkers and a held happy ghast still hold items (#11091).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 13:28:51 +01:00
Horizon agentandClaude Opus 5.5 1b221b01da Native mods: native_take_screenshot takes a screenshot as F2 does
A client mod (timed or repeated screenshots, as fabrishot does) asks for the
next frame's screenshot through F2's own path: screenshots/, vanilla's file
name, the chat line. It returns the file it will write; an error without a
window, or while another screenshot waits for the frame, which it would
otherwise replace.

Part of #9606

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 13:27:06 +01:00
CocofireHDandOpenCode Agent c81dc55a1c README: a Performance section from the 2026-10-29 Horizon vs vanilla runs
The comparison #1073 asks for, in the repository: FPS and 1% lows at render
distances 8, 16 and 32 for Horizon, vanilla 26.3 and vanilla with the common
performance mods, from the clean, all-windowed, idle-machine run already saved
in benchmarks/perf/, with the conditions, the startup/load/memory figures and
links to the full reports, the method and playhorizon.dev/benchmarks.

Co-Authored-By: OpenCode Agent <[email protected]>

Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
2026-11-06 13:25:46 +01:00
Evrim CicekandClaude Opus 5.5 335326296c An enderman hurt by the game layer teleports at the hurt, not on its next AI step
Enderman.hurtServer teleports inside the hurt (teleport(), or
repeatedlyTryToTeleport() for a projectile or a thrown potion). Mob::damage
has no world, so it only leaves the tries in counter2 and the enderman's
next AI step ran them. Game::settle_hurt_teleports runs them right after
/damage, and in the creature tick before the mobs tick and after the
systems' outbox: a command after /damage no longer finds it in place, and
under /tick freeze it teleports at all.

Command harness group "enderman command damage": 3/5 -> 5/5.

Fixes #11069

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 15:24:29 +03:00
Horizon agentandClaude Opus 5.5 3815013235 Native mods: native_open_chat opens the chat with text typed in
A client mod (a command macro such as CommandKeys) can open the chat screen
with text pre-filled for the player to finish, as T and / open it. Nothing is
sent. An error with no player in a world or while another screen is open.

Part of #10120

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 13:23:56 +01:00
martipopsandClaude Opus 5.5 6e0b26dbc2 Controllers: fixes from reviewing this branch
- Take back the server list's move arrows as targets: they split Tab's stop on
  the list, and the D-pad still couldn't land on them.
- A right stick on Z takes over in every direction, not only its pulled end.
- A pad's toggle-sneak doesn't count as holding, or a rebound key's release could
  leave the player sneaking.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 07:22:43 -05:00
Delete228andCodex 177548d416 Create native block entities from their actual block state
Co-Authored-By: Codex <[email protected]>
2026-11-06 15:21:47 +03:00
Delete228andCodex ec74bd066b Add item fallback after native mob interactions pass
Keep entity actions and API 29 ahead of item fallback, reuse normal packet and crosshair selection, and let the first response consume with or without an acting-hand swing. Cover bounded native Mob entries with a pending fragment, generated dispatch and actual Game priority/filter tests; leave other living interaction tables explicit.

Co-Authored-By: Codex <[email protected]>
2026-11-06 15:21:46 +03:00
Delete228andCodex 3de12c0c7c Allow held-item break effects to name an explicit equipment slot
Keep ordinary wear, Unbreaking, durability criteria, stack removal and break handling in one path. Existing damage_held callers retain MainHand behavior; callers that model an offhand use may select OffHand without editing stack damage directly.

Validation: native prepared-resource Game test verifies offhand wear, break event48, unchanged main-hand stack, legacy event47 and creative immunity. Strict gameplay clippy, formatting and public-tree checks pass.

Co-Authored-By: Codex <[email protected]>
2026-11-06 15:21:44 +03:00
Horizon agentandClaude Opus 5.5 c960a329ce Detect the machine's architecture in horizon-telemetry
horizon-client-render forbids unsafe code, which the Windows ARM64 build
reported (IsWow64Process2 and sysctlbyname are FFI). The detection lives with
the other system probes in horizon_telemetry::system now.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 13:20:50 +01:00
Riccardo Guerriero 01954d87c5 Merge remote-tracking branch 'origin/main' into fix/illager-target-memory 2026-11-06 13:19:46 +01:00
Ludovic Marie 41f822b659 Merge pull request #10301 from foudretbr/feat/screen-transparent-background-hook
🤖 Agent: foudretbr's rebase onto main (a1ff75a4a) drops the stray `);` that broke `tests/mod_api_hooks.rs`, and the file-wide import reorder. The hook change is the same 9 files: with no mod loaded, every translucent screen background is still the vanilla gradient. The 10 hook tests and tests.test_stamp_native_hooks pass.
2026-11-06 13:19:07 +01:00
Ludovic Marie d6fd737e31 Merge pull request #10382 from voidsipp/fix/10255-slow-event-note
🤖 Agent: merged main so this builds again (its old base still called `mob_to_nbt` without the UUID lookup that #10544 fixed). Burst notes are unchanged: one group per packet kind, each naming the player who spent the most time; the 7 burst and event-timing tests pass on the merged head.
2026-11-06 13:19:03 +01:00
rison1337andCodex GPT-6 52766de1c4 Skip falling-block mesh preparation behind the camera
Cache conservative envelopes of every baked model alternative, invalidating
on mutable model access. Reject rear-origin falling blocks only when their
complete padded model is outside the posed camera's depth clip planes.
Depth-only clipping preserves TAA jitter and screen-edge geometry; shader
packs keep their original path.

Include geometry/cache regression tests and separate ordinary/TAA FPS and
image comparisons, with front/empty controls and an oversized model fixture.

Co-Authored-By: Codex GPT-6 <[email protected]>
2026-11-06 17:19:01 +05:00
Ludovic Marie ce25432200 Merge pull request #11086 from horizon-rust/docs/readme-refresh
🤖 Agent: README/docs-media only; keeps the Project status block and '### Java parity' table the site and tests read, so playhorizon.dev's parity number is unaffected.
2026-11-06 13:18:59 +01:00
Horizon agentandClaude Opus 5.5 7ba6892f93 wet_fire harness: default to the prepared resources when HORIZON_TEST_RESOURCES is unset
parity.py does not set it, so the harness failed at once under parity.py and CI.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 13:18:19 +01:00
Horizon agentandClaude Opus 5.5 0443141598 Mobs stop at shulkers and at a happy ghast held still
Entity.collide adds getEntityCollisions: the boxes of the entities whose
canBeCollidedWith(mover) holds. Mob movement only had block collisions,
so a zombie or a dropped-on mob fell through a living shulker and
through a happy ghast a player holds still by standing on it (#10445).

Seen carries the happy ghast's still timeout and whether it carries
someone; the mob tick lists the slots that may block (shulkers, happy
ghasts) so other mobs pay nothing when there are none.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 13:16:47 +01:00
Delete228andCodex 15405e4ccf Route standard block fluid helpers through native-owned storage
Co-Authored-By: Codex <[email protected]>
2026-11-06 15:16:38 +03:00
Horizon agentandClaude Opus 5.5 bf4657da26 README: a cleaner page with fresh screenshots of today's game and launcher
The README's pictures were from mid-October (launcher 0.2.30, the old
menus), so this retakes them and tidies the page around them.

- Pictures: Nightly 2026-11-05 (66f3981) windowed on the Mac mini's M4
  Pro (Metal) with seed 8675309: a river valley with a village and a long
  view, a cherry-grove sunset, a crimson forest with a piglin, survival
  with the HUD, and two players over LAN; the launcher 0.2.47's Home (with
  those screenshots in its instance) and Mods pages. WebP, 712 KB in all,
  under docs/media/readme/. The five README-only images they replace are
  removed; village/nether/underwater.webp stay (the launcher embeds them).
- Text: a short pitch, what works in the current release, a download
  table with each platform's file and requirements (Windows x64 and Arm,
  macOS Apple Silicon and Intel from 12, Linux glibc), the Microsoft
  sign-in requirement, mods and shader packs (Java mods don't run;
  modpacks import), hosting, agent control, build from source,
  contributing, and Mojang's "NOT AN OFFICIAL MINECRAFT PRODUCT" line.
- Fixed claims: macOS Intel and Windows on Arm are out (not "from the next
  Nightly"); the known-issues line points to docs/known-issues.md.
- Two badges instead of seven: the parity and gap badges the tests and
  tools/parity_gaps.py keep in sync. "Project status", its gap block and
  the "### Java parity: N%" table are unchanged, so the website still
  reads its number and #build-from-source/#project-status still resolve.

Builds on #11040 by Web-mc1, who first noticed the launcher pictures were
outdated and retook them.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 13:14:09 +01:00
Horizon agentandClaude Opus 5.5 594552fec4 Hosting: an AMP template for horizon-server, beside the Pterodactyl egg
packaging/server/amp/ holds a template for CubeCoders AMP's generic module.
Its update stage runs the egg's own install script (the release asset checked
against its SHA-256), the start runs the same horizon-start.sh, and its
settings are the egg's variables with the same defaults. Tests keep both in
step and check the console patterns against the lines the server logs.

Fixes #8238

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 13:13:17 +01:00
Horizon agentandClaude Opus 5.5 3c6c890993 Drop the selector criterion run-log doc
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 13:11:20 +01:00
Horizon agentandClaude Opus 5.5 0efe1c2c13 Player effects also leave out ambient false and show_particles true
MobEffectInstance.Details.MAP_CODEC uses optionalFieldOf("ambient", false)
and ("show_particles", true), so like amplifier 0 those defaults are not
written (the mob writer already does this). The harness gets a default
work dir, so tools/parity.py can run it with no arguments; the run-log
doc is dropped.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 13:11:18 +01:00
Horizon agentandClaude Opus 5.5 a2090d0b0f Entity boolean harness: default work dir; drop the run-log doc
tools/parity.py runs each harness with no arguments, and this one exited
asking for --work-dir; it now uses a fresh folder under .local.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 13:11:18 +01:00
Horizon agentandClaude Opus 5.5 5b12729b5f Happy ghast ambient goes through Mob::voice; drop the capture harness
Mob::voice already plays at the feet (Entity.playSound), stays quiet for a
Silent entity, uses getSoundVolume and HappyGhast's pitch 1.0, so the
special arm is not needed and a Silent happy ghast no longer sounds. The
test checks the Silent case too. The 238-line capture harness ran a Java
server, a native server and a client for 1200 ticks on every ai.rs
change; the resource test covers the position.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 13:11:14 +01:00
Horizon agentandClaude Opus 5.5 d3d24aca55 Optimize World: cancel within a chunk, one sync per region file
- Each worker looks at the cancel flag before every chunk, not only before
  every file, so Cancel stops within a chunk on large region files. A
  canceled file is never swapped in by recreate_region_files.
- The writes of a region file reach the disk once, when the file is done,
  instead of a sync_data after every chunk. On the ThinkPad (NVMe, ext4) the
  per-chunk sync was half of a 1.21.1 world upgrade (187 s -> 89 s, one
  thread); on macOS sync_data is F_FULLFSYNC and costs far more.
- The calling thread is worker 0 and helpers are named World Upgrader #N;
  a helper that fails to start leaves its files to the others.
- Tag modifiers are no longer cloned per chunk; the recreate folder is made
  once per file.
- Tests: the parallel/one-thread comparison also checks vanilla counts
  computed from the fixture; a new test cancels from the first chunk with 4
  workers and checks at most 4 chunks were looked at and that a canceled
  recreate leaves the world unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 13:08:06 +01:00
Horizon agentandClaude Opus 5.5 cf20ccc4f9 rustfmt the new toolchain test
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 13:07:27 +01:00
Horizon agentandClaude Opus 5.5 b02930a2f2 Compile every pack pipeline even after one is refused
compile() stopped handing out jobs once one pipeline was refused, so the
serial rebuild that follows compiled all the remaining programs cold.
Now every job runs and only the first refusal is recorded, so the
rebuild finds the driver cache warm. The test checks the jobs after a
refused one are still made.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 13:06:55 +01:00
Horizon agent 4f7fadfd9e Merge remote-tracking branch 'origin/main' into pr-11049 2026-11-06 13:06:39 +01:00
voidsip 58a5690ec6 Add native client sound parameter interception 2026-11-06 06:05:23 -06:00
Detimes 8dad7f0ac0 Merge remote-tracking branch 'upstream/main' into via-2 2026-11-06 12:04:54 +00:00
Horizon agentandClaude Opus 5.5 11d40ff1b9 Native mods: quinn 0.11 (QUIC) in Cargo.lock through horizon-mod-deps
Mods that speak QUIC (e4mc's relay) couldn't build: their dependencies must
already be in Horizon's lock. quinn joins horizon-mod-deps as an optional
dependency (feature quic: runtime-tokio + rustls-ring, so TLS stays on the
rustls + ring already in the lock). Nothing in the game builds it.

Fixes #9602

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 13:04:20 +01:00
Horizon agentandClaude Opus 5.5 5ca811b47a Merge main into #10210; drop the packet trace and receipts
The merchant fixture compares strict command and menu records, so the raw
packet trace (net/command_trace.rs in the client receive loop, the Java
trace reader, the oracle's serialized channel) and the per-operation
receipts were diagnostics only. The production client no longer writes or
joins anything for them; tools/test_merchant_gate_parity.py is a thin
wrapper like the other command_exec fixtures.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 13:04:15 +01:00
Horizon agentandClaude Opus 5.5 33c431e3ae Docs: Intel Macs and Windows ARM64 have builds now
The README and the hosting guide still said Intel Macs come "from the next
Nightly" and listed no Windows ARM64 launcher; both ship in every Nightly and
launcher release.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 13:02:53 +01:00
Florian ERNSTandCodex d2b61ade91 Omit stale default power from saved TNT
Co-Authored-By: Codex <[email protected]>
2026-11-06 13:01:50 +01:00
Horizon agent 7c32d1c4d6 Merge main into #10242
# Conflicts:
#	crates/horizon-items/src/item_data.rs
#	tools/command_exec_corpus.py
#	tools/test_command_exec_parity.py
2026-11-06 13:01:20 +01:00
Horizon agentandClaude Opus 5.5 c19d97699d Windows ARM64: the x64 build emulated there also starts on DirectX 12
Auto already tries DirectX 12 before Vulkan on Windows ARM64, where Vulkan lost
the device (#7513, #6320 on Adreno), but it read the build's architecture: the
x64 game, which the x64 launcher installs on a Snapdragon PC, still started on
Vulkan. The order now follows the machine's architecture (IsWow64Process2), and
crash reports name the emulation ("x86_64 on aarch64"), on Windows and under
Rosetta 2 on macOS (sysctl.proc_translated).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 13:00:51 +01:00
DetimesandClaude Opus 5.5 cdd4ffe95d Format native_bots.rs
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 12:00:29 +00:00
Horizon agent 35e7942094 Merge main into #10208
# Conflicts:
#	tools/lan_e2e.py
2026-11-06 13:00:21 +01:00
Horizon agent f217a34593 Merge main into #10240
# Conflicts:
#	tools/port_map/client-004.tsv
2026-11-06 13:00:17 +01:00
Horizon agent e9b071e19e Merge main into #10538
# Conflicts:
#	crates/horizon-client-render/src/gfx/pack_post.rs
2026-11-06 13:00:01 +01:00
Horizon agent f15541c0fe Merge remote-tracking branch 'origin/main' into pvp-2/sneak-input
# Conflicts:
#	crates/horizon-client/src/game/remote.rs
2026-11-06 12:59:58 +01:00
Horizon agent 965c9d9aed Merge main into #10219
# Conflicts:
#	crates/horizon-client/src/game/scoreboard.rs
2026-11-06 12:59:46 +01:00
martipopsandClaude Opus 5.5 84efc873cf Input: one device letting go of sneak or sprint leaves the other's hold
Releasing Shift while R3 held sneak in hold mode dropped it for both, and so did
the other way round.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 06:59:30 -05:00
Horizon agent 97f85f4530 Merge main into #10540 2026-11-06 12:57:33 +01:00
Horizon agent 4ce74ca81c Merge main into #10565 2026-11-06 12:56:03 +01:00
Horizon agent 2dcb671e13 Merge main into #10546 2026-11-06 12:55:54 +01:00
Horizon agent 2abb7c86a1 Merge remote-tracking branch 'origin/main' into pr-10531 2026-11-06 12:55:29 +01:00
Horizon agent 964d919d96 Merge main into #10542 2026-11-06 12:55:19 +01:00
Horizon agentandClaude Opus 5.5 904e1d5597 Video settings: LOD Distance gets fine stops from 16 to 256 chunks
A small render distance (8 chunks on a phone or an integrated GPU) could only
add far terrain out to 64 chunks, the slider's first stop. The slider now stops
at 16, 24, 32, 48, 64, 96, 128, 192 and 256 before doubling as before.

Fixes #9694

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 12:55:06 +01:00
Delete228andCodex fd61fa626e Expose block clone stacks to native mods
Co-Authored-By: Codex <[email protected]>
2026-11-06 14:53:55 +03:00
voidsip db25d1d092 Match NBT path quoted escapes and index tokens 2026-11-06 05:53:36 -06:00
Ludovic Marie 398da21689 Merge pull request #10372 from Skr1zzzz/tab-shown-10108
🤖 Agent: merged #10312 (with main) in. The cache compares the whole Scoreboard twice per frame (displayed_ids and layout) and clones it on every change, so a server ticking a sidebar score every tick rebuilds and clones each frame; still cheaper than the old double sort, and tab_list parity passes on a union holding this head.
2026-11-06 12:50:14 +01:00
Ludovic Marie 297a718908 Merge pull request #10312 from Skr1zzzz/mp-fps-4664
🤖 Agent: merged main in: #10328 (merged meanwhile) now caches the layouts of the tags your collect_name_tags keeps, and #10098's new hide_gui argument is passed in your early tab_list::visible check too, so F1 still hides the list on the skip path. Skipping a tag whose entity box (inflated 0.5) is off the frustum matches vanilla, where shouldRender gates the name tag with the entity. 194 selected harnesses run on a union holding this head: tab_list and hud pass, the 8 failures are identical without it.
2026-11-06 12:50:08 +01:00
Horizon agentandClaude Opus 5.5 d4a985e599 Drop the run-log note and the non-standard parity script
tools/test_non_mob_saved_flags_parity.py needs a fresh --work-dir that
parity.py never passes, so every selected run would fail; the unit test
covers the four writers.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 12:48:38 +01:00
DUzzL 8a78ad3315 Apply death_protection death_effects for players and mobs, read mob hands by component 2026-11-06 12:48:18 +01:00
voidsip 82dc682cbc Parse server properties numbers like Java 2026-11-06 05:48:05 -06:00
Evrim CicekandClaude Opus 5.5 09160dc1e2 Merge upstream/main into fix/mounted-attack-box
Minecarts and primed TNT now seat their riders from the riders map;
the branch's seated box is kept on that path.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 14:46:14 +03:00
voidsip f1ef5cb53a Expose local particle spawning to native client mods 2026-11-06 05:43:33 -06:00
Ludovic Marie 28a389b7a3 Merge pull request #10557 from Detimes/feat-10404-older-clients-1b
🤖 Agent: shelf mushroom stands in as brown_mushroom_block (solid, not the walk-through brown mushroom), explorer maps as filled_map; 26.2 refusal from #10477 still in place
2026-11-06 12:42:26 +01:00
Horizon agentandClaude Opus 5.5 8ead30efe3 Native mods on Intel Macs: the launcher brings Rust for x86_64 macOS
The x64 macOS launcher (#1161) installed and ran the game, but native mod builds
stopped with "the launcher has none for macos x86_64": its managed toolchain
had no x86_64-apple-darwin host. Add Rust 1.99.1's official rustc, cargo and
rust-std for it, pinned by the SHA-256 static.rust-lang.org publishes; the
linker is the system's, as on Apple Silicon.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 12:41:15 +01:00
atsauban 052d9ebf20 fix(launcher): support responsive wrapping on narrow windows
Prevent vertical letter-by-letter text stacking and control clipping
when running in narrow tiling or compact displays:
- Top bar switches to compact tab padding and hides long subtitle
- Home actions and subtabs adaptively scroll and wrap without overflowing
- Mods and Versions headers stack title and filters vertically
- Settings navigates via top scrollable pill bar on narrow displays
- About tiles and Account cards stack cleanly
- Setting rows wrap controls instead of shrinking labels to 0 width
2026-11-06 18:40:50 +07:00
Horizon agentandClaude Opus 5.5 2ce4e8da11 Reset a remote trident's life on motion; cover lerpMotion in the remote motion test
ThrownTrident inherits AbstractArrow.lerpMotion, which restarts life even
when the motion is zero. The remote motion test compiles remote_motion out
of remote.rs, so its fixture needs the arrow fields; it now also checks
that zero motion keeps a stuck arrow in place and non-zero motion frees it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 12:38:30 +01:00
Horizon agent 3f1af338cb Merge main into #10201 2026-11-06 12:37:53 +01:00
Evrim CicekandClaude Opus 5.5 d1ce5ddfd9 Merge upstream/main into fix/iron-golem-toss
Upstream's UUID bridge (#10304) now writes a mob anger target's UUID
through mob_to_nbt's uuid_of. The branch keeps its two additions on top:
a target read from the save that is not in the world keeps the loaded
UUID, and the loaded angry_at goes once the anger is over.

A mob only gets its UUID when it is first saved, so an anger target
not saved yet came out with no angry_at (wolves too). mob_tree_nbt now
gives a target that is still in the world its UUID first; the new
anger_at_a_mob_not_saved_yet_names_it test failed without that.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 14:37:40 +03:00
Horizon agentandClaude Opus 5.5 25ce3945c8 Roadmap: link 22 proposal issues to their items; launcher accessibility and local builds as Later items
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 12:37:18 +01:00
Florian ERNSTandCodex 40b1fc9100 Save default Interaction dimensions and response
Co-Authored-By: Codex <[email protected]>
2026-11-06 12:35:47 +01:00
Detimes a61d6fa54f Merge remote-tracking branch 'upstream/main' into hooks-bots 2026-11-06 11:34:12 +00:00
TaquitoSuabe 38d23bc13c gfx: skip broken Haswell Vulkan driver (hasvk) and fall back to OpenGL
- Mesa's hasvk Vulkan driver for Intel Haswell (Gen 7.5, vendor 0x8086) reports conformance version 0.0.0.0 and suffers from severe vertex buffer corruption and polygon tearing across 3D terrain (#9912).
- Check is_broken_haswell_vulkan in open_with_retry to skip non-conformant hasvk on Haswell and fall back cleanly to Mesa crocus OpenGL 4.6 Core Profile.
- Preserve Ivy Bridge (IVB) on Vulkan (#4059).
2026-11-06 04:31:50 -07:00
Delete228andCodex c4c0e6fc6b Expose dropped-item block contact to native mods
Co-Authored-By: Codex <[email protected]>
2026-11-06 14:25:58 +03:00
DUzzL 1fc4fa2529 Upgrade region files in parallel in Optimize World 2026-11-06 12:21:55 +01:00
voidsip d2d046b1be Honor mapped function keys in keyboard input 2026-11-06 05:18:42 -06:00
DetimesandClaude Opus 5.5 964454e1f8 Test that a mod's health 0 kills a guest, in and out of their turn
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 11:18:32 +00:00
foudretbr ae2e74af51 Native mod API: transparent screen background hook and menu blur
Fixes #10131

# Conflicts:
#	crates/horizon-client/tests/mod_api_hooks.rs
2026-11-06 12:17:57 +01:00
martipopsandClaude Opus 5.5 00879f19d7 Move the keyboard-took-over mark out of the mouse button handler
tests/test_mouse_button_events.py compiles that handler alone.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 06:16:01 -05:00
DUzzL 62028656e0 Save common Entity fields for registered Markers 2026-11-06 12:15:42 +01:00
VaspyyyandClaude Opus 5.5 6f2b33ff95 Launcher Settings: update status and Check now move to Updates
Horizon's update status (Up to date, Install) and the launcher's version with
Check now / Update sat under About, while the Updates section held only the
two auto-update switches. They now open Updates, in both looks: the Glass
tiles above the switches, the classic rows at the top of its Updates group.
About keeps the data folder (now a row like Logs), logs and the applications
menu.

Fixes #11035

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 12:14:33 +01:00
DetimesandClaude Opus 5.5 a3abc60adf lan_bot: skip 26.2-layout packets in the 26.2 bots, walk in one move per tick (#10404)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 11:14:30 +00:00
voidsip 5fd9d40d4c Validate packet resource identifiers when decoding 2026-11-06 05:14:09 -06:00
martipopsandClaude Opus 5.5 a4ffae3402 Controllers: reach the recipe search, suggestions, server arrows, Realms tabs and HUD editor
Each was painted by hand or hit-tested with no target, so a pad couldn't use it:
the recipe book's search box had no keyboard, a command's suggestions couldn't be
picked, a server couldn't be moved, Realms Configure's tabs had no bumpers, and
the HUD editor's footer ended its crosshair.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 06:14:06 -05:00
DetimesandClaude Opus 5.5 270d067afa Keep the feedback of a captured vanilla command
A capturing server source is not silent: it takes every sendSuccess and
sendFailure line and informs no operator, so run it that way.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 11:13:31 +00:00
martipopsandClaude Opus 5.5 701af0a693 Controllers: the guide's Ride is what the boat would do, and the keyboard hides it
The boat row said Ride while sneaking, in a boat or on a full one, which refuses
or opens a chest boat's chest. In play only moving the mouse took the pad out
of use, so the guide stayed up through keyboard and mouse-button play.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 06:07:12 -05:00
Detimes a8dc253fe9 Merge via-2c 2026-11-06 11:06:44 +00:00
Detimes d0a80d6eb8 Merge remote-tracking branch 'upstream/main' into native-refresh-commands 2026-11-06 11:06:23 +00:00
Detimes ac7f3dbcf9 Merge via-2b 2026-11-06 11:06:20 +00:00
Detimes 4ae9769ab7 Merge via-2a 2026-11-06 11:06:19 +00:00
DUzzL 3a0d345f06 Use the death-protection item, not the totem id, for the totem animation
The integrated server matched the used item by id (minecraft:totem_of_undying)
and used it up before the client looked for it, so any other item with
death_protection showed a plain totem in the activation animation.

Vanilla LivingEntity.checkTotemDeathProtection takes the first hand whose
stack has DataComponents.DEATH_PROTECTION, keeps a copy before shrink(1), and
uses that copy for the stat, the used_totem trigger and the animation.

Part of #10587
2026-11-06 12:01:57 +01:00
ScholiboiandClaude Opus 5.5 09ac779466 Pearls on a server's client hover in bubble columns as vanilla's do
ThrowableProjectile.tick runs on the client too: handleFirstTickBubbleColumn
before gravity, then applyEffectsFromBlocks' BubbleColumnBlock.entityInside
after the move, with Projectile's unclamped pushes (+0.06 inside, +0.1 at the
surface, -0.03 dragging down). The client's remote_projectile_tick had none of
it, so a pearl held in a stasis chamber on a server sank between the tracker's
updates and snapped back on each one.

Projectile::tick_remote adds both for ThrowableProjectiles, through a new
ProjectileWorld::bubble_column hook (bubble_column_at for the game's world).
The integrated and dedicated servers keep pushing their pearls in the
creature tick, so their projectiles are not pushed twice.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 16:28:27 +05:30
Riccardo Guerriero cd7ce3024a Merge remote-tracking branch 'origin/main' into fix/piglin-admired-drop 2026-11-06 11:58:16 +01:00
DetimesandClaude Opus 5.5 962d7c6a22 Native helpers: per-player feedback, SNBT and map helpers move out of the hook points
They sat in the hook-point section, so the catalog test took them for hooks and hook_points_stay_hookable saw world_items!; now in the helpers' impl block of the same file (targets unchanged), the macro written out.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 10:57:17 +00:00
Riccardo Guerriero 8eb1e7cf4f Merge remote-tracking branch 'origin/main' into fix/illager-target-memory 2026-11-06 11:55:42 +01:00
4e94325ba5 Evokers and illusioners keep an unseen target for 15 seconds
Their target goals call setUnseenMemoryTicks(300): the evoker's player
and villager goals, the illusioner's player, villager and iron golem
goals. Pillagers and vindicators keep the default 60.

Co-Authored-By: Codex (gpt-6.1-terra) <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 11:55:42 +01:00
Riccardo Guerriero 225bcde8b9 Merge remote-tracking branch 'origin/main' into fix/trade-display-drop-chance 2026-11-06 11:52:39 +01:00
1dd1969640 Tests skip when the vanilla data is missing, as CI has none
Co-Authored-By: Codex (gpt-6.1-terra) <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 11:52:39 +01:00
Horizon agentandClaude Opus 5.5 7e13949bcf 26.2 stand-ins: a solid block for the shelf mushroom, filled_map for explorer maps
The shelf mushroom is a solid shelf players stand and bounce on (ShelfMushroomBlock's shape); the brown
mushroom it stood in as has no collision, so a 26.2 client would fall through what the server stands on and
be pulled back. Its block now shows as brown_mushroom_block, whose collision holds the shelf's; the item stays
brown_mushroom. The new explorer maps are filled maps (MapItem), so they show as filled_map, which draws their
map_id, not as an empty map. v26_2_ids.rs regenerated with --substitutes. TooltipDisplay's writer no longer
allocates a list on every write.

Review follow-up for #10557 (Detimes).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 11:51:51 +01:00
Ludovic Marie a1ff75a4a5 Merge pull request #11021 from horizon-rust/fix/audit-issue-flood
🤖 Agent: tooling only. When more than 20 harnesses newly fail at once, the audit now opens one issue instead of 421, and the open-issue lookup no longer stops at 200.
2026-11-06 11:51:49 +01:00
Ludovic Marie bedc6fbeda Merge pull request #11012 from Web-mc1/main
🤖 Agent: tooling only. ensure_work_dir now returns the folder argparse picks, so the --work-dir=PATH spelling no longer crashes four harnesses on sys.argv.index.
2026-11-06 11:51:46 +01:00
DetimesandClaude Opus 5.5 304340fa9f native_boss_bar: check the bar's ranges without a negated call in the hook points
hook_points_stay_hookable reads `!(` as a macro.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 10:50:36 +00:00
DetimesandClaude Opus 5.5 830ee7c7f7 Catalog test: chunk_generate and lod_column live in horizon_world::worldgen::native_generate
The test maps each free hook to its module; the two world generation hooks are free functions in horizon-world.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 10:50:36 +00:00
Riccardo Guerriero ad6e268383 Merge remote-tracking branch 'origin/main' into fix/sleeping-mob-push 2026-11-06 11:49:31 +01:00
445ba1168e Tests skip when the vanilla data is missing, as CI has none
Co-Authored-By: Codex (gpt-6.1-terra) <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 11:49:31 +01:00
Horizon agentandClaude Opus 5.5 31310a69f6 harness_args: return the work dir the run uses
Four harnesses looked the directory up with sys.argv.index('--work-dir'), which raises
ValueError for the --work-dir=PATH spelling this PR keeps. ensure_work_dir now returns the
directory argparse will pick (the last one given) and they use that.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 11:48:41 +01:00
Horizon agentandClaude Opus 5.5 e517343add Nightly audit: one issue when many harnesses newly fail at once
The 2026-11-06 audit tested a main commit that did not compile (mob_to_nbt arity in raids.rs) and opened
421 "Parity regression" issues, one per harness. Above 20 new failures the audit now opens one issue with
the list and the first compiler error, and keeps those harnesses' previous results so the next audit
compares them again. The open-issue lookup no longer stops at 200.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 11:46:50 +01:00
Riccardo Guerriero d49f1a0444 Merge remote-tracking branch 'origin/main' into fix/mob-block-fall-sound 2026-11-06 11:46:33 +01:00
d2fbe234ba Tests skip when the vanilla data is missing, as CI has none
Co-Authored-By: Codex (gpt-6.1-terra) <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 11:46:33 +01:00
DetimesandClaude Opus 5.5 ba4d3d471f Document the player bot helpers in native-mods.md
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 10:44:19 +00:00
DetimesandClaude Opus 5.5 a6a93455e8 Add native_simulate_player_move and native_collision_boxes for server-side player bots
One tick of a server player's own movement from a bot's input, played on a
copy of the body with the movement prediction's replay code, and the block
collision boxes in a box (Level.getBlockCollisions). Read-only; nothing runs
unless a mod calls them. Helper fragment player_bots, docs, unit tests.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 10:44:05 +00:00
martipopsandClaude Opus 5.5 e41c28a92b Controllers: typing on signs and books, the death screen, and joining
- A book's page and signing title weren't fields, so a pad couldn't write one.
- A sign's keyboard closed on Enter after line 1, and nothing reopened it. Enter
  now keeps it open, and A opens it again.
- A keyboard open over chat stayed over the death screen and took every button.
- B answered the death confirm during the second it's meant to wait.
- A guest who joined with a pad got P1's Controllers off in a new profile, so
  that pad did nothing, on this join and every later one.
- Pad presses in another window could join a player.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 05:37:01 -05:00
TheMrZZandCodex gpt-6.1-sol 39c12c2542 Award crafted-item statistics for merchant result takes
Track MerchantResultSlot removal and quick-move counts, reset them at the achievement callback, and award through the actual trading player's stat API without a recipe ID.

Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-11-06 11:34:26 +01:00
DUzzL c545bd4cb1 Keep one world border per level 2026-11-06 11:34:26 +01:00
rison1337andCodex GPT-6 786bd54096 Merge main to pick up mob_to_nbt build fixes
Co-Authored-By: Codex GPT-6 <[email protected]>
2026-11-06 15:33:17 +05:00
DetimesandClaude Opus 5.5 bafc55bbe3 older clients: say which particle stand-ins keep their options
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 10:33:15 +00:00
DetimesandClaude Opus 5.5 a8c6fb9d35 lan_bot: a 26.2 client's world packets come in 26.2's layout
The join test's bot reads packets with the pinned readers; the world
packets the adapter now writes in 26.2's layout are counted (chunks) or
skipped instead.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 10:32:42 +00:00
DetimesandClaude Opus 5.5 bf60148c4f older clients: write 26.2's world packet layouts for 26.2 clients (#10404)
Chunk columns (states down with stand-ins, repacked at 26.2's 15-bit
global palette, block entity types down, light masks as longs),
light_update, level_particles (26.2 field order, one speed, Int count),
explode (no trailing sound flag, silent blasts name intentionally_empty),
move_entity_pos/_pos_rot/_rot, entity_position_sync and animate in
26.2's encodings, swing_animation as 26.2's animate.

Tests write each in 26.2's layout and read it back with translate's
26.2 readers, made pub(super) for that.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 10:31:25 +00:00
DetimesandClaude Opus 5.5 ec0c273cf2 item, recipes: write pot decorations, item-holding component values and tag slot displays in an older version's layout and ids
While ids are renumbered down, the values kept as pinned bytes that hold items
(pot_decorations, container, use_remainder, sulfur_cube_content) are read again
and written with that version's ids, pot sides as 26.2's four item ids, and a
tag slot display as 26.2's bare tag id (a composite of item displays for a list
of items). Quirks::{pot_decorations_are_items, tag_slot_display_is_id} written
the other way; ids::quirks_written gives the quirks of the version written for.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 10:30:52 +00:00
DetimesandClaude Opus 5.5 3571849861 lan_bot: a 26.2 client's walk, dig, swing and teleport confirmations reach the game as 26.3's
The test server keeps every packet the hub hands to the game; a 26.2 bot
confirms its teleports by id alone, walks, starts and stops destroying the
block below in 26.2's action numbers, and swings with each hand.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 10:30:18 +00:00
DetimesandClaude Opus 5.5 cf358cdd4e adapter: write login, respawn, advancements, commands, registry data, maps and stats in 26.2's layout for 26.2 clients (off by default)
layout_26_2 mirrors translate's 26.2 readers for a ClientSession: spawn info game
types back to bytes, the advancement tab position back inside its display,
argument types renumbered down with a string stand-in for 26.3-only parsers,
26.3-only synchronized registries not sent, map decorations and stats in 26.2's
numbering. Enabled with layout_26_2::set_enabled.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 10:29:30 +00:00
DetimesandClaude Opus 5.5 4745f91af6 older-clients: read 26.2's accept_teleportation, player_action and swing as 26.3's
A 26.2 client confirms a teleport by its id alone: the session follows the
player_position packets it is sent and the moves it reports, and fills in
26.3's position and rotation from the teleport it accepts. Its digging
actions past 0 are one up (26.3 inserted CHANGE_DESTROY_DIRECTION at 1), and
its main-hand swing becomes 26.3's punch (an off-hand swing has no packet).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 10:29:07 +00:00
DetimesandClaude Opus 5.5 a0bb275b62 Native mods: native_refresh_commands rebuilds the players' command trees at run time (#10812)
A Game helper (Host::refresh_commands for trait mods): at the start of the
next server tick every mod's native_register_commands is asked again,
LanHost::native_commands and the hub's mod tree are rebuilt, and every
placed player is sent their tree again after native_player_commands_sent,
as PlayerList.sendPlayerPermissionLevel / Commands.sendCommands after
/reload. Hub::store_mod_commands keeps the new tree without sending it, so
each player gets one filtered tree. Servers whose mods never call it are
unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 10:28:45 +00:00
voidsip edc6751798 Keep new lava and fire ignition silent in rain 2026-11-06 04:28:43 -06:00
SenjinandClaude Opus 5.5 d8160a3ded Shader packs: at_velocity for entities
Each entity's vertices are matched with last frame's by entity (Frame::entity_motion, now with
the GPU-posed start at each mark): last frame's pack buffer stays bound at set 1, binding 2, and
binding 3 gives each vertex its index there. at_velocity is this frame's view position less last
frame's (gbufferPreviousModelView joins the block when a pack reads it); a vertex with no match
reads zero, as before.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 11:26:13 +01:00
Detimes fce1fb23d6 Merge remote-tracking branch 'upstream/main' into hooks-all
# Conflicts:
#	crates/horizon-client/src/game/raids.rs
2026-11-06 10:26:00 +00:00
Detimes ef66a2db57 Merge remote-tracking branch 'upstream/main' into hooks-b-all 2026-11-06 10:26:00 +00:00
DetimesandClaude Opus 5.5 4198a4de7f Tests: dispatch-test stubs for the new hook types (EntityDataValue, NativeGenerate, NativeProtoChunk, NativeLodColumn, TickSpan)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 10:21:12 +00:00
Detimes d1288fc34b Catalog test: log_line's target is horizon_client_lib::native_client_hooks
The test maps each free hook to its module; log_line (hooks-10461-a) lives in native_client_hooks.rs.
2026-11-06 10:18:21 +00:00
HiuoyandClaude Opus 5.5 3eda4b1b14 Native hook catalog: tooltip_render_pre is RenderTooltipEvent.Pre; Next hooks have all landed
`tooltip_render_pre` (#10302) is labelled `RenderTooltipEvent.Pre`, so
`horizon-port-pack events` counts the 10 ATM10 mods that listen to it.
The "Next hooks" table in docs/native-hooks-neoforge.md listed
`player_tick_pre` and the client tick pair, which have now landed: the
section lists them with the rest.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 14:16:33 +04:00
voidsip f214ffcc8a Merge remote-tracking branch 'origin/feedback-main-20261106' into fix/round3-1-wet-fire-order 2026-11-06 04:15:07 -06:00
voidsip a82ad5eed6 Merge remote-tracking branch 'origin/feedback-main-20261106' into fix/mob-water-breathing 2026-11-06 04:14:24 -06:00
DetimesandClaude Opus 5.5 7750a91768 Tests: the respawn guest test from main fills Joined.configured (player_channels)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 10:14:00 +00:00
DetimesandClaude Opus 5.5 0580048c9d Clippy: then_some for the closing native store's slots (menu_input)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 10:14:00 +00:00
DetimesandClaude Opus 5.5 82f9fbf1ad Clippy: no explicit deref and sort_by_key in the generation hooks (chunk_generate, lod_column)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 10:13:59 +00:00
Florian ERNSTandCodex 3122ae98c0 Omit Mob-only flags from modeled non-Mob saves
Co-Authored-By: Codex <[email protected]>
2026-11-06 11:13:36 +01:00
Web-mc1 8ccf1404e7 harnesses: keep the --work-dir a caller gives
Five command-oracle harnesses looked for a bare `--work-dir` and appended
their default anyway when the caller wrote `--work-dir=PATH`. argparse takes
the last value, so the caller's directory was silently replaced and the run
wrote its scratch output somewhere they did not ask for. They now share the
guard test_boat_leasher_parity.py already used, and tests/test_harness_args.py
pins both spellings.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <[email protected]>
2026-11-06 10:13:20 +00:00
1de40d7cd3 A use that swings the hand resets the attack charge
ServerPlayer.swingAndResetAttackStrength: the guest's block, item, leash
knot and entity uses reset the attack ticker when their swing starts.

Co-Authored-By: Codex (gpt-6.1-terra) <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 11:12:32 +01:00
martipopsandClaude Opus 5.5 1a6fba23c7 Controllers: what reading a real pad got wrong
- An idle pad's trigger rests at -1, which counted as moved past 0.5, so touching
  it took over from the pad in use.
- An analog trigger jumped to full at gilrs' 0.75, so a higher threshold did
  nothing.
- gilrs drops `~` from SDL rows, so sticks on the nine Linux rows with one ran
  backwards, the Switch 2 Pro among them.
- The 8BitDo Ultimate 2 without a row looked up when pushed down, with X and Y
  crossed.
- Rumble reached the pad from an unfocused window, adaptive triggers stayed on
  over books and signs, a stale lightbar stayed after splitscreen, and in_use
  outlived an unplugged pad.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 05:11:50 -05:00
Horizon agentandClaude Opus 5.5 4fa3bf699b Merge main into #10501 (main builds again after #10544)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 11:09:00 +01:00
fac11b3d6a Cauldrons, beehives, snow golems, farming villagers and lead knots make the vibrations vanilla's do
- LayeredCauldronBlock.lowerFillLevel posts a block change.
- BeehiveBlockEntity.addOccupant posts a block change.
- SnowGolem.aiStep posts snow placement.
- HarvestFarmland.tick posts crop placement.
- LeadItem.bindPlayerMobs and LeashFenceKnotEntity.interact post attachment.

Part of #9776.

Co-Authored-By: Codex (gpt-6.1-terra) <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 11:07:40 +01:00
e3ca63226c Casting and reeling a fishing rod and throwing an eye of ender make the vibrations vanilla's do
- FishingRodItem.use posts cast and retrieval vibrations using the stack's use effects.
- EnderEyeItem.use posts a projectile-shoot vibration.

Part of #9776.

Co-Authored-By: Codex (gpt-6.1-terra) <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 11:07:40 +01:00
77f00c606d A villager's held-up trade item never drops when it dies
ShowTradesToPlayer sets the main hand's drop chance to 0 when it shows
an offer (displayAsHeldItem) and back to 0.085 when it clears the hand
(clearHeldItem).

Co-Authored-By: Codex (gpt-6.1-terra) <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 11:07:25 +01:00
martipopsandClaude Opus 5.5 81bcb3950f Controllers: a slider's hold belongs to one slider on one screen
The hold shared the keyboard's slider_edit, so Start, a scroll, the mouse or a
Tab could leave some other slider held. The pad's own position also survived
Undo, so the next step jumped back near the undone value.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 05:06:34 -05:00
5751bbd65f Native hooks: PlayerInteractEvent.EntityInteractSpecific, the point a player right-clicks on an entity
Add the native hook `entity_interact_specific` (NeoForge's `PlayerInteractEvent.EntityInteractSpecific`): a right-click on an entity hears the point clicked, measured from the entity's position, before `native_interact_entity`; a claim swings the hand and ends the click. Server-side.

Co-Authored-By: DeepSeek Pro via OpenRouter <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 13:58:31 +04:00
Detimes 624ac54cdc Dispatch tests: stub horizon_client_core::text and horizon_protocol::packets::login
player_pre_login and player_profile name those paths in their hook signatures; the generated
dispatch code compiled in the test crate needs them, as the existing horizon_client stubs.
2026-11-06 09:56:23 +00:00
Horizon agentandClaude Opus 5.5 1c0fabcd7e Merge main into this branch
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 10:56:05 +01:00
aba1b1eca4 Interpolate whole-cell volumes four columns at a time; skip air runs at once
Part of #9996.

A chunk's noise volume is made of whole cells, and InterpolatedSampler
filled it one cell at a time: per cell eight corner loads and four Z
lerps, per column two X lerps and a bounds-checked index, then a scalar
loop of stores and additions (fill_cell alone was 6.4% of the Overworld
proto phase's instructions). For a volume aligned to its cells,
fill_whole_cells now walks the four columns of one Z row of a cell from
the bottom of the volume to the top. The X lerps at a level of corners
are computed once (they are the top of one cell and the bottom of the
next: the same lerps of the same samples), the four columns' repeated
additions run as four lanes, and four Ys of the four columns are stored
as four vectors. Each value comes from the same operations on the same
operands in the same order as vanilla's InterpolatedFunction fillCell;
only the order between columns changes. The top cell may be cut short
(the 16x114x16 volumes); a volume that is not aligned, or whose top
corners would clamp, keeps the general path.

fill_noise_inner spent about ten instructions on each air block above
the aquifer's skip height. A downward run of non-positive densities that
stays above it is now skipped in one step: nothing was written for those
blocks and note_skipped only clears a flag, so one call covers the run.

Exact: chunkgen_bench --snapshot files (terrain and decorated) are
byte-identical to main in 16 cases: the Overworld at seeds 12345, -1,
987654321 and -7, the Nether at seeds 12345 and 42, the End at seeds
12345 and 7, sizes 3 to 12. A new test compares sample_volume and the
new path with a transcription of vanilla's sampleWithBlockStep and
fillCell, on an input with NaNs, infinities, signed zeros and
subnormals, for 4x8, 8x4, 4x4 and 8x8 cells, and checks which volumes
the new path takes.

Proto-phase instructions (callgrind, 64 chunks, one thread):
Overworld 5.970 G -> 5.615 G (-5.9%), the Nether 2.868 G -> 2.853 G
(-0.5%).

A first draft by GLM-5.3 and DeepSeek V4.1 Flash, via pi, reviewed and
reworked (the air-run skip).

Co-Authored-By: pi coding agent (DeepSeek-V4.1-Flash) <[email protected]>
Co-Authored-By: pi coding agent (GLM-5.3) <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 10:56:01 +01:00
Detimes bce9527c43 player_client, server_player_state fragments: mod_api.rs is not a comment path
The stamp tool fills mod_api.rs's markers itself and refuses it in "comments".
2026-11-06 09:55:22 +00:00
Detimes 402534f462 run_command_as: run_guest_command_told takes the hub with ? (clippy::question_mark) 2026-11-06 09:55:15 +00:00
Detimes fb9ce454ed Build fix: one more entity_nbt test call of mob_to_nbt gets uuid_of
The same #10304 argument as in the earlier build fix; this call sits in a test only pr_ready's test run compiles.
2026-11-06 09:55:15 +00:00
Detimes f830672067 Merge remote-tracking branch 'upstream/main' into hooks-b-all
# Conflicts:
#	crates/horizon-client/src/game/raids.rs
2026-11-06 09:50:14 +00:00
Detimes 0df8f65eda Merge remote-tracking branch 'upstream/main' into perf-region-inflate-in-place
# Conflicts:
#	crates/horizon-world/src/compression.rs
2026-11-06 09:49:51 +00:00
Nikita Maximilian GrimmandGPT 6.1 Sol c667277fcd chore: merge the updated shader identity core
Preserve the launcher layer while carrying main and the replacement shader screen from the game PR.

Co-Authored-By: GPT 6.1 Sol <[email protected]>
2026-11-06 10:49:49 +01:00
GIGABAIT93andClaude Opus 5.5 10ec872cc1 docs/api/content.md: where a launcher puts content and what it checks
The install side of the Launch API, experimental: what a build loads (the
new `content`), the folder and the on/off file of each kind under
--game-dir, when a change applies, native mods needing a rebuild, what to do
with a .hpack, and the checks before installing as mod-index.md specifies
them (where it comes from, size and SHA-256, the minisign key, revoked[] and
installed copies, the manifest). Listed in docs/api/README.md. Part of #10562.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 11:49:14 +02:00
a2b395d91a A piglin always drops the item it admires, and one that isn't an ingot stops it despawning
Piglin.holdInOffHand equips the item with setItemSlotAndDropWhenKilled,
a guaranteed drop even without a player's kill, and makes the piglin
persistent unless the item is a gold ingot.

Co-Authored-By: Codex (gpt-6.1-terra) <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 10:46:42 +01:00
ae31791c33 A sleeping villager, fox or player isn't pushed by the mobs that walk into it
LivingEntity.push(Entity) does nothing while the entity sleeps, and
doPush reaches the other entity through it. The mob snapshot carries
sleeping (a villager in bed, Fox.isSleeping) and push_entities skips the
push. The sleeper's own tick still pushes the others.

Co-Authored-By: Codex (gpt-6.1-terra) <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 10:46:41 +01:00
Horizon agent 1fa86c919f Merge remote-tracking branch 'origin/main' into review-10557 2026-11-06 10:46:37 +01:00
makscoder b7dbeda41a A brush stops on a mob in the crosshair
BrushItem.onUseTick aims with getHitResultOnViewVector, so a mob between the
player and the block is the nearer hit and the use releases without a stroke.
tick_brush used the block-only pick and brushed through the mob.
2026-11-06 12:45:11 +03:00
GIGABAIT93andClaude Opus 5.5 2710305be4 --launch-api says what content this build loads
`content` in horizon-client's reply, from the constants the build checks
content against: content mods' manifest api lines (APIS without the removed
WebAssembly 0.1), native mods' hooks level and that they need a rebuild, the
shader schema and the line it draws, the resource and data pack formats, the
modpack format and the index schema. A launcher offers only what works. The
generated reference shows the hooks level as a placeholder, since it rises
with every hook. Part of #10562.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 11:44:04 +02:00
makscoder c1de12ab86 A brush or goat horn that uses itself up sends the interaction vibration
LivingEntity.stopUsingItem, reached when a use runs out or when the brush hits
no block, causes the ITEM_INTERACT_FINISH vibration. The brush and instrument
paths cleared the use directly instead, so only letting go of the button made a
sculk sensor hear them.
2026-11-06 12:42:48 +03:00
Ludovic Marie f4dcf8a2fb Merge pull request #10545 from horizon-rust/fix/revert-10416
🤖 Agent: main merged in (the old base had the mob_to_nbt test breakage, since fixed on main); still only the revert of #10416.
2026-11-06 10:42:19 +01:00
Nikita Maximilian GrimmandGPT 6.1 Sol 1295602deb fix(shaders): integrate the new settings screen
Merge main at 0873f24d2, retain its replacement shader screen, and route native selections and options through stable manifest IDs.

Co-Authored-By: GPT 6.1 Sol <[email protected]>
2026-11-06 10:40:49 +01:00
Florian ERNSTandCodex c87e5a6893 Omit the default amplifier from saved player effects
Co-Authored-By: Codex <[email protected]>
2026-11-06 10:39:48 +01:00
Ludovic Marie 0b2f2fda73 Merge pull request #10475 from Hiuoy/feat/player-tick-pre
🤖 Agent: the pre-tick hook swapped each guest's whole state into Game every tick even with no mod loaded; I pushed a guard so `as_guest` runs only when `mod_api::mods()` isn't empty. The fragment passes tests.test_stamp_native_hooks.
2026-11-06 10:39:30 +01:00
DetimesandClaude Opus 5.5 f416696038 cargo fmt: native_npcs before native_packets in game.rs
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 09:38:41 +00:00
Horizon agent ef64ea9d3e Merge remote-tracking branch 'origin/main' into fix/revert-10416 2026-11-06 10:38:23 +01:00
Florian ERNSTandCodex 54cdfe00fe Load entity boolean flags through NBT byte conversion
Co-Authored-By: Codex <[email protected]>
2026-11-06 10:37:37 +01:00
Detimes 03ded99473 Merge remote-tracking branch 'upstream/main' into feat-10404-older-clients-1b 2026-11-06 09:31:26 +00:00
Ludovic Marie 0873f24d20 Merge pull request #10525 from Web-mc1/main
🤖 Agent: verify failed because bake_bricolage.py imports fontTools before it reads its arguments, and CI has no fontTools. I pushed a change that checks the arguments first, so the new usage test passes without fontTools installed.
2026-11-06 10:30:01 +01:00
Ludovic Marie 823cf263ff Merge pull request #10510 from megabytesme/fix/windows-icon-required
🤖 Agent: #10398 asked that a local build in a folder with a space still builds, so I pushed a fallback for GNU windres when the volume has no 8.3 names: outside CI it warns and builds without the icon instead of failing. With the taskbar-icon commit, `cargo check --target x86_64-pc-windows-gnu` passes on the desktop for horizon-client and horizon-client-input.
2026-11-06 10:29:57 +01:00
Hiuoy e5d96bad14 Merge remote-tracking branch 'origin/main' into feat/player-tick-pre 2026-11-06 13:29:54 +04:00
Ludovic Marie 296ac7fbf6 Merge pull request #10379 from voidsipp/fix/10102-prune-region-transactions
🤖 Agent: a failed region already restores its originals, so blocking the server start on any prune error took a server down for one corrupt chunk record. I pushed a change that starts it when no journal is left, with a supervisor test for it. Note that delete mode now writes regions one at a time with fsync, so a large prune takes longer than the old parallel rewrite. 169 horizon-server tests pass on Linux.
2026-11-06 10:29:53 +01:00
Hiuoy a6661aab7e Merge remote-tracking branch 'origin/main' into feat/9921-port-pack-events 2026-11-06 13:29:26 +04:00
Hiuoy 2c92825807 Merge remote-tracking branch 'origin/main' into feat/anvil-update 2026-11-06 13:29:23 +04:00
Riccardo Guerriero 16cdc3afd9 Merge remote-tracking branch 'origin/main' into perf/9996-surface 2026-11-06 10:27:43 +01:00
Riccardo Guerriero ed739050ae Merge remote-tracking branch 'origin/main' into fix/9759-golem-peaceful 2026-11-06 10:27:42 +01:00
Horizon agentandClaude Opus 5.5 58f31cdf25 Merge #10330 (main merged) into #10365
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 10:27:33 +01:00
Horizon agentandClaude Opus 5.5 2e779eed59 Merge main into #10330 (#10304 and #10544 landed)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 10:27:30 +01:00
Riccardo Guerriero 82ac14c4ad Merge remote-tracking branch 'origin/main' into perf/9996-sections 2026-11-06 10:27:23 +01:00
Riccardo Guerriero 1caa6c8931 Merge remote-tracking branch 'origin/main' into perf/9996-startup 2026-11-06 10:27:23 +01:00
Riccardo Guerriero ddb365e6b4 Merge remote-tracking branch 'origin/main' into perf/9996-light 2026-11-06 10:27:23 +01:00
martipopsandClaude Opus 5.5 8578927d70 Merge main into the gameplay guide
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 04:27:19 -05:00
Ludovic Marie 64f691f9f7 Merge pull request #10544 from martipops/fix/raids-mob-to-nbt
🤖 Agent: raids.rs now passes the same `persist.uuid_of` lookup persist.rs gives `mob_to_nbt`, and the four entity_nbt.rs test calls get `|_| None`, so main builds again after #10304 (verify green on this head).
2026-11-06 10:26:17 +01:00
Ludovic Marie 0167f90c29 Merge pull request #10347 from PzmuV1517/shaders/menu-3-translations
🤖 Agent: translations only; %s placeholders kept and every file parses. The languages you flagged as uncertain (ba_ru, cv_cu, sah_sah...) stay worth a native speaker's look.
2026-11-06 10:26:13 +01:00
Ludovic Marie 28d84f2078 Merge pull request #10346 from PzmuV1517/shaders/menu-2-drop-old-pages
🤖 Agent: removes the old pack pages, their two slider kinds and four lang keys in all 87 files; nothing else read option_label_within, checked with clippy -D warnings on the stack over current main.
2026-11-06 10:26:09 +01:00
Ludovic Marie 5b16680739 Merge pull request #10337 from PzmuV1517/shaders/compat-6-paste-flat-matrix-offset
🤖 Agent: The non-constant texel offset becomes offset / textureSize(level 0) added to the coordinate: right for the base level, but at a sampled lower mip the shift is in base-level texels, not that level's (GL moves by texels of the level read). Fine for Arc's FXAA at level 0; worth a note if a pack hits it with textureLodOffset.
2026-11-06 10:26:05 +01:00
Ludovic Marie 086633c199 Merge pull request #10336 from PzmuV1517/shaders/compat-5-iris-macros
🤖 Agent: CAT_* macros come from biomes::CATEGORIES, so they pick up #10499's CAT_MOUNTAIN 17 and CAT_UNDERGROUND 18 with no second table to keep in sync; your test comparing every macro with constants() covers that.
2026-11-06 10:26:00 +01:00
Ludovic Marie fb75cde631 Merge pull request #10335 from PzmuV1517/shaders/compat-4-glsl-leniency
🤖 Agent: The constant evaluator now refuses a non-finite F32 fold with Overflow, and only GLSL function bodies turn that into a run-time expression, so WGSL input still rejects 1.0/0.0 at compile time. Vendored naga's own 133 lib tests pass with the chain.
2026-11-06 10:25:56 +01:00
Ludovic Marie 869d1aa812 Merge pull request #10334 from PzmuV1517/shaders/compat-3-integer-samplers
🤖 Agent: The integer-sampler helpers texelFetch the texel under the point with the coordinate clamped to the edge, which is what GL's nearest read of an integer texture gives; a bias argument is accepted and ignored, as GL does for these. Stack 3/6, checked with the rest of the chain on current main (clippy -D warnings, horizon-shaders and render tests).
2026-11-06 10:25:52 +01:00
2af85dcc84 A mob's damaging fall plays the fall sound of the block it lands on
LivingEntity.playBlockFallSound: the block 0.2 below the feet, at half
its volume and three quarters of its pitch, not for a silent mob or air.
Most mobs play it after their own fall sound, horses and llamas after
the damage, and a llama's damaging fall under six blocks plays it alone.

Co-Authored-By: Codex (gpt-6.1-terra) <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 10:24:38 +01:00
DUzzL 6285cca37c A thrown trident hurts other players on a served world (ThrownTrident.onHitEntity) 2026-11-06 10:19:18 +01:00
Horizon agentandClaude Opus 5.5 44a12076d5 Swap a guest in for player_tick_pre only when a native mod is loaded
as_guest swaps a player's whole state into Game: running it for every
player every tick with no mod to call cost the server for nothing.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 10:17:18 +01:00
DetimesandClaude Opus 5.5 fd2ec97a17 Native hooks: chunk_generate and lod_column reach mods through a callback table
horizon-world cannot call horizon-client's dispatch, so the two generation hooks
call a NativeGenerateHooks table that entry.rs installs (as native_load_hooks
does for the loot hooks). mod_api re-exports NativeGenerate, NativeProtoChunk
and NativeLodColumn for the generated events. Without the table, and without
mods, both hooks do nothing.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 09:16:06 +00:00
DetimesandClaude Opus 5.5 9b08e99433 Native hooks: mod_api contracts for the 16 new hook fragments, dispatch calls filled
Since #6253 the mod API generator needs a mod_api entry for every pending hook
fragment. Cancel flags get {cancel, out: cancel}, menu_input_closed collects its
slots, every other hook listens. The placeholder bodies are filled by
`cargo run -p xtask -- mod-api`.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 09:16:06 +00:00
Skr1zzzz 079d82f968 Text displays: keep their glyphs, and skip the ones whose text is off screen
Every frame each text display built a batch, emitted its styled glyphs and
transformed every vertex, and a display of width or height 0 (what plugins
summon for holograms) is noCulling, kept to the distance rule because its
transformation can put the text anywhere: 200 of them cost ~0.3 ms a frame
with none on screen (#4664).

Each display's local-space text quads are now kept, rebuilt when the text,
its style, opacity, background, shadow, see-through, the font epoch or the
GUI atlas revision change (obfuscated text every frame), and only
transformed per frame. A noCulling display is skipped when the box of its
quads, through this frame's full matrix, misses the frustum: it can't draw a
pixel. Displays with a size keep vanilla's box test; the view range stays.
2026-11-06 12:13:04 +03:00
Detimes a93e07da89 Merge branch 'b-bundle-18' into hooks-b-all 2026-11-06 09:12:35 +00:00
Detimes 37dfe7d880 Merge branch 'b-bundle-17' into hooks-b-all
# Conflicts:
#	crates/horizon-client/src/game/native_hooks.rs
#	crates/horizon-client/src/mod_api.rs
#	docs/native-mods.md
2026-11-06 09:12:34 +00:00
Detimes 58ba651c7b Merge branch 'b-bundle-16' into hooks-b-all
# Conflicts:
#	docs/native-mods.md
2026-11-06 09:12:15 +00:00
Detimes 950cd1058f Merge branch 'b-bundle-15' into hooks-b-all
# Conflicts:
#	crates/horizon-client/src/game/native_hooks.rs
#	docs/native-mods.md
2026-11-06 09:12:08 +00:00
Detimes 93ff841913 Merge branch 'b-bundle-14' into hooks-b-all
# Conflicts:
#	crates/horizon-client/src/game/commands/world_cmds.rs
#	crates/horizon-client/src/game/native_hooks.rs
#	docs/native-mods.md
2026-11-06 09:11:56 +00:00
Arslan ArmanulyandClaude Opus 5.5 6199a21cbe Merge origin/main into shaderpacks/same-size-buffer-declarations
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 14:11:26 +05:00
Detimes 4cd44b1bfa Merge branch 'b-bundle-13' into hooks-b-all
# Conflicts:
#	crates/horizon-client/src/game.rs
#	docs/native-mods.md
2026-11-06 09:11:11 +00:00
Detimes 3dc1eac9a1 Merge branch 'b-bundle-12' into hooks-b-all
# Conflicts:
#	crates/horizon-client/src/game/native_hooks.rs
#	crates/horizon-client/src/lan/hub.rs
#	docs/native-mods.md
2026-11-06 09:10:59 +00:00
Detimes 1065005412 Merge branch 'b-bundle-11' into hooks-b-all
# Conflicts:
#	crates/horizon-client/src/game.rs
#	crates/horizon-client/src/game/native_hooks.rs
#	docs/native-mods.md
2026-11-06 09:10:36 +00:00
PzmuV1517 f4146eea7c Shader packs: rustfmt the pack loading call 2026-11-06 11:10:17 +02:00
Detimes 71e2f6b0f5 Merge branch 'b-bundle-10' into hooks-b-all 2026-11-06 09:10:07 +00:00
Detimes 702055471b Build fix: mob_to_nbt's uuid_of argument in raids.rs and three entity_nbt tests
#10304 added the argument while these calls were merged; upstream/main does not compile without it.
2026-11-06 09:10:01 +00:00
Hiuoy 503727b594 Merge remote-tracking branch 'origin/main' into feat/anvil-update
# Conflicts:
#	crates/horizon-client/tests/mod_api_hooks.rs
2026-11-06 13:08:39 +04:00
Hiuoy 9f4b5e0a80 Merge remote-tracking branch 'origin/main' into feat/9921-port-pack-events
# Conflicts:
#	crates/horizon-port-pack/src/lib.rs
2026-11-06 13:08:29 +04:00
Hiuoy 41eec6978b Merge remote-tracking branch 'origin/main' into feat/player-tick-pre
# Conflicts:
#	crates/horizon-client/src/game/native_hooks.rs
2026-11-06 13:07:52 +04:00
Horizon agentandClaude Opus 5.5 d9b7730a6f Warden anger test: a re-angered suspect keeps its saved anger, as increaseAnger does
min(150, 35) + 20 = 55, then one anger tick: 54. The test pinned the dropped
anger the earlier port left.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 10:05:08 +01:00
rison1337andCodex GPT-6 69d388deb0 Reuse completed armor-trim preparation between frames
Keep the completed ordered request list for the current atlas so an
unchanged overflow does not regenerate and upload every trim each frame.
Preserve final LRU residents, retry failures, and invalidate the cache
when equipment, atlas contents or the graphics device changes.

Fixes #10539

Co-Authored-By: Codex GPT-6 <[email protected]>
2026-11-06 14:01:59 +05:00
Detimes bf7e616ebf hooks-all: integration fixes for the merged hook branches
- NativeEffect (player_effects) and NativeInventoryKind (set_inventory) are pub and re-exported in
  mod_api: the generated mod API refused both fragments.
- native_teleport_player's cross-level teleports fill LevelTeleport's new cause (hooks-move) with
  "unknown"; player_teleport's docs name that case.
- lan_vehicles: the vehicle_moved_v2 call closure is FnMut.
- cargo fmt (game.rs mod lines in order).
2026-11-06 09:00:30 +00:00
Detimes 8887870ec3 Build fix: mob_to_nbt's uuid_of argument in raids.rs and three entity_nbt tests
#10304 added the argument while these calls were merged; upstream/main does not compile without it.
2026-11-06 08:58:30 +00:00
martipopsandClaude Opus 5.5 88cddcec82 Tests: pass mob_to_nbt its UUID lookup in entity_nbt's tests too
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 03:55:48 -05:00
Alysia Zappavigna bc6b668820 Format the pickable projectile tests 2026-11-06 19:54:27 +11:00
Detimes 21d7853cc9 Merge branch 'hooks-pread' into hooks-all
# Conflicts:
#	crates/horizon-client/src/game.rs
#	crates/horizon-client/src/game/native_server.rs
#	docs/native-mods.md
2026-11-06 08:51:10 +00:00
Detimes b2ec69a247 Merge branch 'hooks-vehicle' into hooks-all
# Conflicts:
#	crates/horizon-client/src/game.rs
#	crates/horizon-client/src/game/native_hooks.rs
#	docs/native-mods.md
2026-11-06 08:51:09 +00:00
Detimes 23bb236cfc Merge branch 'hooks-pstate' into hooks-all
# Conflicts:
#	crates/horizon-client/src/game.rs
#	docs/native-mods.md
2026-11-06 08:51:08 +00:00
Detimes af12d6d443 Merge branch 'hooks-login' into hooks-all
# Conflicts:
#	crates/horizon-client/src/game.rs
#	docs/native-mods.md
2026-11-06 08:51:07 +00:00
Detimes b74f04855d Merge branch 'hooks-inv' into hooks-all
# Conflicts:
#	crates/horizon-client/src/game/native_hooks.rs
#	docs/native-mods.md
2026-11-06 08:51:06 +00:00
Detimes 010504d207 Merge branch 'hooks-move' into hooks-all
# Conflicts:
#	crates/horizon-client/src/game/native_hooks.rs
#	docs/native-mods.md
2026-11-06 08:51:06 +00:00
Detimes 24876b536b Merge branch 'hooks-cmds' into hooks-all
# Conflicts:
#	crates/horizon-client/src/game/lan.rs
#	crates/horizon-client/src/game/native_hooks.rs
#	crates/horizon-client/src/game/native_server.rs
#	docs/native-mods.md
2026-11-06 08:51:04 +00:00
Horizon agentandClaude Opus 5.5 aad7542283 Keep the local no-alias fallback for GNU windres
#10398 asked that a local build in a folder with a space still builds: a
volume without 8.3 names gives windres no usable path, so outside CI
such a build warns and goes on without the icon, as before. CI and
every other case still fail without it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 09:50:46 +01:00
2a931e3e63 Peaceful drops a mob's player target and a golem's anger
On Peaceful, vanilla never lets a player be a mob's target
(`LivingEntity.canAttack` refuses a player there), so a mob that already
held one gives it up. A neutral mob also stops being angry and forgets
who hurt it (`NeutralMob.stopBeingAngry` calls `setLastHurtByMob(null)`).
Horizon dropped a player target only for creative and spectator players,
so an iron golem that had been angered kept attacking after the
difficulty was set to Peaceful.

Fixes #9759.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Fekd3xkgubE8VfXrALh2td
Co-Authored-By: Codex gpt-6.1-terra <[email protected]>
2026-11-06 09:50:35 +01:00
Riccardo GuerrieroandClaude Opus 5.5 0855321d41 A surface run above the minimum surface level stops there
Below the level the band takes its other variant, which may give another
block.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Fekd3xkgubE8VfXrALh2td
2026-11-06 09:50:34 +01:00
Riccardo GuerrieroandClaude Opus 5.5 db6a8b7f44 Write surface rule runs of one block at once
`build_surface_shared` applies the compiled rule at every solid block of a
column. In the Overworld most bands specialise to "the ore veins, then one
block" (deepslate), so 1.7M rule applications per 64 chunks produce ~1M block
writes, each walking the rule tree from the top and going through
`set_block`/`set_block_worldgen_column`.

A band variant that is a block rule, or ore veins followed by one, gives that
block at every position where no vein places one. The walk now writes the whole
run of solid blocks at once: the top y through the single-block path, the rest
through `TerrainChunk::fill_solid_run`, stopping at the first y where a vein
can place a block (each vein's first test, its density), at the band's bottom
and at the write bounds. Only blocks that are opaque for the stage's heightmaps
and carry no fluid take this path, so one update at the run's top leaves the
maps as a write per block does.

`CompiledRule::apply` on a sequence also tests its conditional children in the
loop instead of through a recursive call.

Instructions (callgrind, proto phase, 64 chunks, seed 12345): Overworld
6.49 G -> 6.03 G (-7.0%), surface stage 1.98 G -> 1.53 G (-23%), rule
applications 599 M -> 329 M; Nether 2.97 G -> 2.73 G (-8.1%).

A first draft by DeepSeek V4.1 Flash, via pi, reviewed and reworked.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 09:50:34 +01:00
Detimes 5699b862d6 Merge branch 'hooks-perm' into hooks-all
# Conflicts:
#	crates/horizon-client/src/game/native_hooks.rs
#	docs/native-mods.md
2026-11-06 08:49:52 +00:00
Detimes 90a7a7d47b Native mods: a compound mods keep on a player (player_persistent_data)
native_player_persistent_data(uuid) reads and
native_set_player_persistent_data(uuid, data) replaces the compound mods
keep on a player, as NeoForge's Entity.getPersistentData() and Bukkit's
PersistentDataContainer. It is saved in the player's file as
NeoForgeData, beside the keys Horizon keeps without modelling them, and
kept over a death and a level change; an empty compound takes the key
away. Nothing calls them without a mod.

Unproven: not yet compiled or probed live (build queue); the integration branch tests it.
2026-11-06 08:49:26 +00:00
Detimes 66b3059251 Merge branch 'hooks-10461-d' into hooks-all
# Conflicts:
#	docs/native-mods.md
2026-11-06 08:49:13 +00:00
Detimes 341896762f Merge branch 'hooks-10461-c' into hooks-all
# Conflicts:
#	crates/horizon-client/src/game/native_hooks.rs
#	docs/native-mods.md
2026-11-06 08:49:11 +00:00
Detimes 513b1fe43d Merge branch 'hooks-10461-b' into hooks-all
# Conflicts:
#	crates/horizon-client/src/entry.rs
#	crates/horizon-client/src/game/mods.rs
#	crates/horizon-client/src/game/native_hooks.rs
#	crates/horizon-client/src/menu.rs
#	crates/horizon-client/src/native_client_hooks.rs
#	docs/native-mods.md
2026-11-06 08:48:54 +00:00
Alysia Zappavigna be4ce23d67 Let projectiles strike ghast fireballs, as Projectile.isPickable does (fixes #10547) 2026-11-06 19:48:44 +11:00
Detimes b99b9e13f7 Native hooks: item_dropped_v2 can cancel a throw (unproven on a server)
native_item_dropped_v2(player, stack, cancel) is now cancellable, as
NeoForge's ItemTossEvent is: a canceled throw puts the stack back into
the player's inventory (Inventory.add, the held slot first); only what
does not fit is thrown, unheard, and native_item_dropped hears nothing.
The drop statistics and the swing still happen, as in NeoForge. Wanted
by the combat log, freeze and inventory-lock ports.

Unproven: this commit was neither compiled nor run (builds were stopped for
the integration test); `cargo run -p xtask -- mod-api` filled the dispatch.
The listen-only v2 drop hook before it was proven on a server.

Part of #10461.
2026-11-06 08:48:11 +00:00
Detimes c2ec210fea Merge branch 'hooks-10461-a' into hooks-all 2026-11-06 08:47:51 +00:00
Detimes 9edbea0a6d Native hooks: thrown projectiles at the world border (projectile_world_border)
native_projectile_world_border(dimension, entity, kind, pos, velocity,
collide) is heard each tick a thrown projectile (pearl, snowball, egg,
potion...) inside the world border would end its next step outside it,
before 26.3 stops it on the border like a wall (MC-310476). A mod may
change its position and velocity, or set `collide` false: that tick's ray
is then clipped without the border, so it flies through as before 26.3.
Arrows and tridents, which Horizon does not stop at the border, are not
heard. Static (no self), called from tick_projectiles.

Unproven: not compiled or run on a server on this branch (handed to the
hooks-all integration build).

Part of #10461.
2026-11-06 08:47:25 +00:00
Detimes c6ed7f75a0 Native hooks: farmland trampling, cancellable (trample)
native_trample(dimension, entity, kind, pos, fall_distance, cancel) is
heard when a player or mob that fell onto farmland is about to turn it to
dirt, once FarmlandBlock.fallOn's chance and checks have let it (NeoForge's
FarmlandTrampleEvent, Bukkit's PHYSICAL PlayerInteractEvent and
EntityInteractEvent). Canceling keeps the farmland and its crop; the fall
damage is taken either way. The landing rules ask through a new
BlockLandingWorld::trample_allowed (true by default), which the player's
and the mobs' landing worlds answer from a function the game sets once
(block_contact::TRAMPLE); there is no self.

Unproven: not compiled or run on a server on this branch (handed to the
hooks-all integration build).

Part of #10461.
2026-11-06 08:47:25 +00:00
Detimes 5caff4ecb4 Native hooks: where a dead player respawns (player_respawn_position)
native_player_respawn_position(player, name, dimension, pos, yaw, pitch)
is heard once per respawn, after the Respawn click and before the player
is placed (NeoForge's PlayerRespawnPositionEvent, Bukkit's
PlayerRespawnEvent.setRespawnLocation), with their respawn point: a bed
or anchor block's top, a forced point, else the world spawn. Left alone,
the point is resolved as before. A changed dimension or position places
the player exactly there, with no bed or anchor used and no free column
looked for; yaw and pitch set their look, which Hub::respawn now takes
instead of always 0, 0. An unknown dimension or a non-finite value
leaves that part unchanged.

Unproven: not compiled or run on a server on this branch (handed to the
hooks-all integration build).

Part of #10461.
2026-11-06 08:47:25 +00:00
Detimes 893eb8d636 Native hooks: a player's teleport before it reaches their client (player_teleport)
native_player_teleport(player, name, cause, from_dimension, from,
to_dimension, to, relative, cancel) is heard before a server player's
teleport is sent (Bukkit's PlayerTeleportEvent, NeoForge's
EntityTeleportEvent): commands (/tp within a level and into another),
ender pearls, chorus fruit, the spectator teleport menu, a dismount and
other server moves of the player. A mod may move `to`, cancel (the
player and their body stay where they are), or set the look and motion
bits of the position packet's relative flags, so a pearl can keep the
player's speed (0x1f8, what 26.2 sent); the position bits are ignored.

The call sites name the cause in a thread-local (lan::hub::teleport_cause)
that Hub::teleport and Hub::teleport_keeping_motion read; a teleport
without a named cause (respawns, portals, the End gateway, vehicles
carrying riders, waking up, refused moves) is not heard. Command and
spectator teleports into another level carry their cause in
LevelTeleport. The hub reaches the hook through a function the game sets
once (lan::hub::PLAYER_TELEPORT); there is no self.

Unproven: not compiled or run on a server on this branch (handed to the
hooks-all integration build).

Part of #10461.
2026-11-06 08:47:25 +00:00
Detimes 8e83a2c898 Native hooks: a player's move before it is applied (player_move)
native_player_move(player, name, from, to, yaw, pitch, on_ground,
cancel) is heard for each position or look packet that changed either
and passed the server's checks (moved too quickly, moved wrongly, the
anticheat), before Hub::position_change applies it (Bukkit's
PlayerMoveEvent). Canceling sends the player back to `from`, as a
refused move does; a changed `to` puts them there and their client gets
a teleport. Riders, sleepers and moves while a teleport waits are not
heard. The hub reaches the hook through a function the game sets once
(lan::hub::PLAYER_MOVE), as the damage hooks do; there is no self.

Unproven: not compiled or run on a server on this branch (handed to the
hooks-all integration build).

Part of #10461.
2026-11-06 08:47:25 +00:00
Detimes 0c8fb31992 Native mods: respawn a dead server player without the Respawn click (respawn_player)
The helper native_respawn_player(uuid) respawns a dead server player as
their client's PERFORM_RESPAWN does (Spigot's player.spigot().respawn()),
after the players' packets of the server tick, through the same
guest_respawn. Nothing calls it without a mod.

Unproven: not yet compiled or run on a server (builds stopped for the hooks-all integration).
2026-11-06 08:47:13 +00:00
Detimes d117673fe9 Native mods: let a server player fly in any game mode (set_flight)
The helper native_set_flight(uuid, allow, flying) lets a server player fly
in any game mode or not and sets whether they fly now, as Bukkit's
Player.setAllowFlight and setFlying: the guest keeps the mod's mayfly
(allow_flight) until their game mode changes or they respawn, their
abilities packet and the server's body carry it, and the flying check does
not kick them. Without a mod allow_flight stays false and every check is
the game mode's, as before.

Unproven: not yet compiled or run on a server (builds stopped for the hooks-all integration).
2026-11-06 08:47:13 +00:00
Detimes 2efd4b8346 Native mods: seat an entity on a vehicle, take it off, empty a vehicle (mount)
The helpers native_mount(entity, vehicle), native_dismount(entity) and
native_eject_passengers(vehicle) seat a server player or mob on a boat,
minecart or mob, take it off, or take every rider off, as /ride does with
its checks and native_entity_mount (Bukkit's addPassenger, leaveVehicle,
eject). /ride's mount checks move into ride_mount, which both use; the
command's answers are unchanged. Nothing calls the helpers without a mod.

Unproven: not yet compiled or run on a server (builds stopped for the hooks-all integration).
2026-11-06 08:47:12 +00:00
Detimes a8c67d77aa Native hooks: a ridden vehicle's move, cancellable and with its id, mounts included (vehicle_moved_v2)
native_vehicle_moved_v2(player, dimension, vehicle, kind, from, to, cancel)
is heard right after native_vehicle_moved, with the same state swapped in,
and also for a mob a server player rides. A canceled move puts the vehicle
back at from with its riders, a changed to puts it there
(native_teleport_vehicle), as Bukkit's VehicleMoveEvent with a teleport.
v1 keeps its signature, firing point and vehicles. Without mods nothing
moves the vehicle.

Unproven: not yet compiled or run on a server (builds stopped for the hooks-all integration).
2026-11-06 08:47:12 +00:00
Detimes 052443e2a5 Native mods: teleport a vehicle with its riders (teleport_vehicle)
The helper native_teleport_vehicle(dimension, id, pos) teleports a boat,
minecart or ridden mob of the level in place with its riders, as /tp does
a vehicle (Bukkit's Vehicle.teleport): the riders stay seated and a server
player riding it is told, as their client drives it. Only where nobody's
state is swapped in. Nothing calls it without a mod.

Unproven: not yet compiled or run on a server (builds stopped for the hooks-all integration).
2026-11-06 08:47:12 +00:00
Detimes 38a9866a1f Native mods: teleport a server player from any server hook (teleport_player)
The helper native_teleport_player(uuid, dimension, pos, yaw, pitch)
teleports a server player as /tp does (Bukkit's Player.teleport): at once
when they are the acting player or nobody's state is swapped in, else
after the players' packets of the server tick, so a port no longer runs
execute in .. run tp from the console, which waits for the next tick and
does nothing inside a player's hook. Into another level it is a command's
level teleport. Nothing calls it without a mod.

Unproven: not yet compiled or run on a server (builds stopped for the hooks-all integration).
2026-11-06 08:47:12 +00:00
Detimes aa8f5fc12a Native mods: send a player's command tree again (resend_commands)
The helper native_resend_commands(player) sends a player their command
tree again, with native_player_commands_sent, its v2 and the permission
nodes of the mods' commands asked anew, as Bukkit's Player.updateCommands:
after a permissions mod changed what they may use. The tree goes out in
the next server tick, with no player's state swapped in, so the helper
works from any server hook, the player's own included. Nothing calls it
without a mod.

Unproven: not yet built or run (handed to the hooks-all integration branch for the build and proof).
2026-11-06 08:47:05 +00:00
Detimes 329acc6c53 Native mods: the level one typed command runs at (player_command_level)
native_player_command_level(player, name, command, level) is heard in
Game::guest_command after native_player_command, before the server checks
the command: level comes in as the player's own and becomes the level this
one command runs at (GuestCommand.permission), so a permissions mod can
grant /gamemode to a player of level 0 or refuse a command to an operator,
as Paper's VanillaCommandWrapper does with minecraft.command nodes. A mod
command with a permission node falls back to this level. Unchanged, the
command runs at the player's level, as before.

Unproven: not yet built or run (handed to the hooks-all integration branch for the build and proof).
2026-11-06 08:47:05 +00:00
Detimes 0bd927b7c1 Native mods: a permission node on a mod's command (NativeCommand::permission)
NativeCommand::new(usage).permission(node) gives a mod's command a node, as
Bukkit's plugin.yml permission: does. The server then asks
native_has_permission(player, node, level), the command's level as the
default, both before it puts the command in a player's command tree and
before a player's typed command reaches native_perform_command, so a
permissions mod can grant it below that level or refuse it to an operator.
Without a permissions mod the level decides, as for a command without a
node; a dedicated server's console runs every registered command as before.

Unproven: not yet built or run (handed to the hooks-all integration branch for the build and proof).
2026-11-06 08:47:05 +00:00
Detimes 3cc7e3c822 Native mods: put a command the level hides back into a player's tree (player_commands_sent_v2)
native_player_commands_sent_v2(player, name, names, names_hidden) is heard
right after native_player_commands_sent, at the same times, with the root
commands the player's level hides (those a level 4 operator would get) in
names_hidden. A root a mod moves into names is sent with its subtree, as
for an operator of its level, so a command a permissions mod grants
completes and highlights in the player's chat box (Paper's
PlayerCommandSendEvent after a passed permission check). v1 keeps its
signature and firing point. Without mods nothing is granted and the tree
is unchanged.

Unproven: not yet built or run (handed to the hooks-all integration branch for the build and proof).
2026-11-06 08:47:04 +00:00
Detimes 25ac7a9841 Native mods: permission nodes, answered by a permissions mod (permission_check)
The hook native_permission_check(player, node, default_level, result) lets a
permissions mod (a LuckPerms port) say whether a player has a permission
node; the first mod to set result decides. The helper
native_has_permission(player, node, default_level) asks it, for every mod
and for the server, and falls back to the player's operator level against
default_level when no mod answers, as Bukkit's Permissible.hasPermission and
NeoForge's PermissionAPI.getPermission do. Without a permissions mod every
answer is the operator level, so nothing changes for unmodded servers.

Unproven: not yet built or run (handed to the hooks-all integration branch for the build and proof).
2026-11-06 08:47:04 +00:00
MegaBytesMe cfdca1dd6e fix: set Windows taskbar icon 2026-11-06 08:45:12 +00:00
DUzzL 379850ac5c Never build an exit portal for a spectator using a nether portal 2026-11-06 09:41:40 +01:00
Horizon agentandClaude Opus 5.5 e096a156a1 Check bake_bricolage's arguments before importing fontTools
CI has no fontTools: the import failed before main() could print the
usage, so the new test saw a traceback (verify failed).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 09:41:09 +01:00
DUzzL 99b969686e Stop a deflected potion at the breeze and use its own random 2026-11-06 09:41:05 +01:00
TheMrZZandCodex gpt-6.1-sol 4a20680924 Fix HappyGhast ambient sound position
Use the entity position for the special ambient caller and cover adult and baby natural events through the real Mob tick and recording route.

Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-11-06 09:40:23 +01:00
Horizon agentandClaude Opus 5.5 85a5195411 Revert "Natural spawning: a start's height ends at the column's first empty block" (#10416)
parity-main: natural_spawn first fails at f6a9444de (#10416), of 182 merges
since 66f3981f5. The spawn start heights it produces differ from vanilla 26.3
by up to tens of blocks (chunk/10 monster: vanilla y 99, Horizon 62), so
players would see mobs spawn at the wrong heights. Reverted until the change
passes the natural_spawn harness.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 09:38:57 +01:00
Detimes fe7635e545 Cargo.lock: the 12 packages of the new horizon-mod-deps entries (unbuilt)
Written by hand from a scratch resolution seeded with this lock: rusqlite,
libsqlite3-sys, fallible-iterator, fallible-streaming-iterator, hashlink,
vcpkg, yaml-rust2, arraydeque, gif, weezl, color_quant, snap. Existing
entries keep their versions; chrono gains serde and mysql_common gains chrono.
To confirm: cargo metadata --locked must not want to change it.
2026-11-06 08:38:14 +00:00
Detimes 71b36cca26 Native mods: a store menu the hub closes on its own gives the cursor back too (removed) (unbuilt) 2026-11-06 08:37:33 +00:00
DUzzL ac4045d83f Keep the worn-armor snapshot per LAN guest 2026-11-06 09:37:03 +01:00
Detimes ca623aae34 Native mods: another player's inventory in a server menu with vanilla click rules, live or on their file (native_open_inventory_menu) (unbuilt) 2026-11-06 08:35:30 +00:00
martipopsandClaude Opus 5.5 3aaa70563e Raids: pass mob_to_nbt its UUID lookup, so main compiles
#10304 gave mob_to_nbt a fifth argument after the raid observation's call to it
was written, and the two merged without a conflict.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 03:35:09 -05:00
Horizon agentandClaude Opus 5.5 eb649daff5 Merge main into #10506: keep the Enderman hurt teleport
Main added enderman_hurt_teleports after the block contact damage; this
branch drops that call's world argument. Both kept.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 09:34:58 +01:00
Horizon agentandClaude Opus 5.5 1716888f1c Merge main into #10493: keep both ai.rs test modules
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 09:34:32 +01:00
Detimes 8c7807e832 Native helpers: the name over a player's head (set_name_tag) (unbuilt) 2026-11-06 08:34:22 +00:00
DUzzL 98e1e2d67f Credit trident damage_dealt_absorbed to the throwing LAN guest 2026-11-06 09:33:17 +01:00
Detimes 296d765d78 Native helpers: a player's styled name in chat (set_display_name) (unbuilt) 2026-11-06 08:33:08 +00:00
Detimes ca660e8d1f Native mods: server menus with input slots, their contents, the click's cursor and the slots on close (native_open_menu_with_input, native_menu_input_closed) (unbuilt) 2026-11-06 08:32:15 +00:00
Detimes 450b6c8548 Native hooks: leave mod_api to the stamp step; cancel flags named cancel (unbuilt)
The bundle's fragments carry no mod_api entry: session A adds them in one
step (add_mod_api.py, which takes a cancel: &mut bool parameter for the
cancel contract). So interact_entity_v2's claimed and
right_click_block_v2's canceled are named cancel; they act as before.

Not built yet.
2026-11-06 08:32:11 +00:00
Detimes 4cf75b20d9 Native helpers: a player's styled Tab list name (set_list_name) (unbuilt) 2026-11-06 08:32:07 +00:00
Detimes 873480bddc Native hooks: damage with the players' UUIDs and the attacker (living_incoming_damage_v3) (unbuilt)
native_living_incoming_damage_v3(victim, victim_uuid, kind, attacker,
attacker_uuid, direct, damage_type, amount, cancel) fires everywhere v1
fires (health::HURT_SERVER), right after it with the amount v1 left and
the same cancel flag. It adds a player victim's UUID, the causing entity
where v1 leaves 0 (a mob's attacker, a player's melee, the synchronous
attack in progress as living_knockback_v2 falls back to), a player
attacker's UUID and the direct entity. Players are noted by entity id as
each guest's turn starts (game/native_damage.rs, only while a native mod
is loaded), since the hit carries ids only and no Game.

Not done here, as the plan asked: a &mut self host (the victim is
borrowed out of the game inside Mob/Player/Dragon hurtServer, behind the
HURT_SERVER fn pointer) and Bukkit's modifier breakdown (the reductions
run after the hook in horizon-world's living_hurt and Mob::hurt_server).

Sentinel (attacker matched within 500 ms), Wanted, HuskSync and
CombatLog (an id-to-player map each tick) need the names.

Not built yet.
2026-11-06 08:31:35 +00:00
Detimes 0072925aef Native mods: kick a player with a whole text as the reason (kick) (unbuilt)
native_kick(player, reason) disconnects a player with a Text reason in its
colours and translations (Adventure's Player.kick(Component)), through
Hub::kick as /kick does, so the log line and native_player_kicked follow.
It is queued and runs at the tick's end with the mod messages
(flush_native_net), so it works from any server hook, the kicked player's
own included; today a port sends a raw Disconnect and then runs /kick with
one plain line.

Part of #10461.
2026-11-06 08:31:11 +00:00
Detimes 342a45b57a Native helpers: entities shown to one player's client alone (client_entities) (unbuilt) 2026-11-06 08:31:04 +00:00
Detimes ca9437b79f Native hooks: resource pack answers with the pack's id, configuration included (resource_pack_status_v2) (unbuilt)
native_resource_pack_status_v2(player, name, pack, status) is an additive
version of native_resource_pack_status with the answered pack's id
(Bukkit's PlayerResourcePackStatusEvent.getID), so a mod's own pack's
answers are told apart from the server's. In play it is heard just before
v1, which keeps its signature and firing point. The answers to the
server's own pack given during configuration, which the connection thread
handled alone, are kept (lan::Configured::pack_answers) and heard once the
player has logged in, right after native_player_logged_in. The connection
answers the packets exactly as before.

Part of #10461.
2026-11-06 08:30:06 +00:00
Detimes 0c10886ec0 Native mods: horizon-mod-deps adds rusqlite, yaml-rust2, gif, snap and mysql/chrono (unbuilt)
rusqlite 0.41 (bundled + cache, no default features) reads SQLite files such
as a Bukkit plugin's .db; yaml-rust2 0.13 (no default features, safe Rust)
reads and writes config.yml; gif 0.14 and snap 1 are small and pure Rust;
mysql gains chrono for DATETIME columns. All optional, nothing builds them.
Cargo.lock not yet updated.
2026-11-06 08:29:39 +00:00
Horizon agentandClaude Opus 5.5 361766f621 Merge #10312 (with main) into #10372
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 09:29:35 +01:00
Detimes d7c1e71d45 Native mods: each server tick's time and spans, a shared span switch and mob costs a mod samples (native_server_tick_timed, native_spans, native_mob_cost_sampling, native_take_mob_costs) (unbuilt)
tick_server_spans calls native_server_tick_timed with the pass's wall and CPU time and merged spans (TickSpan, re-exported from spans::Span) before handing them to the report. Spans are switched through tick_profile::want_spans by who wants them (HORIZON_SPIKES, horizon-report, a mod), so a mod and a report no longer turn each other's spans off. tick_profile keeps a mod's mob cost sums apart from the report's (two bits, two maps), so both can sample at once; calibration stays the report's. Fragment server_tick_timed.
2026-11-06 08:29:28 +00:00
Detimes 36323e05ae Native mods: each player's play packets once a tick (native_server_packets, native_count_packets) (unbuilt)
While a mod turns counting on, the network threads keep each framed and read play packet's id and body size in the connection's Outbox (io_pool Conn::frame and Conn::read, behind one relaxed load of a static switch, capped at 65536 each way); the server tick's end hands them to native_server_packets per player and way. Off (the default) nothing is kept and the hook never fires. Fragment server_packets.
2026-11-06 08:29:28 +00:00
Detimes 8abf5298fc Native mods: hurt a mob or a player from an attacker (native_hurt_entity) (unbuilt)
native_hurt_entity(dimension, target, amount, damage_type, attacker)
hurts a mob or a player of the level as LivingEntity.hurtServer with
damageSources().mobAttack(attacker) does, the way /damage <target>
<amount> <type> by <attacker> goes: cmd_damage's hurt is moved unchanged
into damage_target, which both call. Returns whether it hurt; false for
another dimension, a target that is neither a mob nor a player of the
level, a missing attacker or a non-finite amount.

Sentinel's own damage (damage set, workaround damage, lightning weapon)
today runs a damage command by UUID.

Not built yet.
2026-11-06 08:29:25 +00:00
Detimes 7fbd43b6a9 Native mods: offer and take away a mod's own resource packs (resource_packs) (unbuilt)
native_send_resource_pack(player, id, url, sha1, prompt, required) sends
ClientboundResourcePackPushPacket with the mod's own pack id (Bukkit's
Player.addResourcePack), checked against the packet's limits (hash at most
40, URL at most 32767 characters); native_pop_resource_pack(player, id)
sends ClientboundResourcePackPopPacket for one pack or, with None, all
(removeResourcePack). Both go through native_send. A decline still kicks
only when the server's own pack is required, as vanilla's
handleResourcePackResponse does.

Part of #10461.
2026-11-06 08:29:23 +00:00
Detimes e5acbdde15 Native helpers: hide an entity from one player's client (hide_entity_from) (unbuilt) 2026-11-06 08:28:56 +00:00
Detimes b842df0899 Native hooks: a mob's target changing, cancellable (living_change_target) (unbuilt) 2026-11-06 08:28:56 +00:00
Detimes 7f91f765b2 Native hooks: the plugin channels a player registers, in configuration and play (player_channels) (unbuilt)
native_player_register_channels(player, name, channels, register) hears
the names a client registers (minecraft:register) or unregisters
(minecraft:unregister), NUL-separated as Bukkit's and Fabric's clients send
them; vanilla reads neither (Bukkit's PlayerRegisterChannelEvent). What a
client registered during configuration, which the connection thread
dropped, is kept (lan::Configured on Joined) and heard once right after
native_player_logged_in; a message in play is heard as it is handled, with
the player's state swapped in, and then goes on as before. The helper
native_player_channels(player) lists a guest's channels
(Player.getListeningPluginChannels). At most 128 names of at most 256
bytes are kept per client.

Part of #10461.
2026-11-06 08:28:35 +00:00
Horizon agentandClaude Opus 5.5 835f062e8e Merge main into #10312 (names.rs, scoreboard.rs)
names.rs: #10328 landed on main; its NameTagLayouts frame now wraps this
PR's frustum-culled collect_name_tags, both test sets kept.
scoreboard.rs: #10098 added the hide_gui argument to tab_list::visible;
the early visibility check here passes !self.ui.hide_gui too.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 09:28:22 +01:00
Detimes 33e0f27a8d Native hooks: a button's, lever's or plate's signal change (block_redstone) (unbuilt)
native_block_redstone(dimension, pos, old, new) is asked, as Bukkit's
BlockRedstoneEvent, when a button is pressed (hand or arrow) or let go by
its release tick, a lever is pulled, or a pressure plate's weight check
changes its signal. A mod may change new; one equal to old keeps the
component as it is, and a held button or plate is asked again at its
next scheduled check. The seam is LevelAccess::redstone_change in
horizon-world (default: leaves the signal), which GameLevel forwards on
an authoritative level. Wire, torches, diodes, observers and the other
sources are not heard yet.

CoreSMPEssentials part B holds its power rods' component on without the
2-tick re-set that leaks a 1-tick off pulse. Test (not run):
a_level_can_keep_a_lever_as_it_is.

Not built yet.
2026-11-06 08:28:19 +00:00
Detimes ce01d5ffeb Native mods: native_set_compass_target, kept over respawns and level changes (unbuilt)
Bukkit's Player.setCompassTarget: Hub.compass_targets by UUID while the player is on; default_spawn sends the target in place of the world spawn wherever it is sent (join, respawn, level change). Without a target the packet is unchanged. Helper-only fragment compass_target.
2026-11-06 08:28:07 +00:00
Detimes 1ffc88650d Native mods: native_boss_bar, boss bars for one player (unbuilt)
Bukkit's BossBar.addPlayer/removePlayer: a NativeBossBar (mod_api) by player UUID in BossState, offered to that viewer by boss_events_for, so publish_boss_bars sends Add/Update/Remove as for the world's bars. Unmodded servers keep an empty map. Helper-only fragment boss_bar.
2026-11-06 08:28:07 +00:00
Detimes d66f3e02ea Native mods: native_title, a title and subtitle for one player (unbuilt)
Adventure's showTitle and /title: SetTitlesAnimation, SetSubtitleText, SetTitleText through native_send. Helper-only fragment title.
2026-11-06 08:28:06 +00:00
Detimes ebb19e976e Native mods: native_send_particles_to, particles for one player alone (unbuilt)
Vanilla's ServerLevel.sendParticles(player, ...) and Bukkit's Player.spawnParticle: within 32 blocks, 512 with force (overrideLimiter). Replaces the hand-built LevelParticles (dust id hard-coded) of plugin ports; same options and checks as GameLevel::native_send_particles. Helper-only fragment send_particles_to.
2026-11-06 08:28:06 +00:00
Detimes 6ad344bec9 Native mods: native_play_sound_to, a sound for one player alone (unbuilt)
Bukkit's Player.playSound for plugin ports, which sent a raw ClientboundSoundPacket through native_send. Queued in game/native_player_fx.rs and sent at the tick's end with the hub back, so it works from any server hook; helper-only fragment play_sound_to.
2026-11-06 08:27:51 +00:00
Detimes fe2ef8529c Native mods: native_mob_attack, a mob's own attack on a mob or player (unbuilt)
Sentinel's native attacks (its default) call Mob.doHurtTarget for the
body. Horizon's do_hurt_target needs the goal's world, and its player is
whichever guest the mob sees, so the port injected a goal and wrote
LanHost.mob_players to bind the body to its target. native_mob_attack
runs do_hurt_target from any server hook with the target guest's view as
the mob's player (a new lan_guest_view, the single-guest form of
lan_guest_views), then hands the hits on that player to the guest and the
rest to handle_mob_events, as a mob's tick does. Helper only: unmodded
servers never call it. Part of #10461.
2026-11-06 08:27:49 +00:00
Horizon agentandClaude Opus 5.5 1f00e33453 Group slow-tick bursts by packet kind and name the top player
The Team review (kacpherek) on #10382: groups filled in arrival order
under a 32 cap hid a late UseItemOn burst behind other players' moves;
count > 1 dropped 69 players sending one 6-9 ms use each; and one slow
event hid the rest of its group. Groups are now one per packet kind (a
bounded set) with the time of their short events only (the slow ones
are noted on their own), and each names the player who spent the most
of it: "213 x UseItemOn under 10 ms each; most from Steve: 140 of
them, 700.0 ms".

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 09:26:36 +01:00
Detimes 21da267e5b Native mods: native_remove_entity takes projectiles and fishing bobbers (remove_projectiles) (unbuilt)
native_remove_entity(dimension, id) also removes projectiles (arrows,
tridents, snowballs, eggs, pearls, fireballs, wind charges, llama spit,
shulker bullets, firework rockets) as /kill does (kill_target), and a
server player's fishing bobber, as Entity.discard: the bobber of the
player in place, or outside a guest's turn any player's of the level
(remove_server_hook in fishing.rs). Displays, markers and interactions go
as before. Sentinel's arrow cleanup and CoreSMPEssentials part B's power
rods need it.

Not built yet.
2026-11-06 08:26:32 +00:00
Detimes 182345dc5e Native mods: mob navigation and look helpers for NPC ports (unbuilt)
Citizens' Navigator and Sentinel walk their bodies to points and
entities, stop them, ask whether they still walk and aim their heads.
Horizon offered this only to a goal (Mob::nav_move_to with the goal's
world), so ports injected a goal that read intents from a global map, or
moved bodies in straight /tp steps through walls. native_mob_navigate,
native_mob_follow, native_mob_stop, native_mob_navigating and
native_mob_look_at call the mob's own navigation and look control
(PathNavigation.moveTo, stop, isInProgress, LookControl.setLookAt) from
any server hook, for a mob of any loaded level, with the world its goals
see. Helpers only: unmodded servers never call them. Part of #10461.
2026-11-06 08:26:27 +00:00
Detimes a59c7468d1 Native mods: maps a server mod draws, kept by the world (native_new_map, native_paint_map) (unbuilt) 2026-11-06 08:26:00 +00:00
Detimes 07acadab21 Native mods: native_set_entity_rotation for mobs and mannequins (unbuilt)
native_move_entity turns only the entities Horizon keeps without
simulating them, and native_teleport_mob takes no rotation, so NPC ports
turned their bodies with execute .. rotate or tp .. facing every few
ticks. The helper is /rotate (Entity.forceSetRotation) for an entity of
any loaded level, with an optional head yaw (setYHeadRot) for a mob.
Helper only: unmodded servers never call it. Part of #10461.
2026-11-06 08:25:28 +00:00
Detimes 4855ac0767 Native hooks: the entity data one viewer is sent, changeable for them alone (entity_data_to) (unbuilt)
native_entity_data_to(viewer, entity, values) is a typed shortcut of
native_server_packet for ClientboundSetEntityDataPacket: the entries one
guest is about to get for one entity, changeable for that viewer alone, so
a mod can show one player glowing to one viewer without touching the
server's own flags (TAB's and Essentials' per-viewer glow, which resend
their own packets on a timer today). Heard just before native_server_packet
for the same packet. Switched on by native_watch_entity_data, which decodes
that packet only. EntityDataValue is horizon_protocol's entity_data::Value.

Part of #10461.
2026-11-06 08:25:27 +00:00
Detimes aad1954499 Native hooks: a server player's bobber on a block (fishing_hook_landed) (unbuilt)
native_fishing_hook_landed(player, hook, dimension, block, face) hears a
server player's flying fishing bobber meeting a block on its way, and
each tick it lies on one (on the ground as the tick began or ends: the
block under its centre, its top), as Bukkit's ProjectileHitEvent for a
FishHook. It is heard at the end of FishingHook.tick with the bobber back
in place, so a mod can remove it there. hook_block_hit keeps its result;
a sibling hook_block_hit_at also gives the block and face.

CoreSMPEssentials part B starts its power rods from the plate, button or
lever the bobber lands on; native_projectile_impact leaves bobbers out.

Not built yet.
2026-11-06 08:25:18 +00:00
Detimes e55c423833 Native mods: what each open server level holds (native_level_stats, native_level_entities, native_level_block_entities) (unbuilt)
NativeLevelStats counts a level's loaded and forced chunks, block entities and ticking ones (BlockEntities::ticking_len), and its entities (players included) and block entities by type. The entity list reuses the selector walk (level_candidates, now pub(in crate::game)) with a parked level swapped in as /execute in does; block entity types come from the block (lan::chunks::block_entity_type), which the enum's variants cannot tell apart, so this replaces the planned BlockEntity::type_id(). Fragment level_stats.
2026-11-06 08:25:07 +00:00
Detimes db26ac1679 Native mods: native_entity_by_uuid and native_merge_entity_nbt for any loaded level (unbuilt)
A stage plugin keeps its actors by UUID and moves, changes or removes them
in whatever level they are in; Horizon's entity handles reach the level in
place only, so ports ran tp/data merge entity by UUID on the console.
native_entity_by_uuid finds the entity (players included) in any loaded
level and returns its dimension, id and type; native_merge_entity_nbt is
native_entity_merge_nbt for a named loaded level. Helpers only: unmodded
servers never call them. Part of #10461.
2026-11-06 08:24:56 +00:00
Detimes 9bf6fb8323 Native mods: an item stack from SNBT (native_item_from_snbt, ItemStack::from_nbt, mod_api::parse_snbt) (unbuilt) 2026-11-06 08:24:47 +00:00
Detimes 4b1319096e Native hooks: a mod's terrain in the far terrain (lod_column) (unbuilt)
native_lod_column(dimension, column, seed, flat, column_out) is asked
for each column GeneratorWorker::lod_grid, surface_sample and
surface_column sample. A mod whose native_chunk_generate builds that
chunk answers the column's blocks (and its biome) with
column_out.replace, and the far terrain shows them, with the top, the
water over it and the spans derived as vanilla's sample derives them,
instead of vanilla's noise until the chunk generates.

Vanilla's far sample is computed first and only replaced when a mod
answers: without a mod, one empty call per column and the same columns
(#105).
2026-11-06 08:24:15 +00:00
Detimes 6c3dd1247f Native hooks: a player's fishing rod with its hand (player_fish_v2) (unbuilt)
native_player_fish_v2(player, name, state, hand, cancel) is
native_player_fish with the rod's hand: the hand the cast or reel-in uses
(Bukkit's PlayerFishEvent.getHand), and for a bite the hand holding the
rod, main hand first as FishingHook.shouldStopFishing looks. Asked right
after the unchanged v1 hook with the same cancel flag. CoreSMPEssentials
part B sees off-hand stasis and power rod casts.

Not built yet.
2026-11-06 08:24:02 +00:00
Horizon agentandClaude Opus 5.5 9267013546 rustfmt the bullet Dir/Steps test
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 09:23:57 +01:00
Horizon agentandClaude Opus 5.5 e95b6a26c5 Merge #10330 (main call site, rustfmt) into #10365
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 09:23:57 +01:00
Detimes f4ccc08720 Native hooks: every play packet a player is sent, changeable and cancellable (server_packet) (unbuilt)
native_server_packet(player, packet, cancel) hears each play packet the
server is about to send one guest, decoded, a broadcast once for each
player, where PlayerSender::send hands it over (ProtocolLib's and
PacketEvents' outgoing listeners; NeoForge and Bukkit have no event).
A changed packet is encoded again, a canceled one is not sent. Nothing is
decoded or copied until a mod calls the helper native_watch_server_packets:
until then each packet costs one relaxed atomic load. The hook is a free
function the game sets once (lan::conn::SERVER_PACKET), as player_move is.

Part of #10461.
2026-11-06 08:23:51 +00:00
Detimes 91a7c31b12 Native mods: native_spawn_mob and native_summon reach any loaded level (unbuilt)
Both helpers were refused for any level but the one in place, so a mod
spawning from native_server_tick (where no level is defined) or into
the Nether from the Overworld's tick could not, and Stage-style ports ran
`execute in <dimension> run summon` instead. The named level is now put
in place for the call and put back afterwards, through the same
enter_command_level that `execute in` uses (npc_in_level in the new
native_npcs.rs). A level not loaded is still refused. Unmodded servers
never call these helpers, so nothing changes for them. Part of #10461.
2026-11-06 08:23:46 +00:00
Kacper d3f799f5d5 Merge pull request #10368 from TheMrZZ/fix/h331-arrow-hit-category
Play ordinary arrow hits in the neutral sound category
2026-11-06 09:23:40 +01:00
Detimes 14e39641c2 Native hooks: a right click on a block with the hand (right_click_block_v2) (unbuilt)
native_right_click_block_v2(pos, face, hand, canceled) is
native_right_click_block with the hand that clicks (NeoForge's
PlayerInteractEvent.RightClickBlock.getHand), asked right after the
unchanged v1 hook with the same flag, once per hand tried. A cancel does
what a v1 cancel does. CoreWG's guard refuses an off-hand flint and steel
or bone meal; today it sees the main hand only.

Not built yet.
2026-11-06 08:23:26 +00:00
Horizon agentandClaude Opus 5.5 0a1a3c6ffe Merge #10502 into #10501 (both edit pack_post.rs)
Keeps both test modules (mixed_size_tests, undrawn_terrain_tests) and
has_terrain with #10502's doc and undrawn flag after sizes_differ.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 09:23:24 +01:00
Horizon agentandClaude Opus 5.5 cfab7464ed Merge #10304 (main call site fixed) into #10330
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 09:23:11 +01:00
Horizon agentandClaude Opus 5.5 597adf33bd Pass no UUID map at the mob_to_nbt call main added since
c843c1aa0 (the loot-pickup save test) calls mob_to_nbt with the old four
arguments; the bridge's fifth, uuid_of, is |_| None there like the other tests.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 09:23:08 +01:00
Detimes ff07b57b27 Native mods: the server's tick times and ticks a second (native_tick_times, native_tps) (unbuilt)
native_tick_times() returns the last 100 tick durations at vanilla's getTickTimesNanos indices (tick n at n % 100); native_tps() the ticks a second over one, five and fifteen minutes (Bukkit's getTPS), measured between recorded tick ends in FixedStep::record. Helpers only, fragment tick_times.
2026-11-06 08:22:55 +00:00
Detimes 9553a05bb8 Native hooks: a right click on an entity with the hand (interact_entity_v2) (unbuilt)
native_interact_entity_v2(entity, kind, hand, stack, claimed) is
native_interact_entity with the hand that clicks (NeoForge's
PlayerInteractEvent.EntityInteract.getHand), asked right after the
unchanged v1 hook with the same claimed flag: a click v1 claimed is not
asked again. Once per hand vanilla tries, so a mod claiming the main hand
hears one event per click (Citizens' click events and commandtrait
hands, CoreWG, CoreSMPEssentials part B).

Not built yet.
2026-11-06 08:22:47 +00:00
martipopsandClaude Opus 5.5 9d141f2e48 Merge main into the gameplay guide
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 03:22:32 -05:00
Detimes 6847ec0b7b Native hooks: a chunk's terrain from a mod (chunk_generate) (unbuilt)
native_chunk_generate(dimension, chunk, seed, flat, chunk_out,
generate) is asked on the generation workers for every chunk, after its
biomes and structure starts and before the noise (Bukkit's
ChunkGenerator.generateNoise). A mod sets the chunk's blocks through
NativeProtoChunk::set_block and answers NativeGenerate::Replaced with
the shouldGenerate* flags: vanilla then runs only the surface rule, the
carvers (on a superflat level too), the structures and the features it
asks for. set_biomes and clear_structures work with either answer.

Left at Vanilla, nothing changes: the hook is one empty call per chunk
and every vanilla step runs as before. Replaces the cancellable HEAD on
VanillaTerrain::proto that generated the vanilla chunk and overwrote it,
and the coregen_glue module (#104).
2026-11-06 08:22:18 +00:00
Kacper 918767ac08 Merge pull request #10286 from Ragbie/fix/dragon-damage-command
/damage hurts the Ender Dragon and End crystals as vanilla does
2026-11-06 09:21:37 +01:00
Kacper 089a7c1242 Merge pull request #10259 from Ragbie/fix/port-pack-old-layout
Port pack: a mod made for 1.20 is not carried or counted as come over
2026-11-06 09:20:28 +01:00
Kacper 6cfb512e1e Merge pull request #10311 from rison1337/perf/cull-offscreen-blob-shadows
Skip block scans for off-screen entity shadows
2026-11-06 09:20:12 +01:00
Kacper 6f6042da38 Merge pull request #10474 from martipops/fix/pad-hid-layout
Controllers: read Bluetooth Xbox pads from their layout, on any firmware
2026-11-06 09:20:06 +01:00
Kacper 345a66c710 Merge pull request #10014 from Hiuoy/feat/client-logging-hooks
Native hooks: ClientPlayerNetworkEvent.LoggingIn / .LoggingOut (client_player_network_logging_in, client_disconnecting's label)
2026-11-06 09:19:55 +01:00
Kacper 2cc12be695 Merge pull request #10295 from foudretbr/fix/launcher-native-cache-busy
launcher: retry native cache fixture exec on ETXTBSY and copy via temp rename
2026-11-06 09:19:49 +01:00
Kacper 8126b95fb6 Merge pull request #10264 from ilguerro/perf/9996-aquifer
Aquifers keep their 12 nearby cells unpacked and reuse 8 of them a cell down
2026-11-06 09:19:43 +01:00
Kacper a36ce4ef76 Merge pull request #10495 from voidsipp/fix/boat-second-passenger-authority
Use the first passenger for remote boat control
2026-11-06 09:19:36 +01:00
Kacper d53d7bf02a Merge pull request #10087 from yahya-nusseibeh/fix/port-pack-capped-reads
port-pack: cap every jar and pack entry it inflates (launcher OOM from a mod search)
2026-11-06 09:19:27 +01:00
Kacper e42f12db85 Merge pull request #10232 from KBroneZ/claude/fix-10162-pack-templates
Read structure templates from data packs
2026-11-06 09:19:18 +01:00
Detimes a58cb67f76 Native world seed read helper (native_world_seed) (unbuilt)
The world's seed as /seed prints it (Bukkit's World.getSeed), for a
generator mod's preview that must draw the terrain the chunks get.
None off a server, where only a hash of the seed is known. No
behaviour change. Part of the world generation bundle (#98).
2026-11-06 08:19:15 +00:00
Kacper 988f7d1ade Merge pull request #10362 from CocofireHD/fix/10273-reoccuring-freezes-on-diamondfire-server-mcd
Multiplayer: a guest server's resent columns keep the packet's light
2026-11-06 09:19:12 +01:00
Kacper 8e3cade985 Merge pull request #10307 from Skr1zzzz/fix-9190
Friends: Cancel and Escape go back to the screen that opened it
2026-11-06 09:19:07 +01:00
Kacper 8ca4e1d985 Merge pull request #9975 from Hiuoy/feat/client-tick-hooks
Native hooks: ClientTickEvent.Pre / .Post (client_tick_pre, client_tick_post)
2026-11-06 09:19:01 +01:00
Kacper b5c12d0f76 Merge pull request #10185 from degepie/fix/pack-lang-lenient-merge
Resource packs: read a lang file leniently when merging the layers
2026-11-06 09:18:54 +01:00
Kacper 2251cfb716 Merge pull request #10209 from ilguerro/perf/9997-climate-search
Climate searches pass distances along and stop a box's sum once it is too far
2026-11-06 09:18:49 +01:00
Kacper e5b2d2ea71 Merge pull request #10320 from TheMrZZ/fix/squid-damage-result
Preserve inherited squid damage acceptance result
2026-11-06 09:18:42 +01:00
Kacper c1a7b0d35e Merge pull request #10484 from voidsipp/fix/10445-leashed-happy-ghast
Match Happy Ghast leash movement and distances
2026-11-06 09:18:30 +01:00
Kacper b502fe41ac Merge pull request #9973 from MSedgeMC/fix/trample-hook
Landing: a trample is cancellable, so a claims mod can stop it
2026-11-06 09:18:24 +01:00
Kacper a8c5f833e4 Merge pull request #9924 from Hiuoy/feat/9921-hook-neoforge-labels
Native hook catalog: NeoForge labels for 39 hooks
2026-11-06 09:18:18 +01:00
Kacper 8ca4e05005 Merge pull request #10333 from PzmuV1517/shaders/compat-2-constant-array-crash
Shader packs: a global constant reads an element of a constant array (Ebin no longer crashes)
2026-11-06 09:18:08 +01:00
Kacper adcc59b920 Merge pull request #10332 from PzmuV1517/shaders/compat-1-real-packs-summary
Shader packs: real_packs keeps going past a panicking pack and sums up each pack
2026-11-06 09:18:02 +01:00
Kacper f42833e146 Merge pull request #10489 from TheMrZZ/fix/noai-dolphin-air-reset
Restore NoAI Dolphin air after its superclass tick
2026-11-06 09:17:55 +01:00
Kacper 1bd640668a Merge pull request #10304 from MSedgeMC/fix/uuid-bridge
Entity saves: a UUID bridge for references to a mob
2026-11-06 09:17:49 +01:00
Kacper cc5f7ff239 Merge pull request #10235 from MSedgeMC/fix/armor-stand-light-probe
Armour stands: probe the marker's own box and every other stand's eye, at 15 while burning
2026-11-06 09:17:38 +01:00
Kacper f7a5a3809f Merge pull request #10200 from Hiuoy/feat/break-speed
Native hooks: PlayerEvent.BreakSpeed, how fast a player digs a block
2026-11-06 09:17:32 +01:00
Kacper 1faeccc439 Merge pull request #10293 from IanWorley/fix/tests-on-apple-silicon
Tests: the drop-motion and install.sh signature tests pass on Apple silicon
2026-11-06 09:17:20 +01:00
Kacper 57512281ba Merge pull request #10266 from TheMrZZ/fix/ominous-warning-sound-source
Use neutral source for ominous item warning
2026-11-06 09:17:10 +01:00
Kacper ebafaf775a Merge pull request #10478 from TheMrZZ/fix/spellcaster-spell-ticks
Bind Evoker and Illusioner NBT to their live spell counter
2026-11-06 09:17:04 +01:00
Kacper 2f890f7df9 Merge pull request #10524 from TheMrZZ/fix/mob-local-sounds
Keep local mob sounds out of shared server capture
2026-11-06 09:16:57 +01:00
Kacper 62a7855892 Merge pull request #10499 from arslanarm/shaderpacks/lush-caves-biome-category
Shader packs: biome_category matches Iris's readings, fitted to vanilla's biome tags
2026-11-06 09:16:46 +01:00
Kacper cc3714e6ad Merge pull request #10358 from CocofireHD/fix/8713-no-zoom-menu-in-extras-on-mac
Extras: a Zoom row that reaches the mod that replaced the extra
2026-11-06 09:16:40 +01:00
Kacper d08bf63777 Merge pull request #10127 from CocofireHD/fix/9273-stuck-button-highlight
UI: a button's highlight goes when its screen leaves
2026-11-06 09:16:34 +01:00
Kacper 92dbf8406e Merge pull request #10134 from Hiuoy/feat/9921-more-labels
Native hook catalog: NeoForge labels for five more hooks
2026-11-06 09:16:28 +01:00
Kacper 77ac54a216 Merge pull request #10000 from TheMrZZ/fix/raid-live-village-pois
Validate loaded bed claims for raid villages
2026-11-06 09:16:21 +01:00
Kacper 8815f97746 Merge pull request #10141 from ilguerro/perf/9996-perlin
Perlin volumes skip the Ys a lerp does not read, and batch Ys across cells
2026-11-06 09:16:15 +01:00
Kacper 7e74759055 Merge pull request #10309 from Skr1zzzz/fix-9570
Shader packs: material maps share the stitched colour atlas's layout
2026-11-06 09:16:08 +01:00
Kacper 6c0d2781e6 Merge pull request #10297 from CocofireHD/fix/10037-directional-audio-bug-3d-positional-sound-an
Audio: play multi-channel buffers direct like OpenAL
2026-11-06 09:15:57 +01:00
Horizon agent 790d68e500 Merge remote-tracking branch 'origin/main' into fix-10383
# Conflicts:
#	crates/horizon-client/src/game/maps.rs
#	tools/command_exec_corpus.py
2026-11-06 09:15:51 +01:00
Kacper 45a678e309 Merge pull request #10221 from MSedgeMC/fix/item-light-probe
Dropped items: light the block at the item's eye, and 15 for one on fire
2026-11-06 09:15:51 +01:00
Kacper efaa2a0e3f Merge pull request #10238 from MSedgeMC/fix/entity-light-probe-frame
Mobs and projectiles: light the block at the eye of the frame, not of the tick
2026-11-06 09:15:46 +01:00
Kacper ed5ac7df1f Merge pull request #10483 from voidsipp/fix/10468-finite-native-options
Keep native shader float options finite
2026-11-06 09:15:39 +01:00
Kacper 3e92f5e6fd Merge pull request #10076 from yahya-nusseibeh/fix/launcher-rustls
Launcher: rustls 0.23.46 for four RustSec advisories
2026-11-06 09:15:33 +01:00
Kacper a0e3c383a2 Merge pull request #10012 from some-du6e/optifine/color-properties
OptiFine custom colours: read optifine/color.properties
2026-11-06 09:15:26 +01:00
Kacper 1c9379c29e Merge pull request #10247 from TheMrZZ/fix/noai-glow-squid-countdown
Keep glow squid dark countdown running with NoAI
2026-11-06 09:15:20 +01:00
Kacper 46e88c47b1 Merge pull request #10075 from Ragbie/fix/nautilus-sight-armadillo-panic
A nautilus only goes for a pufferfish it sees, and frost panics armadillos
2026-11-06 09:15:15 +01:00
Kacper 8671d460bf Merge pull request #10480 from voidsipp/fix/snbt-unicode-key-order
Keep UTF-16 key order in compound SNBT
2026-11-06 09:15:09 +01:00
Kacper 9f282e42ea Merge pull request #9992 from UMCEKO/fix/tnt-owner-saved
Primed TNT saves and loads its owner
2026-11-06 09:15:03 +01:00
Kacper 7b0e4fee81 Merge pull request #10310 from xxaxio/fix/launcher-account-skin-overflow
launcher: fix account skin arm-type segmented control overflow and caption layout
2026-11-06 09:14:57 +01:00
Kacper c76e42d88d Merge pull request #10066 from Hiuoy/feat/render-level-stage
Native hook catalog: RenderLevelStageEvent labels for the world drawing hooks
2026-11-06 09:14:51 +01:00
Kacper fe8b0c622f Merge pull request #10328 from rison1337/perf/cache-name-tag-layout
Cache unchanged entity name-tag glyph layouts
2026-11-06 09:14:45 +01:00
Kacper c8137fbec9 Merge pull request #10302 from foudretbr/feat/tooltip-render-surface
Native mod API: item tooltip render surface and container item drawing
2026-11-06 09:14:33 +01:00
Kacper a4d6ba2781 Merge pull request #10062 from MSedgeMC/feat/scissor-intersect
A nested GUI scissor clips to the intersection and restores the outer one
2026-11-06 09:14:27 +01:00
Kacper 84cc6b5d64 Merge pull request #10089 from yahya-nusseibeh/fix/telemetry-store-off-workers
Telemetry server: hub, intake and export off the async workers too
2026-11-06 09:14:21 +01:00
Kacper 5258de5549 Merge pull request #10038 from BnCatt/older-servers/1.21.11-either-holders
Older servers: read 1.21.11's EitherHolder item components
2026-11-06 09:14:15 +01:00
Kacper c939630a79 Merge pull request #10135 from TaquitoSuabe/perf/gl-occlusion-culling
gfx: enable early depth occlusion culling on OpenGL
2026-11-06 09:14:09 +01:00
Horizon agentandClaude Opus 5.5 c72e75d8cf Merge main into #10384: keep the exported liquid flag
Main checked bubble columns by name (6bd507f89); this branch reads the
block's exported liquid flag, which covers the same three blocks and any
mod block marked liquid, so the name list and its test go.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 09:13:58 +01:00
Kacper 932d351e11 Merge pull request #10306 from TheMrZZ/fix/lodestone-tracker-default
Omit the saved lodestone tracker true default
2026-11-06 09:11:39 +01:00
M0-kiiandCodex bd0c8cb4fd Preserve changed filenames and rename sources in tooling checks
Read Git's NUL-delimited UTF-8 output without newline conversion, and
include deleted source paths when files move between check scopes.
Keep pr_ready's line accounting consistent with parity selection.

Fixes #10535

Co-Authored-By: Codex (GPT-6) <[email protected]>
2026-11-06 11:41:32 +03:30
Kacper 8504efabb8 Merge pull request #10001 from Ragbie/fix/enderman-teleport-danger
Endermen teleport only where vanilla lets them
2026-11-06 09:11:31 +01:00
Kacper 63db515796 Merge pull request #10502 from arslanarm/shaderpacks/dimension-folder-gbuffers
Shader packs: terrain without a program in the pack is drawn by nothing
2026-11-06 09:11:25 +01:00
Kacper d629a4f6f6 Merge pull request #10497 from arslanarm/shaderpacks/terrain-block-id-mid
Shader packs: terrain's mc_Entity.y reads 0 off fluids, as in Iris
2026-11-06 09:11:18 +01:00
Kacper 77f2a09212 Merge pull request #10341 from Skr1zzzz/prepare-spans
Spans: the frame's prepare steps
2026-11-06 09:11:12 +01:00
Kacper aa1a7c0c42 Merge pull request #10156 from Hiuoy/feat/10132-living-damage
Native hooks: LivingDamageEvent.Pre and .Post, the damage a living entity's health takes
2026-11-06 09:11:06 +01:00
Kacper 36e6c2dc3f Merge pull request #10231 from TheMrZZ/fix/selector-empty-tags
Correct empty tag selector predicates
2026-11-06 09:10:58 +01:00
Kacper aca06a277b Merge pull request #10282 from TheMrZZ/fix/item-frame-missing-facing
Default loaded item frames to DOWN when Facing is absent
2026-11-06 09:10:52 +01:00
Kacper fedc1c7313 Merge pull request #10175 from vnsmoreira/fix-translucent-color-feedback
Fix shader-pack color-buffer reads in translucent draws
2026-11-06 09:10:41 +01:00
Kacper df6139520e Merge pull request #10034 from Ragbie/fix/mob-spear-reach
A mob's melee reach follows the spear it holds
2026-11-06 09:10:35 +01:00
Detimes 3d47396e9b NBT: read and write one-byte strings whole
Most NBT strings in a saved chunk (names, ids, compound keys) are ASCII.
The reader decoded modified UTF-8 one byte at a time and the disk writer
put every unit through its own limit check. Now an all-ASCII string is
widened in one pass when read, and a string whose units are all one byte
(1 to 127) is written in one extend when it fits under the byte limit;
anything else takes the old per-unit loop, so the bytes and the error
offset are unchanged. A new test checks ASCII, zero, mixed and non-ASCII
strings against a per-unit reference and the limit's error offset.
2026-11-06 08:10:31 +00:00
Kacper 1a57f3660d Merge pull request #10070 from yahya-nusseibeh/fix/launcher-no-remove-var
Launcher: open HORIZON_LAUNCHER_PROJECT once without remove_var
2026-11-06 09:10:29 +01:00
Kacper 1e030fd397 Merge pull request #10348 from TheMrZZ/fix/guest-respawn-world-point
Keep guest respawn world information separate from personal landing
2026-11-06 09:10:23 +01:00
Kacper 4c1c81539f Merge pull request #10360 from TheMrZZ/fix/ageable-integer-load
Load animal age and love with NumericTag integer conversion
2026-11-06 09:10:17 +01:00
Kacper bf6bb5d470 Merge pull request #10343 from rison1337/perf/cull-offscreen-entity-flames
Skip offscreen burning-entity flame geometry
2026-11-06 09:10:11 +01:00
Kacper f4cc8528ae Merge pull request #10508 from Skr1zzzz/head-skin-cache
Player heads: parse a profile once, not on every draw
2026-11-06 09:10:04 +01:00
Kacper c12ad4d2cf Merge pull request #10207 from MSedgeMC/fix/pack-update-keeps-mod-choice
launcher: a pack update keeps the player's mod choices
2026-11-06 09:09:52 +01:00
Kacper b8dccdaa55 Merge pull request #10376 from TheMrZZ/fix/hive-default-saved-patch
Omit default hive block state from saved item patches
2026-11-06 09:09:45 +01:00
Kacper 50ac3abcda Merge pull request #10202 from TheMrZZ/fix/furnace-output-components
Furnaces: require output component identity before cooking
2026-11-06 09:09:38 +01:00
Kacper f4eb3234a8 Merge pull request #10496 from voidsipp/fix/10465-server-address-ports
Match vanilla server-address port syntax
2026-11-06 09:09:30 +01:00
Arslan ArmanulyandClaude Opus 5.5 e1a9161f7e Shader packs: the End under the dragon fight's fog has gbuffers_skybasic's fan
While the dragon fight's world fog leaves out the End's sky, Iris 1.11.7 still
runs a pack's gbuffers_skybasic (render stage SKY, vertex colour fogColor), and
the gl_Vertex values our probe packs read there (render distances 12 and 8) fit
a fan around the horizon: from (0, -16, 0) under the eye to eight rim points 16
above it, every 45 degrees, at the render distance. Horizon ran no sky program
in the End, so packs that paint their sky there left that band empty.

Frame carries the boss bar's world fog; PackSky draws only the fan then, in
the fog colour.

Part of #10491

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 13:06:31 +05:00
Horizon agentandClaude Opus 5.5 3728c4b7d1 Play every arrow and trident tick sound on NEUTRAL
The crossbow hit, trident hit and trident hit_ground sounds come from the same
Entity.playSound as the arrow hit (AbstractArrow.onHitBlock / onHitEntity,
ThrownTrident.onHitEntity), so they take getSoundSource(), NEUTRAL, too; the
name-based guess put them on PLAYERS.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 09:03:29 +01:00
Detimes 304276d8dc Region chunks: inflate zlib straight into the chunk's buffer
A saved chunk's zlib stream was inflated by a new Decompress, 8 KiB at a
time into a stack block, each block then copied onto the output, which
grew by doubling from empty. Now each thread keeps one inflater, reset
for every chunk, and a whole stream that fits is inflated straight into
the output, reserved at about eight times the stream (a chunk's usual
ratio) and never past one byte over the limit. Any stream that fails
(damaged, cut short, over the limit) is decoded again the old way, so the
error and what the output keeps are exactly as before. A new test
compares both on whole, cut and damaged streams around every limit.
2026-11-06 08:03:25 +00:00
Horizon agent 899d87e22a Merge remote-tracking branch 'origin/main' into w10368 2026-11-06 09:03:00 +01:00
Horizon agent 1fd348869c Merge branch 'w10339' into w10286
# Conflicts:
#	crates/horizon-client/src/game/dragon.rs
2026-11-06 09:02:54 +01:00
Horizon agentandClaude Opus 5.5 ec6b1817a5 Start the server after a prune that rolled itself back
A failed region restores its originals before the error comes back, so
the world is whole: blocking the start then took a server down for a
corrupt chunk record. Only a recovery that cannot finish stops it now.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 09:02:36 +01:00
Horizon agentandClaude Opus 5.5 07c3ef02d0 Dragon scanning turn: vanilla degree conversion in double
DragonSittingScanningPhase.doServerTick computes Math.acos(dot) * 180.0F / (float) Math.PI
and Mth.atan2(..) * 180.0F / (float) Math.PI in double, not a multiply by the float
RAD_TO_DEG constant. Also moves the Silent test doc comment back onto its test.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 09:01:37 +01:00
Kacper a02fdabe6f Merge pull request #10061 from MSedgeMC/fix/region-file-compression
Region files are written with the compression server.properties names
2026-11-06 09:00:51 +01:00
Kacper d16b78e715 Merge pull request #10022 from DUzzL/fix/9964-burning-powder-snow
Burning players and mobs destroy powder snow they stand in
2026-11-06 09:00:43 +01:00
Kacper e5cc020034 Merge pull request #10272 from MSedgeMC/fix/item-interact-start
Starting an item use posts item_interact_start
2026-11-06 09:00:36 +01:00
Kacper 799fcccba4 Merge pull request #10279 from MSedgeMC/fix/jukebox-play-vibration
A playing jukebox posts jukebox_play every twenty ticks
2026-11-06 09:00:28 +01:00
Kacper 54feca80d1 Merge pull request #10246 from MSedgeMC/fix/disable-mob-generation
The End's no-creatures rule comes from the data, not the dimension
2026-11-06 09:00:22 +01:00
Kacper c1f8fee513 Merge pull request #10226 from MSedgeMC/fix/creative-search-focus
The creative search box loses the focus with its tab
2026-11-06 09:00:15 +01:00
Kacper 8ca476e489 Merge pull request #10172 from MSedgeMC/feat/creative-chat-key
The chat key opens the creative inventory's search tab
2026-11-06 09:00:07 +01:00
Kacper efabb913d0 Merge pull request #10098 from MSedgeMC/fix/f1-tab-list
F1 takes the player list away
2026-11-06 08:59:58 +01:00
Kacper bed239a2cd Merge pull request #10154 from Vaspyyy/fix/stonecutter-take-sound-once
Play a station's take sound once per shift-click
2026-11-06 08:59:51 +01:00
Kacper 63ddd76a15 Merge pull request #10100 from yahya-nusseibeh/fix/nbt-heap-quota
Network NBT: no more tags than vanilla's quota admits
2026-11-06 08:59:44 +01:00
Kacper 208f1c010f Merge pull request #10101 from DUzzL/fix/10051-frame-item-count
Keep one item in item frames filled by /item or NBT
2026-11-06 08:59:35 +01:00
Kacper 935e3927ab Merge pull request #10004 from DUzzL/fix/9960-allay-aim-eyes
Allay: aim deliveries at the player's eyes
2026-11-06 08:59:28 +01:00
Kacper 412594e68d Merge pull request #10016 from lightcodegit/fix/loot-guest-and-double-chest
Loot: guests roll a container's table on open; double chests roll both halves
2026-11-06 08:59:21 +01:00
Kacper 63d25cbd63 Merge pull request #10155 from lightcodegit/fix/push-entities-up
Snow, dripstone clay and spreading sculk lift what stands on them
2026-11-06 08:58:57 +01:00
Kacper 43aba8dbe5 Merge pull request #10126 from lightcodegit/fix/spectator-cant-open
A refused spectator opening gets the red action bar
2026-11-06 08:58:50 +01:00
Kacper 9ee9bb4d0e Merge pull request #10105 from DUzzL/fix/10041-adventure-can-place-on
Adventure mode: honor can_place_on for buckets and flint and steel
2026-11-06 08:58:42 +01:00
Kacper 4d5f0a965f Merge pull request #10010 from DUzzL/fix/9976-scaffolding-projectiles
Fix projectiles hitting the underside of scaffolding
2026-11-06 08:58:35 +01:00
Kacper 585e28d1fb Merge pull request #10186 from degepie/fix/enter-block-trigger
Fire the enter_block trigger from the server's block pass
2026-11-06 08:58:28 +01:00
Kacper 966a63278e Merge pull request #10094 from DUzzL/fix/10045-enderman-anger-harming
Splash of harming makes endermen teleport instead of hurting them
2026-11-06 08:58:21 +01:00
PzmuV1517 155bca35fd Shader packs: a pack's pipelines compile together on several threads
The driver compiles each program of a shaders/ pack as its pipeline is made,
and a pack's pipelines were made one at a time while the pack was laid out:
Metal's compiler service sat on one core for 5 to 50 s on a big pack (BSL,
Complementary, Photon) with four others idle.

The pack is now laid out first, its pipelines recorded as jobs whose pipeline
goes in a slot of its pass, then made all at once on up to 8 threads (wgpu
keeps error scopes per thread, so each is still checked). When wgpu or the
driver refuses one, the pack is built again a program at a time, as before,
so that program is skipped with its note exactly as it was.

Cold loads on an M5 (each pack's first load, nothing in the Metal cache),
pipeline creation: Bliss 6.3 s -> 1.8 s, Solas 5.2 s -> 1.6 s, Complementary
Unbound 9.0 s -> 2.3 s.
2026-11-06 09:58:20 +02:00
PzmuV1517 aff26bf9f9 Shader packs: a pack builds on a thread of its own while the game draws
Loading a shaders/ pack made its pipelines on the main thread, and the driver
compiles each program there: the game froze for 5 to 50 s on a big pack
(Complementary Reimagined, Photon), with no frame drawn.

The renderer now builds the pack on a "Shader pack" thread (load_shader_pack)
with its own handles to the device and the inputs, and the game keeps drawing
without a pack meanwhile; each frame poll_shader_pack checks the thread and
runs the pack once it is built, logging its programs and notes as before. A
pack the thread refuses or panics on is turned off as before. Picking another
pack, or none, drops one that is still building.
2026-11-06 09:58:20 +02:00
Kacper cf8a5a51b4 Merge pull request #10097 from DUzzL/fix/10052-hotbar-q-same-tick
Send the carried slot before a drop
2026-11-06 08:58:13 +01:00
Detimes 3dd74dcc64 Native read-only chunk snapshots (native_chunk_snapshot)
A NativeChunkSnapshot of a loaded chunk of an open level (block states,
WORLD_SURFACE heights, noise biomes, the fuzzy biome zoom's cell) that
shares the chunk's sections copy-on-write and can be read on another
thread, with native_block_map_color, native_biome_name and
native_biome_tint to draw it. Replaces the engine internals a web map
reads today. No behaviour change. Part of #10461.
2026-11-06 07:57:06 +00:00
Kacper 22462182e3 Merge pull request #10494 from voidsipp/fix/10466-final-live-config-values
Apply only final config values to the running server
2026-11-06 08:56:41 +01:00
Kacper c8c4a29367 Merge pull request #10363 from CocofireHD/fix/8668-container-to-hopper-minecart-transfer-speed
Pin the hopper minecart's one-item-a-tick transfer (#8668)
2026-11-06 08:56:34 +01:00
Horizon agent 0c47273286 Merge remote-tracking branch 'origin/main' into w10339 2026-11-06 08:56:14 +01:00
Kacper f24cc569db Merge pull request #10515 from Alysia42/tnt-first-tick
Tick TNT that joined during a tick in that tick's late pass
2026-11-06 08:56:09 +01:00
Kacper c3140e1bae Merge pull request #10192 from KBroneZ/claude/fix-10160-state-provider
upgrade-datapack: a simple state provider becomes a state object
2026-11-06 08:56:03 +01:00
Kacper 10b0942e4f Merge pull request #10191 from KBroneZ/claude/fix-10159-empty-noise
upgrade-datapack: a noise with no octaves becomes a silent 26.3 noise
2026-11-06 08:55:56 +01:00
Kacper 81aeeacf55 Merge pull request #10522 from kacpherek/perf/smp-server
Server: boats and minecarts stop walking every mob, and a Perfetto trace of every thread
2026-11-06 08:54:22 +01:00
Horizon agentandClaude Opus 5.5 8e05ff9b80 Warden anger: a suspect angered again before its UUID lookup keeps its saved anger
AngerManagement.increaseAnger adds angerByUuid's anger for a new suspect
(removeInt, uncapped). The mob layer knows no UUIDs, so convertFromUuids' port
dropped the saved anger of a suspect that was angered again first (a reloaded
warden hearing the player within its first anger tick); it is added there now.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 08:52:28 +01:00
Kacper a0261a708a Merge pull request #10190 from KBroneZ/claude/fix-10158-default-states
Read default_block and default_fluid in 26.3's block state form
2026-11-06 08:52:19 +01:00
Kacper 894f7a0684 Merge pull request #10167 from DUzzL/fix/10113-f3-keys-main-menu
Handle F3 debug keys on the title screen
2026-11-06 08:52:13 +01:00
Kacper d5b1c59f67 Merge pull request #10507 from DUzzL/fix/10447-lead-creative-spectator
Keep the lead in creative and drop mob leads when the holder spectates
2026-11-06 08:52:07 +01:00
Kacper b0ea840d13 Merge pull request #10128 from DUzzL/fix/10048-ladder-mob-push
Mobs on a ladder or vine are not pushable; no crit while climbing
2026-11-06 08:52:00 +01:00
Horizon agent c05ef4bc91 Merge #10330 (bullet target, bridge, main) into #10365
Co-Authored-By: Claude Opus 5.5 <[email protected]>

# Conflicts:
#	crates/horizon-client/src/entity_nbt.rs
2026-11-06 08:51:57 +01:00
Kacper 479d2082a8 Merge pull request #10187 from DUzzL/fix/10073-horse-jump-anim
Fix horse rear ending before a donkey or mule lands
2026-11-06 08:51:52 +01:00
Kacper d4b24f27d6 Merge pull request #10206 from DUzzL/fix/10157-shulker-tooltip
Fix numbered translation args in container item tooltips
2026-11-06 08:51:46 +01:00
Kacper 3ad30a2021 Merge pull request #10488 from AlfaRojo/fix/crossbow-flame-ignite
Crossbows: Flame ignites the arrows they shoot
2026-11-06 08:51:38 +01:00
Horizon agentandClaude Opus 5.5 a04eee71ba Shulker bullet load: wrap Dir as from3DDataValue, keep a negative Steps
Direction.LEGACY_ID_CODEC reads a byte through from3DDataValue, which wraps it
(abs(b % 6)), and getIntOr("Steps", 0) keeps any int; the read clamped Steps to
0 and passed an out-of-range Dir through.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 08:51:18 +01:00
Horizon agent c29349aaf0 Merge #10304 (UUID bridge, with main) into #10330
Co-Authored-By: Claude Opus 5.5 <[email protected]>

# Conflicts:
#	crates/horizon-client/src/entity_nbt.rs
2026-11-06 08:50:35 +01:00
Horizon agentandClaude Opus 5.5 8e35638625 Merge main into #10304
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 08:50:22 +01:00
MegaBytesMe 017caac28c fix: track Windows resource tool changes 2026-11-06 07:46:17 +00:00
Florian ERNSTandCodex 061648308e Keep local mob sounds out of shared server capture
Co-Authored-By: Codex <[email protected]>
2026-11-06 08:41:33 +01:00
GabrielNichols 3dd2820398 Open Recipes settings from the inventory browser 2026-11-06 04:38:32 -03:00
Kacpherek f80bf5d7f3 Vehicles: test a mob's position before working out its box
A boat's or minecart's box query (boat_contacts, boat_obstacles,
cart_entity_collisions) asked every mob of the level for its box, and
Mob::bounds reads the mob's whole size (dims: sleeping, baby, species, scale)
across several cache lines. While boats and carts tick, the widest half width
and tallest height of the mobs (BoxReach) are worked out once, and a mob whose
position is farther than that from the query is skipped without its box. The
reach is worked out again when a mob joins or leaves, and nothing a vehicle
does resizes a mob, so the same mobs are found.
2026-11-06 08:36:08 +01:00
GabrielNichols ce90330576 Sync Recipes content scenario with official main 2026-11-06 04:32:30 -03:00
Detimes 3a20f23ee6 Native mods: read and set a player's attributes (player_attributes)
native_attribute(uuid, attribute) gives an attribute's base value and its
value with every modifier (Bukkit's AttributeInstance.getBaseValue and
getValue); native_set_attribute_base sets the base as
/attribute .. base set does; native_set_attributes loads a saved
attributes list onto the player as vanilla's AttributeMap.load reads a
player's file. A lower max_health caps the health, and the client gets
UpdateAttributes from the tick's sync. Nothing calls them without a mod.

Unproven: not yet compiled or probed live (build queue); the integration branch tests it.
2026-11-06 07:31:33 +00:00
Detimes 865e30ecb6 Native mods: read, give, take and replace a player's effects (player_effects)
native_effects(uuid) reads a player's effects as NativeEffect { id,
amplifier, duration, ambient, particles }; native_add_effect gives one as
vanilla's LivingEntity.addEffect (Bukkit's addPotionEffect), so a weaker
or shorter one waits under it; native_remove_effect takes one off as
/effect clear does; native_set_effects replaces them all, what a mod
restoring a saved player needs instead of writing PlayerEffects into the
hub's guests. The client gets UpdateMobEffect with the ambient and
particle flags from the tick's sync. Horizon keeps no hidden-icon flag on
an effect, so none is offered. Nothing calls them without a mod.

Unproven: not yet compiled or probed live (build queue); the integration branch tests it.
2026-11-06 07:31:33 +00:00
Detimes 3e6a1990ce Native mods: read a player's food, set health, food and experience (player_status)
native_player_food(uuid) reads a player's food level, saturation and
exhaustion; native_set_player_status(uuid, health, food, xp) sets each
that is given, as Bukkit's setHealth, setFoodLevel, setSaturation,
setExhaustion, setLevel, setExp and setTotalExperience. The write goes
through the player's turn as /kill and /effect reach a player
(for_player), so their client gets SetHealth and SetExperience with the
tick's sync. Health is capped at the maximum; 0 or less kills as /kill
does, with "<player> died" (Bukkit's setHealth(0), LivingEntity.die with
the generic source). The mod's food is not heard as the player's own
change (native_food_level_changed) outside their turn. Nothing calls it
without a mod.

Unproven: not yet compiled or probed live (build queue); the integration branch tests it.
2026-11-06 07:31:32 +00:00
Detimes 56b7dcbe62 Native mods: a player's compound as the server saves it (player_compound)
The helper native_player_compound(uuid) gives a player's compound as the
server would save it now (players/data/<uuid>.dat, playerdata::player_tag),
vanilla's ServerPlayer.saveWithoutId: what a mod keeping players in step
across servers (a HuskSync port) stores, instead of calling player_tag on
the hub's guests itself. None inside a player's turn, where their live
state is out of their record, and for a player not on. The new module
native_player_state.rs holds this group's helpers. Nothing calls it
without a mod.

Unproven: not yet compiled or probed live (build queue); the integration branch tests it.
2026-11-06 07:31:32 +00:00
Detimes 7c411b3a58 Renumber registry ids for a 26.2 client, with stand-ins for what 26.2 lacks
What a Horizon server sends a 26.2 client (accept_older_clients) now carries 26.2's
registry ids: tags, block states, entity, block entity, block, attribute, sound and
particle ids, and items and components through the existing Way::Down writers, which
now leave out components the client lacks. A creative slot or container click from the
client is read with its ids up.

generate_registry_id_adapter.py --substitutes writes, from both versions' data
reports, a stand-in for each 26.3 entry 26.2 lacks (poplar as birch, wool and concrete
slabs and stairs as stone ones of the same shape, ...); what has none (the cushion
entity) isn't sent. v26_2_ids.rs is regenerated with it.

Part of #10404.
2026-11-06 07:29:28 +00:00
DUzzL b6e947c9a4 Merge remote-tracking branch 'upstream/main' into fix/10051-frame-item-count
# Conflicts:
#	crates/horizon-client/src/game/commands/items.rs
2026-11-06 08:29:22 +01:00
Ludovic Marie 0d21d5520e Merge pull request #10477 from Detimes/feat-10404-older-clients-1a
🤖 Agent: a 26.2 client is now refused in configuration before any untranslated registry data, tags or play packets (bebf670); 26.3 path byte-for-byte unchanged with the option off
2026-11-06 08:27:12 +01:00
Detimes 29a0f2dc72 Native mods: players the server knows, by name or id (profile_cache, #10461)
native_profile_by_name(name) gives the id and name, and
native_profile_by_uuid(uuid) the name, of a player who is on, else one
in a dedicated server's profile cache (usercache.json): vanilla's
GameProfileCache, Bukkit's getOfflinePlayer without its network
lookup. They ask no profile service and make up no offline player;
UserCache::find_by_name reads without changing the cache. native_player_head
names a player who is off from the cache too. Ports parse
usercache.json or ask Mojang themselves today.
2026-11-06 07:25:24 +00:00
Detimes bb4f25c694 Native mods: any player's held stack and equipment from any server hook (player_equipment)
Unproven: not compiled or run yet (build queue stopped for the integration build); see proof/hooks-pread/PROOF_PLAN.md.
2026-11-06 07:25:15 +00:00
Detimes 2a86196e83 Native mods: the server's player limit, folder and log file (server_files)
Unproven: not compiled or run yet (build queue stopped for the integration build); see proof/hooks-pread/PROOF_PLAN.md.
2026-11-06 07:25:15 +00:00
Detimes ea6382f2a1 Native mods: a player's statistics and advancements, read and replaced (player_records)
Unproven: not compiled or run yet (build queue stopped for the integration build); see proof/hooks-pread/PROOF_PLAN.md.
2026-11-06 07:25:15 +00:00
Detimes 2f4294aba1 Native mods: what a player's client told the server (player_client)
Unproven: not compiled or run yet (build queue stopped for the integration build); see proof/hooks-pread/PROOF_PLAN.md.
2026-11-06 07:25:15 +00:00
Detimes eacaa4db69 Native mods: more of a player on NativeServerPlayer (server_player_state)
Unproven: not compiled or run yet (build queue stopped for the integration build); see proof/hooks-pread/PROOF_PLAN.md.
2026-11-06 07:25:14 +00:00
AkirabaneandClaude Opus 5.5 db1cde61a8 dedicated: transfer-status, the counts a proxy reads before it sends players
A proxy that balances players between servers has to know, before it sends
one, how many players a server has and whether a login would be refused as
full. transfer-status answers with counts only: the players placed, held and
expected, the hand-overs under way, the places the player limit counts, the
limit, and whether the server is full. It names no player, changes nothing
and writes nothing to the log, since a proxy asks it often. Like the player-*
requests, it is refused while [transfer] is off.

Refs #7484

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 08:23:45 +01:00
Web-mc1 89bac30424 tests: pin how the dev tools take their arguments
Both tools above used to crash, or write into a flag-named path, instead of
asking for what they need. These tests run them in an empty folder and check
that they exit non-zero with a usage line and write nothing.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <[email protected]>
2026-11-06 07:20:12 +00:00
Web-mc1 cd5b93a54a bake_bricolage: ask for its two fonts
Both arguments were read unguarded, so running it with none, or with one, was an
IndexError rather than the command its docstring shows. Print the usage line.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <[email protected]>
2026-11-06 07:20:12 +00:00
Web-mc1 35bb6ede00 json_load_fixtures: ask for an output directory
The script read sys.argv[1] straight away, so a missing argument ended in an
IndexError traceback, and a mistyped flag created a directory named --help and
filled it with 66 MB of fixtures. Print the usage instead: a flag is not a path.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <[email protected]>
2026-11-06 07:20:12 +00:00
Detimes ca41bb4780 Packets: one zlib compressor per thread, reset for each packet
frame::contents made a new ZlibEncoder for every packet above the
compression threshold, which allocates and clears zlib's window and hash
tables (some 300 KiB) each time. A thread-local Compress, reset before each
packet and driven with the encoder's own calls (the input unflushed, then
finish), writes the same bytes. A new test checks them against a fresh
encoder for bodies from 256 bytes to 300 KiB, in both orders.
2026-11-06 07:19:47 +00:00
Arslan ArmanulyandClaude Opus 5.5 da06786848 The dragon fight's world fog leaves out the sky, as vanilla does
GameRenderer.renderLevel gives LevelRenderer.render shouldRenderSky false
while a boss bar makes world fog (the Ender Dragon's), so the frame is only
cleared to the fog colour. Horizon drew the End's sky box behind the island
during the fight. fog::sky_pass_skipped now holds vanilla's conditions:
LevelRenderer.addSkyPass's (lava, powder snow, blindness or darkness) and
the boss fog.

Part of #10491

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 12:19:07 +05:00
martipopsandClaude Opus 5.5 1cfaf0bc96 Controllers: each choice of a controller setting describes itself
The description at the bottom of the page stayed the same whichever value was
picked, on the Gameplay Guide, Button Glyphs, On-screen Keyboard and Lightbar.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 02:13:53 -05:00
Detimes f1586af649 Native hooks: set one checked inventory slot, take exact stacks (helpers)
native_set_inventory_slot(uuid, kind, slot, expected, new) puts a stack
in any slot of a player's inventory or ender chest, armour and off hand
included, if it still holds exactly `expected`, and returns what was
there (Bukkit's setItem). native_take_stacks(uuid, slots) takes exact
stacks out of the given slots, all or none. Both are checked like
native_move_stack and work live or on the player's file. Wanted by the
InvSee, AdminKit and Wanted ports (armour slots, splitting, stakes);
Wanted edits LanHost guests directly for this today.

Unproven: not yet built or run on a server (handed to the hooks-all integration test).

Part of #10461.
2026-11-06 07:12:26 +00:00
Detimes a7c6eff569 Native hooks: replace a player's whole inventory (set_inventory helper)
native_set_inventory(uuid, kind, slots, selected) replaces a player's 41
inventory slots or 27 ender chest slots (Bukkit's
PlayerInventory.setContents), live or in their file like
native_inventory. For the inventory it sets the held hotbar slot, empties
the cursor and the 2x2 crafting grid, and sends a player on the server the
whole inventory at once. The HuskSync port writes Guest fields directly
for this today.

Unproven: not yet built or run on a server (handed to the hooks-all integration test).

Part of #10461.
2026-11-06 07:12:26 +00:00
Detimes 8e61d452e0 Native hooks: a guest's block placement before it happens (block_place)
native_block_place(player, name, stack, pos, state, cancel) is heard
where BlockItem.place has found the position and the state and checked
survival, before anything is set (Bukkit's BlockPlaceEvent, NeoForge's
BlockEvent.EntityPlaceEvent). A canceled placement is a failed one: no
block, no use, no swing, and the client gets its blocks and inventory
back. The position and state come from the existing dry run
(Behaviours::placement_state). Ports cancel placements today by claiming
native_use_item_on, which also stops doors, chests and the item's other
uses.

Unproven: not yet built or run on a server (handed to the hooks-all integration test).

Part of #10461.
2026-11-06 07:12:26 +00:00
Detimes 531a026a14 Native hooks: a guest's drag over slots, cancellable (inventory_drag)
native_inventory_drag(player, name, window, kind, slots, cancel) hears a
guest's quick craft in their own inventory or a vanilla menu once, at its
end, with the kind and the window slots the client dragged over (Bukkit's
InventoryDragEvent). A canceled drag's end is handled like a canceled
click: the drag ends empty and the client's prediction is answered with
the server's slots and cursor.

Unproven: not yet built or run on a server (handed to the hooks-all integration test).

Part of #10461.
2026-11-06 07:12:26 +00:00
Detimes 96d477b41b Native hooks: a guest's inventory and menu clicks, cancellable (inventory_click)
native_inventory_click(player, name, window, slot, button, mode, cancel)
hears a guest's click in their own inventory or a vanilla menu they have
open before any handler applies it (Bukkit's InventoryClickEvent). A
canceled click becomes a drag start of no type, which every menu takes as
resetQuickCraft and nothing more, so nothing moves and the client's
predicted slots and cursor are answered with the server's, as for any
click the server refused. Drags and a mod's own menus are not heard.
Wanted by the HuskSync and Essentials ports (data lock, freeze).

Unproven: not yet built or run on a server (handed to the hooks-all integration test).

Part of #10461.
2026-11-06 07:12:26 +00:00
Detimes 2e5d156644 Native hooks: a guest's throw, cancellable (player_launch_projectile_v2)
native_player_launch_projectile_v2(player, name, stack, cancel) is heard
where ProjectileItem.use creates a snowball, egg, ender pearl or a mod's
thrown item, before the projectile exists (Bukkit's ProjectileLaunchEvent
for a player). A canceled throw is a failed use: nothing is thrown, used
or put on cooldown, v1 hears nothing, and the guest's client, which took
the item at once, gets its inventory back. Wanted by the HuskSync port's
data lock (no ender pearls while locked).

Unproven: not yet built or run on a server (handed to the hooks-all integration test).

Part of #10461.
2026-11-06 07:12:26 +00:00
Kacpherek 02bdb03444 Vehicles: seat riders and find boat contacts without a pass over every mob per vehicle
position_riders finds every vehicle's riders in one pass over the mobs instead
of one per boat, minecart and primed TNT. A boat tests a mob's box before
anything else, and works out the scoreboard teams only when something touches
it, instead of once per boat a tick.
2026-11-06 08:10:31 +01:00
Kacpherek c7183d6232 Trace: every thread's spans as a timeline Perfetto opens
HORIZON_TRACE=FILE, horizon-ctl trace FILE [SECONDS] and horizon-trace on a
dedicated server's console write each span of every thread as a Chrome trace
event, so what ran beside what, and what a thread waited for, can be read off
in ui.perfetto.dev. A thread that records spans but is never taken from (a far
terrain or lighting worker while spans are on) now drops them past 16k instead
of growing for as long as spans stay on.
2026-11-06 08:10:30 +01:00
Florian ERNSTandCodex 8b33877963 Bind spellcaster NBT to the live spell counter
Load and save Evoker and Illusioner SpellTicks using the counter already consumed by casting goals and server AI. Cover the real codec and actual Game tick path.

Co-Authored-By: Codex <[email protected]>
2026-11-06 08:05:53 +01:00
Alysia Zappavigna 3b56859e04 Tick TNT that joined during a tick in that tick's late pass (fixes #10063) 2026-11-06 18:05:23 +11:00
Detimes 157208790d Native mods: an online player's textures and a head stack of a player (player_textures, #10461)
native_player_textures(uuid) gives the textures property (value and
signature) of a player who is on, as their player list entry carries
it (Paper's Player.getPlayerProfile). native_player_head(uuid) gives a
minecraft:player_head whose profile holds the player's id, and their
name and skin when they are on (Bukkit's SkullMeta.setOwningPlayer).
Ports look the current skin up at the session server today, or build
heads with a name-only profile.
2026-11-06 07:03:55 +00:00
Detimes 16750b8c90 Native hooks: a player logging in, held for the mods' answers (player_pre_login, #10461)
native_player_pre_login(player, name, address, defer) runs on the
connection's own thread once the session service or the proxy has named
the player and before the server's own checks (Bukkit's
AsyncPlayerPreLoginEvent). A mod that needs time, a permissions or
inventory load, or may refuse, pushes a Receiver<Result<(), Text>> onto
defer and answers from any thread: Ok lets the player on, Err refuses
them with the reason. The login waits for every answer, 20 s at most in
all; a missing or dropped answer refuses with a plain Disconnected.
Today such loads start in native_player_login, where a database failure
cannot refuse the player.
2026-11-06 07:02:40 +00:00
Detimes 6564832337 Native mods: set a profile property and show the player again (profile_refresh, #10461)
native_set_profile_property(player, name, value) sets or removes a
property of an online player's profile (Paper's Player.setPlayerProfile);
native_refresh_player(player) shows them again at the end of their
level's pass: everyone who sees them gets their list entry anew and
their entity again, and the player is respawned in place into the same
level with all data kept (Paper's hidePlayer/showPlayer plus the
respawn), so their own client draws a new skin too. Both work from any
server hook, native_server_tick included. They replace the raw HEAD
injection on Game::flush_native_net that edits Guest.properties and
sends the player info itself.
2026-11-06 07:01:16 +00:00
Detimes 413c2b1593 Native mods: command arguments that ask the server for completions, and the request split into label and arguments (tab_complete_v2) (unproven) 2026-11-06 07:00:51 +00:00
Detimes 78f3b6fca9 Native mods: look up a server command by name, aliases resolved (command_info) (unproven) 2026-11-06 07:00:50 +00:00
Detimes 7495f4c56b Native mods: run a console command that reaches the mods' commands (run_mod_command) (unproven) 2026-11-06 07:00:50 +00:00
Detimes 90a091b024 Native mods: run a command as a player, at their level, with its answer (run_command_as) (unproven) 2026-11-06 07:00:50 +00:00
Detimes 41413878d0 Native mods: run a console command and keep its result and answer (run_command_captured) (unproven) 2026-11-06 07:00:50 +00:00
Detimes 9b670c7cfc Native hooks: a joining player's profile on the connection thread (player_profile, #10461)
native_player_profile(player, name, properties) runs on the
connection's own thread once the server has let a player in and before
it sends LoginFinished. A mod may change, add or remove the profile's
properties (a skin's textures): the player's own client and every
player list get the new ones (Velocity's GameProfileRequestEvent,
Paper's PlayerProfile at AsyncPlayerPreLoginEvent). It replaces the raw
HEAD injections on lan::conn::login_and_configure and
Game::lan_guest_joined that skin and Floodgate ports use today.
2026-11-06 07:00:34 +00:00
Florian ERNSTandCodex 3443abbde5 Keep superclass air refill for NoAI Nautilus
Skip only the Nautilus custom air handler under NoAI, preserving the existing Breath/drown path and normal custom behavior. Cover actual Game and full Mob ticks against fresh Java.

Co-Authored-By: Codex <[email protected]>
2026-11-06 07:59:32 +01:00
Ludovic Marie 98af9399cd Merge pull request #10509 from horizon-rust/perf/launcher-prepared-cache
🤖 Agent: is_prepared now answers from a 5 s per-version cache (21.4 µs to 0.21 µs per call); need_prepared and the CLI still run the full per-file check, so an empty JSON is caught before launch.
2026-11-06 07:53:06 +01:00
Detimes 0da0001f38 Native mods: a player's permission level, also at login (player_permission, #10461)
The helper native_player_permission(player) gives the permission level of a
player on the server or joining it (0 to 4, ops.json's for an operator,
PlayerList.getProfilePermissions). It works inside native_player_login for the
player that hook hears, whose level the hub set on admitting them, so a login
gate that lets operators through needs no ops.json of its own. The hook's
contract is unchanged.
2026-11-06 06:51:55 +00:00
Detimes a4a64c560d Native mods: send a plugin channel message to a player (send_custom_payload_to_player, #10461)
The helper native_send_custom_payload_to_player(player, channel, data) queues
a ClientboundCustomPayloadPacket on any namespace:path channel outside
horizon: (at most ClientboundCustomPayloadPacket's 1 MiB) for one player's
client, vanilla or Horizon, sent at the tick's end with the mod messages, so
it works from any server hook. Bukkit's Player.sendPluginMessage.
2026-11-06 06:51:55 +00:00
Detimes 27200f64cd Native hooks: a player's plugin channel message (custom_payload_from_client, #10461)
native_custom_payload_from_client(player, channel, data) hears a guest's
ServerboundCustomPayloadPacket in play on any channel that isn't Horizon's own
(horizon:mod_net, horizon:quiet_commands, horizon:lod), which the server drops
today. Like mod messages it waits for the tick's end, with the hub back, so
every helper works; it is heard from a dead player too, as vanilla's
handleCustomPayload doesn't look at health. Horizon's own channels are handled
as before. The client side is #6774.
2026-11-06 06:51:55 +00:00
Detimes 01fec61d2d Handshake: a host of up to 32767 characters behind a proxy (#10461)
With proxy forwarding on (forwarding.secret, offline mode, not the integrated
server's own player) the handshake's host is read with Short.MAX_VALUE as its
limit, as Paper does behind a proxy, so a proxy's data after the host (a
Floodgate player's, often longer than 255 characters) no longer drops the
login. Without forwarding the limit stays vanilla's 255; it is the only string
read with that limit.
2026-11-06 06:51:55 +00:00
Detimes ac6ff90c34 Native hooks: a client's handshake on the connection thread (handshake, #10461)
native_handshake(address, host, intent, cancel, reason) is a free function in
horizon_client_lib::lan::conn, called on the connection's own thread for each
network connection's handshake, before the server acts on the intent. A mod may
rewrite the host (a proxy's data after it, Floodgate's, included) or refuse the
connection: a login or transfer is disconnected with the reason, a status ping
is closed unanswered. Wired through NativeClientHooks like player_chat. Nothing
changes without a mod.
2026-11-06 06:51:55 +00:00
Horizon agentandClaude Opus 5.5 bebf670623 Turn a 26.2 client away before untranslated registry data
Only packet ids are translated for an older client so far (#10404): the
registry data, tags (registry ids) and play packets (chunks, light, login,
player_action numbering...) would reach a real 26.2 client in 26.3 ids and
layouts. With accept_older_clients on, such a client now logs in and is
told so in configuration, right after the known packs and before any of
them. NetConfig::older_clients_play (tests only, not in server.toml) keeps
the play path reachable for the id renumbering test until the later parts
translate the contents.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 07:51:40 +01:00
MegaBytesMe 376be8985d fix: handle spaces in Windows resource paths 2026-11-06 06:51:09 +00:00
Horizon agent 5daf0853e3 Merge remote-tracking branch 'origin/main' into review-10477 2026-11-06 07:44:24 +01:00
Horizon agentandClaude Sonnet 5.5 bb8a9d9524 Launcher: pages drawn ask "is it prepared?" from memory, not the disk
#10389 made is_prepared parse research-lock.json and the manifest, list the
prepared folder and stat every table on each call. Pages call it while drawing
(instance rows, the Play menu, can_start), so every frame did file I/O.

is_prepared now keeps its answer per version folder for 5 s and drops it when
the launcher installs, removes, reuses resources or a job exits.
check_prepared runs the full check and refreshes the answer; starting the game
or a server (need_prepared), the CLI and the post-prepare auto-play use it, so
an empty JSON is still caught before a launch.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-06 07:44:15 +01:00
voidsip 23dcce8ab0 Merge main and retain both mob regression suites 2026-11-06 00:43:10 -06:00
voidsip e58593bb50 Fix wet mob extinguishing after fire damage 2026-11-06 00:39:49 -06:00
voidsip a4586852b1 Merge remote-tracking branch 'refs/remotes/origin/leash-refresh-main' into fix/10445-leashed-happy-ghast
# Conflicts:
#	mods/horizon/src/test/java/dev/horizonlab/parity/VanillaCommandExecOracle.java
2026-11-06 00:31:03 -06:00
Detimes c87a60d013 Native hooks: creative slots with the whole stack (creative_slot_v2)
native_creative_slot_v2(player, name, slot, stack, cancel) is an
additive version of native_creative_slot with the whole ItemStack put in
the slot (none for an emptied one), components included. It is heard
just before v1, which keeps its signature and firing point and hears
nothing of a packet canceled here.

Part of #10461.
2026-11-06 06:30:14 +00:00
Detimes 960634a9ad Native hooks: throws with the whole stack (item_dropped_v2)
native_item_dropped_v2(player, stack) is an additive version of
native_item_dropped with the whole ItemStack thrown, components included
(enchantments, unbreakable, attribute modifiers...), heard just before
v1, which keeps its signature and firing point.

Part of #10461.
2026-11-06 06:30:13 +00:00
Detimes a425fe4152 Native hooks: pickups with the whole stack, thrower and position (item_picked_up_v2)
native_item_picked_up_v2(player, stack, thrower, position, canceled) is
an additive version of native_item_picked_up: the whole ItemStack with
its components, the item entity's thrower UUID (none for block and mob
drops) and its position. It is heard just before v1, which keeps its
signature and firing point and hears nothing of a pickup canceled here.

Part of #10461.
2026-11-06 06:30:11 +00:00
Detimes 192898e730 Native hooks: where a joining player is placed (player_spawn_location)
native_player_spawn_location(player, name, dimension, position,
rotation) is heard once per join after the saved data is loaded (or a
newcomer's spawn found) and before the level is chosen and the world is
sent (Spigot's PlayerSpawnLocationEvent). A mod that changes them places
the player there, in that level, with no teleport afterwards. An unknown
dimension or a non-finite value leaves that part unchanged.

Part of #10461.
2026-11-06 06:30:10 +00:00
Detimes 14af676a89 Native hooks: player-versus-player damage with both players (player_hurt_by_player)
native_player_hurt_by_player(attacker, attacker_entity, victim,
victim_entity, source, damage_type, amount, canceled) names both players
by UUID and network entity id, and how the hit came: melee, sweep, or the
projectile's entity type (arrow, spectral arrow, snowball, egg, a mod's
thrown type). It is heard where native_living_incoming_damage is, at the
victim's hurtServer past its guards (pvp rule and teams allow it, victim
not invulnerable or dead), and just before it: changing the amount
reaches v1, canceling skips the hurt and v1 alike. The PvP call sites
name the hit in a thread-local scope that hurt_server reads.

A snowball or egg does zero damage, where vanilla's Player.hurtServer
stops: it is heard there with amount 0 and nothing the mods set applies.
Thrown tridents hurt no players in Horizon yet and are not heard.

Part of #10461.
2026-11-06 06:30:09 +00:00
Detimes 3515e99feb Native hooks: a player logging out while still present (player_logging_out)
native_player_logging_out(player, name) is heard when a placed player's
connection is gone (lost, kicked, quit) but before their menu closes and
they are saved and removed (Bukkit's PlayerQuitEvent): they are still in
their level and listed in native_server_players. No state is swapped in;
the hub is back in self.lan for the call, so the UUID helpers and
native_run_command reach them. A death a mod causes there (`kill <name>`)
is died at once (death message, drops, native_player_died), since the
leaving player gets no further tick to die in; they are then saved dead.
Not heard when the server stops, where native_player_logged_out already
tells with them still listed. Unmodded servers are unchanged: the death
step only runs when the hook took the player from alive to zero health.

Part of #10461.
2026-11-06 06:30:08 +00:00
Ludovic Marie 3a81751180 Merge pull request #10458 from TheMrZZ/fix/h028-stand-fall-save
🤖 Agent: Entity.checkFallDamage subtracts (float)ya into the double, which the f64::from(.. as f32) step reproduces, and Entity.load reads fall_distance back. Main merged in again after #10422 landed (GitHub read the criss-cross bases as a conflict); the diff to main is this PR's own 6 files.
2026-11-06 07:28:07 +01:00
Horizon-agentandClaude Opus 5.5 ec72efe92d Merge main into #10458 after #10422 landed
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 07:25:35 +01:00
Ludovic Marie 87c51ab9c9 Merge pull request #10471 from arslanarm/shaderpacks/shadow-pass-block-entities
🤖 Agent: `camera_list` is now collected into a Vec on every frame, even with no pack, where the old iterator was lazy; it's one small allocation per frame. The shadow-only set and the spawner's `shadow_pass_drew` scan (linear in shadow_visible per spawner) run only under a pack whose shadow draws entities.
2026-11-06 07:23:45 +01:00
voidsip f2a63474b9 Match vanilla server-address port syntax 2026-11-06 00:22:03 -06:00
voidsip 24224f4728 Respect the controlling passenger for remote boats 2026-11-06 00:21:23 -06:00
voidsip 4e580b2732 fix(server): apply only final values in config batches 2026-11-06 00:21:04 -06:00
Arslan ArmanulyandClaude Opus 5.5 4eb7a27e58 Shader packs: terrain without a program in the pack is drawn by nothing
A pack (or the dimension folder it uses) with no gbuffers program for terrain
had the game draw terrain itself, into colortex0 and depth. Iris 1.11.7 draws
none: with probe packs of ours having no gbuffers programs, at the root or in
world-1 below a root that has them, colortex0 held only the sky (or fog
colour), entities and particles, depthtex0 had no terrain, and colortex1 to 7
none either; water and lava were missing too.

When no program of gbuffers_terrain's chain (nor _solid or _cutout) is in the
folder, the pack takes solid and cutout terrain from the game and draws
nothing; the same for translucent terrain and gbuffers_water's chain. A
program that is there but fails to load still leaves terrain to the game.

Fixes #10455

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 11:18:32 +05:00
Arslan ArmanulyandClaude Opus 5.5 0a14bac24c Shader packs: biome_category matches Iris's readings, fitted to vanilla's biome tags
`biome_category` read 0 (CAT_NONE) in lush caves and the other cave biomes,
where Iris 1.11.7 reads 5 (CAT_PLAINS), and differed in 12 more vanilla
biomes. A probe pack of ours read Iris's value in each of vanilla 26.3's 67
biomes (`fillbiome` around the camera): they fit vanilla's `is_*` biome tags
(BiomeTags; `is_savanna` unused), then desert, swamps, mushroom fields and
snowy plains by name, NONE for the void, and PLAINS for the rest. Mountain
biomes read 17, which Iris names CAT_MOUNTAIN; CAT_UNDERGROUND is 18.

Biomes vanilla doesn't have keep the old guess from words of their name.

Fixes #10453

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 11:18:32 +05:00
Kacper 108c130237 Merge pull request #10340 from CocofireHD/fix/10327-docs-readme-md-index-the-benchmark-and-perfo
docs: index benchmark and performance result pages in docs/README.md
2026-11-06 07:18:18 +01:00
Kacper 73fdce3c01 Merge pull request #10375 from voidsipp/fix/10326-docs-index
Index the missing design, server and release docs
2026-11-06 07:18:10 +01:00
Kacper bf22a35ab3 Merge pull request #10350 from xxaxio/docs/10325-index-player-launcher-docs
docs/README.md: index missing player, launcher and platform pages
2026-11-06 07:18:04 +01:00
Kacper e41b6599ec Merge pull request #10351 from xxaxio/docs/10324-remove-stale-not-ported-notes
docs: remove stale "not ported" notes
2026-11-06 07:17:58 +01:00
Kacper 0c2aec59f6 Merge pull request #10234 from KBroneZ/claude/fix-10164-forceload
Load chunks in /forceload add, as vanilla does
2026-11-06 07:17:48 +01:00
Kacper 50c2ade6c6 Merge pull request #10233 from KBroneZ/claude/fix-10163-template-loot
Keep a placed template's container loot table
2026-11-06 07:17:42 +01:00
Kacper 3281781334 Merge pull request #10370 from xxaxio/fix/10322-map-can-place-on-banner
game/maps: honour can_place_on when using a map on a banner in adventure mode
2026-11-06 07:17:35 +01:00
Kacper 5871fa922f Merge pull request #10278 from Joansitoh/fix/10044-underwater-sky
Stop the underwater sky scan on kelp and seagrass
2026-11-06 07:17:25 +01:00
Kacper 16bb8b3e65 Merge pull request #10277 from Joansitoh/fix/10044-bucket-liquid
Replace a bubble column when a bucket empties into it
2026-11-06 07:17:18 +01:00
Kacper 1e35996644 Merge pull request #10274 from DUzzL/fix/10250-zip-end-record-disk
Normalize zip END records with equal non-zero disk numbers
2026-11-06 07:17:12 +01:00
foudretbr 8be5c59fce Native mod API: item tooltip render surface and container item drawing
Fixes #10199
2026-11-06 07:17:10 +01:00
martipopsandClaude Opus 5.5 426786c862 Controllers: A takes hold of a slider, and the gameplay guide has levels
Left and right changed a slider as soon as the focus passed over it, so moving
through a settings page could change a value. A now takes hold of it, left and
right move it, A or B lets go and up or down moves on, with a white outline
while held.

The gameplay guide's on/off switch becomes Full, Basic and Off. Basic keeps the
right column. A file saved with the old switch keeps its meaning.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 01:16:58 -05:00
Florian ERNSTandCodex 3aaff8da93 Restore NoAI Dolphin air after its superclass tick
Use the existing species maximum after shared Mob tick work, preserving the normal underwater countdown. Cover the real full-tick path and actual Game commands against fresh Java.

Co-Authored-By: Codex <[email protected]>
2026-11-06 07:16:46 +01:00
Ludovic Marie 499b049803 Merge pull request #10422 from TheMrZZ/fix/h046-loot-pickup-save
🤖 Agent: Mob.addAdditionalSaveData always writes canPickUpLoot(), so moving it out of the put-if-absent defaults is the fix (Allay and sulfur cube overrides now win over a loaded byte). Main merged in after #10393 landed; clippy and the entity_nbt tests pass on the merge.
2026-11-06 07:16:40 +01:00
voidsip 3e90d34739 Respect breathing effects and bubble columns for mobs 2026-11-06 00:13:10 -06:00
Ludovic Marie 29825eb774 Merge pull request #10411 from DUzzL/fix/10318-piglin-gear-compare
🤖 Agent: piglin_replace follows Piglin.canReplaceCurrentItem in order (PREVENT_ARMOR_CHANGE first, then wanted/unwanted, then Mob.canReplaceCurrentItem), with getPreferredWeaponType null for babies.
2026-11-06 07:12:21 +01:00
Horizon-agentandClaude Opus 5.5 ded0d1a283 Merge main into #10458
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 07:11:56 +01:00
Horizon-agentandClaude Opus 5.5 1c8729793f Merge main into #10422
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 07:11:51 +01:00
Ludovic Marie 69c1400e58 Merge pull request #10389 from CocofireHD/fix/10303-my-game-crash-after-launch
🤖 Agent: `is_prepared` now reads the manifest and stats every top-level JSON on each call, and the launcher calls it from render paths (glass_home, glass_palette, can_start): a few hundred µs per frame at most, but a cache keyed on the folder would remove it. I dropped the unused `is_json_file`; launcher clippy and backend tests pass.
2026-11-06 07:11:07 +01:00
Ludovic Marie 948c6d6a88 Merge pull request #10438 from megabytesme/fix/windows-cross-compile-icon
🤖 Agent: checked on Linux with mingw: the build script now compiles `main_res.o` for x86_64-pc-windows-gnu and passes it to the link, which the host-gated `#[cfg(windows)]` skipped. I kept the warn-only fallback outside CI for windres paths with a space (#10398).
2026-11-06 07:11:03 +01:00
Ludovic Marie b8c058a249 Merge pull request #10386 from voidsipp/fix/10355-boat-attack-knockback
🤖 Agent: causeExtraKnockback's non-living branch is Entity.push (no knockback resistance, no halving), as strike_boat does; the guest path still checks the attack range before skipping look_at_entity. We merged #10434 into this branch: both extend the boat fixture and harness.
2026-11-06 07:10:59 +01:00
Ludovic Marie 5a0c5d522f Merge pull request #10437 from voidsipp/fix/10401-snbt-float-text
🤖 Agent: for one-digit mantissas {:.1e} picks the closest two-digit decimal, which is what Java's Float/Double.toString does (Float.MIN_VALUE prints 1.4E-45). We switched the harness to the shared release world_probe: debug at CARGO_BUILD_JOBS=1 would have compiled horizon-world again in CI.
2026-11-06 07:10:55 +01:00
Ludovic Marie ff27525aaf Merge pull request #10448 from masa-masa-0314/fix/realms-add-realm-strings
🤖 Agent: RealmsPdpScreen shows the trial description (and a Try for free button) only when trialAvailable; Horizon does not ask for it, so we dropped the description and kept the title with Buy, vanilla's no-trial layout. Titles are drawn plain where vanilla underlines/bolds them: small follow-up.
2026-11-06 07:10:52 +01:00
Ludovic Marie 54112649da Merge pull request #10413 from DUzzL/fix/10319-golem-reset-anger
🤖 Agent: start() matches NeutralMob.stopBeingAngry plus IronGolem.startPersistentAngerTimer (nextInt(381) + 400 = rangeOfSeconds(20, 39)) at target priority 4. We moved the creeper test's doc comment back onto its own test: the new test had been inserted between them.
2026-11-06 07:10:48 +01:00
Ludovic Marie ef207a7c92 Merge pull request #10391 from masa-masa-0314/feat/realms-tier-distance
🤖 Agent: the tier fetch and both PUTs hit the same paths as RealmsClient.getRealmTierConfiguration/updateRenderDistance/updateSimulationDistance, and a reply missing either range is an error as in vanilla; nothing beyond what the vanilla settings tab offers.
2026-11-06 07:10:43 +01:00
Ludovic Marie c86e080406 Merge pull request #10388 from voidsipp/fix/conversion-command-tags
🤖 Agent: only Tags move to the new id, as ConversionType.convertCommon copies them; the old raw compound (UUID, species data) is left behind, which the save/reload test checks.
2026-11-06 07:10:39 +01:00
Ludovic Marie 26243ba3b7 Merge pull request #10392 from voidsipp/fix/7436-fishing-ground-motion
🤖 Agent: hook_block_hit now runs on the client before moving, as FishingHook.checkCollision does on both sides, and gravity is f64::from(0.03f32) (getDefaultGravity is a float).
2026-11-06 07:10:35 +01:00
Ludovic Marie a4f021ec67 Merge pull request #10414 from voidsipp/fix/4832-remote-pig-steering
🤖 Agent: matches Pig/Strider.getControllingPassenger (saddled, first passenger a player holding the right stick in either hand); the server side reads the same rule through moved_by_client, so it no longer simulates the mount twice.
2026-11-06 07:10:31 +01:00
Ludovic Marie a7bf97e67d Merge pull request #10417 from voidsipp/fix/next-1-glint-override
🤖 Agent: component_bool matches NbtOps.getBooleanValue (doubleValue != 0, so 0.5f and NaN are true, Short 256 too), not NumericTag.asBoolean's byteValue; that is the Hypixel glint_override report.
2026-11-06 07:10:28 +01:00
Ludovic Marie 11e2482ae5 Merge pull request #10426 from voidsipp/fix/levitation-fall-distance
🤖 Agent: LivingEntity.aiStep resets fall distance for slow_falling or levitation before travel, NoAI included; dropping the later travelInAir-style reset is right, it erased the current tick's descent.
2026-11-06 07:10:24 +01:00
Ludovic Marie 363906684c Merge pull request #10430 from voidsipp/fix/10397-packet-utf8-replacement
🤖 Agent: packet_utf8 differs from from_utf8_lossy only on ED A0..BF (encoded surrogates), which Java consumes as one replacement; the length limits still use the encoded bytes first and UTF-16 units after, as Utf8String.read does.
2026-11-06 07:10:20 +01:00
Ludovic Marie a567b64f20 Merge pull request #10433 from voidsipp/fix/10399-unreadable-moderation-lists
🤖 Agent: reading the bytes once and parsing them removes the exists()-then-load race too; only NotFound starts an empty list, so a directory or a locked ops.json now stops the stopped-server command instead of being overwritten.
2026-11-06 07:10:16 +01:00
Ludovic Marie faa7e2746f Merge pull request #10407 from DUzzL/fix/10323-evoker-fangs-teams
🤖 Agent: fangs_allied follows Entity.isAlliedTo both ways (AbstractIllager tag only when both are teamless, Evoker's vex root-owner rule); guests on the evoker's team are still bitten, as before, which is a follow-up rather than a regression.
2026-11-06 07:10:12 +01:00
Horizon-agent b4197ddd94 Merge remote-tracking branch 'origin/main' into w-10471
# Conflicts:
#	crates/horizon-client-render/src/gfx.rs
2026-11-06 07:08:12 +01:00
Ludovic Marie ad8663977c Merge pull request #10459 from TheMrZZ/fix/armor-stand-command-equip-events
🤖 Agent: follows LivingEntity.onEquipItem (same item+components or firstTick: nothing; EQUIPPABLE on the new stack picks equip over unequip) and data merge keeps first_tick. We removed the docs/parity run-log file.
2026-11-06 07:03:36 +01:00
Ludovic Marie 0b3a4bda9c Merge pull request #10424 from TheMrZZ/fix/hostile-sound-categories
🤖 Agent: all nine families are Monster subclasses (entity.hostile.* is Monster's shared hurt/death/fall set), so HOSTILE is right; the list scan is 34 short strings per sound, negligible. We removed the docs/parity run-log file.
2026-11-06 07:03:32 +01:00
Ludovic Marie 5fb8f59ea0 Merge pull request #10432 from TheMrZZ/fix/numeric-damage-component
🤖 Agent: DataComponents.DAMAGE goes through Codec.INT (Number.intValue: truncation, Long keeps its low 32 bits), and dynamic::as_int already does that. We removed the docs/parity run-log file.
2026-11-06 07:03:29 +01:00
Ludovic Marie 62d2b18c16 Merge pull request #10400 from TheMrZZ/fix/inline-fishing-tool-context
🤖 Agent: LootCommand.dropFishingLoot's no-tool endpoint passes ItemStack.EMPTY, and the code only reads tool_stack when a tool, mainhand or offhand was given, so an omitted tool stays empty. We removed the docs/parity run-log file.
2026-11-06 07:03:25 +01:00
Ludovic Marie 17541e8bff Merge pull request #10387 from TheMrZZ/fix/baby-pig-passenger-attachment
🤖 Agent: Pig BABY_DIMENSIONS has PASSENGER (0, 0.5, 0), so the one match arm is right. We removed docs/parity/baby-pig-passenger-attachment.md: run logs and hashes belong in the PR description, not in docs/.
2026-11-06 07:03:22 +01:00
Skr1zzzz a8107ee025 Player heads: parse a profile once, not on every draw
head_skin decoded the textures property (base64, JSON, URL checks) and hashed
the skin location (SHA-1) for every head on every frame, before asking the
atlas whether the skin was resident; an armor stand's worn head also turned
its profile NBT into JSON each frame. With 60 stands wearing textured heads
and 36 heads in the inventory that was ~9 us a head a frame (#4664: a server
full of menu heads, CPU-bound with the inventory open).

The pure part (profile -> textures -> skin location, failures included) is
now kept in a bounded cache beside the resolver; worn heads reuse the JSON of
an unchanged profile. The download state, atlas residency, drawn tracking and
eviction re-uploads still run per draw.
2026-11-06 09:02:52 +03:00
Kevin RamirezandClaude Fable 5.1 927b546c9a Crossbows: Flame ignites the arrows they shoot (#10482)
`Game::shoot_crossbow` set the arrow's weapon and Piercing count but never
ran the weapon's `projectile_spawned` ignite, which only the bow path
applied. In vanilla, `AbstractArrow`'s constructor runs
`EnchantmentHelper.onProjectileSpawned` for every arrow built with a
weapon, bow or crossbow, so a Flame crossbow shoots burning arrows that
prime TNT and set entities on fire.

The bow's `bow_projectile_ignite` becomes `ignite_spawned_arrow(weapon,
arrow)`, shared by both weapons; the crossbow ignites each arrow of its
volley after Piercing, as the constructor does. The port map rows follow
the rename, and a resource-backed regression checks no Flame, Flame and
Flame with Multishot.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-11-06 00:02:20 -06:00
Ludovic Marie fff2a81e41 Merge pull request #10395 from DUzzL/fix/10321-spawnpoint-bubble-column
🤖 Agent: confirmed in Blocks.<clinit>: water, lava and bubble_column are the only three blocks with .liquid(), so the name list is complete for Block.isPossibleToRespawnInThis.
2026-11-06 07:02:01 +01:00
Ludovic Marie 1f883e7a79 Merge pull request #10402 from Skr1zzzz/server-tick-spans
🤖 Agent: `tick_blocks` starts a fresh `laps` after the scheduled ticks, so the stretch between `tb_entity_inside` and `tb_broadcast` shows only through `random_ticks`, `tb_scheduled` and `tb_fluids`. The counters and laps cost one relaxed load each when spans are off.
2026-11-06 07:01:57 +01:00
Ludovic Marie c956e11ff8 Merge pull request #10470 from DUzzL/fix/10441-ghastling-sounds
🤖 Agent: HappyGhast.getHurtSound/getDeathSound pick GHASTLING_* by isBaby, and happy_ghast::sounds already does exactly that, so the new voice_sounds arm is the whole fix.
2026-11-06 07:01:53 +01:00
Ludovic Marie f6a9444deb Merge pull request #10416 from Skr1zzzz/spawn-parity
🤖 Agent: checked against 26.3: `ChunkAccess.getHeight` returns `getFirstAvailable - 1` and `getRandomPosWithin` adds the 1 back, so topEmptyY is Horizon's `world_surface` itself. The old +1 made the bound 130 for a column solid to y 63. run-parity's natural_spawn harness checks the draws end to end.
2026-11-06 07:01:50 +01:00
Ludovic Marie 0610b830ca Merge pull request #10434 from voidsipp/fix/4545-boat-paddle-transition
🤖 Agent: rowing_time now gates on paddle_controller like AbstractBoat.getPaddleState (flag AND a controlling passenger) and clamps a as Mth.clampedLerp does; #10386 carries this branch merged, queued after it.
2026-11-06 07:01:46 +01:00
Ludovic Marie 8a21328406 Merge pull request #10439 from Skr1zzzz/random-ticks
🤖 Agent: the `[false; 256]` table relies on a local palette never holding more than 256 entries (global palettes keep the per-block path). The test covers 1, 2, 256 and 300 states, a stale palette entry, and the LCG draws against an independent oracle.
2026-11-06 07:01:39 +01:00
Ludovic Marie d165254d41 Merge pull request #10393 from TheMrZZ/fix/h011-orb-saved-uuid
🤖 Agent: matches Entity.saveWithoutId: the orb's command NBT now reads its registered uuid_for(id) instead of a fresh random UUID per read; queued first of the 10393/10422/10458 stack (the other two carry this branch merged).
2026-11-06 07:01:34 +01:00
Ludovic Marie 0125ca5281 Merge pull request #10435 from Hiuoy/feat/model-layers
🤖 Agent: `native_bake_layer` re-reads entity-models.json on every call for a vanilla layer no mob has (the doc says so), so a mod that bakes per frame would hitch; caching that document is the natural next step. The humanoid-against-zombie#main bake test is the right oracle, and the fragment stamps cleanly (tests.test_stamp_native_hooks).
2026-11-06 07:01:29 +01:00
Ludovic Marie 743d6229fe Merge pull request #10419 from senjinthedragon/resize-free-first
🤖 Agent: the early return for an unchanged size stays ahead of the new `device.poll(wait)`, so the GPU sync happens once per real size change. A drag-resize pays it per new size, a fair price for not holding both sets of buffers.
2026-11-06 07:01:25 +01:00
Ludovic Marie 6df8d31833 Merge pull request #10456 from arslanarm/shaderpacks/smooth-across-teleports
🤖 Agent: `world_seen` never resets inside a `Custom`, so the restart on entering the Nether relies on the pack being rebuilt for each dimension. If `PackPost` is ever kept across dimensions, it will need resetting there.
2026-11-06 07:01:21 +01:00
Ludovic Marie cc4911fc2b Merge pull request #10457 from arslanarm/shaderpacks/colortex-mips-blend
🤖 Agent: only `Source::Color` gets the linear-mip sampler; shadowcolor, custom textures and the lightmap keep `Linear` (nearest level), which matches what was measured. Buffers without mipmaps read the same as before.
2026-11-06 07:01:17 +01:00
Ludovic Marie 8d1a46ef24 Merge pull request #10467 from arslanarm/shaderpacks/shadow-depth-filter-precision
🤖 Agent: the 256ths weights are a fit to one NVIDIA GPU but now apply on every GPU, so an AMD or Intel reading of Iris may move them later. The cost is two floors and a multiply per filtered shadow read.
2026-11-06 07:01:13 +01:00
Ludovic Marie a6a228a193 Merge pull request #10469 from arslanarm/shaderpacks/shadow-pass-spectator
🤖 Agent: the spectator rule keys on `pack_ids.is_some()`, so a native pack's shadow keeps the first-person model, as the test pins. The other game modes stay as before, since only spectator was measured.
2026-11-06 07:01:09 +01:00
Ludovic Marie 57140bb20f Merge pull request #10473 from arslanarm/shaderpacks/terrain-vertex-grid
🤖 Agent: the +1/32 bias moves only places within one float step under a 32768th line, so #9839's water tops (29094.34) and 13/15 offsets (28398.93) still floor as before, and the new test case pins the 1, 0.5 and 9.5 edges.
2026-11-06 07:01:05 +01:00
Ludovic Marie 0c7ddd45c5 Merge pull request #10436 from arslanarm/shaderpacks/shadow-pass-depth-culling
🤖 Agent: with the near and far planes dropped, the shadow pass now draws every section inside the box's sides out to its reach, so a pack with tight planes draws more shadow terrain than before. Worth watching in the cave perf numbers. #10471 conflicts with this in gfx.rs; I'll merge main into it once this lands.
2026-11-06 07:01:00 +01:00
Horizon-agentandClaude Opus 5.5 c9b0011952 Merge #10434 (boat paddles) into #10386
Both extend the boat authority fixture and harness: keep the paddle and
attack cases, both Case fields and both docstring paragraphs. The harness
keeps main's fast-profile build with #10434's --resources option.
Queued after #10434, so either landing first leaves the other mergeable.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 06:57:38 +01:00
Horizon-agentandClaude Opus 5.5 a25a11fbf7 Merge #10422 and #10393 into #10458
All three add a command_exec fixture group at the same lines and a test
module at the end of entity_nbt.rs; keep every group, flag, dispatch block
and module. Queued in order so whichever lands first leaves the others
mergeable.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 06:56:29 +01:00
Horizon-agentandClaude Opus 5.5 9f9daee867 Merge #10393 (orb saved UUID) into #10422
Both add a command_exec fixture group at the same lines; keep both groups,
their isolation sets, flags and dispatch blocks. Queued in order so either
landing first leaves the other mergeable.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 06:55:28 +01:00
DUzzL 468a73f128 Keep the lead in creative and break mob leads when the holder spectates 2026-11-06 06:54:41 +01:00
Horizon-agentandClaude Opus 5.5 c542322582 Float text harness: build the shared release world_probe
A debug world_probe at one job compiles horizon-world from scratch beside
the release probe every other horizon-world harness uses.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 06:54:15 +01:00
Horizon-agentandClaude Opus 5.5 264d5f38c7 Realms product page: the trial header without its description
Horizon does not ask trialAvailabilityTask. Vanilla shows the trial
description (and offers the trial in the footer) only when one is
available; otherwise the header is the title alone and the footer offers
Buy, which is what this screen shows.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 06:53:56 +01:00
Horizon-agentandClaude Opus 5.5 dd23018283 Keep the creeper test's doc comment on its own test
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 06:53:11 +01:00
Horizon-agentandClaude Opus 5.5 3b575a0016 Drop the run-log document from the PR
The PR description already holds the vanilla reference and the measured
results; docs/ keeps player and contributor documentation only.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 06:52:48 +01:00
Horizon-agentandClaude Opus 5.5 94d295d20c Drop the run-log document from the PR
The PR description already holds the vanilla reference and the measured
results; docs/ keeps player and contributor documentation only.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 06:52:45 +01:00
voidsip 625f6ca1a5 Match Happy Ghast leash movement and distances 2026-11-05 23:52:43 -06:00
Horizon-agentandClaude Opus 5.5 ec080faa9f Drop the run-log document from the PR
The PR description already holds the vanilla reference and the measured
results; docs/ keeps player and contributor documentation only.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 06:52:42 +01:00
Horizon-agentandClaude Opus 5.5 1d1b060e1a Drop the run-log document from the PR
The PR description already holds the vanilla reference and the measured
results; docs/ keeps player and contributor documentation only.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 06:52:37 +01:00
Horizon-agentandClaude Opus 5.5 5e8d9a5123 Drop the run-log document from the PR
The PR description already holds the vanilla reference and the measured
results; docs/ keeps player and contributor documentation only.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 06:52:26 +01:00
voidsip d12a260a53 Reject overflowing native shader float options 2026-11-05 23:52:06 -06:00
martipopsandClaude Opus 5.5 a7954d4bdc Merge the pad fix (#10474) into the gameplay guide
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 00:51:17 -05:00
voidsip 0d70c87082 Keep UTF-16 key order in compound SNBT 2026-11-05 23:47:01 -06:00
Horizon-agentandClaude Opus 5.5 3649dfb9eb json_file: drop is_json_file, which nothing calls
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 06:45:51 +01:00
Horizon-agentandClaude Opus 5.5 94d8386027 build.rs: keep the no-icon fallback for local builds
The GNU windres the launcher's native mod builds use can't take a path with a
space, so a local build there must go on without the icon (#10398 asked to keep
it); CI still fails without it. One Windows block for the stack and the icon.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 06:45:50 +01:00
Detimes da8c2cfc55 Native hooks: each line written to the log (log_line)
native_log_line(level, thread, message, line) hears each message
written to logs/latest.log, on the log's writer thread after the file
has it, so the thread that logged never waits on a mod and a Discord
console bridge needs no tailing of latest.log. line is the text as the
file got it. The logger calls it through logging::set_line_hook,
installed before the log opens; trait mods are reached through
NativeClientHooks. A callback that panics is not called again, so its
own panic line can't loop.

Refs #10461
2026-11-06 05:42:47 +00:00
Detimes 2f2eecf40d Native hooks: every console and RCON line before it runs (console_line)
native_console_line(source, line, cancel, replies) hears each line the
dedicated server's console is about to run, before vanilla's commands,
the dedicated-only ones and mods' registered ones: source is console
(typed on the console, or sent by horizon-server as the console) or
rcon. Set cancel and it does not run; replies are logged as the
console's feedback, so they also reach the RCON or horizon-server
caller. Until now native_perform_console_command only heard commands
a mod registered, so an audit mod could not see ordinary console
commands (Bukkit's ServerCommandEvent / RemoteServerCommandEvent).

Refs #10461
2026-11-06 05:42:46 +00:00
TheMrZZandCodex gpt-6.1-sol 81cf3ef358 fix: name actual macro argument NBT type
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-11-06 06:41:16 +01:00
Detimes 6e4fbfa79f Native per-player view distance (native_set_player_view_distance)
A mod limits one player's view distance below the server's: only that
player is told its radius (SetChunkCacheRadius) and is sent and tracks
chunks within it; native_player_view_distance reads it. The guest's
distance is ChunkMap.getPlayerViewDistance under the limit; with no limit
(every unmodded server) it is exactly as before. Part of #10461.
2026-11-06 05:40:48 +00:00
Detimes 9c23922432 Native world border read helper (native_world_border)
A level's world border bounds as the server enforces it, for a web map's
border line and a pre-generator's border selection. Vanilla 26.3 keeps a
border per level (ServerLevel.getWorldBorder, each level's
data/minecraft/world_border.dat); Horizon keeps one for every level, so
the helper answers the same bounds for each level until that is fixed.
No behaviour change. Part of #10461.
2026-11-06 05:40:07 +00:00
Detimes 2d2936b9df Native chunk tickets in a level nobody is in (native_force_chunk_in)
native_force_chunk only reaches the level in place, so a mod could not hold
a chunk of the Nether or the End from a server-wide hook, and a level not
open yet never ticked: a pre-generator for those levels never advanced.
native_force_chunk_in takes the dimension; a level held this way opens at
the next server tick and ticks while held. Unmodded servers hold no mod
tickets, so nothing changes for them. Part of #10461.
2026-11-06 05:39:58 +00:00
HiuoyandClaude Opus 5.5 5c1d4d53f6 Native hooks: PlayerTickEvent.Pre, the start of a player's server tick (#10464)
native_player_tick_pre() runs once a tick for every living player in the
world, at the top of their ServerPlayer.tick, before their digging, item
use and cooldowns. native_player_tick (API 31) already ends the same tick.
It fires in tick_guests after the checks that skip a dead player and one on
a server trip, so the pair brackets the same players. Single player's own
player is a guest of its internal server.

The fragment player_tick_pre.json labels it PlayerTickEvent.Pre (side
server, not cancellable) for horizon-port-pack events. 23 of ATM10's mods
listen to it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 09:36:24 +04:00
martipopsandClaude Opus 5.5 b32520088e Controllers: read Bluetooth Xbox pads from their layout, on any firmware
SDL rows match a firmware version exactly, and gilrs' copy has no row for
045e:0b13 at 0x0520. The pad then falls back to gilrs' default mapping: its
right stick is on ABS_Z/ABS_RZ, which nothing read, its triggers on
ABS_GAS/ABS_BRAKE, which that mapping doesn't name, and X and Y stay crossed
because it lists C and Z.

Recognise that layout from what the pad reports, so a new firmware works
without a row.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 00:28:36 -05:00
Arslan ArmanulyandClaude Opus 5.5 460ecfce99 Shader packs: terrain's mc_Entity.y reads 0 off fluids, as in Iris
Terrain's `mc_Entity.y` was -1 on every block but water and lava, and -1 on
those too when the pack has no block.properties. Iris 1.11.7, with probe packs
of ours, reads 1 on water and lava and 0 on every other block, whether
block.properties maps it, leaves it out, or the pack has no such file.

The sprite layers' table now starts each layer at (-1, 0), with y 1 on the
fluid sprites, before block.properties is read.

Fixes #10452

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 10:27:02 +05:00
Arslan ArmanulyandClaude Opus 5.5 b1574b9721 Shader packs: a program writing buffers sized relative and in pixels draws while they match
A composite program writing two colortex buffers was skipped whenever their
`size.buffer` lines differed, even when one relative size and one pixel size
came to the same size (`0.25 0.25` and `320 180` at 1280x720). Iris 1.11.7
draws it (a probe pack of ours: texel (100, 50) of both reads 100.5, 50.5 and
texcoord 0.314, 0.281).

Sizes of one kind that differ still skip the program as the pack loads. A mix
of the two kinds is kept, and the pass is skipped only at render sizes where
the buffers it writes resolve to different sizes.

Fixes #10454

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 10:26:01 +05:00
Arslan ArmanulyandClaude Opus 5.5 c9bc6104b3 Shader packs: a terrain vertex a float step under a 32768th sits on it
A shader pack's terrain gl_Vertex floors each vertex's place in its
section to a 32768th. Plants whose random offset is 9/15 of a block came
out a 32768th under a half block, where our probe packs read Iris
1.11.7's exactly on it: we guess, untested, that our float sums put them
a hair under the line. A place up to 1/32 of a 32768th under a grid line
is now on it.

Part of #8464

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 10:26:00 +05:00
Arslan ArmanulyandClaude Opus 5.5 02c05de455 Shader packs: shadowtex filters in whole 256ths, as our Iris readings fit
A pack's texture() on shadowtex0/1 through a plain sampler blended the
four texels with the exact fraction. Iris 1.11.7's filtered reads on our
NVIDIA GPU fit weights in whole 256ths: the fraction rounded half up on
each axis, the far corner's weight their product over 256 rounded half
up, the others the rest of each axis. #8464's lit probe then differs
from Iris's at no pixel of ow_ground_noon.

Part of #8464

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 10:25:52 +05:00
Arslan ArmanulyandClaude Opus 5.5 6a737285e8 Shader packs: the shadow pass takes the block entities of the sections it draws
A shader pack's shadow pass drew the block entities the camera's view
had built: a chest beside the view, in a section whose terrain the
shadow pass draws, was left out, and a spawner's mob out of the view
and out of those sections went in. Our probe packs' Iris 1.11.7 shadow
maps hold a chest wherever the shadow pass draws its section's terrain
and nothing of the one spawner in our scenes, which lies outside them.
The block entities of the sections the shadow pass drew last frame are
now added hidden from the camera, and a spawner's mob that is out of
the view and out of those sections is left out.

Part of #8464

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 10:25:51 +05:00
Arslan ArmanulyandClaude Opus 5.5 1354db6f08 Shader packs: a spectator's own model stays out of the shadow pass, as in Iris
In first person, a shader pack's shadow pass drew the spectator's own
model (its head) at the camera. Our probe packs' readings of Iris
1.11.7's shadow map in spectator mode hold the terrain below the camera
there.

Part of #8464

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 10:25:42 +05:00
HiuoyandClaude Opus 5.5 3e4828d791 port-pack events: ModConfigEvent counts as hooked when its three events are
37 of ATM10's mods listen to the parent ModConfigEvent, which hears the
three events NeoForge's config page documents under it: Loading, Reloading
and Unloading. config_loading, config_reloading and config_unloading answer
them, so the parent now counts as hooked rather than as another phase's:
93 of 326 names, up from 92.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 09:24:40 +04:00
Detimes d503874bff Let 26.2 clients join a Horizon server, off by default: handshake and packet ids
A server.toml option, [server] accept_older_clients (horizon-accept-older-clients), off
by default. On, a login handshake stating 776 opens an adapter::ClientSession on the
connection instead of being refused, and a 26.2 status ping is told its own protocol.
The session follows the connection's states as Session does, mirrored, and renumbers
packet ids both ways with the inverse of the generated 26.2 tables, in the blocking
login path and in the io_pool play path. Off, nothing changes.

Part of #10404.
2026-11-06 05:22:16 +00:00
Florian ERNSTandCodex f97e4b9527 Emit game events for armor-stand command equipment changes
Co-Authored-By: Codex <[email protected]>
2026-11-06 06:12:01 +01:00
masa-masa-0314andZCode 42849387d1 Realms: the template rows' website and trailer buttons
`RealmsSelectWorldTemplateScreen.Entry`'s 15x15 `icon/link` and
`icon/video_link` buttons beside each row's version, opening the
template's page or trailer; the drawn rectangles are kept for the click.

Co-Authored-By: ZCode <[email protected]>
2026-11-06 13:05:17 +08:00
TheMrZZandCodex gpt-6.1-sol 504b987937 Save armor stands' actual fall distance
Adopt and project the existing fall counter, including Java's per-movement float narrowing before double accumulation. Preserve travel and landing-call order, and cover the real tick/save route plus decoded saves without raw data.

Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-11-06 05:59:09 +01:00
masa-masa-0314andZCode c81874d772 Realms: the settings sliders' tooltips
`createDistanceOption`'s cached constant tooltips, shown while the
pointer is over either distance slider.

Co-Authored-By: ZCode <[email protected]>
2026-11-06 12:55:30 +08:00
Ludovic Marie a589a3dd9b Merge pull request #10449 from horizon-rust/fix/native-stale-engine-target
🤖 Agent: staged files now get the current time, so a Nightly's source unpacked with its commit's older mtimes can no longer pass for already-compiled crates (Windows/macOS fs::copy kept them); one-time stage rewrite for old stages.
2026-11-06 05:53:41 +01:00
Arslan ArmanulyandClaude Opus 5.5 8d38356bf4 Shader packs: smooth() keeps going through teleports, as in Iris
A custom uniform's smooth() restarted on every frame whose camera biome was
unknown, so after each teleport it started again, and it started from the
unknown frame's values (biome -1, biome_category 0, temperature 0): the
frame that restarted it also stored them, and the first frame that knew the
biome went on from there.

Now smooth() restarts only until the first frame that knows the camera's
biome, that frame included, and then keeps going for as long as the pack
runs. With a probe pack of ours, Iris 1.11.7 kept smoothing across
teleports within a dimension and started again when the player entered
the Nether.

Fixes #10450
Part of #9307

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 09:53:24 +05:00
Arslan ArmanulyandClaude Opus 5.5 4c0ef1d7df Shader packs: colortex mipmaps blend their two nearest levels, as in Iris
A shader pack's colortex read between two mipmap levels (textureLod at a
fractional level) took the nearest level: the buffers' sampler had the
default, nearest, mipmap filter. In Iris 1.11.7 our probe packs read every
colortex, in composite and in final, as the two nearest levels mixed. The
buffers now get a sampler of their own that blends them; shadowcolor and
the other textures keep theirs (not measured).

Fixes #10451
Part of #9307

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 09:53:24 +05:00
Horizon agentandClaude Opus 5.5 4b24e21a8a launcher: format the stamping test
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 05:49:06 +01:00
DUzzL ebafc91999 Use ghastling hurt and death sounds for baby HappyGhasts 2026-11-06 05:47:17 +01:00
HiuoyandClaude Opus 5.5 e925e25781 port-pack events: ModelEvent.RegisterAdditional is looked up as RegisterStandalone
NeoForge's documentation describes one event under two names. 1.21.1's
"Registering Additional Models" (ModelEvent.RegisterAdditional, one
ModelResourceLocation per model) is "Registering Standalone Models"
(ModelEvent.RegisterStandalone, a key and a baker per model) from 1.21.5 on.
Both versions use the same sentence, and the example method is still
registerAdditional. standalone_models (API 151) answers it, so the 47 ATM10
mods that listen to it now count as hooked: 92 of 326 names, up from 91.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 08:46:39 +04:00
Horizon agentandClaude Opus 5.5 1fe1d5e860 launcher: stamp staged native source files with the current time
A Nightly's source keeps its commit's mtimes when unpacked, and fs::copy
keeps them on Windows and macOS, so a changed engine file reached
native/stage/ older than the previous build and Cargo reused the old
horizon-core. sync now sets each written file's mtime to now; a stage
written before this is rebuilt once.

Fixes #10421

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 05:41:23 +01:00
masa-masa-0314andZCode 382f264b6a Realms: the Add Realm screen as vanilla's product page
The screen behind Add Realm is 26.3's `RealmsPdpScreen`: the header's
logo, the trial header, then the friends, minigames and private-server
entries with their 80x45 pictures and 182-wide text, scrolling between
the header and the footer, whose buttons are the PDP's own title (the
buy link) and Back.

Its old strings were dead: `mco.title` never existed, and 26.3's
deprecated.json removes `mco.selectServer.popup` and
`mco.selectServer.buy`, so the screen drew raw keys.

Co-Authored-By: ZCode <[email protected]>
2026-11-06 12:39:31 +08:00
repositorykreml1nandCodex f4b9bc1471 Merge upstream main to resolve PR #10015 conflicts
Keep the command-block /say LAN regression and upstream legacy PvP checks,
with both registrations intact. Preserve upstream function bodies and the
original say regression unchanged.

Validation: Python syntax/AST and registrations checked; public-tree check
passed; Python unittest suite passed (41 skipped). pr_ready could not start
Rust checks because Cargo is unavailable in this restored environment.

Co-Authored-By: Codex <[email protected]>
2026-11-06 07:37:01 +03:00
Ludovic Marie 679f8c6e3f Merge pull request #10440 from horizon-rust/fix/options-emote-key
🤖 Agent: options.txt is vanilla's key list again; the emote key is saved as emoteKey in horizon-extras.txt and an old key_key.emote line is migrated, options parity passed on the desktop.
2026-11-06 05:18:23 +01:00
Horizon agentandClaude Sonnet 5.5 c6d5769e4f Options: rustfmt keybinds.rs
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-06 05:13:24 +01:00
Ben 1ad899c7b5 fix: embed Windows icon resources for cross builds 2026-11-06 04:13:14 +00:00
voidsip b9439b3508 Match Java text for tiny floats and doubles 2026-11-05 22:12:54 -06:00
Arslan ArmanulyandClaude Opus 5.5 6a8ea6fc18 Shader packs: the shadow pass draws terrain outside the pack's near and far planes
Our probe pack's Iris 1.11.7 shadow maps hold the terrain at every depth we
put it along the light (from 535 blocks beyond the shadow camera to about 120
before it), whatever shadowNearPlane and shadowFarPlane the pack sets, or
none. Horizon dropped the sections outside the shadow projection's near and
far planes; since #9768 those are the pack's own planes with no offset, so
in our probe's cave scene with the planes at 0.05 and 256 the surface toward
the light was missing from the shadow map. A shaders/ pack's shadow pass now
culls by the box's sides only; Horizon's own packs keep their planes.

Fixes #10403

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 09:06:26 +05:00
Florian ERNSTandCodex 1e1c6a900f Decode numeric damage components with Codec.INT semantics
Co-Authored-By: Codex <[email protected]>
2026-11-06 05:03:08 +01:00
voidsip ff381b5dc4 fix(server): reject unreadable lists in stopped moderation commands 2026-11-05 22:02:46 -06:00
HiuoyandClaude Opus 5.5 7dc8ba568f Native mods: entity model layers built in code, baked and drawn (#10425)
A NeoForge mod builds its entity models in code (createBodyLayer: a
MeshDefinition of CubeListBuilder boxes at PartPoses, a LayerDefinition),
registers them in EntityRenderersEvent.RegisterLayerDefinitions, bakes them
with EntityModelSet.bakeLayer and draws the ModelPart tree it posed in
setupAnim with ModelPart.render. Native mods get the same:

- the builders and ModelPart, ported from vanilla (mobs/model_layers.rs):
  box UV from texOffs, mirror, deformation, texture scale and visible faces
  as ModelPart.Cube bakes them; poses, reset_pose, get_child;
- native_layer_definitions, a collect hook asked once at start, keyed by
  ModelLayerLocation ids in the mod's own namespace;
- native_bake_layer: a registered layer, or vanilla's by its
  entity-models.json name (minecraft:zombie#main);
- native_draw_model_part: ModelPart.render under a mob's or block entity's
  frame, textured from an atlas sprite.

The humanoid mesh built with these builders bakes to exactly vanilla's
minecraft:zombie#main (an ignored test against the prepared resources).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 08:02:00 +04:00
Ludovic Marie a1710b6962 Merge pull request #10429 from horizon-rust/perf/seen-snapshot
🤖 Agent: the kept buffer is cleared at the end of every tick, so no villager Arc<villager::Seen> outlives the tick, and #[inline] on Mob::seen only changes where each Seen is written (in its slot instead of a stack copy): same values, same order. creature_prepare 1.2 → 0.8 ms in both A/B rounds.
2026-11-06 04:56:53 +01:00
voidsip e7956591f3 Match vanilla replacement decoding for packet strings 2026-11-05 21:53:19 -06:00
Horizon agentandClaude Sonnet 5.5 4b352e89c1 Options: the emote key tests read a versioned options.txt, rustfmt
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-06 04:52:55 +01:00
Horizon agent 2726d8040b Merge remote-tracking branch 'origin/main' into perf/seen-snapshot
# Conflicts:
#	crates/horizon-client/src/game/creatures.rs
2026-11-06 04:52:00 +01:00
Horizon agentandClaude Sonnet 5.5 6da74ac228 Options: the emote key lives in horizon-extras.txt, options.txt is vanilla's again
Options parity regression from #10092: key.emote was in the vanilla key list, so options.txt
gained a key_key.emote line and every key after it shifted. It is now a Horizon-only mapping
(HORIZON_MAPPINGS): bound in Controls and translated like the others, saved as emoteKey in
horizon-extras.txt. A key_key.emote line written by the broken build is read once and dropped.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-06 04:44:03 +01:00
voidsip 6716cb7407 Merge remote-tracking branch 'refs/remotes/validation/worker7-main' into fix/4545-boat-paddle-transition
# Conflicts:
#	crates/horizon-client/src/game/remote_vehicles.rs
2026-11-05 21:43:23 -06:00
yahya-nusseibehandClaude Opus 5.5 58d5f3b519 Merge main into fix/nbt-heap-quota
Brings the branch up to date and re-runs CI: verify failed once in
horizon-relay's slow_first_lines_and_too_many_connections_are_closed (a
timing test, 5.6 s against its 3 s on a loaded runner), which this
change doesn't touch.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
2026-11-06 05:43:15 +02:00
voidsip 8181ce0222 Reset mob fall distance before travel 2026-11-05 21:42:45 -06:00
Ludovic Marie 76b8b4eb83 Merge pull request #10423 from horizon-rust/fix/sculk-ticking-mobs
🤖 Agent: a mob that did not tick still gets its last position refreshed (no step), so one moved by a command while out of range doesn't count a walk or a landing once it ticks again; damage and mount events stay for every mob since vanilla raises them from hurt/startRiding. Proven on vanilla 26.3 with ore_probe2.py (settled pig/item stop lighting redstone ore once the player is 6 chunks away).
2026-11-06 04:40:22 +01:00
Florian ERNSTandCodex 6303017b03 Classify the verified hostile sound families as hostile
Co-Authored-By: Codex <[email protected]>
2026-11-06 04:38:19 +01:00
Horizon agent e16a5ef0da Merge remote-tracking branch 'origin/main' into fix/sculk-ticking-mobs 2026-11-06 04:37:28 +01:00
voidsip 76addba6cc Tick remote boat paddles and stop empty boats rowing 2026-11-05 21:36:53 -06:00
Skr1zzzz cc643f4af4 Random ticks: a section's candidate mask classifies each palette entry once
Rebuilding a changed or new section's random-tick candidate mask asked
whether the block state ticks randomly for each of its 4,096 blocks. A local
palette holds a handful of states: each entry is now classified once and the
mask built from the decoded palette indices. Single-value and global
palettes keep the previous path. Same mask, same draws.
2026-11-06 06:35:23 +03:00
TheMrZZandCodex gpt-6.1-sol c843c1aa05 Save the modeled mob loot-pickup flag
Write CanPickUpLoot on every save through the existing virtual getter, preserving the Allay and sulfur-cube overrides instead of retaining stale raw bytes. Cover decoded and mutated models plus frozen actual command reads.

Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-11-06 04:30:58 +01:00
Skr1zzzz 283c6723c0 Natural spawning: a start's height ends at the column's first empty block
NaturalSpawner.getRandomPosWithin draws Y in [minY, getHeight(WORLD_SURFACE)
+ 1], and ChunkAccess.getHeight is getFirstAvailable - 1: the top is the
first empty block. Horizon's world_surface already returns that block and
the spawner still added one, so every start could land one block higher
than vanilla's, in the air above the surface, where vanilla's draw never
goes; nextInt's bound was one larger too, so the draws differed from
vanilla's from there on.
2026-11-06 06:22:19 +03:00
Ludovic Marie 1532c9c153 Merge pull request #10331 from horizon-rust/perf/lan-entity-state-cache
🤖 Agent: idle entities (outside the simulation-level entity-ticking range) no longer run sendChanges, as in vanilla's ChunkMap.tick, so their 60-tick MoveEntityPos stops; a bare state (no data) is never paired, and the idle check follows the tracker's viewers (placed or held). tick_lan −1.66 ms (−24 %) in 3 A/B pairs.
2026-11-06 04:22:17 +01:00
voidsip 23a880843d Preserve custom model data flags through the item render bridge 2026-11-05 21:20:17 -06:00
voidsip 7b4634a912 Fix client steering for saddled pigs and striders 2026-11-05 21:20:10 -06:00
Ludovic Marie 2daa740748 Merge pull request #10412 from horizon-rust/fix/remote-player-offhand-nametag
🤖 Agent: queued. Every tracked entity now records what it rides, so any passenger (item/block display included) hides the player's tag as vanilla's isVehicle does; one risk checked: a removed vehicle leaves a stale id in passengers' vehicle, harmless since tags look up live entities.
2026-11-06 04:19:50 +01:00
Ludovic Marie cd547654d9 Merge pull request #10406 from horizon-rust/fix/aistep-ticking-mobs
🤖 Agent: the gate is the set of mobs the loop actually ticked (Creatures::ticked), not a second ticking-area test, so a mob that walks out of range during its own tick still finishes its pickups as in vanilla; proven against a vanilla 26.3 server (pickup_probe.py: only the ticking villager and zombie take their item).
2026-11-06 04:08:51 +01:00
Horizon agentandClaude Opus 5.5 0ed3868329 Name tags: any passenger hides its vehicle's name, as isVehicle does
Vanilla's LivingEntityRenderer.shouldShowName hides a name tag for any
entity with a passenger (`!entity.isVehicle()`). The client only counted
mobs, text displays, armor stands and interactions as passengers, so a
server that hangs its own name over a player with an item display, a
block display or any other entity still got the player's tag drawn
under it: two names (#5666).

Every tracked entity now records the vehicle SetPassengers puts it on,
and the name tags read that, whatever the passenger's type. The typed
lists stay for placing the riders.

The rest of #5666 was fixed already on main: the sword drawn in the
off hand by #5111 (each remote hand renders its own stack), the double
tag of custom-named players by #8457, and text display and armor stand
riders by #9754.

Fixes #5666

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 04:07:46 +01:00
SenjinandClaude Opus 5.5 7b80ab60c7 Shader packs: free the old buffers before a resize makes the new ones
A pack with large buffers (SEUS PTGI at 3440x1440) held both sets at once while resizing and ran
out of video memory going back to fullscreen.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 04:00:58 +01:00
voidsip 6beb50ebcd Preserve boolean glint overrides in item model views 2026-11-05 20:58:38 -06:00
Horizon agent c1d16aabde Merge remote-tracking branch 'origin/main' into perf/seen-snapshot 2026-11-06 03:55:25 +01:00
Horizon agent c0c3096e85 Merge branch 'fix/aistep-ticking-mobs' into fix/sculk-ticking-mobs 2026-11-06 03:55:04 +01:00
Horizon agent 74150d5ef8 Merge remote-tracking branch 'origin/main' into fix/aistep-ticking-mobs 2026-11-06 03:54:43 +01:00
Skr1zzzz d3678c9112 Atlas readback tests skip without an adapter, as the module's others do
CI runners have no GPU: the two material-atlas tests panicked in
request_adapter instead of returning like layers_land_in_their_cells.
2026-11-06 05:53:06 +03:00
DUzzL dfb9757d97 Evoker fangs: honour scoreboard teams when deciding allies 2026-11-06 03:50:47 +01:00
Skr1zzzz a58d5700f3 Merge remote-tracking branch 'origin/main' into server-tick-spans
# Conflicts:
#	crates/horizon-client/src/game/creatures.rs
2026-11-06 05:50:36 +03:00
Skr1zzzz 9cdcf66e69 Spans: inside the server tick's mob spawning, block and creature steps
A slow tick named spawn_natural, tick_blocks and tick_creature_entities but
nothing inside them. With 100 bots they take ~11, ~21 and ~10 ms a tick, and
an index for spawn_selection's nearest-player shortlist saved nothing: the
time was elsewhere. Their steps now have laps (sn_*, tb_*, tc_*), and the
natural-spawn pass counts its attempts and spawns (sa_attempts/sa_spawns,
one note a tick). They show the attempt loop itself: ~18,850 attempts a tick
for ~3 spawns (#3910). Only when HORIZON_SPIKES is set.
2026-11-06 05:48:23 +03:00
Horizon agentandClaude Opus 5.5 c442edabd9 Merge origin/main; idle states follow the tracker's viewers (held players too)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 03:43:53 +01:00
Florian ERNSTandCodex 29f04e40ed Pass the tool to inline fishing loot
Co-Authored-By: Codex <[email protected]>
2026-11-06 03:42:10 +01:00
Horizon agentandClaude Opus 5.5 04f6bd1214 Sculk events: only mobs and items that tick step, land and flap
Steps, landings, splashes, flaps and stepping on redstone ore come from
an entity's own tick in vanilla (Entity.move, applyEffectsFromBlocks
from LivingEntity.aiStep and ItemEntity.tick), and ServerLevel.tick
ticks only the entities in entity-ticking chunks. Here every loaded mob
and dropped item on the ground stepped on the block under it each tick:
a pig or an item that had settled on redstone ore kept lighting it after
the player went six chunks away.

Those events are now for the mobs tick_creature_entities ticked (and
those standing in the ticking area, as the sculk fixture posts before
the mobs tick) and the items in the ticking area. A mob that did not
tick keeps where it is as its last position, without a step. Damage and
mounting are raised by hurt and startRiding, so they stay for every mob.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 03:29:26 +01:00
TheMrZZandCodex gpt-6.1-sol 27d093c94f Use registered XP-orb UUID in saved command reads
Reuse the existing identity getter rather than generating a UUID per read. Cover repeated saved reads, absent raw input and stale raw UUIDs through the actual Game getter and command fixture.

Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-11-06 03:21:06 +01:00
voidsip 36c0d3f938 Predict fishing bobber block hits before client movement 2026-11-05 20:20:54 -06:00
DUzzL 6bd507f89d Treat bubble column as liquid when validating a respawn point 2026-11-06 03:20:54 +01:00
masa-masa-0314andZCode 7a846eebc6 Realms: the settings tab's render and simulation distances
RealmsSettingsTab's two distance sliders, fed by
getRealmTierConfiguration (asked until it answers, as
tierConfigurationFetched does) and saved with updateRenderDistance /
updateSimulationDistance only when a value changed, alongside
saveSettings' existing name, description and region update.

Co-Authored-By: ZCode <[email protected]>
2026-11-06 10:17:04 +08:00
DUzzL bd152b1ecf Piglins: compare armour and weapons when both or neither item is wanted 2026-11-06 03:13:01 +01:00
Florian ERNSTandCodex 6f9981bfbf Use the baby pig passenger attachment when seating riders
Co-Authored-By: Codex <[email protected]>
2026-11-06 03:10:51 +01:00
voidsip 6cf1b6b74c Keep command tags when converting mobs 2026-11-05 20:10:33 -06:00
DUzzL 12454e54bd Port ResetUniversalAngerTargetGoal to iron golems 2026-11-06 03:08:44 +01:00
Horizon agentandClaude Opus 5.5 6b2c83656c Mob snapshot: build each Seen in its slot and keep the buffer
tick_creature_entities snapshots every loaded mob (`Seen`, ~400 bytes)
before the loop and again after each mob's tick. Mob::seen lives in the
client lib and was not inlined into the game crate, so each one was
built on the stack and copied into its slot, and the snapshot's
megabytes were allocated and freed every tick.

Mob::seen is #[inline] now, and the emptied buffer stays in Creatures
for the next tick. The snapshot holds the same values.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 03:07:34 +01:00
voidsip d7e1eaf955 Apply melee extra knockback to boats 2026-11-05 20:06:28 -06:00
08393ef6f7 Parse entity-models.json once at startup
Part of #9998.

Startup parsed entity-models.json (2.4 MB) into a serde_json::Value six
times: for the handoff's GameCpuResources, MobTypes, the player type, both
player arm models and the spin-attack model. The rest worker did five of
those one after another. Nothing in the document changes during a loader
generation, and a pack change starts a new one.

load_resources now parses it once on its own step beside load_core and
shares it as an Arc<EntityModelDocument> with every model loader. A
missing or malformed document leaves each caller on its old path, so
errors and fallbacks are unchanged; a dedicated server draws no models and
skips the parse.

Callgrind, the whole resource load in the game (to "resources loaded"):
21.576 G -> 20.631 G instructions (-4.38%); the entity-model path alone
1.594 G -> 0.349 G (-78.1%). A new test builds MobTypes from a shared
document and from its own parse and compares every type, model and layer.

A first draft by DeepSeek V4.1 Flash (pi coding agent), reviewed by Claude.

Co-Authored-By: pi coding agent (DeepSeek-V4.1-Flash) <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Fekd3xkgubE8VfXrALh2td
2026-11-06 03:04:20 +01:00
ebe3818eb7 Light a section from its palette's light info, not its state ids
light_chunk filled its 48x48xheight region from every non-air section by
unpacking 4,096 state ids into a scratch array, then reading each one
back, looking up LightTables::info and storing 2 of its bytes. That round
trip was 35% of a light job, and the flood fill only ever reads those 2
bytes.

PalettedContainer::unpack_ids_mapped decodes the packed storage through a
lookup table in one pass (out[i] = map[id of entry i]), with a fast path
for 4 bits per entry, a linear palette's usual width. light.rs's new
states_of maps a section's palette to the packed light info once (at most
256 entries), then copies the region's 16-wide rows from the staged
values. A section whose palette emits light stages full infos and seeds
block light and the queue per cell, in the same order as before. Air now
writes info[0], which is 0, the value the region already holds (asserted
in debug builds).

Callgrind, gen_light_bench --seed 1234 --radius 4 --threads 1 (81 lit
columns): the light job 1.991 G -> 1.260 G instructions (-36.7%), the
whole run -1.15%. The light job is 5-7% of the workers' CPU, so this is
about 2% of the chunk pipeline.

Exact: gen_light_bench's block and light checksum is identical for three
seeds and centres at 1 and 4 threads, light_chunk_matches_old_algorithm
(an independent light implementation over random 3x3 regions) passes
unchanged, and the new section_states_match_the_unpacked_ids compares
states_of with a walk over unpacked state ids for one value, 4 to 8 bits
and the global palette, with and without light sources.

A first draft by DeepSeek V4.1 Flash (pi coding agent), reviewed by Claude.

Co-Authored-By: pi coding agent (DeepSeek-V4.1-Flash) <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Fekd3xkgubE8VfXrALh2td
2026-11-06 03:04:19 +01:00
voidsip b67b146635 Validate forced Overworld respawn points 2026-11-05 20:03:51 -06:00
CocofireHDandopencode 7c57f5f906 prepared files: treat a killed export as unused
A killed exporter left an empty gameplay-facts.json (or another table) behind.
The preparer only checked that the path exists, so "prepare again" kept the
empty file, and the launcher called the folder prepared and started the game
on it. Trust an earlier export only when it still parses, and treat a folder
with an empty top-level JSON table as not prepared so Play prepares it again.

Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
2026-11-06 03:01:43 +01:00
voidsip 04f7aa963b Keep treasure map targets when carrying undecorated copies 2026-11-05 19:55:51 -06:00
voidsip 11dda135a8 Attribute bursts of short LAN events in slow tick notes 2026-11-05 19:52:54 -06:00
b2423d6d62 Write chunk-section blocks with one position lookup and walked words
Part of #9996.

BlockSection::set and BlockSection::fill_run are 7.0% of the proto phase
in the overworld and 9.4% in the Nether (callgrind on generate_proto).
Each write located its packed word twice, and fill_run rebuilt the word
mask for every block.

PackedStorage now hands out a Slot (word and bit shift) that read_at and
write_at take, so set locates the entry once; the palette-growth branch,
which re-packs the words, looks it up again. A run in one column moves
256 entries per Y, so while a word holds a whole number of those steps
(every width generation produces) the bit shift stays the same along the
run and WordRun writes each block with one load, mask and store. Widths
that do not divide the stride go one set per block, with the same palette
edits in the same order.

Live palette slots hold distinct states (sections loaded from disk are
rebuilt through set), so "already this state" is old_id == id and "was
air" is old_id == air_id, both resolved once per call. set on a uniform
section has its own small path.

Palettes, their order, the free list, counts, the chosen width and the
packed words are unchanged: the 8 chunkgen_bench snapshot files (terrain
and decorated; overworld seeds 12345 and -1, the Nether, the End) are
byte-identical to main, and a new unit test replays random set/fill_run
sequences over every palette width against main's fill_run.

Proto-phase instructions: overworld 6.497 G -> 6.340 G (-2.41%), Nether
2.974 G -> 2.859 G (-3.84%).

A first draft by DeepSeek V4.1 Flash (pi coding agent), reviewed by Claude.

Co-Authored-By: pi coding agent (DeepSeek-V4.1-Flash) <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Fekd3xkgubE8VfXrALh2td
2026-11-06 02:52:28 +01:00
CocofireHDandopencode 7cc9057fa6 Extras: keep the Zoom hint out of the Mods page's way in
The not-installed Zoom row pushed Page::Mods from Extras, which
every_setting_and_page_is_reached_from_one_place refuses: Options > Mods
already has the one way in. It is now a note that says where to install
Zoom; once the mod is built in the row is still its switch and Settings
button.

Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
2026-11-06 02:47:58 +01:00
voidsip 8a65d28c0d fix(server): recover interrupted region pruning before startup 2026-11-05 19:47:51 -06:00
Ludovic Marie 69fab597a6 Merge pull request #10203 from Detimes/feat-10174-mod-deps
🤖 Agent: Checked on the desktop against main 9f8f6842d (merges cleanly, lock passes --locked). All 19 new lock packages are permissive (MIT/Apache/BSD-3/ISC-style webpki/Zlib/BSL option on ryu); the only build scripts (mysql, mysql_common) just pass target env vars, no downloads; TLS is rustls+ring only, flate2 uses miniz_oxide, no openssl/native-tls/-sys/cmake. cargo tree for horizon-client (default, --no-default-features, --target all) is identical to main, so players' game and native-mod compile are unchanged; the launcher's cold cargo fetch gains 21 crates / +2.5 MB once (noise in wall time). Note for later: webpki 0.22 and rustls-pemfile are unmaintained upstream; worth moving to mysql 28 when it no longer rewrites the lock.
2026-11-06 02:45:58 +01:00
Florian ERNSTandCodex 8dd47b5ef2 Omit default hive block state from saved item patches
Co-Authored-By: Codex <[email protected]>
2026-11-06 02:37:28 +01:00
voidsip 790e5f14d4 docs: index design server and release guides 2026-11-05 19:35:16 -06:00
CocofireHDandopencode 3c01dce30d resources: name the JSON file a parse error came from
serde's "expected value at line 1 column 1" says nothing about which of the
prepared folder's tables is damaged, so a player with a killed export only saw
a crash report. Read the core tables through horizon_world::json_file, which
prefixes the path, and have the startup check name a top-level table left
empty before the window opens.

Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
2026-11-06 02:28:51 +01:00
Skr1zzzz 643d699d51 Player list: keep the shown list's layout between frames
While Tab is held every frame sorted every entry twice (once for the faces,
once for the rows), allocated team keys inside the comparator, rebuilt and
measured every row's text, split the header and footer, and pruned the
health states with a scan per row (#10108). TabList now keeps the sorted
order and the laid-out rows, rebuilt only when their inputs differ (players,
scoreboard, header/footer, width, font and language); the faces reuse the
order; sort keys are borrowed; the health states go by a uuid set. Hearts
still animate every frame.
2026-11-06 04:24:37 +03:00
Horizon agentandClaude Opus 5.5 66dc3229a0 Mobs outside the simulation distance pick nothing up
Mob.aiStep's looting, the villagers' item sensor and the fox, dolphin,
panda, piglin, allay and sulfur cube item passes run after the mob loop
over every loaded mob. In vanilla they are part of the mob's own tick,
and ServerLevel.tick ticks only the entities in inEntityTickingRange: a
villager six chunks from the nearest player, loaded but outside the
simulation distance, took the bread at its feet here and not there.

The loop records the mobs whose tick ran (Creatures::ticked), and those
passes leave the others alone.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 02:20:47 +01:00
AxxioandAntigravity 2948fb86ed game/maps: honour can_place_on when using a map on a banner in adventure mode
In adventure mode, vanilla allows ItemStack.useOn if the item's can_place_on component matches the clicked block. Previously, use_map_on_banner checked only !self.may_build() and returned early without checking can_place_on.

This honours can_place_on_in_adventure on the banner's block position, allowing adventure mode players with matching maps to toggle banner markers.

Co-Authored-By: Antigravity <[email protected]>
2026-11-06 02:18:05 +01:00
Ludovic Marie 9f8f6842db Merge pull request #10366 from horizon-rust/perf/sculk-entity-events
🤖 Agent: each mob footstep scanned every mob three times (two id lookups and the warden/allay listener loop); now an event with no sculk block in reach and no warden or allay within 24 blocks skips the context and the listeners. Listeners give up past 16 blocks before any side effect, so vibrations are unchanged.
2026-11-06 02:16:56 +01:00
TheMrZZandCodex gpt-6.1-sol 7d5d34ae32 Play ordinary arrow hits in the neutral sound category
Override only ArrowSound::HitGround through Audio::play_on. Add an actual Game/Java block-hit capture using the existing headless client, Studio recorder and official sound codec.

Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-11-06 02:16:39 +01:00
MSedgeMC 6fa16423ac Entity saves: a warden's anger keeps its suspects by UUID
`Warden.addAdditionalSaveData` stores an `AngerManagement`: a `suspects` list of
{uuid, anger} pairs, and nothing else about the anger. Horizon wrote an empty
`anger` compound and read nothing back, so a reloaded warden was calm.

`AngerManagement` now keeps the suspects that are not in the level by UUID next
to the live ones, both calming by one an anger tick, and looks the UUIDs up
again every two anger ticks through the game layer's map
(`Game::who_by_uuid`, reached from the mob layer through `MobWorld`'s new
`WhoLookup` seam). The chunk save names a suspect by its UUID, minting one for
a loaded suspect that has none yet.
2026-11-06 01:09:42 +00:00
MSedgeMC 712305de32 Entity saves: a UUID bridge for references to a mob
A save can now name an entity it only knows as a `Who`: `mob_to_nbt` takes a
`uuid_of` resolver, `who_uuid` writes a `Who` as a UUID and `who_by_uuid` turns a
stored UUID back into the `Who` of a loaded player or mob. `EntityMemory::uuid_of`
supplies the id -> UUID map the game layer keeps, and the game layer's `mob_by_uuid`
now resolves through the same seam.

The neutral mobs' `angry_at` is the first writer: anger at another mob now saves
that mob's UUID instead of nothing. The warden's suspect list and the shulker
bullet's target are the two follow-ups this bridge unblocks; neither is written
here.
2026-11-06 01:09:42 +00:00
Ludovic Marie 1525ae0e3b Merge pull request #10364 from horizon-rust/fix/founder-claim-use-checkout
🤖 Agent: launcher-only fix. Maps the service's 403 use_checkout / not_offered_here to their own messages (86 tables), and drops one_click after use_checkout so the Shop opens the free checkout. Risk: none on the hot path; only the claim's error branch changes. Launcher tests 719 passed on the desktop.
2026-11-06 02:09:16 +01:00
Ludovic Marie 09450fb6f9 Merge pull request #10361 from horizon-rust/perf/mob-loop-overhead
🤖 Agent: villager_pickups tested every loaded villager against every dropped item, looking up tags before the reach: that was 12.8 of 23 ms of the mob phase in the load test (22.7 → 8.1 ms after this). The new ItemScan keeps the full scan's order and falls back to it if the item list changes under it.
2026-11-06 02:07:26 +01:00
Florian ERNSTandCodex 97d414cc4d Load animal age and love with NumericTag integer conversion
Co-Authored-By: Codex <[email protected]>
2026-11-06 02:04:52 +01:00
Horizon agentandClaude Opus 5.5 53bc816fa1 Launcher: rustfmt the claim refusal test
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 02:03:19 +01:00
Ludovic Marie 2993ef0b20 Merge pull request #10116 from horizon-rust/pvp/legacy-knockback
🤖 Agent: merging. Every knock_back caller outside the legacy rules passes legacy = false, so the 26.3 path is unchanged. 1.8's air lift and the unhalved sprint push only run under the hosted-world switch.
2026-11-06 01:53:48 +01:00
Horizon agentandClaude Opus 5.5 1411281eea Launcher: a refused founder claim says why and leads to the checkout
The cosmetics service answers POST /v1/claim with 403 use_checkout where
Paddle sells (the set goes through its free checkout there) and 403
not_offered_here where the claim is left out. The launcher mapped neither,
so a player saw "Horizon's cosmetics service had a problem" (Discord bug
thread, 2026-11-05).

- Both codes are the Shop's own refusals now, each with its own words, in
  all 86 lang tables.
- After use_checkout the saved answer drops one_click, so the Wardrobe stops
  offering "Claim for free" and the Shop's button opens the free checkout on
  playhorizon.dev.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 01:52:42 +01:00
Skr1zzzz f0de57ae23 Merge remote-tracking branch 'origin/main' into mp-fps-4664
# Conflicts:
#	crates/horizon-client/src/game/scoreboard.rs
2026-11-06 03:52:21 +03:00
Ludovic Marie 12d7eaccdb Merge pull request #10115 from horizon-rust/pvp/legacy-server
🤖 Agent: merging. [combat] is only written when legacy is on, and unknown keys are rejected. The game reads it in dedicated.rs, and a bad section logs and keeps 26.3's rules. The test now looks for the section line, not the template's '# [combat]' comment, which made verify fail on the first head.
2026-11-06 01:51:17 +01:00
Horizon agent 92bc3bc40f Merge remote-tracking branch 'origin/main' into perf/sculk-entity-events 2026-11-06 01:50:16 +01:00
Horizon agent d6d7cc3e29 Merge remote-tracking branch 'origin/main' into perf/mob-loop-overhead 2026-11-06 01:49:56 +01:00
Ludovic Marie 342824b506 Merge pull request #10114 from horizon-rust/pvp/legacy-rules
🤖 Agent: merging. legacy_rules() is remote.is_none() && combatRules, so a window on any server keeps 26.3's cooldown and damage. Measured over LAN: 1.8 rules 7 and 7 (diamond axe, charged then right after a swap), 26.3 9 and 3.6.
2026-11-06 01:48:43 +01:00
Skr1zzzz 2f075689f5 Multiplayer: skip name tags whose entity is off screen before laying out their text
Every frame built, styled and measured the name tag of every player within
64 blocks, drawn or not: with 150 players and the camera facing the sky that
was ~0.74 ms a frame (#4664). A tag is now built only for an entity vanilla
would render, its culling box grown by 0.5 in the frame's frustum
(EntityRenderer.shouldRender), checked before any text work; armor stands
likewise. Status and native snapshot queries still see every tag.

other_players finds each player's entity through one id->index map instead
of scanning every entity per player (the first match, as iter().find).
2026-11-06 03:47:29 +03:00
Ludovic Marie 195515000a Merge pull request #10112 from horizon-rust/pvp/legacy-effects
🤖 Agent: merging. The 1.9 hit sounds and particles are filtered on receipt (remote.rs, lan_particles.rs), so nothing sent changes. Hurt sounds and crit particles stay, and 1.8 hit sounds are off by default.
2026-11-06 01:46:16 +01:00
rison1337andCodex GPT-6 3f11bff706 Keep blob-shadow culling enabled with native mods
Remove the blanket trait-mod presence bypass while preserving shader-pack guards. Verify the projected shadow in a game with an active native camera hook.

Co-Authored-By: Codex GPT-6 <[email protected]>
2026-11-06 05:44:30 +05:00
Ludovic Marie 1ff952a4f0 Merge pull request #10111 from horizon-rust/pvp/legacy-hand
🤖 Agent: merging. Legacy only moves the hand's matrices, and only for items with vanilla's flat display (pack-moved and post-1.8 items keep 26.3's). It's off by default, and the renderer is the only reader of the equip state it changes. On scripted fights against a vanilla server, #10117's Modern-vs-Legacy packet comparison matched on every scenario that is stable between two identical clients.
2026-11-06 01:43:50 +01:00
CocofireHDandopencode 27b110085e Docs: note the Zoom row on the Extras page
Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
2026-11-06 01:40:05 +01:00
CocofireHDandopencode 5bb149a282 Extras: a Zoom row that reaches the mod that replaced the extra
Zoom moved from the built-in extras to Horizon's official native mod
(#2030), so the Extras page lost its Zoom setting. Players looking for it
there, including the macOS report in #8713, found nothing.

The Extras page now keeps a Zoom row: with the mod built in it is the mod's
own on/off switch plus a Settings button that opens its page; without the
mod it opens the Mods page where it is installed. The cause is not
macOS-specific: every platform lost the row when Zoom moved.

Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
2026-11-06 01:40:04 +01:00
rison1337andCodex GPT-6 4b3116c54f Keep flame culling enabled with native mods
Remove the blanket trait-mod presence bypass. Retain shader-pack and TAA guards, and document the native camera-hook image checks.

Co-Authored-By: Codex GPT-6 <[email protected]>
2026-11-06 05:37:40 +05:00
PzmuV1517 7b2ce7a1bc Shaders: the shader screen's words in every language
The shader screen's 17 new captions and descriptions, in the 86 languages besides English.
2026-11-06 02:30:40 +02:00
PzmuV1517 9e90050c60 Shaders: the old shader pack pages go
The shader screen (shader_ui) draws the pack list and every pack's settings,
so the options screen's own rows for them go: the Shader Packs and Shader
Options button lists, a Horizon pack's option pages, their actions and the two
pack slider kinds, with the label fitter only they used and the four lang keys
only they read.
2026-11-06 02:30:40 +02:00
PzmuV1517 131d5d73e2 Shaders: the shader screen reads the picked pack's settings however it opens
A button that pushes the Shader Packs page (as one on the Options screen would) listed the packs but left the picked pack's settings unread until a pack was picked again. Every way into the screen now reads them with the list.
2026-11-06 02:30:40 +02:00
AxxioandAntigravity b08f1074b1 docs: remove stale "not ported" notes
Co-Authored-By: Antigravity <[email protected]>
2026-11-06 01:28:10 +01:00
AxxioandAntigravity 9a076b6a24 docs/README.md: index missing player, launcher and platform pages
Co-Authored-By: Antigravity <[email protected]>
2026-11-06 01:26:57 +01:00
Horizon agentandClaude Opus 5.5 b40c996fb9 Mob looting: walk only the items in the cells a reach meets
A mob near some dropped item tested every item on the level (2,000 in
the load test: 1.5 ms a tick). ItemAreas now keeps the items of each
cell, and ItemScan walks the list in its order from one item in a met
cell to the next, testing what the scan tested; it keeps up with the
items the walk removes, and tests every item from there on if the list
changes any other way. The villagers' pickups walk the same way.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 01:26:11 +01:00
PzmuV1517 f488baa0ab Shaders: the shader pack list and each pack's settings in the settings frame
Shader Packs... opens a screen in the frame of the graphics and controller
settings (settings_shell) instead of two vanilla button lists. Its first page
lists the installed packs (a tick box on the one in use; picking it again
turns shaders off), the pack folder, and Horizon's own Shadow Cache under
"Built-in settings". Under the picked pack's name, the page list holds the
pack's own screens (a shaders/ pack's screen.NAMEs, a Horizon pack's
[[pages]]), as "Pack settings": switches, sliders, choices, profiles and links
to deeper screens, with the pack's own descriptions in the strip. The search
finds an option on any of the pack's screens. A shaders/ pack's changes apply
with Apply or when the screen closes; picking another pack keeps the last
one's changes first.
2026-11-06 02:24:17 +02:00
Florian ERNSTandCodex 1d13a300fa Keep guest respawn world information separate from personal landing
Co-Authored-By: Codex <[email protected]>
2026-11-06 01:23:39 +01:00
Horizon agent 99e1caf4db Merge remote-tracking branch 'origin/main' into perf/sculk-entity-events 2026-11-06 01:22:32 +01:00
Horizon agent 400335608e Merge remote-tracking branch 'origin/main' into perf/mob-loop-overhead 2026-11-06 01:22:31 +01:00
CocofireHDandopencode 54642a5e80 Pin the hopper minecart's one-item-a-tick transfer (#8668)
MinecartHopper.tick has no transfer cooldown in 26.3: it calls tryConsumeItems every tick, and consumedItemThisFrame caps a tick at one item even when makeStepAlongTrack tries too. The reported pre-#2250 whole-stack pickup is gone; add the regression test that catches it coming back, and cite the per-frame cap next to hopper_cart_suck.

Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
2026-11-06 01:20:48 +01:00
Ludovic Marie e5875b0b19 Merge pull request #10342 from horizon-rust/agent/native-call-attribution
🤖 Agent: launcher-only fix in native mod error tracing; fmt/clippy/613 lib tests green on the desktop, new test fails without the fix.
2026-11-06 01:18:17 +01:00
TheMrZZandCodex gpt-6.1-sol 94b5bf1aa0 Fix dropped-item Health short conversion
Floor floating Health values before short narrowing and preserve integer narrowing. Add an actual decode-to-save regression and an isolated Game/Java command fixture.

Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-11-06 01:18:04 +01:00
Horizon agentandClaude Opus 5.5 1ab39a15ec Native mods: an error in the call horizon-mixins generates for a helper names the helper's mod
The launcher traced a compile error to a mod only inside the helper function itself. horizon-mixins
wraps each helper in a block that calls it, `{ fn helper(…) { … } helper(…) }`. When Horizon
changes a type the helper's signature mentions (flair 1.0.0: `extra` became five-element tuples),
the error lands on that generated call, so no mod was named and the whole build failed instead of
leaving that mod out. A block holding only the helper and its call now counts as the helper's.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 01:16:25 +01:00
rison1337andCodex GPT-6 5905a4977f Skip offscreen burning-entity flame geometry
Cull conservative bounds of flame billboards before light lookup and vertex expansion. Keep shader/mod/TAA paths and unsupported dimensions, with emitted-geometry and edge-image coverage plus paired FPS samples.

Co-Authored-By: Codex GPT-6 <[email protected]>
2026-11-06 05:16:18 +05:00
Skr1zzzz 98d37c0887 Spans: the frame's prepare steps
HORIZON_SPIKES named almost nothing inside bench's prepare phase: with 150
players on a server and the camera facing the sky, prepare took ~0.43 ms a
frame and its spans accounted for ~0.005 ms. Each step between the fixed
ticks and the renderer now has a lap (pr_hud, pr_entities, pr_lights,
pr_frame, ...), dynamic lights two finer ones. They showed the cost: name
tags (eg_name_tags) built every frame for every player, drawn or not (#4664).
2026-11-06 03:13:25 +03:00
CocofireHD be24d83d97 Merge remote-tracking branch 'origin/main' into fix/10273-reoccuring-freezes-on-diamondfire-server-mcd
Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
2026-11-06 01:12:19 +01:00
CocofireHDandopencode 89ca6821b8 test: keep docs/README.md links and benchmark index resolving
Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
2026-11-06 01:10:13 +01:00
CocofireHDandopencode 4eafef1180 docs: index the benchmark and performance result pages
Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
2026-11-06 01:10:11 +01:00
CocofireHDandopencode 561f797c04 Multiplayer: a guest server's resent columns keep the packet's light
A resident column the server sends again was always relit here, for a
Horizon server's bulk edits whose packet carries the light from before
its relight. A proxy's next server resends its whole world at the same
coordinates, so every column queued light jobs for itself and its eight
neighbours, which a small CPU could not work through.

Relight a resent column only when the server announced Horizon's quiet
commands, the same feature flag Horizon servers send in configuration;
any other server's packet light is final, as vanilla applies it. A
reconfiguration resets the announcement until the new server sends its
own.

Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
2026-11-06 01:05:31 +01:00
Evrim Cicek dcfa4b07f7 Merge upstream/main 2026-11-06 03:05:16 +03:00
Evrim CicekandClaude Opus 5.5 00b9953741 Dragon: hover's fly speed, Mth.wrapDegrees, Mth trig on the server step, scanning's 10 degrees
From review, each measured against vanilla:
- DragonHoverPhase.getFlySpeed is 1, not the default 0.6: a hovering dragon thrown upwards
  climbs back at vanilla's rate (new harness case, 12 commands off before).
- wrap / wrap64 are Mth.wrapDegrees: Java's % then one step of 360. rem_euclid lost the low
  bits of a negative angle (-10.123456 became -10.123444) every tick (unit test).
- The parts' placement, the strafe aim and the flap timing use Mth.sin / Mth.cos and vanilla's
  float casts.
- DragonSittingScanningPhase turns only while the player is more than 10 degrees off the
  heading, with Mth.atan2 and vanilla's degree constant; dist is clamped in both divisions.
- A dragon's parts start at the origin, as vanilla's part entities do until the first aiStep,
  so its first scanning turn aims from there.

The command execution harness gets "dragon scanning turn" (a dragon facing a survival player and
one turned away): 6/12 before, 12/12 now. All dragon and End harnesses match; the whole command
execution harness: 15832/15832.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 03:05:04 +03:00
Horizon agentandClaude Opus 5.5 e82ac45192 Format the idle-entity change
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 01:04:26 +01:00
MSedgeMC fc8db41b0d fix: a shulker's bullet keeps its target through a save
ShulkerBullet.addAdditionalSaveData writes `Target` (the UUID it homes on, only
while it has one), `Dir` (`Direction.LEGACY_ID_CODEC`, the step it is on, absent
until it picks one), `Steps` and the per-axis target deltas `TXD`/`TYD`/`TZD`,
over `Projectile`'s owner and LeftOwner; readAdditionalSaveData reads them back
and keeps `Target` as an `EntityReference` it re-resolves every tick.

Horizon's `saved_with_chunk` turned a bullet with a target away - which is every
bullet a shulker fires - and `spit_or_bullet_to_nbt` refused it too, so the chunk
save dropped the bullet, and nothing read a step or a delta back.

The writer writes those keys now, the reader reads `Dir`, `Steps` and the deltas
into the kind, and `Target` goes through the UUID bridge: `Game::save_who_uuid`
answers it with `who_uuid` over `EntityMemory::uuid_of` (nothing for a mob the
level has no UUID for, never a made-up one), and `Game::bullet_target` resolves
the stored UUID with `Game::who_by_uuid`, which names the player or a mob that is
loaded. A bullet whose target is not loaded comes back with none and falls, as
`ShulkerBullet.tick` does with a target it cannot find, rather than homing on
whatever its old id means now.

A loaded bullet's own random stream is seeded from where it is, since
`Entity.random` is not saved: the same stand-in `Projectile::new` gives an
arrow's.
2026-11-06 00:03:08 +00:00
MSedgeMC b07b8f865b Entity saves: a UUID bridge for references to a mob
A save can now name an entity it only knows as a `Who`: `mob_to_nbt` takes a
`uuid_of` resolver, `who_uuid` writes a `Who` as a UUID and `who_by_uuid` turns a
stored UUID back into the `Who` of a loaded player or mob. `EntityMemory::uuid_of`
supplies the id -> UUID map the game layer keeps, and the game layer's `mob_by_uuid`
now resolves through the same seam.

The neutral mobs' `angry_at` is the first writer: anger at another mob now saves
that mob's UUID instead of nothing. The warden's suspect list and the shulker
bullet's target are the two follow-ups this bridge unblocks; neither is written
here.
2026-11-06 00:03:08 +00:00
Ludovic Marie 764df32826 Merge pull request #9987 from horizon-rust/mods/hotload-design
🤖 Agent: merging the mod hot-load design note, docs only. The point to check is the native measurement it rests on: a one-line gameplay change rebuilds in 570 s against 812 s cold, and mold doesn't help, which is why the run-time switch came before background builds.
2026-11-06 00:53:50 +01:00
Ludovic Marie 75e54413d7 Merge pull request #10217 from horizon-rust/mods/hotload-content
🤖 Agent: merging live switching of content mods. The point to check is the server's world tables: with no data pack they used to stay on the startup resources, so a switched mod's data never applied. After a live switch they are now built from the new root (data_switched), checked in game by a mod function that loads, unloads and loads again.
2026-11-06 00:53:47 +01:00
PzmuV1517 623aa1d3ea Shader packs: a texel offset given as a parameter moves the coordinate
GLSL asks a texel offset to be a constant expression; GL drivers also take one a
function receives as a `const in ivec2` parameter (Arc's FXAA samples its neighbours
that way, so its final pass was skipped in every dimension). Such an offset now moves
the coordinate by that many texels of the base level; a constant one stays an offset.
2026-11-06 01:52:10 +02:00
PzmuV1517 82e2c96a20 Shader packs: CAT_* and PPT_* are macros of the stage files too
The biome categories and precipitations custom uniforms compare with are defined for the
GLSL as well, with the same values: Sildur's Enhanced Default reads
`biome_category == CAT_THE_END` in gbuffers_skytextured, which failed on the unknown
name. BIOME_* still needs the world's registry and stays in custom uniforms.
2026-11-06 01:52:10 +02:00
PzmuV1517 ee50f8ae1a Shader packs: empty declarations, precision on parameters, infinities in functions
Three things GL drivers take that naga's GLSL front end refused:
- an empty declaration (`;`) at global scope in any version, as a macro that writes a
  function leaves after it (DrDestens: 56 programs skipped);
- a precision qualifier on a function parameter (`float rand(highp vec2 p)`,
  TrailerShaders: final and composite);
- a constant expression in a function whose value is infinite or NaN
  (`1.0 / toeX` with toeX 0, Epoch's tonemap at its defaults): it is computed at run
  time, as GL does, instead of folded into a literal that can't hold it. WGSL keeps
  refusing it.
2026-11-06 01:52:10 +02:00
PzmuV1517 bfc40195eb Shader packs: texture() and textureLod() read integer samplers
GLSL lets a pack sample a usampler2D or isampler2D with texture(); GL drivers read an
integer texture with nearest filtering, so it is the texel the point falls in. naga
rejects sampling an integer image, and Mello, Voyager, Exposa and Reverie lost the
programs that read their integer colortex buffers that way (Mello 20, Voyager 42, Exposa
48). Those calls now go to helpers that texelFetch the texel under the point, clamped to
the edge, at level 0 (textureLod: the level it names).
2026-11-06 01:52:10 +02:00
PzmuV1517 cb65d4f80d Shader packs: a global constant reads an element of a constant array
`const vec4 s = vec4(scale[0].x, ...)` over a constant array read the index and the
array from the function's arena while folding in the module's: naga panicked (index out
of bounds) and Ebin Resurrected failed to load. Both now come from the global arena.
2026-11-06 01:52:10 +02:00
PzmuV1517 04116c33c6 Shader packs: real_packs reads past a pack that panics and sums up each pack
A panic in one pack no longer stops the run: it is caught, and each pack ends with a
SUMMARY line of the programs skipped in each dimension. A pack without shaders.properties
keys is read as one (every key at its default), not a failure.
2026-11-06 01:52:10 +02:00
PzmuV1517 1da773a501 Shader packs: a matrix built from one vector takes its components column by column
`mat2(v)` with a vec4 `v` is the matrix with columns v.xy and v.zw in GLSL, as when the
components come as several arguments. naga's GLSL front end made each column the whole
vector, which failed validation ("Composing 0's component type is not expected"):
Bloop's water normals (prepare1) were skipped.
2026-11-06 01:52:10 +02:00
PzmuV1517 a02c7fcd4b Shader packs: DH_BLOCK_* are defined, with or without Distant Horizons
Packs compare block ids with Distant Horizons' block materials outside any
DISTANT_HORIZONS guard (Exposa: `blockID == DH_BLOCK_LEAVES`), so Iris defines them in
its standard macros whether the mod is there or not. Exposa's composite and composite1
failed on the unknown names; DH_BLOCK_UNKNOWN to DH_BLOCK_ILLUMINATED are now 0 to 15,
the mod's EDhApiBlockMaterial numbers.
2026-11-06 01:52:10 +02:00
PzmuV1517 19f87e8059 Shader packs: an integer vertex output travels flat without saying so
GLSL asks a fragment input of integer type to be `flat`, but since 4.30 only the
fragment stage's interpolation qualifier counts: a vertex stage may write
`out uint frag_worldTN;` and GL drivers link it to `flat in uint`. naga left such an
output without interpolation and refused it (Alpha Piscium: 111 programs). An integer
vertex output or fragment input with no qualifier is now flat.
2026-11-06 01:52:10 +02:00
PzmuV1517 83e2920c5e Shader packs: a paste that makes no single token stays its tokens
`colors2_colorspaces_ ## a ## _to_ ## b ##(x)` (Alpha Piscium) pastes the name's last
part to its `(`. The preprocessor made one token of `..._6_to_8(`, which named no macro,
so the call to the function-like macro `colors2_colorspaces_6_to_8` was left for the
compiler as an unknown function. As GL drivers' preprocessors do, the paste now stays the
tokens it is made of, and the name is rescanned with its `(`.
2026-11-06 01:52:10 +02:00
rison1337andCodex GPT-6 9734a8a255 Cache unchanged entity name-tag glyph layouts
Retain bounded local text quads while updating camera transforms, lighting and cosmetic marks every frame. Invalidate on text/font/atlas/bidi changes and bypass animated or inline content.

Co-Authored-By: Codex GPT-6 <[email protected]>
2026-11-06 04:49:27 +05:00
Florian ERNSTandCodex cd7d6a3dcc Preserve inherited squid damage acceptance result
Co-Authored-By: Codex <[email protected]>
2026-11-06 00:48:21 +01:00
Ludovic Marie e0bd7cd69a Merge pull request #8742 from Akirabane/feat/7484-request-logs
🤖 Agent: the request log reads the player's name before the request runs, so a release or drop that removes the guest still logs who it was. The e2e this PR adds ran on this exact stack against current main: 31/31 over real control sockets with horizon-bots.
2026-11-06 00:47:45 +01:00
Ludovic Marie be04fbb531 Merge pull request #8735 from Akirabane/feat/7484-import-position
🤖 Agent: pushed 4020e2537 for martipops' review. It refuses a rotation that overflows f32 (1e39 became inf/NaN after the cast), refuses a named position when the record still has a RootVehicle to restore (the vehicle would undo it), keeps recentered on retries, and documents that velocity is the server's body only. Tests added for each; union-tested on the desktop.
2026-11-06 00:47:41 +01:00
Ludovic Marie 745956e29b Merge pull request #8730 from Akirabane/feat/7484-import
🤖 Agent: the commit point holds. place_arrival writes the .dat with write_dat_durable plus the advancements and stats with write_durable, each a Result, before ok, and the player stays held on any error. Retries after the commit answer through placed_arrival. martipops' two reviews are answered point by point in the comment above. server_transfer_e2e passed on the final stack (31/31).
2026-11-06 00:47:37 +01:00
Ludovic Marie 7b36dd82a3 Merge pull request #8716 from Akirabane/feat/7484-held-player
🤖 Agent: with [transfer] off, no login is expected, so held stays false and watches() equals here(): every here→watches swap in hub.rs and the loading_chunks refactor are no-ops for ordinary servers. With it on, a held player is a viewer but not tracked, and gets list changes through broadcast_watching. Union-tested with main and the stack on the desktop, plus server_transfer_e2e (31/31).
2026-11-06 00:47:32 +01:00
Horizon agentandClaude Opus 5.5 153044c095 Sculk: a mob's game event costs nothing when nothing can hear it
tick_mob_sculk_events posts every loaded mob's footsteps, landings and
splashes. Each event looked its mob up by a scan of every mob (for the
context and the supporting block) and asked every mob whether it was a
warden or an allay, so a server with thousands of mobs paid three scans
of them for each footstep near no listener.

Now, as GameEventDispatcher only visits the listeners around the event:
while the mobs' events are posted, mobs are found by id through a slot
map, the wardens and allays are listed once with their positions, and
an event with no sculk block in reach and no warden or allay within 24
blocks skips the context and the listeners. A mob that stands as it
stood with no step due (every mob outside the simulation distance)
leaves at once, and a mob riding nothing, as before, skips the mount
bookkeeping. Mods still get every event (native_game_event).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 00:43:01 +01:00
masa-masa-0314andZCode 8db0ec744e Realms: the reset-world screen, world templates and world upload
RealmsResetWorldScreen's tiles, RealmsSelectWorldTemplateScreen's paged
list, the local-world picker and RealmsUploadScreen with the tar.gz
packer and the upload body. Follows the 26.3 bytecode of those screens,
client.worldupload.RealmsWorldUpload/RealmsUploadWorldPacker/FileUpload
and RealmsClient.fetchWorldTemplates/resetWorldWithTemplate/
requestUploadInfo/putIntoMinigameMode.

Co-Authored-By: ZCode <[email protected]>
2026-11-06 07:42:06 +08:00
Horizon agentandClaude Opus 5.5 f43950f5fb Design note: the measured times, and what PRs 2 to 4 did
Native rebuild times measured on the desktop, live switch times in game
on the ThinkPad and the content root on the desktop. Option A as built:
trait mods switch at run time, RenderHooks and the launcher's build-all
come next. The browser's buttons and index fetch as implemented.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 00:38:39 +01:00
Horizon agentandClaude Opus 5.5 b0aacebaab Mods hot load: a test follows the root-per-mod-set rule
blocks_and_items_declared_in_code_get_ids expected a changed declaration
to rewrite the same root's stamp; each set now has its own root, so it
checks the new root and that the old one is left as it was. Found by
running the content-root tests on real prepared resources.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 00:37:43 +01:00
Horizon agentandClaude Opus 5.5 1d8a1699ad Mob loop: villagers test reach before item tags, no player view copies
villager_pickups tested every loaded villager against every dropped item,
looking the item up in the villager food table and tags before the reach:
on a server with hundreds of villagers and thousands of items that was
most of the mob phase. The reach comes first now, and a villager with no
item in its cells skips the scan (ItemAreas, as the looting passes do).

Mobs outside the simulation distance only check despawning, which reads
the nearest player and the difficulty: they no longer copy the player
view they see. A ticking mob borrows its guest's view for its tick (the
tick reads it through &MobWorld) instead of cloning it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 00:30:30 +01:00
Ludovic Marie 42e6d9cc49 Merge pull request #10263 from foudretbr/fix/unknown-block-state-panic
🤖 Agent: unknown states now read as air at the three places they enter (chunk palettes, block updates, BlockFacts::get), and the palette scan is skipped when nothing is unknown, so a vanilla-width chunk costs one maybe_has over its palette. Vanilla would disconnect on such an id (MissingPaletteEntryException) instead; air matches what Horizon already did for a wider host, so I took it. Union-tested on the desktop (clippy, wide_states, horizon-items).
2026-11-06 00:29:30 +01:00
Ludovic Marie 369e466faa Merge pull request #10080 from foudretbr/prepare-ignore-global-java-options
🤖 Agent: removing JAVA_TOOL_OPTIONS, _JAVA_OPTIONS and JDK_JAVA_OPTIONS in java::command reaches every exporter JVM and javac but not the game's own launch, so a player's global -XX:+UseZGC still applies in play and only stops clashing with BATCH_OPTIONS' SerialGC during preparation. Built and tested in a union on the desktop (clippy, horizon-prepare tests).
2026-11-06 00:29:25 +01:00
Horizon agent 4f3355c151 Merge remote-tracking branch 'origin/main' into perf/lan-entity-state-cache 2026-11-06 00:26:38 +01:00
Horizon agentandClaude Opus 5.5 34ddea78e1 Merge #8735's review fixes into #8742
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 00:22:26 +01:00
Horizon agentandClaude Opus 5.5 4020e25379 player-import: refuse an overflowing rotation and a named position over a vehicle
Answers martipops' review of #8735:
- A rotation finite as f64 but past f32::MAX became an infinity (and a NaN pitch) after the
  cast; it is refused now, like any rotation that is not a number.
- A record with a vehicle still to restore (RootVehicle) seats the player back on it at the
  vehicle's own place, which undid a position the request named; the two are refused together.
- A retried import of the same hand-over, or a retry after the commit, answers the same
  `recentered` as the first reply (kept on the guest until it is placed).
- docs/server.md says the velocity is the server's only: the arrival's teleport carries no
  motion, as a vanilla login's does not.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 00:22:22 +01:00
Horizon agentandClaude Opus 5.5 7dcc5b46f3 Server: send idle entities' changes as ChunkMap does, describe them bare (#10253)
ChunkMap.tick runs ServerEntity.sendChanges only for an entity that changed
section, needs sync, or is in DistanceManager.inEntityTickingRange. Horizon
ran it for every tracked entity, so an idle one sent its position every 60
ticks and advanced its update clock. EntityState::idle now carries the
range (the simulation level, Levels::in_entity_ticking_range), and the
tracker skips sendChanges for an idle entity that stayed in its section and
needs no sync; a lead change still goes out at once (Leashable).

The game then stops describing such entities in full every tick: an idle
mob or item that was in the tracker's last pass at the same place, carries
no passengers, and is already tracked by every client within its tracking
range is described bare (where it is, none of its data). The tracker never
reads its data and never pairs a bare state, so every pairing still sends
the entity as it is (game/lan_idle.rs).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 00:21:37 +01:00
Skr1zzzz d5479156d7 Multiplayer: the hidden player list and its skins stop costing per player
draw_tab_list ran every frame and, through remote_tab_players, cloned every
listed player and formatted two object-skin keys for each before it knew
whether the list was shown; update_remote_skins then searched every entity
once per level player and cloned the whole skin map. With 150 players and
none drawn a frame still cost ~0.4 ms more than with none (#4664).

- The list's visibility comes from the listed count; the players are built
  only while it is shown or when it opens/closes (narration).
- The id:/name: object skins publish only when a player, its skin or the
  atlas's object skins changed (a fingerprint and an atlas revision), so
  inline faces keep the server's skin as before.
- update_remote_skins finds players and mannequins through one id->index map
  and moves the resolved skins in instead of cloning them.
2026-11-06 02:18:39 +03:00
masa-masa-0314andZCode f221ae33ef Realms: the invite-code list and its edit screen
The Players tab's second button now opens RealmsInviteCodesScreen: the count in the
title, the subtitle, a row per code with its expiration (or Never), Copy / Edit / Delete
per row, and Create. RealmsEditInviteCodeScreen edits one code: its active flag, the
seven InviteCodeExpiration choices (a click on the slider track picks the nearest — this
screen has no drag event yet), Copy to the clipboard (a new Outcome::Copy the title
turns into Action::CopyText), Delete, Done and Back.

RealmsClient gains inviteCodes (GET api/v1.0/invite/codes?realmId={id}),
createInviteCode (POST .../invite/codes), updateInviteCode (POST .../update) and
deleteInviteCode (DELETE .../delete/{code}), all on the invite-code service with the
token in x-mojangauth, with InviteCode's DTO. The existing redeemInviteCode now goes
through the same helper. An existing expiration reads back as the closest of the seven
choices (InviteCodeExpiration.closestTo).

Co-Authored-By: ZCode <[email protected]>
2026-11-06 07:16:04 +08:00
Ludovic Marie 2b74332fbe Merge pull request #10117 from horizon-rust/pvp/wire-stream-legacy
🤖 Agent: merging. The default --clients vanilla,horizon run is unchanged, and so is main's one-client capture (now 'one or two of'). horizon-legacy only adds a horizon-extras.txt in its own scratch home. On the desktop, Modern vs Legacy matched on 14 of 16 scenarios; eat and dig_teleport also differed between two identical Modern clients there.
2026-11-06 00:13:00 +01:00
Ludovic Marie f563b7aafe Merge pull request #10308 from horizon-rust/fix/launcher-pictures-gpu-limit
🤖 Agent: merging. Pictures are now sized to the window × scale and capped by the atlas limit (the wgpu adapters' min on Linux, 16384 on Metal/DX), instead of a fixed 3840. RSS stays flat at 173 MiB for 5 min on a real GPU. One thing to watch: the Linux probe opens its own Vulkan/GL instance at startup on a background thread, and until it answers pictures cap at 8192.
2026-11-06 00:12:56 +01:00
Skr1zzzz b73ef3faa3 Shader packs: material maps share the stitched colour atlas's layout
With a stitched colour atlas (sprites at their own sizes), the normal and
specular atlases were still packed as a grid by PackAtlas::material, so a
block's material texels sat under another sprite's colour (#9570). The
companion atlases now reuse the colour atlas's size, rectangles and padding;
each map is nearest-sampled from its single level into its sprite's
rectangle (an fs_material entry point), and animated sprites copy into the
same rectangles. The grid path is unchanged.
2026-11-06 02:12:54 +03:00
Evrim CicekandClaude Opus 5.5 b7841886cf A summoned Ender Dragon hovers where it was put, as vanilla's does
EnderDragon's constructor sets EnderDragonPhase.HOVERING; only EnderDragonFight.createNewDragon
sets the holding pattern. Dragon::new started every dragon holding its pattern, so a dragon from
/summon, a spawn egg or a save without DragonPhase flew off. It now starts hovering, and
create_new_dragon sets the holding pattern.

Hovering had never run on the server, and the flight step diverged there: the target is a hair
away, where Vec3.normalize returns zero under 1.0E-5 and vanilla's Mth.sin/cos table and
Mth.atan2 turn the dragon differently from the exact functions. The step now uses them, the
doubles of moveRelative's input vector and vanilla's 0.91F and 0.8F as doubles.

The command execution harness gets "summoned dragon hovers" (phase and position every 20 ticks
over 120): 0/13 before, 25/25 now. The dragon harnesses (server dragon, contacts, NoAI, not the
fight's, death outside the End, death roar, forced End, spawn eggs in fluids, dispensers) match.

Fixes #10285

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 02:11:40 +03:00
rison1337andCodex GPT-6 868e494b0c Cull off-screen blob shadows before scanning blocks
Use conservative projected-shadow bounds, retaining shadows whose casters are outside the view. Keep shader packs and native mods on the original path. Add resource-backed bounds coverage and measured FPS results.

Co-Authored-By: Codex GPT-6 <[email protected]>
2026-11-06 04:11:09 +05:00
AxxioandAntigravity 576c5942f1 launcher: fix account skin arm-type segmented control overflow and caption layout
In the glass theme account settings (skin_panel), long translated option
labels such as French 'Bras classiques' protruded outside the segmented
control pill due to an inherited ml(40px) margin and cross-axis stretching.
Additionally, the multi-sentence caption wrapped into a 10-line block inside
the narrow column, clipping off the bottom on 1080p displays.

- Remove the ml(40px) margin in glass theme mode.
- Set items_start() on the control column so segments size to natural width.
- Adjust segments option padding to 12px with flex_none and justify_center.
- Reorganize skin_panel so the caption spans full width across the bottom.
- Adjust Microsoft card width to 360px and reduce vertical padding in glass_who_plays.

Co-Authored-By: Antigravity <[email protected]>
2026-11-06 00:10:36 +01:00
Skr1zzzz 575fadbeeb Friends: Cancel and Escape go back to the screen that opened it
Join a Friend opens from the title screen's Friends button and from the
server list. Both exits always went to the server list, so the title's
entry landed the player on a list they never opened (#9190, item 10).
The page now remembers whether the title opened it.
2026-11-06 02:10:26 +03:00
Horizon agentandClaude Opus 5.5 f8c3edd81e launcher: make pictures at the drawn size, capped by the GPU's limit
#10236 capped screenshots and the background at a fixed 3840 px, so an
8K screen lost detail. Pictures are now made at what the screen shows:

- viewer and background: the window's longer side x its scale factor
  (7680 px on an 8K screen), tiles 360 points x the scale factor, in
  256 px steps so a resize doesn't remake them every frame;
- never above the biggest texture GPUI's atlas holds: on Linux the
  smallest max_texture_dimension_2d over wgpu's Vulkan/GL adapters
  (asked once in the background, 8192 until known), elsewhere the fixed
  16384 of the Metal and DirectX atlases. A larger picture would make
  the atlas add a max-size texture on every frame (zed#66595).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 00:10:21 +01:00
Florian ERNSTandCodex 6df80522aa Omit the saved lodestone tracker true default
Co-Authored-By: Codex <[email protected]>
2026-11-06 00:08:11 +01:00
MSedgeMC f16012e26c Entity saves: a UUID bridge for references to a mob
A save can now name an entity it only knows as a `Who`: `mob_to_nbt` takes a
`uuid_of` resolver, `who_uuid` writes a `Who` as a UUID and `who_by_uuid` turns a
stored UUID back into the `Who` of a loaded player or mob. `EntityMemory::uuid_of`
supplies the id -> UUID map the game layer keeps, and the game layer's `mob_by_uuid`
now resolves through the same seam.

The neutral mobs' `angry_at` is the first writer: anger at another mob now saves
that mob's UUID instead of nothing. The warden's suspect list and the shulker
bullet's target are the two follow-ups this bridge unblocks; neither is written
here.
2026-11-05 23:06:45 +00:00
martipopsandClaude Opus 5.5 0c0d33cb29 Controller e2e: run cases at once with --jobs
Each case already has its own game, home and socket. One at a time stays the default.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 18:06:04 -05:00
martipopsandClaude Opus 5.5 edc49a0118 Controllers: the gameplay guide, what the buttons do now, in play and in inventories
In play the HUD's top corners name the buttons for what the crosshair is on, what's held and how the player is
moving. In an inventory the bottom corners do: plain face buttons at the bottom right, everything else at the
bottom left, by what the crosshair is on and what's carried. Rows follow bindings and step around what was drawn.
The mouse or keyboard taking over, a prompt, F1, F3 and the setting hide them.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 18:06:04 -05:00
foudretbr 5a92cfb6b1 launcher: retry native cache fixture exec on ETXTBSY and copy via temp rename
Under Linux, executing a freshly copied test fixture can fail with ETXTBSY (os error 26) if another test thread forked while the file was being written. Copy the fixture via a temporary file before renaming it into place, and retry exec/spawn if ETXTBSY occurs.

Fixes #10271
2026-11-06 00:04:15 +01:00
Horizon agentandClaude Opus 5.5 5ba18cae86 PvP & HUD: rustfmt
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 00:01:54 +01:00
Horizon agentandClaude Opus 5.5 f06b417ac4 Server: the [combat] test looks for the section, not the template's comment
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 00:01:54 +01:00
Horizon agentandClaude Opus 5.5 346a0a8d81 lan_e2e: legacy-pvp hits right after a hotbar swap for an uncharged hit
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 00:01:54 +01:00
Horizon agentandClaude Opus 5.5 fc8781c2ce PvP & HUD: document Legacy's sounds and particles
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 00:01:54 +01:00
Horizon agentandClaude Opus 5.5 6d36365aeb PvP & HUD: rustfmt
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 00:01:54 +01:00
Horizon agentandClaude Opus 5.5 3400a0316b PvP & HUD: 1.8 knockback under legacy combat rules
With the legacy rules on, a player's knockback follows 1.8.9's
EntityLivingBase.knockBack: the motion halves and the 0.4 push lifts in the
air too (26.3 lifts only from the ground), and knockback resistance is a
chance to take none instead of a cut. The sprint and Knockback push is added
on top as attackTargetEntityWithCurrentItem's addVelocity (0.1 up) instead
of going through knockback, for players and mobs alike.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 00:01:54 +01:00
Horizon agentandClaude Opus 5.5 4d4c3ff32f Server: [combat] legacy = true in server.toml for 1.8 combat rules
horizon-server accepts and keeps a [combat] section (off by default, left out
of the file while off); the dedicated game reads it itself, like
[anticheat], and turns on the legacy rules a hosted world's PvP page switch
gives. A wrong section is logged and fights keep 26.3's rules.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 00:01:54 +01:00
Horizon agentandClaude Opus 5.5 932050ffab lan_e2e: legacy-pvp scrolls the PvP page to its switch
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 00:01:54 +01:00
Horizon agentandClaude Opus 5.5 04c3b710cf PvP & HUD: Legacy leaves out the sweep, damage hearts and 1.9 hit sounds
Under Combat feel Legacy the client draws no sweep_attack or
damage_indicator particles a server sends (1.8.9 had neither), and with
'1.8 hit sounds' plays none of the entity.player.attack.* sounds 1.9
added. Hurt sounds, crits and enchanted hits stay. Only what this client
shows changes; nothing is sent.

1.8.9 drew no red tint over armour (LayerArmorBase.shouldCombineTextures
is false, so setBrightness skips it) and the body's flash is red at 0.3,
as 26.3's: both already match.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 00:01:54 +01:00
Horizon agentandClaude Opus 5.5 a6f2414081 PvP & HUD: document Combat feel
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 00:01:54 +01:00
Horizon agentandClaude Opus 5.5 62db23c23b PvP & HUD: rustfmt
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 00:01:54 +01:00
Horizon agentandClaude Opus 5.5 eabc4160a8 PvP & HUD: Combat feel Legacy draws the hand as 1.8.9 did
Combat feel: Modern (default) or Legacy (1.8.9). Legacy changes only the
picture: a flat item in first person takes 1.8.9's ItemRenderer pose
(swing, eating and drinking, bow pull) through 26.3's own display
transform, a sword is held up while use is held (1.8.9's block pose,
swinging as 1.7.10 did), the main hand no longer dips after each hit,
and the attack indicator hides unless the player keeps it. The clicks
and every packet stay 26.3's. Poses 1.8.9 lacked (crossbow, trident,
spear, brush) and resource packs' own displays stay modern.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 00:01:54 +01:00
Horizon agentandClaude Opus 5.5 cccc296d9a PvP & HUD: the fall distance is already an f64
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 00:01:54 +01:00
Horizon agentandClaude Opus 5.5 270e435b3f PvP & HUD: 1.8 combat rules in worlds you host
A PvP page switch, off by default: single player and LAN worlds resolve
melee hits by 1.8.9's rules (EntityPlayer.attackTargetEntityWithCurrentItem):
no attack cooldown and no sweep, 1.8.9 weapon damage (ItemSword 4 / ItemAxe 3 /
ItemPickaxe 2 / ItemSpade 1 plus ToolMaterial), Strength x(1 + 1.3 per level)
and Weakness -0.5 per level, Sharpness 1.25 per level, and critical hits that
allow sprinting and need no charge. Only a game that is the server uses them;
a window playing on a server never does, so what it sends stays the same.

The window passes the switch to its server thread each frame, like the
distances. lan_e2e's legacy-pvp check measures two diamond axe hits 12 ticks
apart: 7 and 7 under the rules, 9 then a cooldown-cut hit without.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 00:01:54 +01:00
Horizon agent 5f625c748e Merge remote-tracking branch 'origin/main' into pvp/wire-stream-legacy
# Conflicts:
#	tools/test_wire_stream_parity.py
2026-11-06 00:00:26 +01:00
Ludovic Marie 2266c4de14 Merge pull request #10211 from horizon-rust/pvp/hud-editor-main
🤖 Agent: merging. These are #10079's commits merged with main and nothing else; #10079 had landed in the already-merged pvp/hud-layout. The editor opens only from a world and is the only screen that writes the layout.
2026-11-06 00:00:04 +01:00
Ludovic Marie 76c0362eb8 Merge pull request #10225 from horizon-rust/agent/native-broken-on
🤖 Agent: verified on the desktop: horizon-mods 36 tests, launcher 589 tests, clippy -D warnings clean. broken_on is read only from the registry index (the publish job adds it from horizon-mods' daily native-compat run). The warning shows only when the entry's Horizon is the commit the mod would build on, so a mod pinned elsewhere doesn't warn.
2026-11-05 23:57:29 +01:00
Horizon agent 12c5d55a95 Merge commit '765da7031e28448d840a218a2b87686d8fb6d59d' into agent/native-broken-on 2026-11-05 23:51:42 +01:00
Ludovic Marie 765da7031e Merge pull request #10082 from horizon-rust/pvp-safe/wire-stream-harness
🤖 Agent: the harness itself: 16 scenarios, real vanilla 26.3 client vs Horizon, per tick and field; on main 11/16 match, with the series + #9918 16/16. Named-only and solo in parity_hosts (needs a virtual screen, ~15 min). Merge of main resolved a game.rs mod-list conflict (tick_profile + tick_script).
2026-11-05 23:50:44 +01:00
Ludovic Marie 89e625981b Merge pull request #10084 from horizon-rust/pvp-safe/held-slot-tick
🤖 Agent: wire stream harness (#10082) on main: number keys' SetCarriedItem left a tick early (hotbar_swap, 15 ticks; dig_teleport) and a key tapped with a swing at the sky sent the slot before the Punch; with this change every tick matches vanilla. Head = cc7200cd7a's +35 −43 plus merges of #10083, #10085 and main (conflicts in remote.rs resolved to the tree the harness after-run built and passed).
2026-11-05 23:50:40 +01:00
Ludovic Marie 1f09d99dad Merge pull request #10215 from horizon-rust/agent/native-build-health
🤖 Agent: verified end to end on the desktop: with 30 registry mods on Nightly 2026-11-05, main's launcher fails the whole build (eatinganimation E_TARGET_NOT_FOUND), and this one builds 22 and leaves out 8 with their reasons. MIXINS_REV moves to 5a64af9 (horizon-mixins#27, #28). The risk to watch: a compile failure costs one full recompile per round, capped at 3.
2026-11-05 23:50:36 +01:00
Horizon agentandClaude Opus 5.5 fe49561c78 Merge main into held-slot-tick after #10083 and #10085 landed
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 23:48:21 +01:00
CocofireHDandopencode 22807a3f3a Audio: play multi-channel buffers direct like OpenAL
Stereo music and records were downmixed to mono and panned by the camera, so turning away hard-panned them and distance attenuation could silence them. OpenAL Soft plays a multi-channel buffer without spatialization or distance attenuation (AL_SOFT_source_spatialize auto), as vanilla's HRTF-off context does.

Keep the decoded channel count through the cache and mixer; mono buffers keep the existing panning and linear attenuation. Add a mixer test that stereo frames stay direct and unattenuated at any camera angle or distance.

Fixes #10037

Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
2026-11-05 23:47:14 +01:00
Ian WorleyandClaude Opus 5.5 b27965b4ca test: pass the drop-motion and install.sh signature tests on Apple silicon
Vanilla's Math.sin is HotSpot's intrinsic, which rounds one of the drop
test's throws a bit lower on AArch64 than on x86-64; a vanilla 26.3 run
on an M-series Mac gives the AArch64 value. The install.sh fixture only
holds the Linux x86-64 archive and ran the cases with /usr/bin's
OpenSSL, LibreSSL on macOS: a fake uname picks that archive, and the
cases get the OpenSSL 3 the script checked for.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 16:41:36 -06:00
Horizon agent 152eaed3bd Merge remote-tracking branch 'origin/agent/native-build-health' into agent/native-broken-on 2026-11-05 23:36:37 +01:00
Horizon agent efbd46aaad Merge commit '6d99e14ff0c70ae60bc56b67829b526409fa068e' into agent/native-build-health
# Conflicts:
#	launcher/lang/af.json
#	launcher/lang/ar.json
#	launcher/lang/az.json
#	launcher/lang/ba.json
#	launcher/lang/be.json
#	launcher/lang/bg.json
#	launcher/lang/br.json
#	launcher/lang/bs.json
#	launcher/lang/ca.json
#	launcher/lang/cs.json
#	launcher/lang/cv.json
#	launcher/lang/cy.json
#	launcher/lang/da.json
#	launcher/lang/de.json
#	launcher/lang/el.json
#	launcher/lang/eo.json
#	launcher/lang/es.json
#	launcher/lang/et.json
#	launcher/lang/eu.json
#	launcher/lang/fa.json
#	launcher/lang/fi.json
#	launcher/lang/fil.json
#	launcher/lang/fo.json
#	launcher/lang/fr.json
#	launcher/lang/fur.json
#	launcher/lang/fy.json
#	launcher/lang/ga.json
#	launcher/lang/gd.json
#	launcher/lang/gl.json
#	launcher/lang/haw.json
#	launcher/lang/he.json
#	launcher/lang/hi.json
#	launcher/lang/hr.json
#	launcher/lang/hu.json
#	launcher/lang/hy.json
#	launcher/lang/id.json
#	launcher/lang/ig.json
#	launcher/lang/is.json
#	launcher/lang/it.json
#	launcher/lang/ja.json
#	launcher/lang/ka.json
#	launcher/lang/kk.json
#	launcher/lang/kn.json
#	launcher/lang/ko.json
#	launcher/lang/ky.json
#	launcher/lang/la.json
#	launcher/lang/lb.json
#	launcher/lang/li.json
#	launcher/lang/lmo.json
#	launcher/lang/lo.json
#	launcher/lang/lt.json
#	launcher/lang/lv.json
#	launcher/lang/mk.json
#	launcher/lang/mn.json
#	launcher/lang/ms.json
#	launcher/lang/mt.json
#	launcher/lang/nl.json
#	launcher/lang/no.json
#	launcher/lang/oc.json
#	launcher/lang/pl.json
#	launcher/lang/pt-br.json
#	launcher/lang/pt.json
#	launcher/lang/ro.json
#	launcher/lang/ru.json
#	launcher/lang/sah.json
#	launcher/lang/se.json
#	launcher/lang/sk.json
#	launcher/lang/sl.json
#	launcher/lang/so.json
#	launcher/lang/sq.json
#	launcher/lang/sr.json
#	launcher/lang/sv.json
#	launcher/lang/szl.json
#	launcher/lang/ta.json
#	launcher/lang/th.json
#	launcher/lang/tr.json
#	launcher/lang/tt.json
#	launcher/lang/uk.json
#	launcher/lang/ur.json
#	launcher/lang/uz.json
#	launcher/lang/vec.json
#	launcher/lang/vi.json
#	launcher/lang/yi.json
#	launcher/lang/yo.json
#	launcher/lang/zh.json
#	launcher/lang/zu.json
2026-11-05 23:36:06 +01:00
Florian ERNSTandCodex 27d81ba1b9 Omit empty Tags from modeled entity saves
Co-Authored-By: Codex <[email protected]>
2026-11-05 23:35:58 +01:00
TheMrZZandCodex gpt-6.1-sol e5e49e6242 Save spectral arrow duration and its default
Keep spectral duration in the loaded arrow state and always write its integer
value, including the default 200. Cover the live command route and load/save
without raw input while retaining broader type and display diagnostics.

Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-11-05 23:33:29 +01:00
Ludovic Marie 6d99e14ff0 Merge pull request #10189 from horizon-rust/pvp-safe/click-slot-order
🤖 Agent: inventory in the wire stream harness: a shift-click's changed slots went out 10, 37 where vanilla's Int2ObjectOpenHashMap iterates 37, 10; now byte-identical, unit test pinned to the captured order.
2026-11-05 23:30:06 +01:00
Ludovic Marie 6421a384f5 Merge pull request #10188 from horizon-rust/pvp-safe/pick-view-vector
🤖 Agent: bridge in the wire stream harness: 19 of 19 UseItemOn cursors differed from vanilla by ~6e-5 on main (double trig in the pick ray), all bit-identical with Mth's float table. Hot path cost: one table lookup per frame instead of libm sin/cos.
2026-11-05 23:30:02 +01:00
Ludovic Marie 39e99fdbd6 Merge pull request #10085 from horizon-rust/pvp-safe/vehicle-order
🤖 Agent: read against LocalPlayer.sendChanges and AbstractBoat.tick (rider look, vehicle, then sprint; paddles first). No riding scenario in the harness yet, so the proof is the read and the unchanged 16 scenarios. Land before #10084, which merges this branch.
2026-11-05 23:29:58 +01:00
MSedgeMC e08c7a3ee0 port_map: seven citations name functions the tree does not have
A ledger build from today's upstream/main reports seven map gaps, all rows
whose where cell names a function that no longer exists: PrimedTnt.explode
points at Game::tick_tnt (it is tick_one_tnt), FlatLevelSource.createState at
FlatTerrain::structure_settings (from_settings), PacketProcessor's hand-off at
conn::read_loop (io_pool::Pool::push), three ServerCommonPacketListenerImpl
keep-alive rows at conn::write_loop (that loop is wait_alive), and
PackSelectionScreen.Watcher is a class row written as a port.
2026-11-05 22:29:55 +00:00
Ludovic Marie ad1a6a3101 Merge pull request #10083 from horizon-rust/pvp-safe/dig-sequence
🤖 Agent: verified with the wire stream harness (#10082): on main, dig_teleport answered a teleport mid-dig with a new START (sequence 34) instead of vanilla's ABORT; with the series every tick matches. pr_ready clean except the desktop nix-shell's uname-less install.sh test.
2026-11-05 23:29:54 +01:00
Horizon agentandClaude Opus 5.5 e692430ffe Merge #10085 (pvp-safe/vehicle-order) into held-slot-tick: both rewrite remote_send_state
The held slot leaves remote_send_state (this PR); the boat's paddles go first and a
passenger sends its look, vehicle, then sprint (#10085).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 23:26:29 +01:00
Horizon agentandClaude Opus 5.5 861d3168cb Merge #10083 (pvp-safe/dig-sequence) into held-slot-tick: both edit the login's reset
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 23:26:03 +01:00
Horizon agentandClaude Opus 5.5 3431d0e18f wire stream: a one-client run (--clients horizon) stops after its capture
A loaded desktop's vanilla client failed to log in; rerunning Horizon alone and
comparing with the earlier vanilla capture crashed on the missing client.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 23:23:56 +01:00
Evrim CicekandClaude Opus 5.5 d95d0be4e4 /damage on the dragon: a perched dragon refuses a direct arrow, the hit pushes it, the right guest gets the kill
From review:
- A perched dragon shrugs off a direct arrow, trident or wind charge given as `by`
  (AbstractDragonSittingPhase.onHurt is 0, under 0.01): the command is refused.
- dealDefaultKnockback: a counted hit of a type not in #no_knockback pushes the dragon away from
  the command's source position (`at`, else the direct entity), or in no direction without one.
- credit_dragon_kill switches to the credited guest whenever they are not the one acting, as the
  mob kill credit does, so a guest's command that credits another guest awards that guest.

The harness scenario summons its dragons holding their pattern (DragonPhase 0): vanilla summons a
hovering dragon, which is sitting, and Horizon one holding its pattern; that gap is separate.
The intermediate build: 36/39; now 39/39.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 01:22:38 +03:00
Ludovic Marie c4e4800d44 Merge pull request #10241 from martipops/controller/buttons-prompts-v2
🤖 Agent: the keyboard path is a mechanical move from keys.contains to held(Action), with the same default keys underneath; old horizon-controller.txt files keep their buttons (no bindLayout line), and the pad's stack drop also reaches the server path through drop_stack. Union pr_ready green.
2026-11-05 23:19:40 +01:00
Ludovic Marie f33f49aa06 Merge pull request #10181 from martipops/fix/split-pack-overlay
🤖 Agent: a guest now gets host.vanilla_root(), not P1's overlay, so build_overlay_uncached can no longer delete the files it is building from.
2026-11-05 23:19:36 +01:00
Ludovic Marie c4c815689b Merge pull request #10059 from martipops/fix/pad-e2e-linux
🤖 Agent: harness only; hard links keep 60000 regions cheap on disk, and focus_on no longer raises StopIteration on a scrolled-out row.
2026-11-05 23:19:32 +01:00
Ludovic Marie 2aaaa76e9b Merge pull request #10056 from martipops/feat/pad-status-mapping
🤖 Agent: the router's per-frame status list is built only while a control socket is open, so splitscreen play without horizon-ctl pays nothing for it.
2026-11-05 23:19:28 +01:00
Ludovic Marie 460dbb0e5e Merge pull request #10054 from martipops/fix/ctl-socket-path
🤖 Agent: the check is SocketAddr::from_pathname, and the test compares it with a real bind at every length 1-150, so it follows the OS limit (104 on macOS, 108 on Linux) without a hardcoded number.
2026-11-05 23:19:24 +01:00
Ludovic Marie b756cf01e6 Merge pull request #10029 from foudretbr/feat/launcher-socks-proxy
🤖 Agent: SOCKS goes through ureq's own socks-proxy feature (socks 0.3.4, MIT/Apache), and the loopback SOCKS5 test proves the request reaches an .invalid host only through the proxy. Nothing changes for players without a proxy set.
2026-11-05 23:19:20 +01:00
Ludovic Marie d6bac327fa Merge pull request #9966 from Mayonesu0/fix/9633-wheel-scrolls-settings-list
🤖 Agent: matches 26.3: OptionsList inherits AbstractScrollArea.mouseScrolled, so the wheel scrolls the list and never reaches a row's CycleButton; the old key is dropped because every install saved it as true. Main merged in, union pr_ready green.
2026-11-05 23:19:15 +01:00
Ian WorleyandClaude Opus 5.5 8f4888a2ef docs(native-mods): document client world snapshots
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 16:14:46 -06:00
Ian WorleyandClaude Opus 5.5 d1eef82997 feat(native-api): client world snapshots for mods' worker threads
Client mods that bake from the world in bulk (a minimap's tiles, a world
map export) had to clone `game.world.chunks` and read `world_biomes!`,
which pins them to one Horizon commit. Four stable helpers replace that:

- `native_client_chunks(from, to)` shares the client world's loaded
  chunks of an area as `NativeClientChunks` (Send + Sync): block state,
  biome id, loaded, each column's top non-empty section, build height.
- `native_client_chunks_changed(&snapshot)` names the chunks that differ
  from the snapshot now. A held chunk is copied before any edit, so the
  same allocation means the same chunk.
- `native_client_biomes()` gives the biome table the terrain is drawn
  with (`NativeClientBiomes`): names and tints by `NativeBiomeId`.
- `native_client_biomes_changed(&biomes)` says when that table was
  replaced (a mod's seasons, a resource reload, another world).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 16:14:46 -06:00
Ian WorleyandClaude Opus 5.5 dfa637e389 fix(native-mods): a world is known once logged in, and a pack reload covers it
A connection reported its world while still connecting, before a failed
login could drop it, and a resource-pack reload's loading screen still
counted as playing.

Co-Authored-By: Claude Opus 5.5 <[邮箱]>
2026-11-05 16:14:45 -06:00
Ian WorleyandClaude Opus 5.5 897f17114a fix(native-mods): keep a friend's relay in its world identity
Round-two review finding: a long invite's identity kept only the host's
addresses when it had any. Two friends on other networks behind the
same private address and the fixed direct port (25580), each reached
through a relay, shared one NativeWorldId. The relay's address and
nameplate now stay beside the addresses; neither secret takes part.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 16:14:45 -06:00
Ian WorleyandClaude Opus 5.5 8f75db7d5c fix(native-mods): portal loading covers the world; friends' worlds stay apart
Review findings on the session state helpers:

- NativeClientState.screen read remote_loading(), which leaves out the
  portal loading screens because they draw no panorama blur, so a
  minimap drew over them. A connection's screen is now one predicate,
  remote_screen_up(), which remote_overlay draws by too.
- Every long friend invite showed as the same "Friend's world", so two
  friends' worlds shared one NativeWorldId and a mod's per-world data.
  A friend's world is now friend: and the invite's public part (the
  host's addresses, else its relay nameplate), never its secret.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 16:14:45 -06:00
Ian WorleyandClaude Opus 5.5 6975556e3d feat(native-mods): session state a client HUD reads
A minimap or waypoint mod can now read what it needs through stable
helpers instead of Game's fields, on a server's client and in single
player alike:

- native_client_state() gains screen (NativeScreenKind: playing, chat,
  death or another screen), debug_screen (F3), world (NativeWorldId:
  single player's save folder or the server's address, for per-world
  data) and sleeping_pos (the bed, LivingEntity.getSleepingPos).
- native_biome(pos): the biome at a block as Level.getBiome picks it,
  through BiomeManager's zoom.
- native_data_dir(mod_id) joins the helper catalog.

The window's connection remembers the save it joined through memory,
its own server thread's or P1's for a splitscreen seat, so the world
identity needs no mixin on open_integrated. A friend's world is named
as players see it, never by its invite secret.

Helper-only API fragment session_state.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 16:14:45 -06:00
Horizon agent a35ea8b72a Merge remote-tracking branch 'origin/pvp-safe/wire-stream-harness' into pvp/wire-stream-legacy 2026-11-05 23:11:05 +01:00
Horizon agent ec21e06600 Merge remote-tracking branch 'origin/main' into pvp-safe/wire-stream-harness
# Conflicts:
#	crates/horizon-client/src/game.rs
2026-11-05 23:08:51 +01:00
Horizon agentandClaude Opus 5.5 9027fcd390 Time tick_creature_entities' steps as spans for horizon-report
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 23:07:47 +01:00
Florian ERNSTandCodex 9c5982d8e0 Default loaded item frames down when Facing is absent
Co-Authored-By: Codex <[email protected]>
2026-11-05 23:07:30 +01:00
Horizon agentandClaude Opus 5.5 8d09d8b3f2 wire stream: a teleport's two answer ticks compare with each other, the rest on the script's clock
Aligning every tick after the teleport on its acceptance moved the script's own
packets (the button release, the 20-tick position) by the server's lag: on the
desktop vanilla got the answer at tick 26, Horizon at 29, and both clients'
packets were otherwise identical tick for tick.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 23:06:50 +01:00
Horizon agentandClaude Opus 5.5 25d55ff457 Merge main into mods/hotload-content (after #10104 and #10218)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 23:06:43 +01:00
Ludovic Marie 4f4a931b21 Merge pull request #10269 from horizon-rust/agent/redstone-catalog-harness
🤖 Agent: harness and examples-only probe, nothing players run. New head: CI's parallel shards raced on one schematic .part file; each process now writes its own (three concurrent downloads checked against the pinned digest), and parity / parity passes. Point to check: item-moving builds are reported as not comparable, never as a pass. Result: 111/111 verified contraptions match vanilla.
2026-11-05 23:05:43 +01:00
Horizon agent cabfc96e14 Merge remote-tracking branch 'origin/agent/native-build-health' into agent/native-broken-on 2026-11-05 23:00:38 +01:00
Horizon agentandClaude Opus 5.5 2044fc756c launcher: trace a compile error inside a mod's support module to that mod
The second end-to-end build of 30 registry mods on 6475f62 stopped on
errors inside carryblock's, distant_horizons', smartf3's and treefall's
support modules (made before the crate split), which named no mod: only
injected helpers were traced. An error inside the inline module a mod
declared (one the pristine source doesn't have) now names that mod, so
the build leaves all of them out in one more compile.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 23:00:08 +01:00
MSedgeMC 1665d177c6 test: a playing jukebox posts jukebox_play every twenty ticks
A windowless game with a resident chunk, a jukebox at (0, 64, 0) and a disc put
in through use_jukebox: 41 ticks of the jukebox tick post the event at ticks 0,
20 and 40 and nothing else, and a jukebox that is never played posts none.
2026-11-05 21:59:53 +00:00
MSedgeMC 36dac313c8 fix: a playing jukebox tells the listeners around it
JukeboxSongPlayer.tick posts GameEvent.JUKEBOX_PLAY at the block every
PLAY_EVENT_INTERVAL_TICKS (20) ticks of a song, right before the music
particles:

  37: invokevirtual #97   // Method shouldEmitJukeboxPlayingEvent:()Z
  44: getstatic     #101  // Field GameEvent.JUKEBOX_PLAY
  56: invokeinterface     // LevelAccessor.gameEvent:(Holder,BlockPos,Context)
  66: invokestatic  #104  // Method spawnMusicParticles:(LevelAccessor,BlockPos;)V

Horizon drew the note particle in the same twenty-tick branch and posted
nothing, so a sculk sensor beside a playing jukebox never clicked and an allay
never heard the song, although the stop half (JUKEBOX_STOP_PLAY with level
event 1011) was already posted when the jukebox went away.
2026-11-05 21:59:53 +00:00
Evrim CicekandClaude Opus 5.5 25bec21906 /damage hurts the Ender Dragon and End crystals as vanilla does
cmd_damage had no arm for them, so both said "Target is invulnerable".

EnderDragon.hurtServer is hurt(body, ...): a quarter plus up to 1; reallyHurt runs only with a
player behind the source or an #is_explosion type and refuses fire, and hurt succeeds either
way unless the dragon is dying or the hit is under 0.01. EndCrystal.hurtServer refuses an
Invulnerable crystal (unless generic_kill, out_of_world or a creative player), fire and the
dragon's own damage; else it is removed, and blows up unless the damage is an explosion.

The command execution harness gets a "dragon command damage" group: 15/31 before, 31/31 after.

Fixes #10265

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 00:59:38 +03:00
Horizon agentandClaude Opus 5.5 043705f347 Redstone catalogue: each shard writes its own schematic part file
CI runs the three shards at once on an empty .local/lra-catalog: two of them
downloaded the same schematic into one `.part` name and one rename found it
gone. The part file now carries the process id.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 22:58:28 +01:00
Ludovic Marie 0f5dc8d6d5 Merge pull request #10268 from horizon-rust/agent/redstone-catalog-oracle
🤖 Agent: test-only oracle (mods/horizon/src/test). Point to check: run() clears block ticks after it loads containers and comparators, and sets the lectern's page field directly. Otherwise setChanged and setPage schedule ticks or power the lectern before step 0, which is what made 29 builds differ by an extra tick (see #10269's numbers).
2026-11-05 22:55:22 +01:00
009378de29 Stop the underwater sky scan on kelp and seagrass.
canSeeSkyFromBelowWater stops when a block dampens light and is not liquid(). Kelp and seagrass hold water, so guardians under them were still rolling the 1-in-20 and spawned less often.

Co-Authored-By: Grok <grok-4.7> <[email protected]>
Co-authored-by: Cursor <[email protected]>
2026-11-05 22:53:40 +01:00
b9607ce998 Replace a bubble column when a bucket empties into it.
emptyContents destroys the block only when it is not liquid(). A bubble column is liquid and not a LiquidBlock, so breaking it first played destroy particles and a sculk vibration.

Co-Authored-By: Grok <grok-4.7> <[email protected]>
Co-authored-by: Cursor <[email protected]>
2026-11-05 22:53:40 +01:00
MSedgeMC 9fb4fde63b test: a use start is an item_interact_start vibration
A windowless game from the prepared resources, a bow in hand: the two events the
use posts are read back from the game's own event recorder, at the player's feet
(10, 64, -4). A spear, whose use effects set interact_vibrations false, posts
neither.
2026-11-05 21:49:11 +00:00
MSedgeMC d2cd07f1ac fix: starting an item use posts item_interact_start
LivingEntity.startUsingItem ends with
this.useItem.causeUseVibration(this, GameEvent.ITEM_INTERACT_START), and
ItemStack.causeUseVibration posts only when the stack's use effects ask for
interaction vibrations - the same gate and the same position stopUsingItem
already used for ITEM_INTERACT_FINISH. Horizon posted only the finish event, so
a sculk sensor beside a player drawing a bow never clicked and an allay never
heard the use start.

use_vibration takes the event now, and start_using_item calls it where vanilla
posts it: after the use is set up, before the kinetic weapon's map (which this
change leaves alone).
2026-11-05 21:49:11 +00:00
Ludovic Marie 83da211d38 Merge pull request #10267 from horizon-rust/agent/redstone-catalog-data
🤖 Agent: data and reader only, nothing players run. The catalogue is WTFPL from maybeachromium (LICENSE beside it). Point to check: tools/lra_schem.py is the one parser of untrusted downloaded schematics. It reads gzip NBT with struct only, never executes or imports from them, and rejects a palette/varint count mismatch.
2026-11-05 22:48:26 +01:00
Florian ERNSTandCodex dec4b523f3 Use neutral source for ominous item warning
Co-Authored-By: Codex <[email protected]>
2026-11-05 22:42:57 +01:00
Horizon agentandClaude Opus 5.5 b7df353fdf Merge main into fix/9633-wheel-scrolls-settings-list
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 22:42:57 +01:00
MSedgeMC 3236b57773 Declare no extra comment paths for the trample hook fragment
stamp_native_hooks.py refuses a fragment that lists a guarded file
(native_hooks.rs is the generated source it stamps itself), so the fragment
declared an invalid comment path and the repo-wide stamp check failed.
2026-11-05 21:35:17 +00:00
foudretbr d55c2840a7 Block states: map states past registry to air instead of panicking
A server with mods or states the client does not have can send block states outside our registry. Instead of panicking with an out of bounds slice access when indexing BlockFacts or unpacking palettes, map unknown states to air.
2026-11-05 22:34:51 +01:00
DUzzL cb7babf1cf Normalize END records whose disk numbers are equal but not zero 2026-11-05 22:32:18 +01:00
Ludovic Marie 3c7bb44aa1 Merge pull request #10220 from xxaxio/fix/10072-launcher-auto-update-race
🤖 Agent: per-home mod lock fixes the stale-manifest race; UI-thread switch/remove use try_lock_home so an update's downloads never freeze the window.
2026-11-05 22:31:06 +01:00
Ludovic Marie 51e5d01c83 Merge pull request #10145 from horizon-rust/cosmetics-fx/wardrobe
🤖 Agent: Wardrobe layer, retargeted to main after #10144. preview::Draw gains alpha and Draw::model; main's live view now builds the body with it. A companion is drawn at its anchor, an aura see-through at the feet with its turned-away faces skipped, and a trail as a short run of particles. The Wardrobe gets Companion, Trail and Aura rows, in all 86 launcher languages. Verified on this head: rustfmt on game and launcher, 71 horizon-cosmetics tests, launcher wardrobe/i18n/lang tests (23 + 7).
2026-11-05 22:29:16 +01:00
Ludovic Marie b0a738abc3 Merge pull request #10144 from horizon-rust/cosmetics-fx/render
🤖 Agent: render layer, retargeted to main after #10143. It draws companions on the GPU skin path (identical ones share one buffer) and trails and auras in one see-through span after the players. Nothing is drawn beyond 48 blocks or outside the view. Measured in-game with HORIZON_COSMETICS_LOCAL: +0.6 ms prepare at 16 wearers and +1.1 to 2.4 ms at 64, mostly the auras built on the CPU; the follow-up is #10251. Verified on this head: horizon-client clippy -D warnings.
2026-11-05 22:26:58 +01:00
Evrim CicekandClaude Opus 5.5 c572bb7f88 Port pack: folder entries don't vote in old_layout; the report's reason in one line
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 00:25:58 +03:00
Ludovic Marie c8eee9842f Merge pull request #10143 from horizon-rust/cosmetics-fx/settings
🤖 Agent: settings layer, retargeted to main after #10142. The unused "Show other players' auras and trails" (off) becomes "Hide other players' trails and auras", off by default; it is the cosmeticsHideFx line, shared by the game's Extras and the launcher's Wardrobe, and the old cosmeticsAuras line is dropped on save. Translated into the game's 75 languages and the launcher's 86. Verified on this head: clippy -D warnings on game and launcher.
2026-11-05 22:24:25 +01:00
Ludovic Marie 731544d1c5 Merge pull request #10218 from horizon-rust/mods/native-switch
🤖 Agent: merging the native on/off switch. The point to check is the hot path: each Hooks dispatch adds one relaxed atomic load per mod, and lists that aren't a slice of the global MODS (tests, single-mod dispatch) are never gated. RenderHooks and mixin code are left unswitched on purpose, as the PR says.
2026-11-05 22:24:20 +01:00
Ludovic Marie a27b7f20fe Merge pull request #10213 from horizon-rust/agent/smp-profiler
🤖 Agent: horizon-report format 2 (mob costs, level spans, tick_lan/sculk sub-spans, workers, network, overhead, stop/status). verify green on eb55d13; live on the SMP since 21:08Z as prof2 at 0.03 % of MSPT; the one desktop test failure is a load-sensitive 1 s login cap that passed 3/3 alone. Only spans and counters added, no gameplay change.
2026-11-05 22:24:16 +01:00
MSedgeMC 9329e154cb Merge remote-tracking branch 'upstream/main' into HEAD 2026-11-05 21:21:52 +00:00
MSedgeMC 72a1b13eae Merge remote-tracking branch 'upstream/main' into fix/trample-hook 2026-11-05 21:21:41 +00:00
Ludovic Marie 2ed3ede17f Merge pull request #10142 from horizon-rust/cosmetics-fx/runtime
🤖 Agent: runtime layer, retargeted to main after #10140. follow.rs (frame-rate independent lag, snap after a teleport, kept out of the crosshair cone and clear of the head) and trail.rs (spawns by distance walked, 24 particles per player and 512 in all) are pure horizon-cosmetics code, plus the fx_bench example. Nothing in the game calls them yet. Verified on this head: clippy -D warnings and 68 horizon-cosmetics tests.
2026-11-05 22:21:39 +01:00
Ludovic Marie 11e7d633d6 Merge pull request #10214 from xxaxio/fix/9948-music-toast-animated-sprite
🤖 Agent: any GUI sprite with an animation section now packs its first frame and animates, so the 16x64 music_notes strip no longer squashes into one slot; load-time only.
2026-11-05 22:21:35 +01:00
Evrim CicekandClaude Opus 5.5 2ea59c309b Port pack: a mod made for 1.20 is not carried or counted as come over
check already refused a jar whose data folders are named as before 1.21; carry and report did
not, so a 1.20 pack's mods were carried and counted as partial while 26.3 read none of their
recipes, loot tables, tags or structures (Prominence II: 149 of 308 "come over", 28,948 recipes
and 8,784 loot tables in recipes/ and loot_tables/).

old_layout moves to carry and reads every folder 1.21 renamed (structures/, advancements/,
tags/blocks/, ...), by the larger share: a 1.20 jar may hold a misnamed singular tag folder and
a 1.21 jar a stale plural copy. carry writes old_layout to carry-report.json and carries
nothing; the report's row says why, is not one Port can carry over, and the launcher does not
count it as portable.

Fixes #10249

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 00:21:33 +03:00
Ludovic Marie 8dcf45e95e Merge pull request #10171 from xxaxio/fix/9957-sulfur-cubes-parity
🤖 Agent: sulfur cube blast is TNT-interaction only with mobGriefing (NONE without), ENTITY_INTERACT sourced by the cube, tnt.disabled overlay and used stat.
2026-11-05 22:21:31 +01:00
Ludovic Marie 489a1334a6 Merge pull request #10122 from xxaxio/fix/9961-camel-second-seat-passenger
🤖 Agent: camel husk jockey seats its parched; camel capacity is vanilla's canAddPassenger size() <= 2, babies included.
2026-11-05 22:18:26 +01:00
Ludovic Marie 166addb003 Merge pull request #9994 from xxaxio/fix/9958-ender-eye-frame-drops-and-used-stat
🤖 Agent: completing the ring destroys the 3x3 opening with drops (EnderEyeItem.useOn destroyBlock(pos, true)) and awards ender_eye used.
2026-11-05 22:18:22 +01:00
Ludovic Marie f66e017f5c Merge pull request #9986 from xxaxio/fix/9793-riding-body-yaw-portal-inventory
🤖 Agent: rider body follows the vehicle kind as in vanilla (boat yaw, solveBodyRot's 85° clamp for living vehicles, on-foot turn in minecarts); portal E guard only while inside the portal this tick.
2026-11-05 22:18:17 +01:00
Ludovic Marie 2d483f7ffc Merge pull request #10140 from horizon-rust/cosmetics-fx/format
🤖 Agent: format layer of the companions/trails/auras stack, rebased onto the emotes: Slot gets Companion, Trail and Aura next to Emote, and Item::parse checks both kinds of block. Item::animate also builds its animated-part table once at parse instead of searching by name every frame, which saves about 0.3 ms a frame at 64 wearers (profiled). Verified on this head: clippy -D warnings on game and launcher, 60 horizon-cosmetics tests; nothing draws the new slots yet.
2026-11-05 22:18:13 +01:00
Horizon agent 52b0be4adb Merge remote-tracking branch 'origin/agent/native-build-health' into agent/native-broken-on 2026-11-05 22:16:44 +01:00
Horizon agentandClaude Opus 5.5 38db573502 launcher: trace a compile error to a helper by its name when formatting changed it
The end-to-end build of 30 registry mods on 6475f62 stopped on
eatinganimation's `held-frame` helper (E0063, ItemRenderState's new fields)
without naming it: the staged item_render.rs carries the helper with a
trailing comma rustfmt added, so its tokens no longer equal the plan's
helper_source. A helper is now also the function of its name in the staged
file, when the pristine source has no function of that name; two mods'
helpers of one name still name neither.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 22:16:23 +01:00
Horizon agentandClaude Opus 5.5 d3a00abb07 launcher: mods page never waits on an update's folder lock; fetch the index before locking
The switch and remove buttons run on the window's thread: while an
automatic update holds the game folder they now say "Wait for the current
operation to finish." instead of freezing the launcher until its downloads
end. auto::run takes the lock after the registry index arrives.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 22:15:52 +01:00
Horizon agentandClaude Opus 5.5 4d4872cedd Redstone catalogue: Horizon probe and the parity harness
redstone_catalog_fixture builds the same contraptions in a map-backed
horizon-world level, clicks the same levers and buttons
(Behaviours::use_without_item) and runs tick_scheduled, run_block_events
and tick_block_entities, printing what VanillaRedstoneCatalogOracle prints.

tools/test_redstone_catalog_parity.py reads the catalogue, downloads the
archive's schematics into .local/lra-catalog/ (pinned to da4dd51, one
sha256 over all 188), turns each entry into a case (switches off except
those the catalogue held on, a neighbour update on every block, then input
steps: every vector up to 8 bits, random ones beyond, random changes for
stateful builds) and compares both sides step by step. verified entries
must match; partial and unverified ones are reported. Entries whose inputs
are analog levels, lecterns or bare wire are listed as unsupported for
now. Three shards; --full runs larger sweeps and every vector up to 16
bits.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 22:15:27 +01:00
Horizon agentandClaude Opus 5.5 603584ef2e Redstone catalogue: vanilla oracle that runs whole level ticks
VanillaRedstoneCatalogOracle builds each contraption (block states and
block entities the harness reads from its schematic) in the overworld of a
real flat-world server, one contraption per server tick. Comparators keep
their saved output, containers and lecterns get real items (set silently:
no comparator ticks, no lectern pulse), and the comparator level of every
block entity is printed for the Horizon probe. The game clock keeps
running between contraptions (LevelTicks orders chunks by trigger time);
the probe starts from the time printed.

Levers and buttons are driven with useWithoutItem, and each tick runs what
ServerLevel.tick runs for blocks: scheduled block and fluid ticks, block
events and block entities, so pistons move. After each tick it prints the
outputs that changed, and at the end of each step the tick it ended on,
whether the level went quiet and a hash of the contraption's box. A tick
budget per contraption (the probe counts the same) bounds machines that
never settle; containers at or next to hoppers, droppers, dispensers and
crafters are watched every tick, and a changed level marks the run as not
comparable (the probe does not move items). HORIZON_CATALOG_TRACE=1 adds
every block tick and every block at a step's end.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 22:15:27 +01:00
Horizon agentandClaude Opus 5.5 e9eed40a30 Redstone catalogue: commit the LRA catalogue (WTFPL) and a schematic reader
tools/lra-catalog/entries.jsonl is maybeachromium's catalogue of the Logical
Redstone Archive (#10103): input and output positions, encodings and checks
for every entry, one entry per line. They shared it under the WTFPL
(LICENSE). The archive's schematics have no licence and stay out of the
repository.

tools/lra_schem.py reads Sponge schematics (versions 1 to 3), written from
the format's specification.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 22:15:27 +01:00
TaquitoSuabe cf92894885 gfx: enable early depth occlusion culling on OpenGL (Backend::Gl)
Cull::supported previously gated early-depth occlusion culling to Vulkan and Metal. Enable wgpu::Backend::Gl when the device supports SHADER_EARLY_DEPTH_TEST alongside indirect first instance and storage buffer writes.
2026-11-05 14:14:46 -07:00
MSedgeMC eb649762c3 Merge remote-tracking branch 'upstream/main' into HEAD 2026-11-05 21:03:30 +00:00
Florian ERNSTandCodex 0e05b77d95 Keep glow squid dark countdown running with NoAI
Co-Authored-By: Codex <[email protected]>
2026-11-05 22:00:30 +01:00
Horizon agent 20dc224154 Merge remote-tracking branch 'origin/main' into cg53-pr-10220 2026-11-05 21:58:37 +01:00
Horizon agentandClaude Opus 5.5 7a08446cfe sulfur cubes: the blast keeps blocks without mobGriefing; the cube sources ENTITY_INTERACT
SulfurCube.tickFuse explodes with ExplosionInteraction.TNT only when
mobGriefing is on (NONE otherwise), and equipItem's gameEvent has the
cube, not the player, as its source. Drop tests that checked constants.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 21:58:29 +01:00
MSedgeMC 4fea4cfd63 test: the shipped presets' disable_mob_generation, through the client seam
The three vanilla dimensions, built from the shipped data, must answer what
their noise settings say: the overworld and the nether add creature packs at
generation, the End does not. This is the value `spawn_generation_creatures`
now returns on, read through `VanillaTerrain`, so it covers the whole path from
`end.json` to the gate instead of just the JSON read.

With the setting read as a hardcoded false (the pre-change behaviour, where only
`Dim::End` decided) it fails as
`assertion left == right failed: disable_mob_generation of minecraft:the_end —
left: false, right: true`.
2026-11-05 20:58:11 +00:00
MSedgeMC 61e96ef269 fix: the noise settings' disable_mob_generation skips the creature packs
`NoiseBasedChunkGenerator.spawnOriginalMobs` reads
`NoiseGeneratorSettings.disableMobGeneration` and places nothing when it is
set. The shipped presets are explicit about it: `end` sets it true, the other
six set it false. Horizon never read the field and instead skipped the pass for
`Dim::End` alone, so a datapack dimension that used the End's noise settings -
or any preset asking for no creature packs - still got them, and the reason the
End was empty lived in a dimension check rather than in the data.

The setting now reaches the client seam: `Generator`'s settings carry it, the
pipeline trait exposes it next to `sea_level`, `VanillaTerrain` answers it, and
`spawn_generation_creatures` returns before doing any work when it is set. The
existing `Dim::End` check stays as well, so a preset that fails to load cannot
start filling the End; for the shipped data the two agree, since `end.json` is
the only preset that sets the flag.
2026-11-05 20:58:06 +00:00
Horizon agentandClaude Opus 5.5 0a2d899426 mobs: camel passengers follow Camel.canAddPassenger (size <= 2, babies too)
Vanilla's Camel.canAddPassenger accepts a rider while two or fewer ride,
with no baby check. Drop two tests that only checked their own inputs.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 21:58:03 +01:00
Horizon agent 9e317340d9 Merge remote-tracking branch 'origin/main' into cg53-pr-10171 2026-11-05 21:58:03 +01:00
Horizon agentandClaude Opus 5.5 2228fd7be1 end: propagate the level events' error; drop a test that checked its own loop
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 21:57:23 +01:00
Horizon agent 188ad5b2d1 Merge remote-tracking branch 'origin/main' into cg53-pr-10122 2026-11-05 21:57:23 +01:00
Horizon agentandClaude Opus 5.5 9e239c1c0c perspective: rider body follows vanilla per vehicle kind; portal key guard only while inside
A boat sets its passenger's body to its yaw (AbstractBoat.clampRotation);
a living vehicle's rider is drawn from the vehicle's body with the head at
most 85 degrees off and a fifth of the offset past 50
(LivingEntityRenderer.solveBodyRot). Minecarts and cushions keep the
on-foot turn, as vanilla's LivingEntity.tick does. The inventory key is
held back only while the last tick found the player in the portal, not
during the effect's fade-out.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 21:57:16 +01:00
Horizon agent 78c6d024dd Merge remote-tracking branch 'origin/main' into cg53-pr-9994 2026-11-05 21:57:16 +01:00
martipopsandClaude Opus 5.5 2cce72df97 Merge upstream/main into controller/buttons-prompts-v2
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 15:56:51 -05:00
Horizon agent 54fd1a76d8 Merge remote-tracking branch 'origin/main' into cg53-pr-9986 2026-11-05 21:56:02 +01:00
TheMrZZandCodex gpt-6.1-sol 93365662aa Save default icon flags in custom potion effects
Resolve an absent saved show_icon from show_particles while preserving
explicit icon bytes and the remaining potion fields. Keep this in the
saved item encoder, without changing the general component view.

Add resource-free decoded/modelled codec coverage and a guarded,
zero-tick chest command regression against the pinned Java behavior.

Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-11-05 21:52:58 +01:00
HiuoyandClaude Opus 5.5 b5f50bc41d Native hooks: AnvilUpdateEvent, a mod's say in an anvil's result (#10230)
anvil_update fires where AnvilMenu.createResult ends, once vanilla has
worked out the result, while the left slot holds an item: on the host for
each player's menu (single player's server thread, a LAN or dedicated
host), before prepare_anvil, and in the window's own menu. A mod is told
the inputs, the rename box's text and vanilla's result. It may change the
output, the level cost and the material cost, or cancel, which leaves no
result (NeoForge's CommonHooks.onAnvilUpdate).

The host asks again after a rename that leaves the result as it was, as
setItemName runs createResult. The window's click prediction starts from
the station's cost and material cost, so a result a mod made can be taken
there as on the server. The window names the player as the server knows
them.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 00:52:39 +04:00
Riccardo GuerrieroandClaude Opus 5.5 9ee63c0510 Aquifers keep their 12 nearby cells unpacked and reuse 8 of them a cell down
Part of #9996.

NoiseBasedAquifer::compute_substance is 4.7% of the overworld's proto
phase by itself. A sixth of that went to rebuilding the 12 grid cells
around a block whenever its aquifer cell changed: every location was
unpacked again from its packed long, and the neighbourhood was copied
as a 288-byte Option. The fill walks each column from the top down, so
a rebuild usually follows the cell just above.

Each drawn location is now also kept as the coordinates BlockPos.getX,
getY and getZ read back from it, and the 12 cells are arrays of grid
index, coordinates and horizontal distance to the column. One cell
down, the 8 cells at the old anchor's y and the y below move up two
slots and only the 4 new ones are loaded, in vanilla's loop order. A
cached fluid status is read inline; computing a missing one is out of
line. Default air, water and lava are classified by their IDs before
any block name is read.

Every block, pending fluid update, error and cached cell is the same as
before: the same operations in the same order, the same positional
random draws for the same cells. A test runs the previous search
against the new one over whole columns, scrambled blocks and positions
outside the grid, and the snapshots match byte for byte.

A first draft by DeepSeek V4.1 Flash, via pi, was reviewed and
reworked.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WZzq8LUjYucp4kaN6UfSg7
2026-11-05 21:52:13 +01:00
MSedgeMC cf6a402c41 port_map: EntityRenderer.getBoundingBoxForCulling, the interpolated box
The ledger named the four renderer overrides but not the method they override;
it now says the box is the frame's, grown by 0.5, and points at the dropped
item's own copy of it.
2026-11-05 20:49:37 +00:00
MSedgeMC 4ebdcbf7b2 test: the dropped item's cull box over the frame
An item sliding from x = 0 to 1 in camera space: the box is -0.625..0.625 at the
start of the frame, -0.125..1.125 halfway through and 0.375..1.625 at its end,
where the old code had the last on every frame. A falling item's box floor is
65.0 halfway down a 66 -> 65 drop instead of the tick's 64.5.
2026-11-05 20:49:37 +00:00
MSedgeMC 0fdc892ef9 fix: cull a dropped item at the box the frame draws
EntityRenderer.shouldRender tests the frustum against
getBoundingBoxForCulling(entity, partialTick), which EntityRenderer leaves as
Entity.getInterpolatedBoundingBox(partialTick) - ItemEntity overrides neither -
grown by 0.5 all round.

The dropped-item pass built that box from the tick's own position, so for the
frames of a tick in which an item crossed the frustum edge it was culled at the
cell it was about to reach: an item sliding from x = 0 to 1 had its box at
0.375..1.625 (camera space) on every frame, where at the start of the frame it
stands at -0.625..0.625.

dropped_item_cull_box(previous, position, alpha, camera) is that box now, the
interpolated position grown by the item's 0.125 half-width and 0.5, with the
0.25 height below and 0.75 above.
2026-11-05 20:49:37 +00:00
Ludovic Marie 5849fa33b3 Merge pull request #10104 from horizon-rust/mods/in-game-browser
🤖 Agent: merging the in-game mod browser. The point to check is that install never trusts the download: each file goes through horizon_mods::index::check_file (size, SHA-256, signature, manifest against the index) before it reaches horizon-mods/, and revoked files are refused. Tests pin both cases with a real .hmod.
2026-11-05 21:46:40 +01:00
Horizon agentandClaude Opus 5.5 f23fd18f41 Cosmetics: companions, trails and auras in the Wardrobe
The Wardrobe's picture (horizon_cosmetics::preview, its live view
included) draws a companion at its anchor, an aura see-through at the
feet and a trail as a short run of particles behind; the Wardrobe gets a
row for each slot.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 21:42:38 +01:00
Horizon agentandClaude Opus 5.5 725efc0eca Cosmetics: draw companions, trails and auras in the game
A companion is posed on no bone: it follows its anchor with its lag,
keeps clear of the head and, for the player's own in first person, out of
the middle of the view. Trails spawn by distance walked and are drawn as
camera-facing particles; auras blend at the feet with their back faces
dropped. Trails and auras of every player go in one see-through span after
the players. Others' follow "Other players' cosmetics" and "Hide other
players' trails and auras". Nothing is an entity or reaches a server.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 21:42:37 +01:00
Horizon agentandClaude Opus 5.5 f17a44249f Cosmetics: "Hide other players' trails and auras"
The setting that was "Show other players' auras and trails" (off, for
items that didn't exist) becomes a hide switch, off by default: others'
trails and auras show like their other cosmetics unless the player hides
them. In the game's Horizon settings and the launcher's Wardrobe, the same
cosmeticsHideFx line of horizon-extras.txt; the old cosmeticsAuras line is
dropped on save.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 21:42:37 +01:00
Horizon agentandClaude Opus 5.5 7df444ab89 Cosmetics: how companions follow and trails spawn
follow.rs: a frame-rate independent lag toward the anchor, a snap after a
teleport, and the two rules that keep a companion out of the way (out of
a cone round a first-person crosshair, clear of the head). trail.rs:
particles spawned by distance walked only, placed from their age alone,
24 per player and 512 in all. fx_bench measures their CPU cost for 1, 16
and 64 wearers.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 21:42:35 +01:00
Horizon agentandClaude Opus 5.5 2a76fd1a00 Cosmetics: companion, trail and aura item format
Three new slots that ride no bone: a companion floats beside the player,
a trail leaves particles where they walk, an aura glows faintly at their
feet or round the body. Their blocks, limits and checks live in fx.rs;
item.rs reads them, refuses a bone on them and measures their extent.
Nothing draws them yet.

Item::animate no longer looks its parts up by name every frame (a tree
search and string compares per animation, for every wearer): the animated
parts are found once, when the item is read. At 64 wearers in view that
was 0.3 ms a frame on an i7-7500U.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 21:42:35 +01:00
MSedgeMC 9794dd7ca9 port_map: Entity.getLightProbePosition, the eye over the frame
The ledger had no row for the probe the renderers read; it now names the mob and
projectile port and where the armour stand's override lives.
2026-11-05 20:40:55 +00:00
MSedgeMC 5d76ff0225 test: the mob and projectile probes over the frame
A mob walking 0.9 -> 1.1 (eye 1.62) reads the cell at 0 at the start of the frame
and 1 from the boundary on, where the old probe read the tick's cell 1 on every
frame; a falling one reads 71.62 then 70.62. An arrow falling 64 -> 65 reads
64.13, 64.63 and 65.0, so the cell at 65 only at the end of the frame, where the
old probe read 65.0 throughout.
2026-11-05 20:40:52 +00:00
MSedgeMC caa5c31dd0 fix: light a mob and a projectile from the block its eye is in that frame
EntityRenderer.getPackedLightCoords takes BlockPos.containing of
Entity.getLightProbePosition(partialTick), and Entity leaves that method as
getEyePosition: getEyeHeight() over getPosition(partialTick), which is
Mth.lerp(partialTick, xo, x) on each axis.

The mob pass probed floor(position) with position.y + eye_height(), and the
projectile pass the same on its own position: the tick's own position, so a mob
walking from x = 0.9 to 1.1 was drawn lit by the cell at 1 for every frame of the
tick, and a falling one by the cell it was about to reach, instead of the cell it
was drawn in.

Mob::light_probe(partial) and Projectile::light_probe_y(partial) now give the
probe of the frame, through mobs::living::eye_probe_cell (Mth.lerp plus the eye
height, floored per axis).

Old/new for a mob walking 0.9 -> 1.1 (eye 1.62): cell x = 1 on every frame before,
x = 0 at alpha 0 and 1 from the boundary on. For an arrow falling 64 -> 65: 65.0
before, 64.13 at alpha 0 and 64.63 at alpha 0.5, the cell at 65 only at alpha 1.
2026-11-05 20:40:49 +00:00
martipopsandClaude Opus 5.5 9dae50aefd Controllers: bindable actions, prompts over gameplay, and button art
Held mappings are named by an Action rather than read by key, per device, and more actions can be bound: hotbar
slots, fullscreen, toolbars, social and quick actions. Held drop repeats and throws the stack. Readers of the
movement keys see the left stick, and a bumper steps the hotbar through the wheel's gates. Prompts over gameplay
are pad-driven and keep what's behind them, the keyboard opens on every field, and focus follows its widget.
Buttons and the crosshair draw from Horizon's own art, one file per pad family, instead of text labels.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 15:37:30 -05:00
MSedgeMC 4465421885 port_map: ArmorStand.getLightProbePosition is ported, not skipped
The row said the probe was drawing-only and skipped; it is what selects the block
an armour stand is drawn with, so it now points at the code that walks it.
2026-11-05 20:35:23 +00:00
MSedgeMC 1d38cdeba7 test: the armour stand's probe cells and packed light
Four cases over the jar's values: a full stand's eye at 65.7775 (cell 65, where
the old base + 0.5 read 64), a small stand at 64.2 (65.1875, cell 65, where the
old read 64), a marker's box scanned for a 15 at (0, 65, 0) and falling back to
the first cell walked (0, 64, 0), y walked before z, the cell above the box left
out, and the marker branch ignoring the partial tick; and a burning stand's block
light at 15 with the sky light left as the probe cell's.
2026-11-05 20:35:23 +00:00
MSedgeMC 249341e7cc fix: light an armour stand from its own light probe
EntityRenderer.getPackedLightCoords reads the block Entity.getLightProbePosition
is in, and ArmorStand overrides that method:

- a marker walks BlockPos.betweenClosed of getDimensionsMarker(false) - the box it
  would have without the marker flag, 0.5 by 1.975, or 0.25 by 0.9875 when small -
  from position() with no partial tick, and returns the first cell whose
  max(block, sky) brightness is 15, else the brightest, ties going to the first
  cell BlockPos walks (x fastest, then y, then z);
- every other stand is Entity.getLightProbePosition, the eye: eyeHeight 1.7775 for
  a full stand and 0.9875 for a small one (BABY_DIMENSIONS), at the frame's
  interpolated position.

Horizon read floor(base + 0.5) for every stand, so a stand standing on a lit floor
in a dark room was drawn with the floor's light: a full stand on y = 64 read the
cell at 64 where its eye at 65.7775 is in the cell at 65, and a hologram marker on
a dark ceiling was lit from its feet's block instead of the brightest cell of the
box it would have without its flag.

EntityRenderer.getBlockLightLevel also puts a stand that isOnFire at 15 block
light, which was not read at all.
2026-11-05 20:35:23 +00:00
Florian ERNSTandCodex 85c277b5e2 Correct empty tag selector predicates
Co-Authored-By: Codex <[email protected]>
2026-11-05 21:31:34 +01:00
Horizon agentandClaude Opus 5.5 27c2e6457f Mods hot load: rustfmt
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 21:26:56 +01:00
Horizon agentandClaude Opus 5.5 eb55d13be3 horizon-report: overhead counts the tick's own spans; other worker jobs counted, not timed
On a smoke server (2 ms ticks) the overhead line charged the chunk pump's spans of the
wait between ticks (1,788 a tick) to MSPT. Only the spans the tick records count now,
and a dedicated server records spans anyway for its slow-tick line (HORIZON_SPIKES,
on by default), so they are listed apart and left out of the report's own cost.
The 'other' worker jobs ran at 278k/s for 0.0 ms each: two thread-CPU clock reads
each cost more than the job; they are counted only. A sum of no players' rates no
longer reads -0.0.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 21:26:41 +01:00
MSedgeMC 309ddb48ed port_map: CreativeModeInventoryScreen.selectTab is the creative tab switch 2026-11-05 20:20:54 +00:00
MSedgeMC 87a0ccc90c test: the creative search box loses the focus with its tab
`Creative::select`: the box starts unfocused, the search tab focuses it, and
another tab takes the focus away.
2026-11-05 20:20:54 +00:00
MSedgeMC 1e36a5da1f fix: the creative search box loses the focus with its tab
`CreativeModeInventoryScreen.selectTab` shows and focuses the search box only
on the search tab; any other tab hides it, lets it lose the focus and clears
its query. Horizon focused the box when the search tab was selected but never
took the focus away, so the box stayed focused behind every other tab.

`Creative::select` now sets the focus from the tab it shows, keeping the blink
reset the search tab always had.
2026-11-05 20:20:54 +00:00
Horizon agentandClaude Opus 5.5 50d9f3d7f7 launcher: warn before installing a native mod that doesn't build on your Horizon
The registry's daily check (horizon-mods native-compat.yml) marks the
versions horizon-mixins refuses on the newest Nightly with `broken_on`
({horizon, code}) in index.json. The browser's fit line now says
"Doesn't build on the Horizon you play (CODE): a build leaves it out until
its author updates it" when that Horizon is the one the mod would build on.

- horizon-mods index: `Version.broken_on` (empty and absent by default).
- native_fit: `Fit::Broken { code }` for a hook mod, or a mod pinned to the
  played commit, when an entry names that commit.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 21:19:51 +01:00
foudretbr f15ca8d82a Preparing ignores the player's global Java options
A collector chosen in JAVA_TOOL_OPTIONS or _JAVA_OPTIONS stopped every JVM
horizon-prepare starts, as they pass -XX:+UseSerialGC: "Multiple garbage
collectors selected", so the version was never prepared. java::command, which
they all go through, now removes those variables and JDK_JAVA_OPTIONS.

Fixes #10067
2026-11-05 21:17:58 +01:00
MSedgeMC 6ad03dd306 port_map: the dropped item's light probe and block light
The ItemEntityRenderer row names where its light comes from now, and
EntityRenderer.getBlockLightLevel's note covers dropped items beside mobs and
projectiles.
2026-11-05 20:14:11 +00:00
MSedgeMC 837ec11a1a test: the dropped item's probe, frame position and packed light
The three cases quote the old values: the failing probe cell of a falling item
(64 against the eye's 65), the tick's own position against the frame's halfway
one (64.7125 rather than the destination 65.2125), and a burning item's block
light (15 over a dark cell's 0, sky untouched).
2026-11-05 20:14:11 +00:00
MSedgeMC a26ffbb007 fix: light a dropped item from the block its eye is in
EntityType.ITEM declares eyeHeight(0.2125), and EntityRenderer.getPackedLightCoords
reads the block of Entity.getLightProbePosition, which Entity leaves as
getEyePosition: the item's position plus that eye height, at the position the frame
draws (getPosition(partialTick)), not this tick's own.

The dropped-item pass probed the cell of base + 0.1 at the tick's position, so a
falling item at y = 64.85 was lit by the cell at 64 (the floor's light) while its
eye at 65.0625 sits in the cell at 65, and the light jumped a frame early when an
item crossed a cell boundary.

EntityRenderer.getBlockLightLevel is isOnFire ? 15 : the level's block light, so a
burning item is now drawn at full block light with the level's sky light; the
item's fire ticks already clear for a fire-immune stack, which ItemEntity.fireImmune
makes never on fire.

A picked-up item's particle is not an entity and keeps the cell it is drawn in.
2026-11-05 20:14:11 +00:00
AxxioandAntigravity 46d83389ac launcher: serialize mod mutations per game home (fixes #10072)
Background automatic registry updates and manual mod operations (install,
remove, enable, disable) could mutate the same game home folder concurrently.
Because auto-update loads `mod-browser.json` before downloading and saves upon
completion, a manual install or removal occurring during the download would
be overwritten by the stale snapshot upon completion, losing manually added
mods or reviving removed mods. In addition, temporary save files could collide.

Introduce a per-game-home re-entrant mutex (`browser::lock::lock_home`)
covering mod mutation critical sections across background auto-updates and
manual operations. Use unique atomic temporary file paths during save to
prevent collision.

Co-Authored-By: Antigravity <[email protected]>
2026-11-05 21:11:12 +01:00
Horizon agentandClaude Opus 5.5 422c36b236 wire stream: eat's bites last 10 ticks, far from the server's 32
A loaded server catching up ran a 20-tick bite's remaining ticks in a burst and finished the apple.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 21:08:47 +01:00
Florian ERNSTandCodex 8089757807 Reject missing advancement selector criteria
Co-Authored-By: Codex <[email protected]>
2026-11-05 21:05:15 +01:00
Horizon agentandClaude Opus 5.5 532421c5f0 launcher: leave out the native mods that break a build, not the build
One mod that horizon-mixins refuses or that doesn't compile used to fail the
whole native build (#horizon-support 2026-11-05: eatinganimation's moved
target, hitbox_plus's support module, worldedit's moved chat.rs). The build
now leaves that mod out, with the mods that need it, and builds again:

- isolate.rs names the mod a horizon-mixins diagnostic is about (mod_id, its
  manifest, its support module's name or destination), or the mods the
  compiler's errors were traced to, and adds their dependents.
- A refusal costs one horizon-mixins run; a compile failure one compile, at
  most 3 times. A server engine export still fails as before.
- native-mods.json records `left_out`, so the same mods turned on read as up
  to date, and the Mods page says "X was left out: made for another Horizon
  version / clashes with Y / it needs Z, which was left out" (86 languages).
- MIXINS_REV → 5a64af9: horizon-mixins#27 resolves `horizon_client::…`
  targets that moved into horizon-client-lib, #28 follows workspace globs for
  a module's name and puts the mod id on module errors.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 20:58:01 +01:00
AxxioandAntigravity cfccbb8f40 hud: slice animated GUI sprites to first frame in atlas packing (fixes #9948)
Co-Authored-By: Antigravity <[email protected]>
2026-11-05 20:54:48 +01:00
TheMrZZandCodex gpt-6.1-sol 62d0687fea Merge main and retain the crafting map regression group
Keep the map-crafting, transmute, mob-effect and frame-map groups and their
isolation sets. The merged game passes the focused 62-record Java replay.

Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-11-05 20:51:24 +01:00
Horizon agent e78ed4a7a1 Merge remote-tracking branch 'origin/main' into pvp/hud-editor-main 2026-11-05 20:43:04 +01:00
martipopsandClaude Opus 5.5 db65974cd8 Controllers: new default buttons
New players get the new layout. A file that bound a button by hand keeps it through the change.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 14:40:02 -05:00
TheMrZZandCodex gpt-6.1-sol 751423b897 Defer villager trade reputation to the eligible AI phase
Retain the real last trader on the villager when a trade is accepted,
then deliver the coalesced reputation event after Brain.tick. Keep it
across menu close and live NoAI edits, and refresh current offer prices
when the event actually runs.

Add an actual-owner merchant regression with complete raw capture and
separate strict-all byte-order diagnostics.

Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-11-05 20:39:57 +01:00
MSedgeMC eb6d139e34 test: a pack update keeps the player's mod choices
The fixture registry takes a pack version so the test can install
cozy 1.0.0, turn coords_hud off as the Mods page does, and install
cozy 1.1.0: the file is updated and the mod is still off. Turned back
on, 1.2.0 keeps it on. With the old unconditional set_enabled(m.on)
the update re-enables it and the test fails at 'the update kept it
off'.
2026-11-05 19:39:47 +00:00
MSedgeMC fc6f53a337 fix: a pack update keeps the player's mod choices
install_version_in() applied java::set_enabled(home, id, m.on) for
every mod of the pack on every install, and installing a registry pack
again is how it updates. horizon-mods.txt is the same list the Mods
page toggles, so a mod the player turned on or off in a pack profile
was reset to the pack author's 'on' by the next pack update.

The install now reads the pack's previous record (pack.json's 'id
version' entries) and carries each listed mod's current on/off choice
through the install, which turns a mod on as an explicit install. A
mod the new pack version adds takes the pack's own 'on', and a fresh
install has no previous record so every mod does, as before. The Java
mod path already keeps '.disabled' through updates the same way.
2026-11-05 19:39:47 +00:00
DUzzL a13106bc0b Format container tooltip lines with numbered translation arguments 2026-11-05 20:35:02 +01:00
Florian ERNSTandCodex 16d11c6bb2 Require matching furnace output components before cooking
Co-Authored-By: Codex <[email protected]>
2026-11-05 20:28:16 +01:00
HiuoyandClaude Opus 5.5 b248489d15 Native hooks: PlayerEvent.BreakSpeed, how fast a player digs (#10198)
break_speed fires at the end of Player.getDestroySpeed, on the Game that
works out a dig: the window's own prediction, and the host's (single
player's server thread, a LAN or dedicated host) for each player. A mod is
told the block and vanilla's speed, and may change it;
BlockBehaviour.getDestroyProgress then divides it by the hardness.

tick_mining now takes its progress from destroy_progress, like the other
digs, so the speed is worked out in one place. The remote client asks it
for blocks of hardness 0 too, so a mod's speed of 0 stops those as well.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 23:28:08 +04:00
DUzzL 219a56f260 Unstick arrows and tridents the server sets moving (AbstractArrow.lerpMotion) 2026-11-05 20:27:46 +01:00
MSedgeMC a266ac1c43 test: an empty-criteria advancement is done and is the one auto-granted
`Progress::done` on an advancement parsed from `{"criteria": {}}` (true, no
requirement groups to wait for) against one with a criterion (false), and
`Tree::automatic` over a tree holding both: only the empty one is listed, which
is what the load pass grants the rewards of.

Without the `done` change the first assertion fails at once
(`assertion failed: Progress::default().done(&empty)`), and `Tree::automatic`
does not exist to compile against.
2026-11-05 19:25:02 +00:00
MSedgeMC 349ec13a9b fix: an advancement with no criteria is done and pays out
`AdvancementProgress.isDone` loops over the requirement groups, so with no
groups at all it answers true: an advancement whose `criteria` map is empty is
finished from the moment the player's progress loads. `PlayerAdvancements.
checkForAutomaticTriggers` walks every advancement, and for those with no
criteria calls `award(holder, "")` - which finds no `""` criterion to grant, so
it neither toasts nor announces - and then `AdvancementRewards.grant`s it. The
rewards are granted again on the next load, because there is no criterion to
record them under.

Horizon required `!requirements.is_empty()`, so such an advancement was never
done and never paid out. `Progress::done` now keeps vanilla's shape (every group
satisfied, vacuously true with no groups), `Tree::automatic` lists the
advancements nothing can grant, and the progress load grants their rewards
through the new `grant_rewards`, which is the `AdvancementRewards.grant` half of
`award` split out so the toast and announcement stay with a real completion.

Scope, plainly: **no vanilla advancement has an empty criteria map** - all 1224
in the shipped data have at least one criterion - so nothing in the game's own
data changes. What changes is a datapack's: before this, an advancement it
declared with `"criteria": {}` could never be obtained and its rewards never
arrived, where vanilla hands it over on load.
2026-11-05 19:24:58 +00:00
DUzzL 878342f98e Keep a synched horse rear until its flag clears 2026-11-05 20:22:16 +01:00
Riccardo GuerrieroandClaude Opus 5.5 6165c33ebe Climate searches pass distances along and stop a box's sum once it is too far
Part of #9997.

ClimateIndex::search, vanilla's Climate.RTree search, is about 5% of
overworld proto generation (callgrind, generate_proto, seed 12345) and 2%
of the nether's. About 21 boxes are measured per search: the root's two
children and the 19 below.

Three changes, none of which changes which leaf a search returns:

- The best leaf's distance goes down into each subtree and back up with
  its result. Vanilla measures the best leaf again at the start of every
  subtree and after it returns; the values are the same.
- The offset axis is always 0 in the target, so each box's squared
  offset distance is stored once instead of measured every time.
- When every bound in the tree and every target value is within 2^29,
  no term or sum of a distance can overflow, so a partial sum only grows.
  A box's sum then stops once it reaches the best distance so far: the
  search would only compare it with that distance and skip the box. The
  axes are added depth first, as surface biomes are points in depth.
  Otherwise the full wrapping sum is used as before.

The search still walks the same children in the same order from the
same previous leaf, and replaces the best only on a strictly smaller
distance, so ties go the same way. New tests compare it with a copy of
the old search over long target sequences that keep the previous
result, on the vanilla overworld and nether lists and on generated
lists full of ties, inverted spans, extremes and values at the bound.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WZzq8LUjYucp4kaN6UfSg7
2026-11-05 20:15:24 +01:00
Ludovic MarieandClaude Opus 5.5 e6d6ce8df2 Revert the command registries change: not shown to work in game
Commands typed in the window parse tags against the window's own tags,
which no server packet updates; that gap predates live switching and
needs its own change.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 20:14:15 +01:00
foudretbr 1f15474046 Launcher: the proxy hint names SOCKS in every language
The Settings hint now reads (http://host:port, socks5://host:port). Each language keeps its translation, without the word HTTP and with the socks5 form added; i18n's tests want every string in every table.
2026-11-05 20:14:11 +01:00
foudretbr 114dbd0e21 Launcher: downloads can go through a SOCKS proxy
Turn on ureq's socks-proxy feature and stop refusing SOCKS addresses, in port-pack's parse and the launcher's settings. socks5h, curl's name for SOCKS5 with remote DNS, is read as socks5, which already works that way in ureq. A test runs a request through a stand-in SOCKS5 proxy.

Fixes #8143
2026-11-05 20:14:10 +01:00
DetimesandClaude Opus 5.5 6e083af988 Native mods: crates/horizon-mod-deps puts mysql, redis, tungstenite and rusty-opus in Cargo.lock (#10174)
A workspace member with no code whose dependencies are all optional: Cargo
locks them, so a native mod's crate may name them, and no build compiles them
(horizon-client's dependency tree, default and --no-default-features, is
unchanged). All four are pure Rust and use the rustls + ring already locked.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 19:11:58 +00:00
martipopsandClaude Opus 5.5 d8781cb9df Splitscreen: a player's pack reload no longer deletes P1's overlay
Fixes #10179.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 14:10:42 -05:00
andrescamilomartinezandAntigravity 2.0 313f65e049 fix(client): prevent taskbar overlap and window shift in borderless fullscreen
When entering borderless fullscreen on Windows while the window is
maximized, winit's WM_NCCALCSIZE handler forces the client area to
match the desktop work area (rcWork), which excludes the taskbar.
Because the window is sized to monitor dimensions, this clamps the
bottom edge above the taskbar and shifts the top edge offscreen by the
taskbar height (~48px), leaving the taskbar visible on ultrawide and
standard displays (upstream MC-311683).

Ensure that any transition to fullscreen unmaximizes the window first,
and explicitly pass the active monitor handle to Borderless mode so
the window covers the entire display area without vertical offset.

Fixes #9984

Co-Authored-By: Antigravity 2.0 <[email protected]>
2026-11-05 14:08:33 -05:00
Horizon agentandClaude Opus 5.5 abf29eb414 Multiplayer: a click lists its changed slots in the order vanilla's hash map iterates
MultiPlayerGameMode.handleContainerInput puts the changed slots into a new fastutil
Int2ObjectOpenHashMap and ServerboundContainerClickPacket writes them as it iterates: the key 0
first, then the table from its last position down. Horizon listed them in slot order. The wire
stream harness's inventory scenario caught it: a shift-click from slot 37 into slot 10 went out
37, 10 from vanilla and 10, 37 from Horizon, a byte-level difference any server can see.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 20:08:04 +01:00
Riccardo GuerrieroandClaude Opus 5.5 e2b9c29ff0 Breaking a painting, a cushion or a boat makes a vibration
Killing an entity that is no mob posts ENTITY_DIE (frequency 15) at the
entity, as vanilla's kill paths do: BlockAttachedEntity.kill (cushions
punched, burnt, unsupported, struck or blown up; paintings; empty item
frames), ArmorStand.kill (broken, burnt out, blown up, /kill's
generic_kill), VehicleEntity.destroy (boats and minecarts, not a creative
hit, which only discards them) and Entity.kill from /kill for the other
non-living kinds. The attacker is the source when a hit kills it
(attributedTo), otherwise the entity itself. A frame holding an item only
gives the item up and does not die (ItemFrame.hurtServer). The event runs
on the server's path, so single player, LAN guests and dedicated servers
all get it.

New lan_e2e check entity-die: a sneaking host and LAN guest break each
kind 3 blocks from a sculk sensor, which now hears 15.

Part of #10046

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WZzq8LUjYucp4kaN6UfSg7
2026-11-05 20:06:34 +01:00
MSedgeMC 4b8abba21c test: an automatic update keeps a disabled mod off
Install ruby 1.0.0, turn it off in the Mods page, run the automatic
update to 1.1.0: the file is replaced and the mod is still off. Turned
back on, the next update keeps it on, and an explicit install of a
disabled mod turns it on again. Without the fix the first update
re-enables it and the test fails at 'the update kept it off'.
2026-11-05 19:04:13 +00:00
MSedgeMC e932cade0c fix: an automatic update keeps a disabled mod off
install_version_guarded() turned every mod it installed on with
java::set_enabled(home, id, true), and the automatic registry update
(browser/horizon/auto.rs update) installs through the same function.
A content mod the player had turned off in the Mods page therefore came
back on after every automatic update: 'off' is the absence of the
'mod.<id>:true' line in horizon-mods.txt, which the update re-added.

The guarded install no longer touches the flag. The player's own
install (install_version, which the browser and modpack paths use)
turns the mod on after the guarded install, as before, while the
automatic update leaves the mod's current on/off choice alone. A new
install still starts on because it goes through install_version.
2026-11-05 19:04:13 +00:00
Horizon agentandClaude Opus 5.5 4a5450c6f8 horizon-report: is_multiple_of (clippy)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 20:02:47 +01:00
MSedgeMC 404cf600c1 port_map: CreativeModeInventoryScreen.keyPressed is the chat key's search tab 2026-11-05 19:02:11 +00:00
MSedgeMC b66525f609 test: the creative chat key opens a focused, empty search tab
`Creative::search_tab` and `select` as `Game::key_pressed` uses them: the
search tab is found by its type, selecting it focuses the search box, and the
query the previous tab left is emptied.
2026-11-05 19:02:11 +00:00
MSedgeMC 1b8b51978d feat: the chat key opens the creative inventory's search tab
`CreativeModeInventoryScreen.keyPressed` takes the chat key on a tab that is
not the search as "show the search tab": it selects
`CreativeModeTabs.searchTab()` and sets `ignoreTextInput` so the letter the key
carries never reaches the search box. Horizon had neither: T on a creative tab
did nothing, and the letter went nowhere.

`Creative::search_tab` finds the search tab by its type, so the key selects it
through the same path a tab click uses (`select_creative_tab`, which clears the
quick-craft clicks as `selectTab` does). `Game::key_pressed` takes the chat key
there, and the `ignoreTextInput` flag it sets makes `type_chars_into_screen`
(`charTyped`) drop that one letter and no other.
2026-11-05 19:02:11 +00:00
Horizon agentandClaude Opus 5.5 d216a5477d horizon-report: loose entities of every level; rustfmt
The items, orbs, projectiles, minecarts and boats were counted for the level in place
only (the End, on the SMP, when the report ends), so tick_boats' 3 ms had no count
beside it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 20:00:16 +01:00
Horizon agentandClaude Opus 5.5 216a6218af wire_stream: compare Horizon's Legacy combat feel with Modern, packet for packet
--clients horizon,horizon-legacy plays every scenario with two Horizon clients, the
second with the Legacy feel and every combat extra on, the 1.8 rules included, and
compares their serverbound streams as the vanilla comparison does.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 19:59:02 +01:00
Horizon agentandClaude Opus 5.5 5f69e3a789 Mods: commands see the tags a live switch or a data pack added
The command registries took item and block tags from the resources the
game started with and were built once per world, so /execute and
/give refused a tag a switched mod added (seen in game: Unknown item tag
'c:gems/glint'). They now read the world's tags and are built again
after a live switch's data reload.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 19:57:44 +01:00
AxxioandAntigravity be42c5d25e game, mobs: align sulfur cube prime, vibrations and TNT drop decay with vanilla (fixes #9957)
Co-Authored-By: Antigravity <[email protected]>
2026-11-05 19:56:42 +01:00
Horizon agentandClaude Opus 5.5 ef4463a476 horizon-report: spans inside tick_lan and tick_sculk_entities
On the official SMP, 12.6 of tick_lan's 14.7 ms a tick and all of tick_sculk_entities'
6.4 ms had no span below them. Name the steps: guest ticks, entity states, the hub's
level pass (world sync, chunks, block changes, containers, entity tracking, player
state), the end of the tick and the host events; sculk's prepare, wardens, step-on and
entity game events. Spans cost nothing while off.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 19:56:42 +01:00
Horizon agentandClaude Opus 5.5 ee7a6dd9c8 wire stream: dig_teleport compares the ticks after the teleport aligned on its acceptance
The client sends /tp at a fixed tick, but a loaded server answers 2-6 ticks later from one run to the
next (seen with two identical Horizon clients). The ticks before the teleport compare one to one; from
the client's AcceptTeleportation on they align on it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 19:54:30 +01:00
Florian ERNSTandCodex 7c171a543c Preserve loaded furnace cooking totals until completion
Co-Authored-By: Codex <[email protected]>
2026-11-05 19:53:51 +01:00
degepieandClaude Sonnet 5.5 dcffd6cae9 Fire the enter_block trigger from the server's block pass
23f8bd4fe removed the only consumer of MotionEvent::EnterBlock, and the event was pushed only in the client pass. The server pass pushes it now and the guest server tick triggers it for the acting player.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
2026-11-05 23:52:32 +05:00
Nikita Maximilian GrimmandGPT 6.1 Sol e1e307cbb5 fix(launcher): preserve native shader selection and options
Fixes #10123

Co-Authored-By: GPT 6.1 Sol <[email protected]>
2026-11-05 19:48:31 +01:00
Horizon agentandClaude Opus 5.5 ab9929b5ab horizon-report: per-mob-type cost, level spans, chunk workers, network, overhead, stop/status
The official SMP is mob-bound (5,400 mobs, 73 ms a tick at 60 players) but the report
only said tick_creature_entities took the time, not which mobs. Format 2 adds, without
a restart and at a measured cost:
- mob_costs: one tick in four times every mob's tick and sums it by dimension and type
  (ms a tick, mobs ticked, us a mob);
- level_* spans: each dimension's share of the server tick;
- chunk_workers: streaming::job_stats while recording (load/generate/decorate/light...);
- network: wire bytes in/out per player and in all (connection counters, no new
  per-packet cost);
- overhead: spans and mob samples a tick at their price measured on the machine;
- horizon-report stop / status.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 19:47:54 +01:00
Nikita Maximilian GrimmandGPT 6.1 Sol 64297b8e8b fix(shaders): preserve native pack identity across updates
Part of #10123

Co-Authored-By: GPT 6.1 Sol <[email protected]>
2026-11-05 19:45:17 +01:00
KBroneZandClaude Opus 5.5 7fcf18b6df test: /forceload add loads the chunk for the next command
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 20:44:38 +02:00
KBroneZandClaude Opus 5.5 ed01ae3029 fix: handle the container loot table event in the block update probe
Goes with "keep a placed template's container loot table".

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 20:44:37 +02:00
KBroneZandClaude Opus 5.5 a9c7b82772 fix: load chunks in /forceload add, as vanilla does
`ServerLevel.setChunkForced` calls `getChunk` when a chunk becomes forced, so a
command right after `/forceload add` finds it loaded. Horizon only queued it, so
`execute in minecraft:the_nether run function` with `forceload add`, `place
jigsaw` failed with "That position is not loaded" and a skyblock pack's nether
island never appeared.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 20:44:37 +02:00
KBroneZandClaude Opus 5.5 694d953496 feat: read structure templates from data packs
A data pack's `data/<ns>/structure/*.nbt` were ignored. A jigsaw pool naming one
placed nothing, so `/place jigsaw` failed with "Failed to generate jigsaw" and
`/place template` said the template does not exist; that is how a skyblock pack
places its island. Packs also could not replace vanilla's templates.

The enabled packs' templates now join the worldgen overrides, highest pack first,
over the prepared ones, as `StructureTemplateManager` reads resources. `/place
template` and structure blocks look in the world's `generated` folder, the packs,
then the game's own.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 20:44:36 +02:00
KBroneZandClaude Opus 5.5 2f9ec5be67 test: a placed template's chest keeps its loot table
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 20:44:36 +02:00
KBroneZandClaude Opus 5.5 cf31690cae fix: keep a placed template's container loot table
`LevelAccess::load_block_entity` decoded a template's block entity, and that
drops `LootTable`, so a chest placed by `/place template`, `/place jigsaw` or a
structure block came out empty. A skyblock pack's starter chest has its items in
a loot table. Report the table as a level event and hand it to `take_loot_table`,
as `/setblock` and `/data` do (`RandomizableContainer.tryLoadLootTable`).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 20:44:36 +02:00
KBroneZandClaude Opus 5.5 3f782b89ee fix: upgrade a simple state provider to a state object, not a bare id
26.3 reads a direct block state provider as a state object. A bare string names
a provider in the `worldgen/block_state_provider` registry, so an upgraded
feature with a default-state `simple_state_provider` failed with
`unknown state provider minecraft:air` and was skipped at world start. Vanilla's
own features write `{"id": "minecraft:clay"}`.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 20:44:35 +02:00
KBroneZandClaude Opus 5.5 5062da5567 fix: upgrade a 26.2 noise with no octaves to a silent 26.3 noise
Void packs turn off vanilla's iceberg noises with
`{"firstOctave": -1000, "amplitudes": []}`. The upgrader left this as it was and
called it "more than 32 octaves or negative amplitudes"; 26.3 cannot read it, so
the world failed to start with `invalid world generation field: base_octave`.
With no octaves the noise is 0 everywhere, which 26.3 writes as one muted octave:
`{"base_octave": 0, "amplitude_modifiers": [0.0]}`.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 20:44:34 +02:00
KBroneZandClaude Opus 5.5 69ecedfc01 fix: read default_block and default_fluid in 26.3's block state form
`state()` in the generator read only `{"Name", "Properties"}`, or the id alone,
so noise settings with `{"id", "properties"}` failed the world start with
`invalid generator resources: block state`. That is the form 26.3 uses and the
one `horizon-port-pack upgrade-datapack` writes for any non-default state. Decode
these two fields with `BlockStateJsonCodec`, as the rest of worldgen does.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 20:44:34 +02:00
DUzzL f2dfddea11 Handle F3 debug keys on the title screen 2026-11-05 19:44:34 +01:00
Horizon agentandClaude Opus 5.5 b5ed12bc33 wire stream: compare a click's state id as its lag behind the server; hotbar_swap swings at the sky
A run's state ids count every change its server made since the login, so a click numbered 53 in one
run and 55 in the other were both up to date. hotbar_swap now also taps a number key with a swing at
nothing, where vanilla sends the slot only on the next tick.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 19:43:04 +01:00
Horizon agentandClaude Opus 5.5 e5105f07eb Pick: aim with vanilla's table trigonometry, so a placement's cursor matches to the bit
GameRenderer.pick aims along Entity.calculateViewVector: float radians through Mth.sin and Mth.cos,
the 65536-entry table. Horizon aimed with exact double trigonometry, which moved the hit point a
placement sends (ServerboundUseItemOnPacket's cursor) and an interaction's point by up to ~1e-4 of a
block. The wire stream harness's bridge scenario showed it: same position and look, cursor z
0.7173712 from vanilla, 0.7173119 from Horizon. Anticheats that recompute the cursor from the ray
compare it to vanilla's arithmetic. The other view_vector callers (projectiles, dragon, boats...)
are left for their own checks.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 19:42:49 +01:00
VaspyyyandClaude Opus 5.5 0e33837c78 Play a station's take sound once per shift-click
Shift-clicking a stonecutter result queued ui.stonecutter.take_result for
every item taken, and the server side played every copy at once: cutting
60 granite played the sound 60 times stacked. Vanilla's lastSoundTime
lets it through once per tick. The menu now queues each take sound once
per click (stonecutter, loom, cartography table), and lan_station_sounds
plays one sound per tick.

Fixes #10148

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 19:37:45 +01:00
makscoder 53aad00102 Snow, dripstone clay and spreading sculk lift what stands on them
`Block.pushEntitiesUp` runs when these blocks gain the collision they add on
top. The farmland/path to-dirt path already emits the `push_entities_up` block
event; the three growth paths did not, so a body standing on them sank into the
new block.
2026-11-05 21:35:45 +03:00
martipopsandClaude Opus 5.5 f513a784f0 Merge upstream/main into fix/pad-e2e-linux: #9946 edited the same file
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 13:34:34 -05:00
Vinícius MoreiraandCodex 3c794c2825 Fix shader-pack color-buffer reads in translucent draws
Preserve the preceding scene color when a translucent shader draw reads
and writes the same buffer, rather than sampling a white fallback.
Reuse the existing snapshot mechanism to avoid attachment feedback.

Validated with Sildur’s Vibrant before-and-after captures and an
original asset-free GPU regression.

Co-Authored-By: Codex <[email protected]>
2026-11-05 15:33:25 -03:00
HiuoyandClaude Opus 5.5 29662d963c Native hooks: LivingDamageEvent.Pre and .Post (#10132)
living_damage_pre and living_damage_post fire at the end of
LivingEntity.actuallyHurt, for players, mobs and the ender dragon. Pre is
told what is about to come off the health, once armour, enchantments,
effects and absorption have taken their part, and a mod may change it.
Post is told what came off, 0 when nothing did. The translated actuallyHurt
in horizon-world gets two hook points whose defaults do nothing.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 22:32:13 +04:00
degepieandClaude Sonnet 5.5 c69d03f96e Resource packs: read a lang file leniently when merging the layers
#9560 made Language::load lenient, but the pack merge parsed each layer with strict JSON and dropped the ones that failed, so a lang file with # comments never got that far.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
2026-11-05 23:25:08 +05:00
Florian ERNSTandCodex de4029c1df Merge main and retain raid harness observations
Co-Authored-By: Codex <[email protected]>
2026-11-05 19:16:54 +01:00
Evrim CicekandClaude Opus 5.5 1bf04aa84f The vehicle's box is read now, and only for the vehicle ridden
From review, against vanilla (getAttackBoundingBox and getHitbox read the
current getVehicle()):
- A mob vehicle's box comes from the mob snapshot, which holds it as it is
  after its own tick this tick, not from the last positionRider.
- The seat positionRider records names its vehicle, and is used only while
  the mob rides that one: a dismount or a new vehicle no longer reads an old
  vehicle's box or attachment.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 21:13:30 +03:00
Federico TaglianiandCopilot App 8a2619be29 Make launcher titlebar match glass UI
Co-authored-by: Copilot App <[email protected]>
2026-11-05 19:11:13 +01:00
Florian ERNSTandCodex ff0a8b31f8 Read selected guest progress for advancement selectors
Co-Authored-By: Codex <[email protected]>
2026-11-05 19:11:09 +01:00
HiuoyandClaude Opus 5.5 8ec7d24add port-pack events: RecipesUpdatedEvent is looked up as RecipesReceivedEvent
1.21.1's client had every recipe and 27 of ATM10's mods listen to
RecipesUpdatedEvent to rebuild what they made of them. 26.1's client gets
only the recipe types a mod asks for, in RecipesReceivedEvent (NeoForge's
documentation, "Client-Side Recipes"), which recipes_received answers.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 22:10:13 +04:00
HiuoyandClaude Opus 5.5 62a47df275 Native hook catalog: NeoForge labels for five more hooks
frame, gui_render_post, tooltip_component, experience_orb_pickup and
item_dropped get the NeoForge event ATM10's mods listen to for the same
point: RenderFrameEvent.Pre, RenderGuiEvent.Post,
RenderTooltipEvent.GatherComponents, PlayerXpEvent.PickupXp and
ItemTossEvent. The bracketed notes say where a hook differs (#9921).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 22:07:18 +04:00
CocofireHDandopencode 232b0e85aa UI: a button's highlight goes when its screen leaves
Vanilla's Gui.setScreen calls Screen.clearFocus on the screen it leaves, so
a screen that comes back starts with no widget focused. Horizon kept the
focus a mouse click gave a button (the title's focused_button, pause_focus,
death_focus, the options' focus), so going back to the main menu redrew that
button highlighted (AbstractWidget.isHoveredOrFocused) with the pointer
somewhere else.

The front screen is now tracked with key_screen() (which the keyboard's focus
already uses for its own screen changes); when it changes, every menu drops
its click focus. The startup menu does the same for its title behind the
options, before the loading hands that title to the game.

Fixes #9273

Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
2026-11-05 19:06:06 +01:00
Nikita Maximilian Grimm 02ba821bcf Merge remote-tracking branch 'origin/main' into hdr-note-serve-0-1-until-step-5
Co-Authored-By: GPT 6.1 Sol <[email protected]>

# Conflicts:
#	docs/api/shaders.md
2026-11-05 19:04:22 +01:00
MSedgeMC 3ef1d5a7aa port_map: StatsScreen.keyPressed is the statistics tab bar's keys 2026-11-05 18:04:22 +00:00
MSedgeMC fadcfcd6d0 test: the statistics tab bar takes Ctrl+Tab, Ctrl+Shift+Tab and Ctrl+number
`StatsUi::key` as `Game::stats_key` calls it: the cycle wraps both ways, a
number picks its tab and is clamped to the last one, a key without Control is
left alone, an empty Items or Mobs tab is skipped (and not switched to by its
number), and a switch scrolls the new list back to the top.
2026-11-05 18:04:22 +00:00
MSedgeMC c679dff99e feat: Ctrl+Tab switches the statistics screen's tabs
`StatsScreen.keyPressed` runs `MenuTabBar.keyPressed` first: with Control
held, `TabNavigationBar` cycles the tabs on Tab and Shift+Tab (skipping a tab
that cannot be chosen) and picks one on the number keys, where
`InputConstants.Key.getNumericKeyValue` reads the main row's digits and the
keypad's. Horizon's statistics screen had no keyboard handling at all, so only
the mouse could change tabs.

`StatsUi::key` is that tab-bar key press: it reports whether the bar took the
key, moves to the chosen tab when that tab has rows, and starts that list at
the top as a tab press does. `tab_key` ports `getNextTabIndex` (a digit is
`floorMod(digit - 1, 10)` clamped to the last tab; Tab walks to the next tab
that can be chosen, one full turn at most) and `numeric_key_value` ports
`getNumericKeyValue`. `Game::stats_key` gives it the tabs that have rows
(`stats_tab_active`) and plays the tab's click when the tab changes, and
`Game::key_pressed` hands the statistics screen its keys.
2026-11-05 18:04:22 +00:00
MSedgeMC d0a71973e8 The health HUD state's doc drops the removed heights
native_health_hud_state described left_height and right_height as the ones
that place the selected item's name and the action bar. They are gone, and
the text no longer reads them.
2026-11-05 18:04:12 +00:00
MSedgeMC 15a6831a33 The HUD probe fixture drops the bars' height with the argument
The two calls in hud_fixture/kinds.rs still passed the 39 the old signatures
took. The probe compares against the vanilla client, which reads neither, so
nothing else changes.

cargo clippy --examples -p horizon-client-lib -p horizon-client -- -D
warnings is clean.
2026-11-05 18:03:21 +00:00
Horizon agentandClaude Opus 5.5 a14ca6acf4 wire stream harness: headless slot clicks, the reset's self-teleport, signed block positions, two short bites
- click-slot without a renderer clicks the menu itself (the vanilla driver's handleContainerInput); horizon-ctl's
  clicks where the slot is drawn, which --headless never does.
- The reset's last teleport is the player onto itself: from the console `~` is the world spawn, which put the
  player inside bridge's stone.
- BlockPos.asLong decodes with each part signed.
- eat cuts its two bites short: when a full one ends is the server's entity event, late on a busy server.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 19:00:39 +01:00
Nikita Maximilian GrimmandGPT 6.1 Sol ffd9b8f475 chore: remove agent markers from shader code comments
Co-Authored-By: GPT 6.1 Sol <[email protected]>
2026-11-05 19:00:20 +01:00
Nikita Maximilian Grimm ceeb7841e2 Merge remote-tracking branch 'origin/main' into shader-api-02-serve 2026-11-05 18:59:42 +01:00
Horizon agentandClaude Opus 5.5 cc7200cd7a Multiplayer: the held slot goes out with the packets vanilla's game mode guards, not with any click
MultiPlayerGameMode.ensureHasSentCarriedItem runs in attack, interact, useItem, useItemOn,
releaseUsingItem, dropItem, piercingAttack and continueDestroyBlock only. A swing at nothing,
the start of a dig or an offhand swap sends no slot: a number key pressed in that tick goes out
at the start of the next one (MultiPlayerGameMode.tick). Horizon sent the slot whenever the tick
had a click. The wire stream harness's hotbar_swap scenario caught it: a tap of 2 with a swing at
the air sent SetCarriedItem before the Punch, a tick before vanilla.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 18:59:36 +01:00
makscoder 05bc471106 A refused spectator opening gets the red action bar
ServerPlayer.openMenu, when createMenu returns null for a spectator, sends
container.spectatorCantOpen to the action bar (red). Horizon's refusal in
spectator_open_menu and the chest-boat guest handler was silent.
2026-11-05 20:59:03 +03:00
AxxioandAntigravity 8c3f6e7d7b Seat camel husk jockey's parched in camel second seat (fixes #9961)
Co-Authored-By: Antigravity <[email protected]>
2026-11-05 18:57:44 +01:00
DUzzL 9cf77dcdd7 Run location effects of newly worn armor at once 2026-11-05 18:54:19 +01:00
DUzzL 7858858b94 Make thrown potions bounce off a breeze 2026-11-05 18:54:16 +01:00
Evrim CicekandClaude Opus 5.5 934c4aaabf A mounted mob reaches from its vehicle's width, and a rider is hit from its seat up
Mob.getAttackBoundingBox widens the attacker's box on x and z to its
vehicle's before the melee reach is added, so a zombie on a horse, a
chicken jockey or a mob in a boat reaches as far as the side of what it
rides. LivingEntity.getHitbox starts a rider's box at its seat
(vehicle.getPassengerRidingPosition(this).y), so its legs can't be hit; for
a riding player that is 0.6 above its feet (Avatar.DEFAULT_VEHICLE_ATTACHMENT).

positionRider now records each mob rider's own vehicle attachment and its
vehicle's box (mobs, boats, minecarts, primed TNT, displays), which the
melee check and the slime's touch read.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 20:50:13 +03:00
Claude cc474268e7 Launcher: a server on another computer, run over SSH
An Entry on the Servers list can name a host: its folder is then on that
computer, and Entry::command runs the same horizon-server arguments there
as `ssh HOST horizon-server --dir DIR ...`, with the system's ssh and the
player's keys (BatchMode, so no password is asked for or kept). Each
argument goes to the remote shell in single quotes, and a host that ssh
could read as an option is refused. remote::check asks the server there
for its status and says in plain words what is missing: a key, the host,
horizon-server, or a server in that folder.

Lists written before keep reading as they were; nothing on the page uses
remote entries yet.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Jh5A2PzdusBChqE38VZ3zD
2026-11-05 17:48:14 +00:00
DUzzL 4272d9ae41 Mobs on a ladder or vine are not pushable; no crit while climbing 2026-11-05 18:47:31 +01:00
Nikita Maximilian Grimm 276919beae Merge remote-tracking branch 'fork/shader-api-02' into shader-api-02-serve 2026-11-05 18:47:17 +01:00
Horizon agentandClaude Opus 5.5 e26dae0527 wire stream harness: 15 minutes for a loaded machine's server to start
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 18:45:08 +01:00
yahya-nusseibehandClaude Opus 5.5 45af2b23b2 Network NBT: no more tags than vanilla's quota admits
Vanilla reads network NBT with NbtAccounter's 2 MiB quota, which
charges each tag an estimate of its heap size: at least 8 bytes (EndTag,
the smallest; a byte tag in a list costs 13). Horizon counted only the
bytes read, plus up to 1,000,000 nodes, so a crafted server could send a
1 MB list of byte tags that vanilla refuses and have Horizon build ~1 M
32-byte Tags from it, about 16 times the memory.

- buf::Reader::nbt caps a network NBT value at NBT_QUOTA / 8 = 262,144
  tags (NBT_MAX_TAGS). Every value vanilla accepts has at most that
  many, so nothing vanilla accepts is refused, and one value's tags now
  take at most ~8 MiB here.
- custom::validate checks component values with the same reader, so it
  follows.
- Test: a list of 262,143 byte tags (with its root, 262,144) reads; one
  more is refused although it is a few hundred KB.

A full port of NbtAccounter's per-type charges would be closer still;
this bound needs only its smallest charge, so it can't refuse what
vanilla takes.

Found in yahya-nusseibeh's review of Horizon (L2).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
2026-11-05 19:44:32 +02:00
Horizon agentandClaude Opus 5.5 186a478e21 test_wire_stream: load the decoder by path, so tools/ never shadows a tests/ module
tools/ first on sys.path made unittest discovery import tools/test_world_spawn_parity.py for
tests/test_world_spawn_parity.py.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 18:44:02 +01:00
DUzzL de7d92e134 Keep one item in item frames filled by /item or NBT 2026-11-05 18:43:51 +01:00
yahya-nusseibehandClaude Opus 5.5 e0b19571dd RSA: constant-time private-key operations
The dedicated server and a LAN host decrypt each client's
ServerboundKey with the server's private exponent. That used
square-and-multiply, which multiplies only on the exponent's set bits,
with a data-dependent final subtraction in each Montgomery product, and
no blinding: a client controls the ciphertext and can time the reply,
the classic extra-reduction channel. The key is regenerated every start
(as in vanilla) and a recovered one still needs an on-path position,
but vanilla's JCE blinds this operation.

- Mont::mul subtracts n through a mask chosen from the overflow and
  the borrow of t − n, never a branch (Big::sub_with_borrow,
  Big::select). Every product takes the same steps.
- Mont::pow_secret: a square and a multiply for every bit of the
  exponent's limbs, the product kept by a mask. RsaPublicKey::
  apply_secret uses it; RsaPrivateKey::decrypt and ::sign call that.
  Public-key verification and encryption stay on the faster pow.
- Big::from_be_bytes no longer skips zero bytes: it parses the private
  exponent on every call, and the result is the same.
- Test: pow and pow_secret agree with a reference modular power, for
  moduli near a limb boundary (the overflow case) and exponents with
  leading zero limbs. The 11 existing RSA tests (OpenSSL signature,
  known-key decryption, generated keys) pass unchanged.

No wire or parity change. Multiplicative blinding would be the more
complete fix once a modular inverse exists.

Found in yahya-nusseibeh's review of Horizon (L1).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
2026-11-05 19:40:43 +02:00
MSedgeMC 8c6933bda4 port_map: Hud.extractTabList is tab_list::visible
The row was absent; map-check: 150 rows, no problems.
2026-11-05 17:40:40 +00:00
MSedgeMC 4ea8baade5 test: F1 hides the player list even while Tab is held
tab_list::visible with the HUD hidden answers false for a held key with two
listed players and for a local server with a list objective, and the
scoreboard's LAN test asserts the same through the game's call.
2026-11-05 17:40:32 +00:00
MSedgeMC 15047ffac4 fix: F1 takes the player list away
Hud.extractRenderState calls extractTabList inside `if (!isHidden)`, so with
the HUD hidden (F1) the player list does not draw at all, whatever the
player-list key or the list objective says.

Horizon's hud_layers only skipped the tab list under F1 when the chat screen
was open: with no screen, holding Tab drew the list over the hidden HUD. The
gate is now in tab_list::visible, next to the key and the local-server rules
it already applied, so every caller gets it.
2026-11-05 17:40:23 +00:00
yahya-nusseibehandClaude Opus 5.5 3b71256fcd RCON: one password guess per connection
After a refused login the connection stayed open, so within the 30 s
login window a client could send as many AUTH packets as it liked: with
the 16 connections allowed at once, thousands of guesses a second
against an RCON port on 0.0.0.0, each adding a supervisor log line.
Vanilla does the same, but this module already departs from vanilla for
safety (constant-time comparison, login deadline, client cap), and this
belongs with those.

- A refused login (wrong or empty password) answers as before, waits
  REFUSED_PAUSE (1 s) and closes the connection. Its place stays taken
  meanwhile, so the client cap also bounds how fast passwords can be
  tried: at most 16 guesses a second instead of thousands.
- A command before the login is still refused without closing, and the
  wire protocol is unchanged; standard RCON clients connect again after
  a refusal.
- Tests: a wrong and an empty password each end their connection after
  the pause; a failed login after a successful one does too. The pause
  is a Limits field so tests use 200 ms.

Found in yahya-nusseibeh's review of Horizon (L3).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
2026-11-05 19:37:23 +02:00
DUzzL b20ab60d01 Make a splash of harming teleport an enderman instead of hurting it 2026-11-05 18:35:16 +01:00
yahya-nusseibehandClaude Opus 5.5 c0e2f62b35 Telemetry server: hub, intake and export off the async workers too
The server runs two async workers and keeps SQLite on tokio's blocking
pool (lib.rs, `blocking`), but /hub/vote, /hub/votes and /hub/report and
the token-protected /v1/export/* still called app.store() on the
workers. While a blocking-pool task holds the store mutex (a large batch,
the daily upkeep), two votes or reports from different addresses park
both workers on it and every handler stops answering, /healthz
included; per-address limits don't help, it takes two requests.
/hub/votes also held votes_cache's lock while waiting for the store.

- hub::vote and hub::votes run their store calls through blocking();
  votes reads the cache, releases its lock, counts on the blocking pool
  and stores the result after.
- intake::receive checks the day's count and queues the report in one
  blocking() call, under one store lock as before; report marks the
  issue filed through blocking().
- export::benchmarks and export::hardware read through blocking().
- A failed blocking task answers 503 "try again later" through each
  module's own refusal (the hub's keeps its CORS headers).

Behaviour is unchanged otherwise: the crate's 58 tests pass as they are.

Found in yahya-nusseibeh's review of Horizon (M1).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
2026-11-05 19:32:12 +02:00
Riccardo GuerrieroandClaude Opus 5.5 817c37749f Perlin volumes skip the Ys a lerp does not read, and batch Ys across cells
Part of #9996.

The terrain noise (old_blended_noise) is a lerp between two 16-octave
smeared Perlin stacks. Lerp reads its first branch only where alpha is
not 1 and its second only where alpha is not 0, and in the overworld
about 37% of the coarse points have alpha 1 and 50% alpha 0. The lerp
now passes each branch a bit per Y of each column it reads, and a noise
volume skips the others (DensitySampler::sample_needed,
Noise::add_to_needed; samplers without it still sample everything).

When a column's Y cells change at every Y, as in the stacks'
high-frequency octaves, four Ys in four cells now go through the same
vectorized lane code as four Ys in one cell, each lane with its own
cell's corner terms. Gradient lookups are a loop rather than
array::map, which LLVM had left out of line.

Every value is computed by the same operations in the same order:
snapshots match byte for byte and the tests compare against the
unbatched, unmasked loops.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Fekd3xkgubE8VfXrALh2td
2026-11-05 18:29:50 +01:00
yahya-nusseibehandClaude Opus 5.5 4f49676a37 port-pack: cap every jar and pack entry it inflates
horizon-port-pack read jar entries with read_to_string/read_to_end and
no limit. zip stops an entry only at the size it declares, which can be
4 GiB (more with zip64), and the launcher runs check::check in its own
process on every Java mod a mod-browser search lists, without a click.
So one published mod with a highly compressible mods.toml could abort
the launcher of everyone whose search showed it. A 522 KB jar whose
mods.toml inflates to 512 MiB made `horizon-port-pack check` reach
594 MiB and abort; with this change it peaks at 9 MiB and refuses the
jar.

- crate::entry: read()/read_text() with a per-entry cap, and a Budget
  for a running total over one pass of an archive, so many entries
  under their cap can't add up either. Limits: 16 MiB per text entry
  (JSON, TOML, the manifest), 64 MiB per file kept as it is, 1 GiB per
  archive pass. Only bytes actually read count; an entry declaring more
  than what is left is refused unread.
- Every zip read in the crate goes through it: jar::read's metadata
  (a too-large file refuses the jar), carry's namespace scan and file
  copy, worldgen's piece counts, structure-set and biome tags,
  processors, upgrade::read_pack and import's JSON index. check.rs kept
  its 16 MiB per-class cap and now also refuses a jar whose classes
  together unpack to more than 1 GiB (a thousand entries just under the
  per-class cap was the same attack).
- Tests: the cap and the running total (declared or not, refused
  entries counting), UTF-8, and check() refusing a small jar whose
  mods.toml unpacks past 16 MiB.

No parity change: real mod files are far under these limits.

Found in yahya-nusseibeh's review of Horizon (H1).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
2026-11-05 19:28:47 +02:00
Horizon agentandClaude Opus 5.5 0068b79fc4 parity: wire_stream runs when named (and nightly), alone
It drives a vanilla client on a virtual screen for ~15 minutes: no automatic selection on every client change.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 18:26:04 +01:00
Horizon agentandClaude Opus 5.5 0744579340 wire stream harness: give a loaded machine's vanilla client 15 minutes to reach the world
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 18:25:03 +01:00
yahya-nusseibehandClaude Opus 5.5 24987d6660 Launcher: rustls 0.23.46 for four RustSec advisories
launcher/Cargo.lock pinned rustls 0.23.45 (through ureq 2.12), which
cargo audit flags with RUSTSEC-2026-0346 (client session Debug impls
expose TLS secrets), -0347 (buffered data sent to the outer server after
an ECH rejection), -0348 (Brotli certificate decompression) and -0349
(post-quantum downgrade from an unauthenticated ServerHello), all fixed
in 0.23.46. The launcher carries Microsoft sign-in tokens over this
stack.

- `cargo update -p rustls --precise 0.23.46`: rustls alone moves.
- cargo audit on the new lockfile: no vulnerabilities (8 informational
  warnings: unmaintained or yanked crates, unchanged).

Found in yahya-nusseibeh's review of Horizon (L4).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
2026-11-05 19:21:11 +02:00
Evrim CicekandClaude Opus 5.5 391ce2975d A nautilus's target is within 16 blocks, and pandas and polar bears panic on the same causes
From review, against vanilla:
- Sensor.isEntityTargetable is range(16) from feet to feet, a sphere; the
  nearby scan alone was a cube.
- Panda's PandaPanicGoal and an adult PolarBear's panic use
  #panic_environmental_causes as well; their own copies of it lacked freeze
  and sulfur_cube_hot. They now share armadillo::environmental.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 20:19:00 +03:00
yahya-nusseibehandClaude Opus 5.5 ce92f7e45a Launcher: open HORIZON_LAUNCHER_PROJECT once without remove_var
The mod page opened the project named by HORIZON_LAUNCHER_PROJECT (a
screenshot aid) and then called std::env::remove_var so it wouldn't open
again. By then the download, check and keyring threads are running, and
remove_var races getenv on any of them (glibc's DNS resolution
included): unsound, which is why edition 2024 makes it `unsafe`. The
launcher is on 2021, so it compiled silently.

- A static flag remembers that the page was opened this run; the
  environment is no longer written. Same behaviour: the page opens once,
  after the first search that finds it.

Found in yahya-nusseibeh's review of Horizon (L5).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
2026-11-05 19:11:41 +02:00
DUzzL a7f6ecbdfb Send the carried slot before a drop (ensureHasSentCarriedItem) 2026-11-05 18:11:01 +01:00
Horizon agentandClaude Opus 5.5 f2f3363a0a wire stream harness: start a scenario once the client has taken the reset's teleport
A busy server ran hit_target's reset teleport on the scenario's first tick: the vanilla client's first move
said off the ground and every later move differed. The reset now ends with a teleport, and the scenario waits
until the client has accepted it and ticked 40 more times.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 18:09:23 +01:00
DUzzL 31c1bd1f8b Honor can_place_on for buckets and flint/fire charge in adventure mode 2026-11-05 18:08:40 +01:00
Horizon agentandClaude Opus 5.5 a6730f376e Native mods switch: rustfmt and a doc lint
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 18:07:43 +01:00
Horizon agentandClaude Opus 5.5 aa6fc60c4a Merge mods/in-game-browser into mods/hotload-content
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 18:06:51 +01:00
Horizon agentandClaude Opus 5.5 315d1e424d Mods: a live switch's data reaches the server with no data pack
The server built its world tables from the content root only when the
world had data packs; without one it kept the tables loaded at startup,
so a switched mod's recipes and tags never applied (seen in game:
/recipe give still said Unknown recipe). After a live switch the tables
are built from the new root. An install that waits for a restart says so
on the project page.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 18:06:51 +01:00
Horizon agentandClaude Opus 5.5 b1e41d46ec Mods browser: its search box says it searches mods
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 18:05:56 +01:00
MSedgeMC 7e5063a4d5 port_map: ScreenRectangle.intersection is the scissor stack's cut 2026-11-05 17:03:55 +00:00
MSedgeMC 185908fd38 test: nested GUI scissors clip to the intersection and restore the outer clip
`screen_draw::paint`: an inner scissor draws to the overlap of both, a scissor
with nothing around it is cut to the screen, and one that misses the outer
clip draws nothing while the outer clip takes over again when the inner closes.
2026-11-05 17:03:55 +00:00
MSedgeMC dd0bffd667 feat: a nested GUI scissor is intersected with the one around it
`GuiGraphicsExtractor.ScissorStack` pushes each clip intersected with the one
around it (the screen while there is none, `ScreenRectangle.empty()` when they
miss) and pops it back when it closes. `screen_draw::paint` kept a single clip
that each `Draw::Scissor` replaced: a nested scissor stopped the outer clip
applying at all, and the outer one never came back after the inner closed.

`paint` now keeps the stack: `Draw::Scissor` pushes the intersection with the
enclosing clip (or the screen) and `Draw::NoScissor` clips what was drawn since
and pops back to the enclosing clip. `intersect` is `ScreenRectangle.intersection`.
2026-11-05 17:03:54 +00:00
MSedgeMC c8f1559eb0 port_map: the region file version's configure row is ported
`RegionFileVersion.configure` was the row that named this gap: read from
server.properties and never applied. `Compression::configure` now applies it
from the dedicated server's startup and `compress_records` asks
`Compression::selected()`, so the row says `port` with what each half does.
2026-11-05 17:02:04 +00:00
Evrim CicekandClaude Opus 5.5 1f031f6bd3 A nautilus only goes for a pufferfish it sees, and frost and a hot sulfur cube panic armadillos
NautilusAi.findNearestValidAttackTarget picks the closest #nautilus_hostiles
in water from NEAREST_VISIBLE_LIVING_ENTITIES, the sensor's list of what the
nautilus can see; Horizon took any pufferfish within 16 blocks, through
walls. Its cooldown and 1 in 2 are drawn from the level's random
(level.getRandom()), not the nautilus's own.

Armadillo.actuallyHurt panics (rolls out) on #panic_environmental_causes,
which also holds freeze and sulfur_cube_hot; Horizon listed the other six.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 20:02:04 +03:00
MSedgeMC 2e7afbfe64 test: the region compression follows region-file-compression
Two cases, one per half of the port:

- `compression::tests::the_selected_region_version_follows_the_property` walks
  the four names `RegionFileVersion` registers (`gzip`, `deflate`, `none`,
  `lz4`) against `Compression::selected()`, checks the default before any is
  set is `deflate`, and checks that `zstd` and the wrong case `LZ4` are refused
  and leave the selection alone, as vanilla logs the value and keeps the one it
  had.
- `save::tests::a_chunk_is_written_with_the_selected_region_version` runs the
  function the save path calls, `compress_records`, once per name, and asserts
  the record's codec is the selected one and that its bytes decode back to the
  same chunk.

With `compress_records` back on `Compression::Zlib` the second test fails with
`left: Zlib, right: None` ("written with none") and would then fail on `lz4`
with `left: Zlib, right: Lz4`.

Also here: the helper-parity test next door now dates its expected records with
`Compression::selected()` rather than `Zlib`, so it keeps checking that the
helpers agree with one thread whatever version the process selected instead of
pinning the default.
2026-11-05 17:02:04 +00:00
MSedgeMC 5a3dc52e1f fix: region files are written with region-file-compression
Vanilla's `Main.main` hands `server.properties`' `region-file-compression` to
`RegionFileVersion.configure` right after the settings are saved, and every
`RegionFile` written afterwards uses the version it selected (`getSelected`):
`gzip`, `deflate` (the default), `none` or `lz4`. Horizon parsed the key into
`ServerProperties.region_file_compression` and nothing read it, so every chunk
region was written as `Compression::Zlib` whatever a server asked for.

`Compression::configure(name)` and `Compression::selected()` are the two halves
of `RegionFileVersion` over the same kind of process-wide selection, starting at
`VERSION_DEFLATE`, with `REGION_FILE_NAMES` in the registration order vanilla's
error message lists. A name is matched exactly, as `VERSIONS_BY_NAME` does; an
unknown one changes nothing and says so. The dedicated server's startup calls it
where vanilla does, before the EULA check, and logs vanilla's line for a value
the table does not know. `save.rs::compress_records` encodes each record with
the selection, so the chunks the save path writes carry it; the read side needs
no change, as `Compression` already decoded all four and the header's version
byte names each chunk's codec.
2026-11-05 17:02:04 +00:00
martipopsandClaude Opus 5.5 7a882e8442 pad_menu_e2e: pass on a fast machine and at 16:9, and say when the window isn't
focus_on looked a widget up before stepping, so a row scrolled out of view
ended the case: it now steps until the row is drawn. optimize_world's job
finished before the case could look on a fast machine: 60000 linked regions
take long enough and cost almost nothing. A tiling compositor sizes the hidden
window too, which fails every case that scrolls, so the harness warns.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 11:59:35 -05:00
Horizon agentandClaude Opus 5.5 b4644568dd wire stream harness: an absolute --bin, since horizon-ctl runs from the scratch home
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 17:57:02 +01:00
Horizon agentandClaude Opus 5.5 44c13ed7ac vanilla_client: this machine's natives when prepared.json lists another's
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 17:57:02 +01:00
Horizon agentandClaude Opus 5.5 6039812a76 VanillaTickScript: 26.3's SDL key and mouse codes
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 17:57:02 +01:00
Horizon agentandClaude Opus 5.5 93fc52b7ef wire stream harness: the vanilla client on the virtual screen, with Mesa's EGL
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 17:57:02 +01:00
Horizon agentandClaude Opus 5.5 875f62b6b5 Wire: compare the client's packets with vanilla's, tick by tick
tools/test_wire_stream_parity.py: a Horizon client and a real vanilla 26.3
client join a vanilla server through tools/net_capture.py and play the same
tick-indexed scenarios (walk, sprint-jump, strafe, w-tap, sneak, hits,
sprint combos, block-hit, rod, bow, eat, bridging, inventory, hotbar swaps,
a teleport during a dig). tools/wire_stream.py cuts each serverbound stream
into client ticks (ClientTickEnd ends one) and decodes the packets; every
tick must hold the same packets with the same fields.

Horizon plays a scenario from HORIZON_TICK_SCRIPT (game/tick_script.rs),
before each client tick; vanilla from VanillaTickScript.java, which hooks a
fixed point of Minecraft.tick (Tutorial.onLookAt) and presses the keys
through KeyMapping.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 17:57:02 +01:00
Horizon agentandClaude Opus 5.5 96d60b9daf Multiplayer: sneaking for a use is the input of the last tick, as vanilla's isShiftKeyDown
LocalPlayer.isShiftKeyDown reads input.keyPresses, which LocalPlayer.aiStep
sets from the keys: what the last ServerboundPlayerInputPacket said.
Minecraft.handleKeybinds runs before it, so a sneak pressed in the frame of
a right-click counts from the next tick. Horizon read the live Shift key for
ServerboundInteractPacket's secondary flag and for every secondary-use
check (isSecondaryUseActive): a click in that frame sent a sneaking
interaction a server saw before the sneak itself.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 17:56:54 +01:00
Horizon agentandClaude Opus 5.5 eb7cc8305c Multiplayer: a rider's sprint and a boat's paddles go out in vanilla's order
LocalPlayer.sendChanges sends a passenger's look, then the vehicle it
controls, and only then its sprint (sendIsSprintingIfNeeded); riding a
vehicle it doesn't control, it sends no sprint at all. AbstractBoat.tick
sends the paddles from the boat's own tick, before its passenger's
sendChanges. Horizon sent the sprint first and for any vehicle, and the
paddles last.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 17:56:53 +01:00
Horizon agentandClaude Opus 5.5 9e7b50b560 Multiplayer: a new held slot goes out at the start of the next tick, as vanilla
MultiPlayerGameMode.tick, first in Minecraft.tick and run with a screen
open too, sends the slot chosen since the last tick
(ensureHasSentCarriedItem); LocalPlayer.sendChanges sends no slot. Horizon
also sent it at the end of the tick that changed it, before the input and
the move: a number key pressed in a tick went out one tick early, on the
other side of the movement packet a translating proxy (ViaVersion to an
older server) turns into that tick's flying packet. An action in the same
tick still sends the slot first, as ensureHasSentCarriedItem does.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 17:56:53 +01:00
Horizon agentandClaude Opus 5.5 635fa28928 Multiplayer: a teleport aborts the dig on the wire, and a new level counts predictions from 0, as vanilla
ClientPacketListener.handleMovePlayer accepts the teleport, then runs
MultiPlayerGameMode.stopDestroyBlock: a dig the teleport interrupts (an
anticheat's setback while mining or bridging, a /tp) sends
ABORT_DESTROY_BLOCK and starts the attack strength again. Horizon dropped
the dig without a word, so the server kept counting it.

BlockStatePredictionHandler lives in the ClientLevel: handleLogin (a proxy's
server switch) and handleRespawn into another dimension make a new one, so
the use and dig sequence numbers count from 0 again. Horizon counted on for
the whole connection.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 17:56:52 +01:00
MSedgeMC 51a4b98992 port_map: the item name and the action bar rows name their layout fns
Hud.extractSelectedItemName is hud_overlays::ToolHighlight::draw and
Hud.extractOverlayMessage is hud_overlays::Titles::overlay_message; neither
row existed. map-check: 151 rows, no problems.
2026-11-05 16:54:42 +00:00
MSedgeMC 0e97601d25 test: the item name and the action bar sit at vanilla's heights
text_position_tests asks a ToolHighlight holding a stack and a Titles
holding an action bar for their draw calls at 320x240: the name's text top
is 240 - 59 (and 240 - 45 in a mode that cannot hurt the player), the action
bar translates to 240 - 68 with its text four pixels under that, and a
zeroed timer draws nothing. The font is built from no resources, as
chat_tests does, so the test needs no game assets.
2026-11-05 16:54:42 +00:00
MSedgeMC d48f37c2a4 fix: the selected item name and the action bar keep vanilla's height
Hud.extractSelectedItemName draws the name at guiHeight - 59, plus 14 in a
mode that cannot hurt the player, and Hud.extractOverlayMessage puts the
action bar at guiHeight - 68: both constants, which the health, food, air and
mount rows above the hotbar never move.

Horizon moved both up by max(leftHeight, rightHeight). That is 59 for a
survival player with armour, so the common case agreed by accident, but it is
69 with a second row of hearts and 81 for a player underwater on a mount, and
the two texts floated ten or twenty pixels above where vanilla draws them.

Both use the constants now, and HealthHud no longer keeps the heights: 26.3's
Hud has no such fields, only the locals of extractItemHotbar that the
contextual-info bar reads.
2026-11-05 16:54:42 +00:00
Nikita Maximilian GrimmandClaude Opus 5.5 785a399740 docs: HDR design note: only the 0.1 shim goes with 0.1
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 17:53:11 +01:00
martipopsandClaude Opus 5.5 7ac3021dfb Show each controller's mapping source and uuid in pad status
A mapping bug report needs to know whether gilrs found an SDL row for a pad
and which id such a row would need. pad status now lists mapping_source
(sdl, driver or none) and uuid (32 hex digits, the id an SDL row starts with).

With splitscreen on, PadRouter holds the gilrs reader, so pad status said
backend:false and listed no controllers, unless the reader was lent to P1.
The router now leaves what it sees for the status of each game that has a
control socket; that list is only built when one is open.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 11:53:03 -05:00
Horizon agentandClaude Opus 5.5 56c896f51c docs/mods.md: which switches wait in a world
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 17:51:12 +01:00
HiuoyandClaude Opus 5.5 bb4d24e909 Native hook catalog: RenderLevelStageEvent labels for the world drawing hooks
81 of ATM10's mods listen to RenderLevelStageEvent to draw in the
world at a stage of the frame. Main has three hooks for that, none
labelled:

- world_submit (fragment world_canvas): quads and text right after
  the particles, before the hand;
- world_primitives (fragment): lines, boxes, beams and text at the end
  of the world pass, before the hand;
- world_drawn (API 58): the drawn world's colour, depth and matrices,
  once the whole level is drawn.

Each gets "neoforge": "RenderLevelStageEvent (...)", with where it
draws in words, so horizon-port-pack events maps the 81 mods to them.
world_drawn also gets side client and cancellable false; the fragments
had theirs. NeoForge's user documentation doesn't cover this event,
so the labels use only its name.

Fixes #10036

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 20:50:51 +04:00
Horizon agentandClaude Opus 5.5 aa95310c72 Mods: in a world, content mods of assets and data stay switchable
The Mods page froze every content mod's toggle while a world was open.
Those with only assets and data now reload live, so their toggles stay
active in a world this game serves; the others keep the lock. Each mod
is classified once when the mods are listed, and a native mod's toggle
no longer triggers a resource reload.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 17:50:21 +01:00
Riccardo GuerrieroandClaude Opus 5.5 a88fab01a5 A mob's Thorns hurts the player who hits it
EnchantmentHelper.doPostAttackEffectsWithItemSource runs the victim's
equipment before the attacker's weapon: the player's melee hit and sweep,
arrow, trident and wind charge now meet the mob's own post_attack
effects. Thorns rolls from the level's random, hurts the player from
the wearer by 1-5 and wears the piece by 2 (Unbreaking, breaking with
its entity event); the player's own Thorns doesn't answer a thorns hit,
and a thorns hit that lands whole plays enchant.thorns.hit.

The mace harness gets armored targets with Thorns and Unbreaking; new
lan_e2e check mob-thorns.

Part of #9954

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 17:49:52 +01:00
Nikita Maximilian GrimmandClaude Opus 5.5 02b2f7b9c0 docs: HDR design note: serve shader API 0.1 until 0.2 is on by default
The note kept 0.1 at least until 0.2 leaves experimental, which would keep
today's HDR path (marks, the 16-bit unorm frame, Smooth Gradients) alive for
0.1 packs alone. The shader API is experimental, which stability.md lets
change in any release without notice. So step 5, which turns 0.2 on by
default, also stops serving 0.1: 0.1 packs are refused with stability.md's
message, the registry lists 0.2 packs, and Golden Hour's 0.2 port becomes the
example. Later experimental lines follow the same rule. The shader API page
tells pack authors.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 17:46:24 +01:00
IsT3RiK 44b980def2 Merge remote-tracking branch 'upstream/main' into traduction-messages-mods
# Conflicts:
#	launcher/lang/af.json
#	launcher/lang/ar.json
#	launcher/lang/az.json
#	launcher/lang/ba.json
#	launcher/lang/be.json
#	launcher/lang/bg.json
#	launcher/lang/br.json
#	launcher/lang/bs.json
#	launcher/lang/ca.json
#	launcher/lang/cs.json
#	launcher/lang/cv.json
#	launcher/lang/cy.json
#	launcher/lang/da.json
#	launcher/lang/de.json
#	launcher/lang/el.json
#	launcher/lang/eo.json
#	launcher/lang/es.json
#	launcher/lang/et.json
#	launcher/lang/eu.json
#	launcher/lang/fa.json
#	launcher/lang/fi.json
#	launcher/lang/fil.json
#	launcher/lang/fo.json
#	launcher/lang/fr.json
#	launcher/lang/fur.json
#	launcher/lang/fy.json
#	launcher/lang/ga.json
#	launcher/lang/gd.json
#	launcher/lang/gl.json
#	launcher/lang/haw.json
#	launcher/lang/he.json
#	launcher/lang/hi.json
#	launcher/lang/hr.json
#	launcher/lang/hu.json
#	launcher/lang/hy.json
#	launcher/lang/id.json
#	launcher/lang/ig.json
#	launcher/lang/is.json
#	launcher/lang/it.json
#	launcher/lang/ja.json
#	launcher/lang/ka.json
#	launcher/lang/kk.json
#	launcher/lang/kn.json
#	launcher/lang/ko.json
#	launcher/lang/ky.json
#	launcher/lang/la.json
#	launcher/lang/lb.json
#	launcher/lang/li.json
#	launcher/lang/lmo.json
#	launcher/lang/lo.json
#	launcher/lang/lt.json
#	launcher/lang/lv.json
#	launcher/lang/mk.json
#	launcher/lang/mn.json
#	launcher/lang/ms.json
#	launcher/lang/mt.json
#	launcher/lang/nl.json
#	launcher/lang/no.json
#	launcher/lang/oc.json
#	launcher/lang/pl.json
#	launcher/lang/pt-br.json
#	launcher/lang/pt.json
#	launcher/lang/ro.json
#	launcher/lang/ru.json
#	launcher/lang/sah.json
#	launcher/lang/se.json
#	launcher/lang/sk.json
#	launcher/lang/sl.json
#	launcher/lang/so.json
#	launcher/lang/sq.json
#	launcher/lang/sr.json
#	launcher/lang/sv.json
#	launcher/lang/szl.json
#	launcher/lang/ta.json
#	launcher/lang/th.json
#	launcher/lang/tr.json
#	launcher/lang/tt.json
#	launcher/lang/uk.json
#	launcher/lang/ur.json
#	launcher/lang/uz.json
#	launcher/lang/vec.json
#	launcher/lang/vi.json
#	launcher/lang/yi.json
#	launcher/lang/yo.json
#	launcher/lang/zh.json
#	launcher/lang/zu.json
2026-11-05 17:46:05 +01:00
martipopsandClaude Opus 5.5 1a7a3678bb Refuse a control socket path too long for a Unix socket up front
A --control-socket path over the system's limit was only found when the
game tried to listen, after loading resources: it logged the raw SUN_LEN
error, wrote a crash report and exited 1, leaving startedCleanly:false so
the next start reset fullscreen. It is a bad command line: exit 2 with a
plain message, before the game folder, options.txt or resources are
touched. horizon-ctl launch refuses it as usage too, without starting a
game or creating a log beside the socket. A .json endpoint is TCP and has
no such limit.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 11:45:34 -05:00
Nikita Maximilian GrimmandClaude Opus 5.5 12daf9d13f Golden Hour 0.2: 0.1's look in the grade, Horizon's tone curve
The port ran 0.1's grade on linear values, which darkened midtones by about
nine 8-bit steps. The soft shoulder and the saturation are a look written for
display values, so the grade now runs them on the encoded picture, while the
golden tint, the glow and the vignette stay on light. The pack drops its own
[tonemap]: Horizon's curve clips at white in SDR as 0.1's clamp did, and light
above white survives the grade for HDR. The captures now match 0.1 within
about 0.3 steps on average; what's left is the glow around light sources.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 17:44:38 +01:00
HiuoyandClaude Opus 5.5 513312eb8b Native hook catalog: world_lifecycle also answers ServerStartingEvent
NeoForge documents ServerStartingEvent as fired after
ServerAboutToStartEvent and before ServerStartedEvent. Horizon has
server_about_to_start at the start of loadLevel, and world_lifecycle's
WorldOpenReady once the open has finished, before the tick loop.
Nothing of Horizon's runs between the two NeoForge events, so
WorldOpenReady answers both (30 ATM10 mods listen to Starting).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 20:43:53 +04:00
BnCattandClaude Opus 5.5 6c44743c21 Older servers: read 1.21.11's EitherHolder components
1.21.11 sends instrument, provides_trim_material, damage_type,
chicken/variant and zombie_nautilus/variant as EitherHolders (a flag,
then the entry's holder or its ResourceKey), as it does jukebox_playable;
26.1 made them plain holders. Horizon read the flag as the holder, so a
goat horn's slot came one byte long and the entity data around it one
byte short ("could not translate 1.21.11 packet 20/101").

The flag is read for all six now: a holder reads on as the pinned one,
a key (no pinned numbering for it) drops the component.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 19:43:38 +03:00
Evrim CicekandClaude Opus 5.5 7cead764e8 A held item's attack_range patch, and slimes' reach, follow it too
From review, against vanilla:
- getActiveItem().get(ATTACK_RANGE) includes the stack's patch: an added
  minecraft:attack_range (the codec's defaults: max_reach 3, min_reach 0,
  mob_factor 1) sets the reach, and !minecraft:attack_range takes a spear's
  away.
- AbstractCubeMob.dealDamage calls isWithinMeleeAttackRange, for the player
  it touches and for iron golems; it repeated the default formula.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 19:43:32 +03:00
IsT3RiK f719bbef1c Merge remote-tracking branch 'upstream/main' into traduction-fr
# Conflicts:
#	launcher/lang/fr.json
2026-11-05 17:43:05 +01:00
Horizon agentandClaude Opus 5.5 bdfe6e4dd1 Mods hot load: rustfmt and clippy fixes
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 17:39:33 +01:00
TheMrZZandCodex gpt-6.1-sol 9365b979ac Expose live boat leash data to entity commands
Apply the existing leash writer after the boat's base command tag so live
holders and pending links override stale input data. Keep holder policy
and boat behavior in their existing implementations.

Add an actual Game getter regression and a guarded Java/Game fixture
covering owner clicks, repeated reads, pending UUIDs and detached links.

Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-11-05 17:32:38 +01:00
DUzzL ef05a384d5 Burning players and mobs destroy powder snow they stand in 2026-11-05 17:29:34 +01:00
Evrim CicekandClaude Opus 5.5 d4a2f07032 A mob's melee reach follows the spear it holds
Mob.isWithinMeleeAttackRange takes the active item's attack_range when it
has one: the box is inflated by effectiveMaxRange (max_reach * mob_factor,
2.25 for every spear) instead of the default sqrt(2.04) - 0.6, and a target
within effectiveMinRange (min_reach * mob_factor, 1.0) is out of range.
Horizon always used the default, so a mob given a spear without a
SpearUseGoal (a vindicator, say) hit what stood right beside it and missed
what the spear reached.

New mob AI scenarios: a vindicator with a spear against a player out at the
spear's reach and right beside it, and zombies with a spear (their
SpearUseGoal; the probe now names it as vanilla does).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 19:29:23 +03:00
Horizon agentandClaude Opus 5.5 b90cebc01b Merge main into mods/native-switch
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 17:29:20 +01:00
Horizon agentandClaude Opus 5.5 d9e35999b6 Merge main into mods/hotload-content
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 17:29:04 +01:00
Horizon agentandClaude Opus 5.5 249aa0d4c1 Merge mods/in-game-browser into mods/hotload-content
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 17:25:38 +01:00
Horizon agentandClaude Opus 5.5 8aba8be7a9 Mods browser: rustfmt and a clippy fix
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 17:24:58 +01:00
Horizon agentandClaude Opus 5.5 3fc5f274be PvP & HUD: one stand-in line per widget in the editor
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 17:22:42 +01:00
HiuoyandClaude Opus 5.5 0801940eeb Native hook catalog: label the screen input and render hooks, and item pickup
Seven more hooks whose doc comments already name their NeoForge event:
screen_render_post (ScreenEvent.Render.Post), the five screen input
hooks (ScreenEvent.MouseButtonPressed.Pre, .MouseButtonReleased.Pre,
.MouseScrolled.Pre, .KeyPressed.Pre, .CharacterTyped.Pre), all client,
and item_picked_up (ItemEntityPickupEvent.Pre, server), whose cancel
stands for setCanPickup(TriState.FALSE), with no forced pickup.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 20:21:57 +04:00
Horizon agentandClaude Opus 5.5 3f164ac06a PvP & HUD: the HUD editor in main's new screen and menu matches
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 17:18:51 +01:00
some-du6eandClaude Opus 5.5 a99b380175 Format the client lib's re-exports
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 10:17:37 -06:00
Florian ERNSTandCodex 5cede4ff4e Record refreshed raid loss and harness compatibility proof
Co-Authored-By: Codex <[email protected]>
2026-11-05 17:17:06 +01:00
MSedgeMC dd6a0b938d port_map: ChatScreen.formatChat is the input's draft formatter 2026-11-05 16:16:26 +00:00
MSedgeMC 4077c5e01c test: a restored chat draft draws grey italic and one Backspace clears it
Through `ChatScreen::new` with `is_draft`, `key` and `draw` - the calls
`Game::open_chat` and `Game::chat_key_code` make: the draft's drawn text is
grey and italic, an edited draft and a box opened on ordinary text are not,
and the first Backspace on an untouched draft empties the box where an
ordinary box loses one character.
2026-11-05 16:16:26 +00:00
MSedgeMC 6a51ac58a7 feat: a restored chat draft draws grey italic and clears on the first Backspace
`ChatScreen` keeps the `isDraft` its constructor argument sets: `formatChat`
draws the text grey italic until `onEdited` runs, and `keyPressed` clears the
whole box on the first Backspace while it is still set. Horizon tracked the
draft (`Game::chat_draft`, `chat_draft_shown`) but opened the box as plain
text: `open_chat_method` knew whether the text was the draft and did not pass
it.

`ChatScreen::new` takes `is_draft` (`ChatScreen.init`'s `isDraft`), `edited`
(`onEdited`) clears it, `key` (`keyPressed`) clears the input on a Backspace
while it is set, `format` applies the draft formatter (`formatChat`, which
`ChatScreen.init` registers before `CommandSuggestions.formatText`) and
`resize` keeps it (`ChatScreen.resize` keeps the field).

`Game::open_chat` threads the flag from `open_chat_method`
(`ChatComponent.createScreen`); the in-bed wake-up
(`InBedChatScreen.onPlayerWokeUp`) and `Gui.openChatAndAddText` open plain
text, as vanilla does.
2026-11-05 16:16:26 +00:00
repositorykreml1nandCodex 6d9e1191a2 fix: broadcast command-block say announcements to connected players
Broadcast unsigned say chat independently of feedback capture and keep the
command source display name. Add a two-client LAN regression for both
TrackOutput states and conditional-chain success.

Co-Authored-By: Codex <[email protected]>
2026-11-05 19:16:22 +03:00
makscoder 574b20a662 Loot: guests roll a container's table on open; double chests roll both halves
The host's open unpacks the loot table it still holds; the guest's
(lan_open_container) read the slots without it, so a LAN or splitscreen
player met an empty container that kept its table, and
player_generates_container_loot never fired for them. unpack_opened_loot
now runs for the acting guest too.

ChestBlock.getMenuProvider unpacks both halves of a double chest; only
the clicked half was unpacked. After its own roll is consumed, the
partner's is unpacked too (the consumed own roll ends the recursion).
2026-11-05 19:15:23 +03:00
some-du6eandClaude Opus 5.5 2a767017b9 OptiFine custom colours: read optifine/color.properties
Every documented key: particle, fog and sky colours, potions, spawn eggs,
map, sheep, collar and dye colours (with their old names), text and its
colour codes, the loading screen, clouds, xporb.time, palette.format and
palette.block.* assignments.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 10:15:15 -06:00
HiuoyandClaude Opus 5.5 a8ea55f39a Native hooks: ClientPlayerNetworkEvent.LoggingIn, and LoggingOut's label
A client mod hears the window's player log in to a server, single
player's own included: native_client_player_network_logging_in(), at
the end of the Login arm (ClientPacketListener.handleLogin), after the
new client level. Respawns and dimension changes don't log in again.
Horizon makes the player with the server's first position, so
native_player() is None there; native_send_packet reaches the server.

client_disconnecting, which main fires before a connection is
discarded (Minecraft.disconnect), gets the catalog label
ClientPlayerNetworkEvent.LoggingOut, so horizon-port-pack events finds
it. The docs describe the pair.

Checked in game with horizon-ctl and a local probe mod: one login on
opening a world and one on reopening it, none on a trip to the Nether
or a respawn, and a logout on Save and Quit and on quitting the game.

Fixes #10008

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 20:15:14 +04:00
Horizon agentandClaude Opus 5.5 ece613e260 PvP & HUD: document the HUD editor; it frames only drawn widgets
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 17:10:41 +01:00
Horizon agentandClaude Opus 5.5 5b52010b12 PvP & HUD: a HUD editor to drag, snap and resize the widgets
Edit HUD on the PvP & HUD page (in a world) or the HUD editor key
(unbound at first) opens a screen over the game: each widget in a frame,
dragged anywhere with snapping to the screen's edges and middle and to
the other widgets, the wheel resizing it, a right click sending it back
to its corner; buttons for text colour, shadow, boxes and opacity, Reset
and Done. Widgets with nothing to show yet stand in by their name. The
layout is saved to horizon-extras.txt as it changes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 17:10:40 +01:00
some-du6eandClaude Opus 5.5 6d7496f62a OptiFine custom skies: draw a pack's sky layers over the sky
Each frame picks the layers of the Overworld's or End's sky whose day,
weather, biome and height hold at the camera, eases them over their
transition, and draws them as 3x2 skyboxes with their blend mode, over the
vanilla sky's disc and sunrise (the End's box) and under the sun, moon and
stars. Packs without optifine/sky draw nothing new.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 10:10:20 -06:00
DUzzL fb828fda35 Teleport spectators that use a nether portal, end portal or end gateway 2026-11-05 17:08:32 +01:00
umcekoandClaude Opus 5.5 0e80c10042 Projectiles owned by any non-player entity need mob_griefing
Projectile.mayInteract asks mob_griefing for every owner that isn't a
player, and canBreakBlockInAdventureMode lets no non-player owner break
a block without can_break. An owner UUID that names a loaded armor
stand, boat or other non-mob entity was treated as no owner, so a
burning arrow lit TNT and campfires with mob_griefing off. The arrow now
asks the level for the UUID, as getOwner does.

Fixes #9999

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 19:06:37 +03:00
Florian ERNSTandCodex 9fcd6133ed Merge main and preserve map, frame and raid harness records
Keep all observation kinds when resolving the three shared driver lists.

Co-Authored-By: Codex <[email protected]>
2026-11-05 17:06:23 +01:00
DUzzL 812702d809 Allay: aim deliveries at the player's eyes like vanilla 2026-11-05 17:04:45 +01:00
some-du6eandClaude Opus 5.5 a1a052c35c OptiFine custom skies: read the sky layers of optifine/sky
Parse optifine/sky/world<N>/sky<M>.properties into layers (source, fade
times, blend, rotation, days, weather, biomes, heights, transition), the
sun and moon_phases replacements, and the 3x2 skybox grid's faces.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 09:59:41 -06:00
Florian ERNSTandCodex bfc26f944a Validate loaded bed claims for raid villages
Keep stale home memories while checking the current HOME POI before
counting a loaded bed claim as a village center. Preserve unavailable
claims and existing job/meeting paths.

Co-Authored-By: Codex <[email protected]>
2026-11-05 16:57:12 +01:00
Evrim Cicek 7658a24435 Merge remote-tracking branch 'upstream/main' into fix/enderman-teleport-danger
# Conflicts:
#	crates/horizon-client/src/game/creatures.rs
2026-11-05 18:54:56 +03:00
Evrim CicekandClaude Opus 5.5 394db9011a An enderman's teleport: waterlogged ground, the sculk event, and hurts without AI
From review of the previous commit, against vanilla:
- Enderman.canRandomlyTeleportTo refuses ground that holds water (a
  waterlogged slab, leaves or roots).
- containsAnyLiquid and findBlocksIn scan the same closed range of cells.
- Both teleport sounds play at the feet (Entity.playSound).
- The game's sculk layer now hears GameEvent.TELEPORT from a mob.
- Enderman.hurtServer teleports inside every hurt, not only the rain's: the
  fire, lava and block hurts of baseTick and a fall's, and a NoAI
  enderman's, which never teleported. A lingering cloud's indirect_magic
  draws the chance too; only a thrown potion skips it.

New mob AI scenarios: enderman_rain_waterlogged (red on the previous commit,
it landed on the slabs at tick 2) and enderman_rain_noai.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 18:49:28 +03:00
Riccardo GuerrieroandClaude Opus 5.5 7a7cf79d4b A player's death makes a vibration and counts toward Killed By
ServerPlayer.die posts entity_die at the player, and with a kill credit
awards killed_by for the killer's type. A creeper, sulfur cube or wither
whose own blast kills the player is still credited, as vanilla's player
dies inside the explosion: ExplosionSource carries the mob's type.
New lan_e2e check death-credit.

Fixes #9955

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WZzq8LUjYucp4kaN6UfSg7
2026-11-05 16:48:54 +01:00
AxxioandAntigravity 150423300c end: drop destroyed blocks and award used stat on portal completion (fixes #9958)
When the end portal frame ring is completed by placing the last eye of ender, vanilla destroys blocks in the 3x3 portal opening so that their drops and break effects occur instead of silently deleting them. Also awards minecraft:used for minecraft:ender_eye upon insertion into a frame.

Co-Authored-By: Antigravity <[email protected]>
2026-11-05 16:47:53 +01:00
DUzzL 37fed7a88e Credit damage_dealt_absorbed for a player's projectiles 2026-11-05 16:47:46 +01:00
MSedgeMC 955558305a Merge remote-tracking branch 'upstream/main' into fix/prediction-snap
# Conflicts:
#	crates/horizon-client/src/game/remote.rs
2026-11-05 15:42:26 +00:00
MSedgeMC e28eb5363a port_map: isBaby and canBeABaby name the species table
AgeableMob.isBaby is now a row of its own pointing at Mob::is_baby, which is
`can_be_a_baby && getAge() < 0`; canBeABaby points at
species::can_be_a_baby, and the six classes that override it to false carry a
row each so the false arms are not read as unported.
2026-11-05 15:40:15 +00:00
MSedgeMC 24267c893b test: pin the species a negative age cannot make a baby
living.rs::a_negative_age_is_a_baby_only_where_can_be_a_baby_allows_it drives
Mob::new on the loading table's own types and sets Age to -24000, the
BABY_START_AGE a save or a /summon carries, then asks is_baby, dims and
voice_pitch - the three the game draws and hears from.

A parrot, a tadpole, a frog, a zombie nautilus, a camel husk, a slime, a magma
cube, a piglin brute and a creeper keep their type's box (the tadpole 0.4 by
0.3, the camel husk 1.7 by 2.375, the piglin brute 0.6 by 1.95) and voice at
1.0 ± 0.2. The control is a cow at the same age: is_baby true, Cow's
BABY_DIMENSIONS (0.45 by 0.7, eyes at 0.69) and 1.5 ± 0.2, and an adult box and
voice again at age 0. With is_baby back to `age < 0` the test stops at the
parrot.

entity_nbt's save test now runs a frog and a camel husk beside the slime, the
magma cube and the pig: the first four write no Age and keep none of a raw
compound's Age, the pig writes its modeled -37.
2026-11-05 15:40:15 +00:00
MSedgeMC 3c7bba9bdb fix: a mob that cannot be a baby is not one at a negative age
AgeableMob.isBaby is `canBeABaby() && getAge() < 0`, and canBeABaby is true
across the ageable tree but for the six classes that override it to false:
Frog, Parrot, ZombieNautilus, CamelHusk, Slime and MagmaCube (an
AbstractCubeMob is an AgeableMob). Everything outside that tree inherits
LivingEntity.isBaby's false, so a Giant, a Creeper, a PiglinBrute (it extends
AbstractPiglin and declares no isBaby of its own) and a Tadpole, whose own Age
runs up from -24000, are never babies either; only the zombies, the piglins and
the zoglin answer a synced DATA_BABY_ID instead of an age.

Mob::is_baby read `extra.age < 0` alone. The list of who can be a baby existed
already, but only as mobs::drops::can_be_baby, which gates the metadata baby
flag and the loot table: the box, the voice, breeding, growth and the goals
read the age and so called a frog, a parrot, a creeper or a tadpole with a
negative Age a baby. The list is now species::can_be_a_baby, on the species
table, and is_baby is `can_be_a_baby && age < 0`.

What that changes: a tadpole (Age -24000, its own DEFAULT_AGE) is drawn at
getAgeScale's 1.0 instead of 0.5 and speaks at 1.0 ± 0.2 instead of 1.5 ± 0.2,
as vanilla does; a parrot, a frog, a zombie nautilus, a camel husk, a slime, a
magma cube, a piglin brute, a creeper or anything else the table cannot make a
baby keeps an adult's box, voice, breeding, growth and drops. The Parrot arm of
voice_pitch was there for the same reason and is dead now: Parrot.getVoicePitch
is `getPitch(random)`, the plain range an adult already gets.

AgeableMob.addAdditionalSaveData writes Age only when canBeABaby, and the
branch that drops the values a raw compound carried named four classes by hand;
it asks the ageable table now, which also covers the frog and the camel husk
that were re-saving an Age vanilla writes for neither.
2026-11-05 15:40:15 +00:00
Horizon agentandClaude Opus 5.5 c8a86851b9 Native mods: a trait mod turned off stops at once, and resumes when turned on
Options > Mods' switch now reaches the trait mods' dispatch: the game skips
a turned-off mod's Hooks from the next call, on the server thread and in
the window, and turning it on again resumes it with no rebuild while it is
in the build. Registration at start is untouched, so its content stays
registered; RenderHooks keep drawing until a restart; mixin code still
checks native_on.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 16:38:22 +01:00
DUzzL b1896906d1 Clip projectiles with their own collision context (scaffolding open from below) 2026-11-05 16:36:59 +01:00
Horizon agentandClaude Opus 5.5 408044e9a9 Mods: content mods of assets and data switch on and off without a restart
A content root is built per set of mods (.horizon-cache/mod-content/<stamp>),
so a running game keeps reading the root it was given while a switch
builds another; roots unused for a day are removed. Turning a mod on or
off in Options > Mods, or installing one from Browse Mods, applies at once
when the mod has only assets and data: the client reloads its resources,
then the server reloads its data packs as /reload does and resyncs its
players. A mod that adds blocks, items, mob effects, entity or menu types
or changes world generation keeps loading at start, and the Mods page
says to restart.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 16:36:58 +01:00
DANILIAMMOandCodex GPT-6 86c0211c35 launcher: preserve manually installed Nightlies during pruning
Record a marker only for Nightlies installed by automatic updates. Keep
manual and legacy Nightlies out of GUI and CLI cleanup, while retaining
the newest managed Nightlies separately from game releases.

Add regression coverage for manual, legacy, selected and pinned builds.

Co-Authored-By: Codex GPT-6 <[email protected]>
2026-11-05 20:34:10 +05:00
Horizon agentandClaude Opus 5.5 c0323c7707 Mods: browse Horizon's registry and install content mods from inside the game
Options > Mods > Browse Mods lists the registry's content mods for this
game (the index the launcher reads), searchable by name, tag and category,
with a page per project: authors, licence, categories, description and
changelog, Install or Update, its switch once installed, and its page on
the website. An install is checked as the launcher's (size, SHA-256,
signature, manifest against the index) and recorded in mod-browser.json,
so the launcher lists, updates and removes it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 16:32:54 +01:00
Horizon agentandClaude Opus 5.5 e95658709a Design note: mods from inside the game, loaded and unloaded in seconds
What content mods can switch live (assets and data), what needs a world
reload (registries, worldgen), the in-game mod browser, and the native
fast path: every installed native mod built in and switched at run time,
with the dylib and wasm paths left to arismoko's fork.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 16:31:45 +01:00
AxxioandAntigravity 3c154d3bb6 perspective, game: align riding body yaw and block inventory in nether portals (fixes #9793)
In vanilla (\LivingEntity.tick\), a passenger's body yaw matches its vehicle's rotation
(or living body yaw), clamped to keep head yaw within [-85°, 85°], and does not update
limb swing / walk distance from vehicle motion.

Furthermore, \Minecraft.handleKeybinds\ blocks opening the inventory (\keyInventory\)
while in a portal transition (\handlePortalTransitionEffect\), preventing repeatedly
reopening the inventory screen while standing in nether portals.

- Compute ridden vehicle yaw for boats, minecarts, mobs, and cushions in \perspective.rs\.
- Align rider body yaw to vehicle rotation, clamped to [-85°, 85°] of head yaw.
- Disable walk distance accumulation when riding so the player model does not swing limbs.
- Guard \KeyCode::KeyE\ in \game.rs\ and \
emote.rs\ when in portal transition.

Co-Authored-By: Antigravity <[email protected]>
2026-11-05 16:31:29 +01:00
Evrim CicekandClaude Opus 5.5 debd86ea59 Endermen teleport where vanilla lets them, and water hurts them when it should
Enderman.teleport is LivingEntity.randomTeleport(x, y, z, true,
#enderman_does_not_teleport_to). Horizon took the first full-collision
block as the ground and landed on top of it, checked water only at the
eyes and lava only at the feet, and never looked at the tag, so endermen
teleported onto magma, into fire, sweet berry bushes and wither roses.

Now, as vanilla: the ground is the first #entities_can_teleport_to block
below the spot, and the enderman lands at the spot's height less the blocks
it went down. The ground and every block in the new box must not be in
#enderman_does_not_teleport_to, and the box must hold no liquid. The
motion is kept (Entity.teleportTo -> snapTo), both teleport sounds play at
the new spot, and GameEvent.TELEPORT goes out from the old one.

The new rain scenarios also showed when the water's hurt comes. It is the
end of LivingEntity.aiStep, after pushEntities, with or without AI, not
baseTick. Enderman.hurtServer draws its 1 in 10 teleport chance after
super.hurtServer (its hurt sound's pitch first), also when the hurt
cooldown refused the damage, and teleports at once. Horizon instead forced
a teleport in rain without the draw, one tick late.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 18:25:33 +03:00
Mayonesu0andClaude Opus 5.5 dec18717de Merge upstream main: keep pregen and the renamed wheel key
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 16:24:31 +01:00
masa-masa-0314andZCode dd35df0f18 Realms: the backup list and backup info screens
The Worlds tab's Backup button now opens RealmsBackupScreen: a row per automatic backup
with its date (mco.backup.entry), Restore per row, and Back. A row opens
RealmsBackupInfoScreen: the backup's metadata as translated key/value rows
(mco.backup.entry.<key> when the language has it). Restoring asks nothing (vanilla's
Restore button restores at once) and the list reads again when it is done.

RealmsClient gains backupsFor (GET worlds/{id}/backups) and restoreWorld
(PUT worlds/{id}/backups?backupId={id}, vanilla's 40 s/600 s timeouts), with Backup's DTO
(backupId, lastModified, size, metadata, changeList). The backup id goes into the query
percent-encoded, so it cannot add a parameter.

Download Latest still needs the download-and-unpack flow and is left for its own PR; the
rows show the date and the metadata screen the details (size included).

Co-Authored-By: ZCode <[email protected]>
2026-11-05 23:18:13 +08:00
HiuoyandClaude Opus 5.5 f9a93d080a Native hooks: ClientTickEvent.Pre / .Post (client_tick_pre, client_tick_post)
A client mod hears each client tick of the window, at the top and the
end of vanilla's Minecraft.tick: native_client_tick_pre() before any of
the tick's work, native_client_tick_post() once the world's tick, the
chat queue, music and sounds are done. They fire 20 times a second on
the title screen, while paused and in a world alike, as vanilla's tick
runs, and never on a server (single player's ticks on its own thread).
The fragment labels them ClientTickEvent.Pre / .Post, client, not
cancellable.

Checked in game with horizon-ctl and a local probe mod: about 20 calls
a second of each, always paired, on the title screen, in a creative
world and under the pause menu.

Fixes #9971

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 19:10:36 +04:00
IsT3RiK f3e8afcb03 Launcher: add Glass, Blur and the server modes' notes to every table
These labels reach tr through a variable (the Launcher settings' slider rows, MODE_NOTES in
the server dialog), so the scan for tr("...") never asked for them and no table had them: they
showed in English in every language. Added to every table, in English, and translated in
fr.json.
2026-11-05 16:05:40 +01:00
DANILIAMMOandCodex GPT-6 921a31e8a1 launcher: include dated Nightlies in automatic version pruning
The GUI runs the shared pruning path after saving its selection, but its
release-only filter excludes every dated Nightly. Include those builds
so the existing retention, selection, instance-pin and in-use protections
also apply to GUI Nightly updates.

Add regression coverage for following updates, selected and pinned
Nightlies, and retention across Nightly/release/local builds.

Co-Authored-By: Codex GPT-6 <[email protected]>
2026-11-05 20:03:57 +05:00
MSedgeMC caeaa41bdc Test: a landing change a mod cancels keeps the block as it was
The dispatch test asserts the hook's fields for a guest's trample, that a
cancel stops later mods, and that a change with no mods goes ahead.
2026-11-05 15:03:30 +00:00
MSedgeMC 15f071ff3b Landing: a cancelled trample keeps the block, its crop and its events
The landing's raw write is applied before the game finishes it: the game
now asks the hook there, and a cancel puts the previous block back and
skips the neighbour updates and the level events. The crop above trampled
farmland stays, because breaking it is the neighbour update and not a
write of its own.

`FallEvent::Write` carries the block it replaced, so the game has
something to put back. A guest's landing is asked with that guest's UUID.
2026-11-05 15:03:30 +00:00
MSedgeMC 4985864055 Native hooks: a landing's block change a mod can cancel
A player's landing tramples farmland into dirt and breaks turtle eggs with
no hook a server mod can cancel, so a land-claims mod cannot stop a guest
from trampling inside a claim (#9036, #9873 item 3).

`native_entity_change_block_v2` is asked with the player whose landing it
was, the position and both block names, and can cancel the change. It is
additive: `native_entity_change_block` keeps its signature and firing point.
2026-11-05 15:03:30 +00:00
TheMrZZandCodex gpt-6.1-sol 038365e19b Post-process maps taken from crafting results
Use the level's backed maps for crafting recipe matching and run map
post-processing on the actual stacks taken by swap, pickup and quick-move.
Keep the original-copy callback used by quick-move and preserve cartography.

Add a real Game/Menu regression and a guarded connected-owner Java/Game
fixture covering previews, consumed inputs, transferred maps and saved backing.

Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-11-05 15:52:08 +01:00
masa-masa-0314andZCode 3b29a9fbc5 Realms: the invite-code list and its edit screen
The Players tab's second button now opens RealmsInviteCodesScreen: the count in the
title, the subtitle, a row per code with its expiration (or Never), Copy / Edit / Delete
per row, and Create. RealmsEditInviteCodeScreen edits one code: its active flag, the
seven InviteCodeExpiration choices (a click on the slider track picks the nearest — this
screen has no drag event yet), Copy to the clipboard (a new Outcome::Copy the title
turns into Action::CopyText), Delete, Done and Back.

RealmsClient gains inviteCodes (GET api/v1.0/invite/codes?realmId={id}),
createInviteCode (POST .../invite/codes), updateInviteCode (POST .../update) and
deleteInviteCode (DELETE .../delete/{code}), all on the invite-code service with the
token in x-mojangauth, with InviteCode's DTO. The existing redeemInviteCode now goes
through the same helper. An existing expiration reads back as the closest of the seven
choices (InviteCodeExpiration.closestTo).

Co-Authored-By: ZCode <[email protected]>
2026-11-05 22:51:09 +08:00
IsT3RiK 66beeb539d Launcher: the Mods page's install messages go through tr
The messages the Mods page shows after an install (a mod, a native mod, a data pack, a
modpack, a resource pack) and its "isn't set up" errors were built with format!, so they
stayed in English in every language: "Installed JEI for Horizon 0.1.0 into Horizon. Build to
play with it." on a French launcher. They now use trf with named holes. The 11 strings are
added to every table, in English, and translated in fr.json.
2026-11-05 15:47:58 +01:00
umcekoandClaude Opus 5.5 9d9617fe29 Save and load primed TNT's owner
PrimedTnt.addAdditionalSaveData stores the igniter as an `owner` UUID
(EntityReference.store) and getOwner resolves it against the level when
asked. tnt_to_nbt now writes the live owner's UUID (a guest, the host
player or a mob), and a loaded TNT looks its saved owner up when it
explodes, so the blast is credited to the player or mob that lit it after
a reload, a portal trip or a chunk unload. An owner that is not online
keeps its UUID through the raw compound.

Fixes #9951

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 17:45:51 +03:00
IsT3RiK 3a298db1c0 Launcher French: translate the 38 strings left in English, address the player as vous throughout
38 entries still held their English text (the modpack coverage reasons, Performance mode, the
Vulkan fallback notice and a few command-line hints). 15 entries said tu while the other ~110
said vous, sometimes side by side on one page; they now say vous, as the game's own French
table does. Names that read the same in French (Mods, Console, Nightly, Snapshots...) stay.
2026-11-05 15:45:27 +01:00
HiuoyandClaude Opus 5.5 6d59c2da8d Port pack events: tests and the listener class fixture in tests/events.rs
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 18:44:02 +04:00
HiuoyandClaude Opus 5.5 8a3eafbd61 Port pack: events, a mod's NeoForge listeners against the hooks (#9921)
`horizon-port-pack events <jar or folder>` reads class files for the
NeoForge events a mod listens to (an @SubscribeEvent method's parameter,
a method reference or lambda that takes one event) and the Capabilities
fields it reads, and looks each name up in native-hooks.json's `neoforge`
labels: a hook, another phase's hook, a hook whose label says why it
isn't quite one, or none. Over a folder of jars it ranks the names by
how many mods use them. Names NeoForge changed after 1.21.1 are looked
up under their new ones. It reads names, never code.

On ATM10's 498 jars it takes 8 s: 416 of 497 mods use 326 names, 6,616
uses in all. Main's catalog has a hook for 23% of them, and 43% with
#9924's labels. The top gameplay gaps are ClientTickEvent (95 and 46
mods), RenderLevelStageEvent (81) and ClientPlayerNetworkEvent (53, 35).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 18:42:14 +04:00
Evrim Cicek f732441c84 rustfmt 2026-11-05 17:38:11 +03:00
Nikita Maximilian GrimmandClaude Opus 5.5 2bc92b6348 Serve shader API 0.2 behind HORIZON_HDR_FRAME=float
With the switch, the game draws 0.2 packs ([frame] blend = "linear" is
still refused). A renderer made with a 0.2 pack picked draws a float frame
where the device can, else the 8-bit one; a pack picked or dropped later
runs on the frame there is, logged once. The renderer's material, sky,
weather, vertex and shadow entries decode a 0.2 stage's colours and encode
its results; scene_color, opaque_color and the water mirror are decoded
copies, shadow_color is float, and Frame's sky and fog colours are
decoded. The post chain ends in a window-size picture, and the pack's
[tonemap] (or Horizon's clip) draws it onto the frame, encoded at alpha 1.
With HDR on, HDR output lifts nothing of a 0.2 pack's world.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 15:37:14 +01:00
Mayonesu0andClaude Opus 5.5 429defdece Options: the wheel scrolls a settings list past its buttons
A vanilla list keeps the wheel: AbstractContainerWidget inherits
AbstractScrollArea.mouseScrolled, which scrolls and never reaches the rows, so
CycleButton.mouseScrolled only runs for a button outside a list. Horizon stepped
the row under the cursor, so scrolling a long page changed options.

"Scroll wheel changes settings" keeps that behaviour as an opt-in extra, off by
default. Its saved key is new because the old one was written as true by every
install, and its tooltip has a new key because other languages said "as in
vanilla"; they fall back to English until translated.

Fixes #9633

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 15:33:32 +01:00
HiuoyandClaude Opus 5.5 d9d4d19a1d Native hook catalog: label the hooks ATM10's mods listen to most
A scan of ATM10's 491 mods for their NeoForge event listeners (#9921)
showed nine unlabelled hooks behind events many of them use:
RegisterCommandsEvent (95 mods), ItemTooltipEvent (74), the item and
fluid block capabilities (129, 88), RenderHighlightEvent.Block (41),
ServerStartedEvent / ServerStoppedEvent (48, 41), LivingDropsEvent (36),
LivingKnockBackEvent and ExplosionEvent.Detonate. Each label comes from
the hook's own doc comment and firing point; `cancellable` matches its
mod_api contract, and block_highlight keeps the side and cancellable it
already had.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 18:32:46 +04:00
Evrim CicekandClaude Opus 5.5 068b87cc25 An iron golem's anger target is kept, saved and forgiven like a wolf's
Now that the golem keeps its anger target through updatePersistentAnger:
NeutralMob.playerDied ends anger at the dying player even once the golem
lost sight of them; the save writes the anger target, not the current
target; and anger at a mob is written with that mob's UUID and read back
as anger at a mob, so a reloaded golem that was fighting a monster is not
isAngryAtAllPlayers under universal_anger. A calm mob no longer keeps a
loaded angry_at in its save.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 17:24:18 +03:00
Evrim CicekandClaude Opus 5.5 a5416ca075 Iron golems toss by knockback resistance and keep their anger like vanilla
IronGolem.doHurtTarget throws the target up only when the hit lands, by
0.4f * max(0, 1 - knockback resistance); Horizon threw everything it swung
at by 0.4. The hit and the toss are one event now, like the warden's
sonic boom.

IronGolem.aiStep ends in NeutralMob.updatePersistentAnger(level, true), the
same as a wolf's: every tick it holds a target, a monster too, the anger
timer restarts with a draw of the golem's random. Horizon only angered it at
a player who hit it, so a golem going after a monster drifted from vanilla's
random within a few ticks. The golem now uses the wolf's port, and its
player target goal asks isAngryAt.

LivingEntity.aiStep still drops motion below 0.003 and runs pushEntities
without AI; Horizon's NoAI branch skipped both, which the new mob AI
scenarios (a golem fighting a NoAI zombie, ravager and warden) showed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 17:09:34 +03:00
masa-masa-0314andZCode c3d63d1e98 Realms: the settings tab and the region preference screen
The configuration screen's Settings tab (RealmsSettingsTab): the world's name and
description fields (the click moves the caret between them), the region preference line
and its Region Preference button, and the open/close toggle moved here from the footer
(vanilla keeps it on this tab). Leaving the tab — switching tabs or Back/Escape — saves
the changed name, description and preference through RealmsClient.updateConfiguration
(POST worlds/{id}/configuration with RealmsConfigurationDto's JSON: the active slot, its
settings, the preference and the description).

RealmsPreferredRegionSelectionScreen: the automatic choices and every manual region, a
click selects, Done writes the choice back to the tab and Cancel drops it.

RealmsClient gains getPreferredRegionSelections (GET regions/preferredRegions) and
updateConfiguration; RealmServer reads regionSelectionPreference; RegionSelectionPreferenceDto,
RegionDataDto and RealmSetting serialization join the DTOs.

The tier's render/simulation distance options and the region quality icons are the next
PRs (the icons need sprites the GUI atlas does not carry yet); the region rows say their
name for now.

Co-Authored-By: ZCode <[email protected]>
2026-11-05 22:04:46 +08:00
HiuoyandClaude Opus 5.5 2f9c963068 Native hook catalog: NeoForge labels for the core hooks (#9921)
The hooks that came before docs/native-hooks-neoforge.md had no
`neoforge`, `side` or `cancellable` keys, though NeoForge mods listen to
their events most: server and level ticks, block and item clicks,
attacks, joins, chunk loads, logins. Each value comes from the hook's
own doc comment or the vanilla method the proposal's table names, and
`cancellable` matches the hook's mod_api contract. Metadata only: no
name, signature or call site changes.

The proposal's status, its next-hooks table (most of it has landed)
and its capabilities and rollout notes now match main.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 17:59:50 +04:00
Nikita Maximilian GrimmandClaude Opus 5.5 36021e2559 Golden Hour's shader API 0.2 port, as a test pack of the SDK
The example pack written for 0.2: its sky and post passes in linear light,
with the 0.1 pack's gamma constants decoded, and a [tonemap] stage of its
own that eases colours above three quarters of the headroom toward it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 14:59:12 +01:00
AkirabaneandClaude Opus 5.5 db3dcf2dcc dedicated: log each player-* request, and document a hand-over in order
Each player-* request writes one log line with what became of it: INFO
and what it did, or WARN and the reason it was refused. The line names
the player and, for the export, the import, the confirm, the release and
a named drop, the hand-over id, so the lines of one hand-over can be
found on both servers. The record and the request's body are never
written.

docs/server.md gives a hand-over in order, with the ids, the commit
point and what undoes itself, and the log lines.

tools/server_transfer_e2e.py runs a whole hand-over between two
dedicated servers over their real control sockets: two engines with
[transfer] on and Velocity forwarding, a small relay per engine that
answers the forwarding request as a proxy does, and horizon-bots
clients. It follows the documented order step by step, checks the
target's player file is on disk when the confirm answers and that the
source writes nothing for a player moved away, then aborts a hand-over
back and checks the player plays on.

Refs #7484

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 13:46:57 +01:00
AkirabaneandClaude Opus 5.5 bc2363c93b dedicated: player-import takes the place the request names, and the view follows
The request may name where the player arrives, over the record's
compound: `position`, `rotation`, `on_ground` and, now that #8693 keeps
a guest's motion, `velocity`. They belong to the `position` part. The
pitch is taken as Entity.setXRot takes it (modulo a turn, then cut to
+-90) and the yaw as it is; a velocity component over 10 blocks a tick
is taken as 0, as Entity.load takes a saved Motion. A position must be
within the same bounds as the record's own (Entity.load's clamp:
30,000,512 on x and z, 20,000,000 on y), and a rotation or velocity must
be finite numbers.

When the import moves the player to another chunk than the one its view
was built around, the view moves at once: the chunk centre is sent
before the position to confirm, and the chunks out of reach are
forgotten. The reply says so with "recentered".

Refs #7484

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 13:46:57 +01:00
AkirabaneandClaude Opus 5.5 f5aef42f71 lan: an arrival's join line uses the team and nickname display name
#9829 made the join and leave lines name a player as Player.getDisplayName
does (its team's format and its nickname). place_arrival, which announces
a player placed by player-import or player-confirm, still used the plain
name; it now uses player_display_name too, so an arrival is announced as
a login is.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 13:46:56 +01:00
AkirabaneandClaude Opus 5.5 a66f0fc57d dedicated: a durable commit point, retries after it, and the record's bounds
martipops's second review of #8730.

The commit point now covers the three files and a power loss: the
player file, advancements and statistics are each written to a new
file, synced, then renamed into place, and the folder is synced on Unix
(save::write_durable, write_dat_durable). Each write returns its error,
and the player stays held and imported when one fails. Ordinary saves
are unchanged.

A retry of `player-import` or `player-confirm` after the commit, whose
reply was lost, names the hand-over of a player already placed from it:
it is answered ok, placed, with `"retried": true`, and nothing is done
again. Another hand-over's id is still refused.

The record's health is refused over 1024 and its absorption over 2048
(max_health and max_absorption), its food exhaustion over 40
(FoodData.addExhaustion) and an effect amplifier outside 0 to 255, as
26.3's bytecode bounds them. A stack's count needs no check:
item_from_nbt already takes one outside ItemStack's 1 to 99 as 1.

docs/server.md says what a proxy does when `moved` is refused after the
commit (the freeze ran out, or an operator's /kill), and that it
confirms only while the export's `freeze_ticks_left` leaves it time.

Refs #7484

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 13:45:27 +01:00
AkirabaneandClaude Opus 5.5 4a5c6ab5d1 dedicated: player-import and player-confirm let a held player in
`player-import <uuid> <json>` applies a hand-over's record (from
`player-export <uuid> freeze`) to the held player on the target, whole or
in parts, and tells its client what changed. Unless the request says
`"place": false`, it then places the player as `player-confirm <uuid>
<hand-over>` does.

Placing is the hand-over's commit point: the player file, advancements
and statistics are written to disk synchronously, then the player is
placed, and only then does the reply say ok. A proxy releases the
source after that reply, so a crash of the target no longer leaves the
only current copy in memory. If the file cannot be written the player
stays held and imported.

The record's hand-over id is bound to the held player at the import: a
record without one (an export without freeze) is refused, a retry of the
same record is taken and another hand-over's is not. `player-confirm`
names it, and `player-drop <uuid> [hand-over]` may: a named drop must
name the imported record's. A held player that is leaving (dropped or
kicked, its connection not closed yet) can be neither imported nor
confirmed.

The import reads the whole compound as a file load does: the keys
Horizon does not model, the mods' attachments (given to the game's own
map) and the body's values. The body follows the parts: fall and motion
with `position` (with the saved vehicle), air, fire, freezing and food
exhaustion and timer with `health`. Values no player can have are
refused before anything changes: a position, rotation, motion, health,
absorption, saturation, experience progress or fall that is not a
number, a position past vanilla's clamp, a health of zero or less.

The client is sent the effects the import removed and the recipe book
it brought. Every player gets the arrival's list entry, since every
player is shown its entity; `announce` is the join line only. At
confirm the client is sent its position and inventory again, since what
it did since the import was not acted on.

The control endpoint refuses a request line longer than 16 MiB, which
the game would otherwise read and parse between two ticks.

Refs #7484

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 13:45:27 +01:00
AkirabaneandClaude Opus 5.5 6fae1fdc44 Config golden: add max_hold_seconds to the [transfer] section
The config reply pinned by api-v1-config.json lists [transfer] with its
defaults; this PR adds max_hold_seconds (60 by default), so the golden
lists it too.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 13:45:17 +01:00
rdbtCVSandCodex gpt-6.1-sol 764795ae20 docs(server): regenerate held-player transfer setting reference
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-11-05 13:45:17 +01:00
AkirabaneandClaude Opus 5.5 62646f32ab lan: the login of an expected player is held, and player-drop lets it go
A login that used up a proxy's expectation (player-expect, taken as the
login starts) is held on the target server: it gets the whole join
sequence and the level around it, and its acknowledgements are handled,
but it is not placed. No join message, no list entry for the others, not
among the entities they are shown, not simulated, and nothing else it
sends is acted on. The world is loaded around a held player as around a
placed one. The log says "<name> is held: it arrives from another
server".

A held player nobody takes over is dropped after [transfer]'s
max_hold_seconds (60 by default, 1200 ticks), with one warning: a player
already kicked or dropped, whose connection is slow to close, is not
kicked again on every tick. horizon-server passes the setting with the
others.

A held player keeps a true picture of the others while it waits: a
player who joins meanwhile is added to its list, and one who leaves is
removed from it with its entity, or its client would drop the entity of
a player it has no entry for.

`player-drop <uuid>` forgets the player's expectation and disconnects
the held player without a save or a word to anyone. player-export
refuses a held player. Placing it is the import's work, next.

Refs #7484

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 13:45:16 +01:00
GabrielNichols 37ff7be994 Format the vanilla recipe source call 2026-11-05 03:30:41 -03:00
GabrielNichols 476c083e82 Send owned content recipes in the server book 2026-11-05 03:14:27 -03:00
GabrielNichols 055c47c98d Merge official main into Recipes content scenario 2026-11-05 02:45:32 -03:00
GabrielNichols 31756ebfe2 Index owned content recipes for book placement 2026-11-05 02:30:21 -03:00
GabrielNichols 0e6e28109f Keep the pilot fixture focused on an original content recipe 2026-11-05 01:56:50 -03:00
GabrielNichols e76cb1f87a Use edit-box input when replacing Recipes search 2026-11-05 01:45:55 -03:00
GabrielNichols e770447686 Keep Recipes search editable after an empty result 2026-11-05 01:11:59 -03:00
Claude 82f601072b Server panel: the launcher's Glass look, a page for each part, in-page dialogs
The panel takes the Glass look from docs/design-system.md: the sunset with pixel ridges, frosted
plates, Geist and Bricolage Grotesque Heading, the theme's Hugeicons. What it already did gets a page
each in a sidebar (overview, players, console, backups, crash reports), and confirm()/prompt() become
in-page dialogs. The supervisor serves the theme's fonts and the mark; the CSP gains font-src 'self'.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Jh5A2PzdusBChqE38VZ3zD
2026-11-04 23:29:16 +00:00
MSedgeMC 6510fd2039 The ledger knows Entity.isColliding is ported
The row described it as missing because the prediction only restored the block.
It is game/remote.rs::Game::block_meets_player now, and end_predictions acts on
the answer.
2026-11-04 16:07:38 +00:00
MSedgeMC 817722009c Test the collision and the snap a restored block makes
Entity.isColliding on a full block at the player's feet meets a 0.6 x 1.8 box
standing in it, and a block above its head, the one beside it or an empty shape
does not. The snap takes the remembered position while the teleport guard allows
it, and nothing when the block came back clear of the player or the player was
teleported at the same sequence.
2026-11-04 16:07:38 +00:00
MSedgeMC ea80bba0f7 A refused block change that comes back inside the player snaps it out
ClientLevel.syncBlockState is what BlockStatePredictionHandler.endPredictionsUpTo
calls once the server acknowledges a sequence: when the block takes a state it
did not have, the level restores it, and then, if the prediction remembered a
player position and the player was not teleported since
(lastTeleportSequence < sequence), it asks Entity.isColliding(pos, state) -- the
state's own collision shape at the player's pose against the player's box -- and
calls Player.absSnapTo with the remembered point when they meet.

Horizon's end_predictions restored the block and stopped there, so a break the
server refused under the player, or a placement it pulled back, left the player
standing inside it until its own physics pushed it out.

A prediction now remembers what vanilla's ServerVerifiedState does -- the
sequence, the state the server last gave the block, and the player's position
when the change was first predicted -- and keeps all three through a
retainKnownServerState that only bumps the sequence. end_predictions restores the
block, tests the shape against the player and snaps to the remembered point when
the guard allows it. remote_teleport takes BlockStatePredictionHandler.onTeleport,
so a prediction made before a teleport keeps its block but not its snap.
2026-11-04 16:07:37 +00:00
TheApple05andCodex cc3ee16e46 Merge upstream main after language-key validation fix
Preserve the existing adapter and maintainer history while including the
upstream removal of stale Menu Navigation translation keys.

Co-Authored-By: Codex (GPT-6) <[email protected]>
2026-11-04 15:01:06 +00:00
TheApple05andCodex 42372aa760 Merge current upstream main after older-server review fixes
Preserve published history and upstream shader changes.

Co-Authored-By: Codex (GPT-6) <[email protected]>
2026-11-04 12:59:28 +00:00
TheApple05andCodex d29c8eb63f Fix legacy attribute, llama carpet and block entity conversions
Select attribute and carpet registry names from the actual server release.
Upgrade incoming pre-component block entities on both update and chunk paths,
retaining tag identity and vanilla typed-field remainders.

Add independent registry and vanilla DFU regression fixtures, including
missing IDs, removed types and unchanged current-version tags.

Co-Authored-By: Codex (GPT-6) <[email protected]>
2026-11-04 12:58:52 +00:00
TheApple05andCodex 8852c0e0b8 Merge current upstream main before older-server review fixes
Preserve published compatibility commits and newer maintainer changes before addressing the protocol-764 review. This merge alone claims no new compatibility or parity result.

Co-Authored-By: Codex (GPT-6) <[email protected]>
2026-11-04 12:18:11 +00:00
rdbtCVSandCodex gpt-6.1-sol d3e25901ff Adapt native level particle speed to the per-axis packet representation
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-11-04 12:14:10 +00:00
rdbtCVSandCodex gpt-6.1-sol 54bef9b2fb Merge main and reconcile parallel changes
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-11-04 12:01:48 +00:00
rdbtCVSandCodex gpt-6.1-sol 4e294c46f1 Merge main and reconcile parallel changes
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-11-04 12:01:45 +00:00
rdbtCVSandCodex gpt-6.1-sol 67268cb5b4 Merge main and reconcile parallel changes
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-11-04 12:01:43 +00:00
rdbtCVSandCodex gpt-6.1-sol 6bd3217727 Merge main and reconcile parallel changes
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-11-04 12:01:41 +00:00
TheApple05andCodex 21f7031e72 Merge upstream main before resuming ViaHorizon compatibility
Preserve the published older-server contribution and current upstream changes as separate parents before the next tested extension. No new compatibility or parity result is claimed by this merge.

Co-Authored-By: Codex (GPT-6) <[email protected]>
2026-11-04 10:43:04 +00:00
TheApple05andCodex 7dde5f518c Add experimental native Minecraft 1.20.2 protocol adapter
Co-Authored-By: Codex (GPT-6) <[email protected]>
2026-11-04 05:15:58 +00:00
TheApple05andCodex b65d5f5dcd Fix resource-pack instance identity across replacement and reconnect
Co-Authored-By: Codex (GPT-6) <[email protected]>
2026-11-04 05:12:56 +00:00
TheApple05andCodex f88c50772f Merge upstream main during protocol 764 development
Co-Authored-By: Codex (GPT-6) <[email protected]>
2026-11-04 04:41:58 +00:00
TheApple05andCodex d064372d9d Merge upstream main before protocol 764 compatibility
Co-Authored-By: Codex (GPT-6) <[email protected]>
2026-11-04 04:13:35 +00:00
TheApple05andCodex ca0a90eee3 Add native protocol 765 compatibility for Minecraft 1.20.3 and 1.20.4
Co-Authored-By: Codex (GPT-6) <[email protected]>
2026-11-04 04:01:11 +00:00
TheApple05andCodex c6780b8daf test: establish TCP backpressure before disconnect assertions
Co-Authored-By: Codex (GPT-6) <[email protected]>
2026-11-04 04:01:10 +00:00
TheApple05andCodex aca15c91d4 Merge upstream main before adding 1.20.4 compatibility
Co-Authored-By: Codex (GPT-6) <[email protected]>
2026-11-04 02:40:26 +00:00
TheApple05andCodex 01166b25ae Add experimental Minecraft 1.20.5 and 1.20.6 server compatibility
Translate protocol 766 attributes, food, horse inventories, projectile power,
chat holders and item use into the pinned protocol. Supply the old static
enchantment and painting names, and normalize legacy wolf variant holders.

Reuse verified recipe and boat layouts and cover changed codecs with
independent vanilla wire fixtures, including full chat packet translations.

Co-Authored-By: Codex (GPT-6) <[email protected]>
2026-11-04 02:29:55 +00:00
TheApple05andCodex 311cf0d31f Merge upstream main before adding 1.20.6 compatibility
Co-Authored-By: Codex (GPT-6) <[email protected]>
2026-11-04 01:54:44 +00:00
TheApple05andCodex 9e9078f17f Add experimental Minecraft 1.21 and 1.21.1 server compatibility
Translate protocol 767 packets, recipes, item components and generic boat
variants into the pinned protocol. Preserve old movement and interaction
layouts, and correct simple spell particles across the older adapters.

Add independent vanilla wire fixtures and document the experimental scope.

Co-Authored-By: Codex (GPT-6) <[email protected]>
2026-11-04 01:39:23 +00:00
TheApple05andCodex d00c6bcd25 Merge upstream main before adding 1.21.1 compatibility
Co-Authored-By: Codex (GPT-6) <[email protected]>
2026-11-04 01:02:51 +00:00
TheApple05andCodex a96e04043e Extend native older-server support to Minecraft 1.21.2 and 1.21.3
Translate protocol 768 particles, player-list defaults, vehicle movement and
integer model data through the existing older-server pipeline. Preserve old
creaking heart states and transient entities through explicit registry aliases,
and keep legacy trail lifetimes in the client particle provider.

Co-Authored-By: Codex (GPT-6) <[email protected]>
2026-11-04 00:50:10 +00:00
TheApple05andCodex 424f045246 Merge upstream main before adding 1.21.3 compatibility
Co-Authored-By: Codex (GPT-6) <[email protected]>
2026-11-04 00:29:05 +00:00
TheApple05andCodex dfd4b135ef Extend native older-server support to Minecraft 1.21.4
Translate legacy chunk framing, item patches, recipe previews, chat and
entity metadata into the pinned client representation. Preserve old click
operations through authoritative server resynchronization and cover older
stack/profile layouts with independent vanilla wire fixtures.

Co-Authored-By: Codex (GPT-6) <[email protected]>
2026-11-04 00:04:47 +00:00
TheApple05andCodex baceff1b55 Merge upstream main before adding 1.21.4 compatibility
Co-Authored-By: Codex (GPT-6) <[email protected]>
2026-11-04 00:01:44 +00:00
TheApple05andCodex de808d2d30 Merge upstream main before adding 1.21.4 compatibility
Co-Authored-By: Codex (GPT-6) <[email protected]>
2026-11-03 23:20:25 +00:00
TheApple05andCodex 44338647fa Extend native older-server support to Minecraft 1.21.5
Map protocol 770's packet and registry ids, remove vanilla's unused chunk
padding, renumber command parsers before reading their properties, and
translate player actions, crouch transitions and hanging-entity metadata.
Convert pre-1.21.6 attribute and equippable components in both directions,
with independent vanilla wire fixtures and protocol regression coverage.

Co-Authored-By: Codex (GPT-6) <[email protected]>
2026-11-03 23:15:23 +00:00
TheApple05andCodex 3f41fda7a1 Merge upstream main before adding 1.21.5 compatibility
Co-Authored-By: Codex (GPT-6) <[email protected]>
2026-11-03 22:57:14 +00:00
TheApple05andCodex 57bc596fe1 Extend the native older-server adapter to 1.21.6
Reuse the packet layouts shared with 1.21.7/1.21.8, map the item and sound
registry ids before the lava chicken insertion, and generate inherited tables
only where the vanilla reports differ. Cover shifted discs and sound holders
and exercise the shared metadata and packet conversion cases on both protocols.

Co-Authored-By: Codex (GPT-6) <[email protected]>
2026-11-03 22:53:22 +00:00
TheApple05andCodex 4090de57e0 Merge upstream main before extending older-server support
Co-Authored-By: Codex (GPT-6) <[email protected]>
2026-11-03 22:35:53 +00:00
TheApple05andCodex 1d78698703 Add experimental native protocol support for 1.21.8
Generate vanilla packet and registry mappings, translate the changed layouts
and player metadata, preserve renamed chain states, and extend the offline
protocol probe to fail on decode errors and follow dimension changes.

Co-Authored-By: Codex (GPT-6) <[email protected]>
2026-11-03 22:18:41 +00:00
levzzz 9a344e0c31 Merge main into modapi/entity-state 2026-11-03 01:08:57 +05:00
levzzzandClaude Opus 5.5 d48b349d72 Native mods: teleport, rotation, head yaw, facing, size and pushes through EntityRef (#7387)
teleport_to and set_rotation move and turn any entity as /tp and /rotate
do; head_yaw, set_head_yaw, direction, dimensions and push.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-03 00:19:06 +05:00
levzzzandClaude Opus 5.5 3568cc4cc7 Native mods: name the shared sound and particle checks outside the hook catalog
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-03 00:11:41 +05:00
levzzzandClaude Opus 5.5 d68ada1e13 Native mods: an entity's state flags through EntityRef (#7387)
is_shift_key_down, is_crouching, is_sprinting, is_in_water, is_in_lava,
fire_immune, fire_ticks, air_supply, max_air_supply, is_silent,
set_silent, is_invisible and pose, for players and mobs, and the ones
Horizon keeps for armor stands and items.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-03 00:10:46 +05:00
levzzzandClaude Opus 5.5 c367cb95a2 Native mods: sounds and particles in a level through LevelRef (#7387)
level.play_sound and level.send_particles send to every player in range,
as ServerLevel.playSound and sendParticles do, from any server hook (not
only a level hook's LevelWriteHost). The checks the level-hook versions
make move into native_sound_ok and native_particles_ok, shared by both.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-03 00:03:19 +05:00
levzzzandClaude Opus 5.5 25d9cc84c2 Native mods: a level's seed, difficulty, game rules and weather through LevelRef (#7387)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-02 22:05:17 +05:00
2002 changed files with 220640 additions and 24888 deletions

No files matched your search

+8 -7
View File
@@ -234,14 +234,15 @@ jobs:
run: python ../tools/check_pe_arch.py arm64 target/aarch64-pc-windows-msvc/release/horizon-launcher.exe
- name: Check launcher DLL imports and command-line startup
working-directory: launcher
shell: pwsh
# bash, as launcher.yml's check on the same runner: the launcher is a GUI-subsystem program, and a pwsh
# `$output = & launcher --version` read no output from it (run 42217014588).
shell: bash
run: |
$ErrorActionPreference = 'Stop'
$launcher = 'target/aarch64-pc-windows-msvc/release/horizon-launcher.exe'
python ../tools/check_imports.py $launcher
$output = & $launcher --version 2>&1
if ($LASTEXITCODE -ne 0 -or -not $output) { throw 'horizon-launcher --version failed' }
Write-Output ($output -join "`n")
launcher=target/aarch64-pc-windows-msvc/release/horizon-launcher.exe
python ../tools/check_imports.py "$launcher"
out=$("$launcher" --version)
echo "$out"
[[ "$out" == "horizon-launcher "* ]] || { echo "::error::horizon-launcher --version printed '$out'"; exit 1; }
- name: Build a native-mod game with the launcher's managed toolchain
working-directory: launcher
shell: pwsh
+20 -17
View File
@@ -34,7 +34,9 @@ Feature → files → harness for the most-changed areas: [docs/architecture.md]
- Contributors first: `gh pr list --state open --search <area>` before you start; never duplicate theirs.
- **No hardcoded user-facing strings.** Keep UI translation-ready from the start. In the launcher (`launcher/`), wrap visible text with `tr("...")` or `trf("... {arg} ...", &[("arg", &val)])` from `crate::i18n` instead of raw string literals or `format!`. In the game, use translation keys (`Text::tr`, `lang::get`), never hardcoded English strings.
- **Claim before you start**: comment `/claim` on the issue (no issue yet: open one, then claim it). An assigned
issue is taken: pick another. `/unclaim` when you stop; a claim with no open PR linking it is released after 5 days.
issue is taken, and so is one labelled `in progress` (claimed on Discord): pick another. `/unclaim` when you stop;
a claim with no open PR linking it is released after 5 days. The issues free to claim, without reading threads:
`gh issue list --search 'is:open no:assignee -label:"in progress" -label:tracker -label:proposal -label:"not claimable" -linked:pr'`
- **Controllers reach what registers.** Draw controls with a registering helper (`ui::button`, `settings_shell::foot_button`); `graphics_style::button` and `screen_draw::button` only paint, and registering one part of a screen ends its crosshair, so register all of it.
## Crates
@@ -49,7 +51,7 @@ Game workspace (root `Cargo.toml`); `launcher/` and `crates/horizon-telemetry-se
| `horizon-command` | Command parsing (Brigadier): the vanilla tree, argument types, selectors, suggestions, the tree's network form. Re-exported as `horizon_client::command` |
| `horizon-client-core` | The client lib's bottom layer: `text.rs`, `lang.rs`, `snbt.rs`, `nbt_path.rs`, `component.rs`, `scoreboard.rs`, `bossbar.rs`, `gamerules.rs`, `block_entity_nbt.rs`, registry/particle/sound tables. Re-exported under the old paths (`horizon_client::text`...) |
| `horizon-client-input` | Key bindings and key names, toggle keys, mouse options, controller settings and glyphs, the clipboard, window options (`keybinds.rs`, `gamepad.rs`, `mouse.rs`, `window.rs`...). Re-exported under the old paths |
| `horizon-client-render` | The client's world data, rendering and screens (chunks, meshing, light, streaming, `gfx`, HUD, menus, settings): ~80 modules that horizon-client re-exports under the same paths. Nothing in it reaches gameplay |
| `horizon-client-render` | The client's world data, rendering and screens (chunks, meshing, light, streaming, `gfx`, HUD, menus, settings), modules that horizon-client re-exports under the same paths. Nothing in it reaches gameplay |
| `horizon-world` | Vanilla world data with no gameplay loop: NBT, regions, chunks, `datafix/`, `worldgen/`, `level/` (server-side block behaviour, ticks, redstone), entity physics, items, effects, food, damage |
| `horizon-core` | Simulation primitives: Java `Random`s, `jmath` (HotSpot-exact `Math`), positions, noise, density, paletted storage, `version.rs` (the pinned version) |
| `horizon-protocol` | Network protocol 777: codecs, framing, encryption, `packets/{handshake,status,login,configuration,play}.rs`, `entity_data.rs` |
@@ -63,6 +65,7 @@ Game workspace (root `Cargo.toml`); `launcher/` and `crates/horizon-telemetry-se
| `horizon-mods`, `cargo-horizon-mod` | Content mods (`.hmod`: manifest, content fence, no code), the registry index and `.hpack`, and `cargo horizon-mod` (`new`, `build`, `run`, `check`, `registry`) ([docs/mods.md](docs/mods.md)). Code mods are native, built by the launcher ([docs/native-mods.md](docs/native-mods.md)) |
| `horizon-port-pack` | Porting Java modpacks ([docs/modpacks.md](docs/modpacks.md)) |
| `horizon-optifine` | OptiFine-format pack files below every feature that reads them: `.properties`, the name/range/block matchers, paths and `optifine/` folders, and custom entity models (`cem/`: `.jem`/`.jpm` parsing and baking) |
| `horizon-polytone` | Polytone-format pack files (`assets/*/polytone/`), read clean-room from the public wiki: targets, biome and dimension modifiers and their environment attribute layers (#11486) |
| `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 |
@@ -90,8 +93,8 @@ reads `README.md`, `launcher/links.json`, `docs/api/`, `roadmap/roadmap.json` an
```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 -p horizon-client-lib --lib --bins --tests # skips the 104 examples (pr_ready does this); `--doc` separately
cargo test -p horizon-client-lib --lib mod_content::tests:: -- --ignored # the 5 slow content-root tests, #[ignore]d; pr_ready runs them when mod content, assets, blocks or items change
cargo test -p horizon-client -p horizon-client-lib --lib --bins --tests # skips the examples (pr_ready does this); `--doc` separately
cargo test -p horizon-client-lib --lib mod_content::tests:: -- --ignored # the slow content-root tests, #[ignore]d; pr_ready runs them when mod content, assets, blocks or items change
cargo build --profile fast -p horizon-client # optimized, quick to link: use it to play or run lan_e2e
python3 tools/parity.py --changed origin/main # the harnesses your branch can affect (CI's parity job)
python3 tools/parity.py --changed-files paths.txt --list # which harnesses cover these files, without running them
@@ -103,8 +106,8 @@ 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),
- A harness = `tools/test_<name>_parity.py` + a Java oracle (`mods/horizon/src/test/java/dev/horizonlab/parity/Vanilla*Oracle.java`)
+ a Rust probe: `crates/<crate>/examples/<name>_{probe,fixture}.rs` (mostly horizon-world and horizon-client),
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.
- Developing a native mod? Run `horizon-launcher settings native-profile dev` once. `dev` builds the game crate
@@ -118,7 +121,7 @@ python3 tools/agent_control_e2e.py # horizon-ctl / MCP end to end
Source paths remain in `crates/horizon-client/`. Measure actual harness selection on refreshed
records with `tools/parity.py --changed-files paths.txt --list`; historical counts are not a guarantee.
- Rust integration tests: `crates/horizon-client/tests/` (`lan_bot.rs`, `generation_saves.rs`, `streaming_meshes.rs`).
- Linux needs `libasound2-dev libudev-dev`; `--no-default-features` drops audio and controllers (the server build).
- Linux needs `libasound2-dev libudev-dev`; `--no-default-features` drops audio, controllers, `discord`, `friends` and `heartbeat` (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`
@@ -149,23 +152,23 @@ state stays separate from the library tests. This compiles gameplay twice for ex
| 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`, `spans.rs`; `horizon-client-input/src/window.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/`, `server_terrain.rs` (a server's kept chunks), `bluemap.rs` + `bluemap_detect.rs` (far terrain from its web map), `ticket_levels.rs`; G: `world_loading.rs`, `postprocess.rs`; game.rs `fn update_world` |
| Game loop, tick, frame | `game.rs`: `fn run`, `fn tick`, `fn tick_world`, `fn render`; G: `window_handler.rs` (`impl ApplicationHandler for Game`: `window_event`, `about_to_wait`); L: `clock.rs`, `spans.rs`; `horizon-client-input/src/window.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`, `screen_backgrounds.rs` (panorama, menu blur, border wall) |
| 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/`, `server_terrain.rs` (a server's kept chunks), `bluemap.rs` + `bluemap_detect.rs` (far terrain from its web map), `ticket_levels.rs`; G: `world_loading.rs`, `world_entry.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`; crate `horizon-items`: `items.rs`, `item_data.rs`, `enchant.rs`, `blocks.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` |
| Player actions | game.rs (`fn use_hand`, `fn tick_mining`, `fn break_block`); G: `item_actions.rs` (`fn use_item`), `melee.rs` (`fn attack`), `hand.rs`, `combat.rs`, `consume.rs`, `using_item.rs` (an item in use, tick by tick), `potions.rs`, `shield.rs`, `crossbow.rs`, `trident.rs`, `fishing.rs`; L: `motion.rs` + `motion/` (movement), `collisions.rs`; crate `horizon-items`: `items.rs`, `item_data.rs`, `enchant.rs`, `blocks.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`, `inventory_input.rs` (keys and clicks over a container), `inventory_slots.rs` (slot layout and drawing) |
| 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` |
| 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`, `loading_screen.rs` (the loading screen, the pre-game `App`), `screens.rs` (pause, death), `options.rs`, `lan_screen.rs`, `chat.rs`, `sign_edit.rs`, `books.rs`, `creative_screen.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`, `horizon-anticheat` (L: `lan/anticheat.rs`) |
| Save and load | L: `save.rs` (level.dat, regions, playerdata), `entity_nbt.rs`, `options_file.rs`; `horizon-client-core/src/block_entity_nbt.rs`; G: `saving.rs`, `persist.rs` (entities); `crates/horizon-world/src/{region.rs,nbt.rs,datafix/}` |
| Input | game.rs `fn window_event`, `fn key_pressed`, `fn play_mouse`; crate `horizon-client-input` (`keybinds.rs`, `keymapping.rs`, `key_names.rs`, `mouse.rs`, `input.rs`, `interaction_input.rs`, `gamepad.rs`, `clipboard.rs`); L: `osk.rs`; G: `pad.rs`, `pad_menu.rs` |
| 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), `world_session.rs` (opening and closing a world); `crates/horizon-world/src/{region.rs,nbt.rs,datafix/}` |
| Input | game.rs `fn window_event`; G: `play_keys.rs` (`fn key_pressed`), `play_input.rs` (`fn play_mouse`, toggled keys, the hotbar wheel); crate `horizon-client-input` (`keybinds.rs`, `keymapping.rs`, `key_names.rs`, `mouse.rs`, `input.rs`, `interaction_input.rs`, `gamepad.rs`, `clipboard.rs`); L: `osk.rs`; G: `pad.rs`, `pad_menu.rs`, `pointer_capture.rs` (mouse look, pointer grab) |
| 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`, `mobs/cem.rs` + `mobs/cem_anim.rs` (OptiFine/EMF entity models and their animations); `horizon-client-core/src/lang.rs`; crates `horizon-assets`, `horizon-optifine`; 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)) |
@@ -174,7 +177,7 @@ state stays separate from the library tests. This compiles gameplay twice for ex
## How `Game` is organised
- `struct Game` (game.rs, ~290 fields) holds everything. Its methods are spread over ~180 files: game.rs keeps
- `struct Game` (game.rs) holds everything. Its methods are spread over many 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 stay private to the gameplay module and its descendants. Gameplay tests run in the gameplay library, separate from reusable-library tests.
- Find a feature's code: `grep -rn "fn tick_<thing>\|fn <verb>_<thing>" crates/horizon-client/src/game*`;
@@ -188,7 +191,7 @@ state stays separate from the library tests. This compiles gameplay twice for ex
`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.
(`pack`), both in game.rs, their methods in `game/resource_tables.rs`.
## Single player runs on a server thread
+5 -1
View File
@@ -33,7 +33,7 @@ queue (the hub, or `/suggest` on Discord) rather than to issues, which are for b
Wherever vanilla has a feature, vanilla is the specification, down to the order of random calls. "Parity" here means
Horizon gives the same results as the pinned 26.3 client and server for the same inputs, checked by a **harness**: a
`tools/test_*_parity.py` script that runs a small Java oracle against the real game and a Rust probe against Horizon,
and compares their outputs. There are about 290 of them.
and compares their outputs.
- **Read the vanilla code before writing Horizon's.** `tools/rea_vanilla.py` queries the pinned game bytecode:
`python3 tools/rea_vanilla.py method net.minecraft.world.level.block.DispenserBlock dispenseFrom`.
@@ -61,6 +61,10 @@ another project's source may also be read and used, **only when that project's l
"source available" licenses, all rights reserved (OptiFine), and any project with no license or an unclear one.
Don't read, copy, translate or paraphrase their source, and don't send code an AI tool wrote from it. Being able
to download a mod or a pack does not give you the right to read its files.
- **Java's own library is GPL too.** OpenJDK's source (`java.lang.Math`, `StrictMath`, `FdLibm`, `HashMap`, the
sorts, `Double.toString`, HotSpot's intrinsics...) is GPL-2.0, so it is off limits like the rest. Reproduce Java's
behaviour from its API documentation (the Javadoc) and from what a JVM returns, or from a permissive original such
as Sun's fdlibm C sources (see [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md)); see #6741.
- **Check the license yourself.** Read the LICENSE file at the commit you use and the header of each file you take:
some repositories mix licenses, and a file under another license is off limits. A license that changed: the one at
your commit counts. In doubt, don't: ask in #contributors first.
Generated
+488 -95
View File
File diff suppressed because it is too large. Load diff
+18 -1
View File
@@ -37,8 +37,10 @@ members = [
"crates/horizon-port-pack",
"crates/horizon-shaders",
"crates/horizon-optifine",
"crates/horizon-polytone",
"crates/horizon-shader-api",
"crates/horizon-release-sign",
"crates/horizon-mod-deps",
"xtask",
]
resolver = "2"
@@ -46,7 +48,7 @@ resolver = "2"
[workspace.package]
edition = "2024"
license = "MIT"
rust-version = "1.85"
rust-version = "1.95"
# Ports mirror vanilla's Java signatures, index loops and nested generic types
# one to one so they can be checked against the decompiled sources.
@@ -80,6 +82,21 @@ libmimalloc-sys = { path = "third_party/libmimalloc-sys" }
[profile.dev]
debug = "line-tables-only"
# The native mod API is generated by a build script that parses every file of game/ with syn on
# each gameplay edit; unoptimized that took ~6 s of every rebuild, optimized ~1.5 s.
[profile.dev.package.horizon-mod-api-gen]
opt-level = 3
[profile.dev.package.syn]
opt-level = 3
[profile.dev.package.proc-macro2]
opt-level = 3
[profile.release.package.horizon-mod-api-gen]
opt-level = 3
[profile.release.package.syn]
opt-level = 3
[profile.release.package.proc-macro2]
opt-level = 3
[profile.release]
lto = "thin"
codegen-units = 1
+74 -64
View File
@@ -1,77 +1,99 @@
<p align="center">
<img src="docs/media/banner.webp" alt="The Horizon website: the game you know, rebuilt in Rust" width="100%">
</p>
<p align="center">
<a href="https://github.com/horizon-rust/horizon/actions/workflows/ci.yml"><img src="https://github.com/horizon-rust/horizon/actions/workflows/ci.yml/badge.svg?branch=main" alt="CI"></a>
<img src="https://img.shields.io/badge/Minecraft-Java%2026.3-5D9C3F" alt="Minecraft Java 26.3">
<img src="https://img.shields.io/badge/Rust-stable-B7410E?logo=rust&logoColor=white" alt="Rust">
<img src="https://img.shields.io/badge/vanilla%20parity-100%25-F0795A" alt="Vanilla parity 100%">
<a href="#project-status"><img src="https://img.shields.io/badge/player--visible%20gaps-0%20open-F0795A" alt="Player-visible gaps open"></a>
<img src="https://img.shields.io/badge/platform-Linux%20%7C%20macOS%20%7C%20Windows-2A2470" alt="Linux, macOS and Windows">
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-3B2F7E" alt="MIT license"></a>
<img src="docs/media/banner.webp" alt="Horizon: the game you know, rebuilt in Rust" width="100%">
</p>
<p align="center">
<a href="https://playhorizon.dev"><b>Website</b></a> ·
<a href="#download"><b>Download</b></a> ·
<a href="https://playhorizon.dev/docs/"><b>Documentation</b></a> ·
<a href="https://github.com/horizon-rust/horizon/releases"><b>Release notes</b></a> ·
<a href="https://playhorizon.dev/docs/"><b>Docs</b></a> ·
<a href="https://discord.gg/playhorizon"><b>Discord</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>
<p align="center">
<img src="https://img.shields.io/badge/vanilla%20parity-100%25-F0795A" alt="Vanilla parity 100%">
<a href="#project-status"><img src="https://img.shields.io/badge/player--visible%20gaps-0%20open-F0795A" alt="Player-visible gaps open"></a>
</p>
# Horizon
Minecraft: Java Edition 26.3, rewritten in Rust. Same worlds, same rules, same terrain, checked against the Java game by more than 300 differential harnesses. Free, open source and unofficial.
**Minecraft: Java Edition 26.3, rewritten in Rust.** Your worlds, the same rules and the same terrain as the Java game, checked against it by hundreds of differential tests, in one native program with no Java. Free, open source (MIT) and unofficial: you play it with your own copy of Java Edition.
> Not an official Minecraft product, nor approved by or associated with Mojang or Microsoft. Horizon contains no Minecraft code, assets or data: you need your own copy of Java Edition 26.3.
## Download
Get **the Horizon launcher** from [playhorizon.dev](https://playhorizon.dev) or [the launcher releases](https://github.com/horizon-rust/horizon/releases?q=launcher&expanded=true) (Linux x86-64, macOS on Apple Silicon, macOS on Intel from the next Nightly, Windows x64). It installs and updates the game, prepares its files from your copy of Minecraft, and updates itself.
**You need** a Microsoft account that owns Java Edition ([why](docs/microsoft-account.md)) and a GPU with Vulkan or OpenGL 3.3 (Linux), Vulkan or DirectX 12 (Windows), or a Mac (Apple Silicon, or Intel from the next Nightly). An existing Minecraft install (official launcher, Prism, MultiMC...) is reused; without one, Play downloads 26.3 from Mojang for your account. Step by step: [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 Home page: the selected instance ready to play, the version picker, Join a friend and the instance's tabs"></td>
<td width="50%"><img src="docs/media/launcher-settings.webp" alt="The launcher's Settings page: Microsoft account, offline name, updates, appearance and accent colour"></td>
</tr>
</table>
<p align="center">
<img src="docs/media/readme/valley.webp" alt="A river valley with a plains village, savanna hills and a cherry grove, seen from above with a long view distance" width="100%">
</p>
## What works today
- **The game:** vanilla world generation, mobs, villagers, raids, redstone, every command, the HUD and the recipe book, on vanilla saves. Worlds from 1.8 to 26.2 are upgraded. [Status](#project-status).
- **Extras:** new menus, graphics presets, distant terrain up to 1024 chunks, controllers, zoom, minimap and more, off by default where they change the vanilla look. [Settings](https://playhorizon.dev/docs/settings).
- **Packs:** resource packs, and data packs with functions, predicates, item modifiers and world generation.
- **Multiplayer:** invite friends with a code, Open to LAN, join servers (online mode included). [Multiplayer](https://playhorizon.dev/docs/multiplayer).
- **Mods:** content mods (blocks, items, data) and native Rust mods, built by the launcher with nothing to install. Java mod jars do not run. [Mods](https://playhorizon.dev/docs/mods).
Everything below is in the current release.
In the Rust client, with vanilla assets and no mods or shaders:
- **The game.** Survival, creative, hardcore and spectator; vanilla world generation in all three dimensions, mobs, villagers, raids, redstone, every command, the HUD and the recipe book. Vanilla saves open as they are, and worlds from 1.8 to 26.2 are upgraded.
- **Multiplayer.** Join vanilla and Horizon servers (online mode included), open a world to LAN, or invite friends with a code from the launcher's Home page.
- **Graphics.** Distant terrain up to 1,024 chunks, graphics presets, HDR and controllers, all off by default where they would change the vanilla look ([settings](https://playhorizon.dev/docs/settings)).
- **Packs.** Resource packs, data packs and shader packs ([below](#mods-and-shader-packs)).
- **Mods.** Content mods and native Rust mods, installed from the launcher ([below](#mods-and-shader-packs)).
- **Servers.** Host one from the launcher, or run `horizon-server` on a VPS ([below](#host-a-server)).
- **The launcher.** It installs and updates the game, prepares its files from your copy of Minecraft, and updates itself.
<table>
<tr>
<td colspan="2"><img src="docs/media/sunset.webp" alt="Sunset over a savanna river valley and a plains village"></td>
<td width="50%"><img src="docs/media/readme/sunset.webp" alt="Sunset behind a cherry grove on a hill, with a river below"></td>
<td width="50%"><img src="docs/media/readme/nether.webp" alt="A crimson forest in the Nether, with a piglin among the fungi"></td>
</tr>
<tr>
<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/readme/survival.webp" alt="Survival in a village square: two villagers, flowers, the hotbar, health and hunger"></td>
<td width="50%"><img src="docs/media/readme/multiplayer.webp" alt="Two players in a LAN world: the other player's name tag and the player list"></td>
</tr>
</table>
<sub>Nightly 2026-11-05 on a Mac mini (Apple M4 Pro, Metal): vanilla's textures, no mods or shader packs, render distance 32 with distant terrain on.</sub>
## Performance
**Horizon draws more frames per second than Java Edition, and loads faster, on the same machine, world and path.** `tools/perf_bench.py` runs each game in turn, from a fresh world (seed 1234), waiting for the terrain and then flying it over a village, a forest, an ocean and a mountain. Measured on 2026-10-29/30 on an idle Windows 11 machine (Ryzen 7 9800X3D, RTX 5080), Horizon v0.2.97 as the launcher installs it, 1280x720 in a window, VSync off, three games per figure. Frames per second, mean of the four scenes, with the slowest 1% of frames in brackets:
| Render distance | Horizon v0.2.97 | Vanilla 26.3 | Vanilla + mods |
| --- | --- | --- | --- |
| 8 chunks | 3,289 (1,327) | 2,437 (640) | 2,375 (709) |
| 16 chunks | 2,074 (598) | 1,661 (375) | 1,586 (432) |
| 32 chunks | 1,207 (333) | 1,031 (176) | 879 (264) |
"Vanilla + mods" is Sodium, Lithium, FerriteCore and ImmediatelyFast. Horizon reaches the title screen in 2.9–3.2 s against vanilla's 7.5–7.7 s (the modded client's 10.8–11.1 s), loads a scene's chunks 2.9–3.8 times faster, and playing holds a steady working set of 1.1–1.7 GB against 1.9–2.9 GB. Every scene with its confidence interval and the run's load log: [the 2026-10-29 reports](benchmarks/perf/windows-9800x3d-5080-2026-10-29.md). Method, and how to measure your own machine: [docs/benchmark.md](docs/benchmark.md). [playhorizon.dev/benchmarks](https://playhorizon.dev/benchmarks) puts these runs beside a 2016 laptop, a Mac mini and a gaming desktop.
## Download
Get **the Horizon launcher** from [playhorizon.dev](https://playhorizon.dev), or from [the launcher releases](https://github.com/horizon-rust/horizon/releases?q=launcher&expanded=true):
| Platform | File | Needs |
| --- | --- | --- |
| Windows x64 | `horizon-launcher-setup.exe` | Windows 10 or 11, a GPU with Vulkan or DirectX 12 |
| Windows on Arm | `horizon-launcher-setup-arm64.exe` | Windows 11 on Arm |
| macOS, Apple Silicon | `horizon-launcher-macos-arm64.dmg` | macOS 12 or later |
| macOS, Intel | `horizon-launcher-macos-x64.dmg` | macOS 12 or later |
| Linux x86-64 | `horizon-launcher-linux-x86_64.AppImage` or `.tar.gz` | a glibc distribution, X11 or Wayland, a GPU with Vulkan or OpenGL 3.3 |
**You also need a Microsoft account that owns Minecraft: Java Edition.** You sign in with it in the launcher, which plays only with an account that owns the game ([why](docs/microsoft-account.md)). Horizon contains no Minecraft code or assets: an existing install (official launcher, Prism, MultiMC...) is reused, and without one the launcher downloads 26.3 from Mojang for your account. Step by step: [Install and first launch](https://playhorizon.dev/docs/install).
<table>
<tr>
<td width="50%"><img src="docs/media/readme/launcher-home.webp" alt="The launcher's Home page: the Horizon instance, Play, the version picker, the friend invite field, screenshots and the patch log"></td>
<td width="50%"><img src="docs/media/readme/launcher-mods.webp" alt="The launcher's Mods page: Horizon's mod registry with Zoom selected, and tabs for modpacks, resource packs, shader packs and data packs"></td>
</tr>
</table>
## Mods and shader packs
- **Content mods** (`.hmod`): blocks, items, recipes, loot, models and textures, with no code.
- **Native mods** (`.hnmod`): Rust that the launcher compiles into the game for you, with nothing to install.
- **Java mods** (Fabric, NeoForge, Forge) don't run: Horizon has no Java. The launcher carries over the data and assets inside their jars, and imports modpacks (`.mrpack`, CurseForge zips, Prism instances) the same way ([modpacks](https://playhorizon.dev/docs/modpacks)).
- **Shader packs**: Iris/OptiFine-format packs (`shaders/` folders) and Horizon's own WGSL packs, under Video Settings → Shader Packs ([pack format](docs/shaders.md)).
Mods are off by default: a fresh install plays exactly like vanilla 26.3. The launcher's Mods page browses Horizon's registry and Modrinth. [Mods](https://playhorizon.dev/docs/mods) · [make a mod](docs/mods.md).
## Host a server
The launcher's **Servers** page creates, starts, backs up and updates a server, and invites friends over Tailscale. On a VPS, `horizon-server` installs with one command and needs no Java. [Host a server](https://playhorizon.dev/docs/hosting) ([reference](docs/hosting.md)).
## Modpacks
The launcher imports Modrinth and CurseForge packs and plays the mods Horizon can carry over: today, content mods made of data and assets, not Java code. [Modpacks](https://playhorizon.dev/docs/modpacks).
The launcher's **Servers** page creates, starts, backs up and updates a server, and invites friends. On a VPS, `horizon-server` installs with one command, needs no Java, and takes vanilla 26.3 and Horizon players alike. [Host a server](https://playhorizon.dev/docs/hosting) ([reference](docs/hosting.md)).
## Agent control
@@ -80,13 +102,11 @@ AI agents and scripts can play Horizon. `horizon-ctl`, shipped with the game, re
```sh
horizon-ctl launch --world Survival --seed 5 --wait # finds the version the launcher installed
horizon-ctl state # position, health, food, target, screen...
horizon-ctl find-block birch_log # the nearest logs
horizon-ctl goto -0.5 65 -2.5 --near 2.5 # walks there along a path
horizon-ctl break -1 65 -3 # mines it; replies with the drops
horizon-ctl open-inventory && horizon-ctl click-slot 0 # menus: slots, buttons by label, text fields
```
Commands block until done and reply with the result; `horizon-ctl help` lists them all. [Agent control](docs/native-client.md#agent-control-horizon-ctl-and-mcp).
`horizon-ctl help` lists every command. [Agent control](docs/native-client.md#agent-control-horizon-ctl-and-mcp).
## Build from source
@@ -105,13 +125,9 @@ cargo run --release -p horizon-client
Prepared files land in the gitignored `.local/`: never commit or share them. Options, controls and architecture: [the native client guide](docs/native-client.md); all developer docs: [docs/README.md](docs/README.md).
## Roadmap
What is in progress, what comes next and where to help: [ROADMAP.md](ROADMAP.md), or [playhorizon.dev/hub](https://playhorizon.dev/hub) with the work board and what shipped.
## Contributing
Issues and pull requests are welcome: read [CONTRIBUTING.md](CONTRIBUTING.md) (vanilla first, small pull requests). Questions: [Discord](https://discord.gg/playhorizon). Before a pull request, run the self-check and paste its output:
Issues and pull requests are welcome: read [CONTRIBUTING.md](CONTRIBUTING.md) (vanilla first, small pull requests), and see [ROADMAP.md](ROADMAP.md) or [playhorizon.dev/hub](https://playhorizon.dev/hub) for what's next. Questions: [Discord](https://discord.gg/playhorizon). Before a pull request, run the self-check and paste its output:
```sh
git config core.hooksPath .githooks
@@ -124,7 +140,7 @@ It runs rustfmt, clippy, the touched crates' tests, the Python tests, the public
**Player-visible gaps: 0 open** of the 833 the [port ledger](tools/port_ledger.py) found, reading every vanilla 26.3 method against Horizon's code: vanilla behaviour a player would notice that Horizon doesn't have yet. By area: Animals [0](https://github.com/horizon-rust/horizon/issues/3512); Monsters and bosses [0](https://github.com/horizon-rust/horizon/issues/3513); Entities, projectiles, vehicles and decorations [0](https://github.com/horizon-rust/horizon/issues/3514); Items, enchantments and inventories [0](https://github.com/horizon-rust/horizon/issues/3515); Blocks, redstone and fluids [0](https://github.com/horizon-rust/horizon/issues/3516); World, world generation and saves [0](https://github.com/horizon-rust/horizon/issues/3517); Screens, HUD and client packets [0](https://github.com/horizon-rust/horizon/issues/3518); Entity and block rendering, models [0](https://github.com/horizon-rust/horizon/issues/3519); Server and players on the server [0](https://github.com/horizon-rust/horizon/issues/3520); Mob AI, villagers and raids [0](https://github.com/horizon-rust/horizon/issues/3521). This number goes down as gaps close (`tools/parity_gaps.py`); the harness results below say that what is tested matches.
The table is the project's own audit: every subsystem matches vanilla in every harness that compares it. 100% means 100% of what the harnesses compare, not that nothing else differs.
The table is the project's own audit: every subsystem matches vanilla in every harness that compares it. 100% means 100% of what the harnesses compare, not that nothing else differs. What players still run into: [known issues](docs/known-issues.md).
### Java parity: 100%
@@ -147,14 +163,8 @@ Scores: 0 (absent), 0.25 (started), 0.5 (playable, major features missing), 0.75
| Audio | 1 | None: the sound engine, sound events, music, situational music, ambience and subtitles match vanilla |
| Block behaviours and ticking | 1 | None: block updates, random ticks, redstone, dispensers (6,000 cases), crafters (4,000), container spills (4,000), copper golems and the sculk family (435 scenarios) match |
### Known issues
Skins served over https show the default skin unless already cached; pack icons show the unknown-pack texture in game; item tooltips lack attribute lines.
## Licence
[MIT](LICENSE), for the project's original work; dependencies keep their own licences ([notices](THIRD_PARTY_NOTICES.md)). Minecraft is a trademark of Mojang Synergies AB; its code and assets belong to Mojang and Microsoft under the [Minecraft EULA](https://www.minecraft.net/en-us/eula).
[MIT](LICENSE), for the project's original work; dependencies keep their own licences ([notices](THIRD_PARTY_NOTICES.md)). Horizon is made by [Ludovic Marie](https://lu4ovic.com) ([@ludovic111](https://github.com/ludovic111)) and [its contributors](https://github.com/horizon-rust/horizon/graphs/contributors); support it on [GitHub Sponsors](https://github.com/sponsors/ludovic111).
## Author
Made by [Ludovic Marie](https://lu4ovic.com) ([@ludovic111](https://github.com/ludovic111)).
NOT AN OFFICIAL MINECRAFT PRODUCT. NOT APPROVED BY OR ASSOCIATED WITH MOJANG OR MICROSOFT. Minecraft is a trademark of Mojang Synergies AB; its code and assets belong to Mojang and Microsoft under the [Minecraft EULA](https://www.minecraft.net/en-us/eula).
+40 -12
View File
@@ -266,7 +266,7 @@ Shader packs were removed and are being rebuilt from scratch. They come back, of
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/horizon-rust/horizon/issues/1584), [#1585](https://github.com/horizon-rust/horizon/issues/1585), [#1586](https://github.com/horizon-rust/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/horizon-rust/horizon/issues/1698) to [#1801](https://github.com/horizon-rust/horizon/issues/1801)).
More: [#1561](https://github.com/horizon-rust/horizon/issues/1561) · [#1771](https://github.com/horizon-rust/horizon/issues/1771) · [#1786](https://github.com/horizon-rust/horizon/issues/1786)
More: [#1561](https://github.com/horizon-rust/horizon/issues/1561) · [#1771](https://github.com/horizon-rust/horizon/issues/1771) · [#1786](https://github.com/horizon-rust/horizon/issues/1786) · [#9045](https://github.com/horizon-rust/horizon/issues/9045) · [#6233](https://github.com/horizon-rust/horizon/issues/6233) · [#6485](https://github.com/horizon-rust/horizon/issues/6485) · [#3734](https://github.com/horizon-rust/horizon/issues/3734) · [#3125](https://github.com/horizon-rust/horizon/issues/3125)
</details>
@@ -298,6 +298,8 @@ Far terrain drawn from the world you really built and explored, caves and builds
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/horizon-rust/horizon/issues/718).
More: [#4678](https://github.com/horizon-rust/horizon/issues/4678)
</details>
#### A batch of targeted optimisations
@@ -308,7 +310,7 @@ Several smaller speed-ups, each measured: idle block entities that sleep, cheape
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/horizon-rust/horizon/issues/1461)). Profile first, keep what pays, show before and after numbers.
More: [docs/profiling.md](docs/profiling.md)
More: [docs/profiling.md](docs/profiling.md) · [#7752](https://github.com/horizon-rust/horizon/issues/7752) · [#8251](https://github.com/horizon-rust/horizon/issues/8251) · [#10229](https://github.com/horizon-rust/horizon/issues/10229)
</details>
@@ -322,6 +324,8 @@ Recognise mods built for versions before 26.1 in the mod browser, so they can be
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)).
More: [#10262](https://github.com/horizon-rust/horizon/issues/10262)
</details>
#### A modpack's own resource and shader packs
@@ -406,16 +410,6 @@ The panel's later features (bans, worlds, server.properties import, logs and cra
</details>
#### 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/horizon-rust/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>
#### Join servers on older versions
Join servers that run older Minecraft versions, such as 1.21 or 1.8 PvP servers, from Horizon.
@@ -536,6 +530,8 @@ Contraptions and physics like the Create and Aeronautics mods, as a mod for Hori
Issue [#805](https://github.com/horizon-rust/horizon/issues/805) (porting QOL mods) closed with this answer: it would come as a native Horizon mod.
More: [#6302](https://github.com/horizon-rust/horizon/issues/6302) · [#5313](https://github.com/horizon-rust/horizon/issues/5313) · [#5556](https://github.com/horizon-rust/horizon/issues/5556)
</details>
#### Proximity voice chat as a native mod
@@ -558,6 +554,8 @@ Every past and future Minecraft version prepared for Horizon automatically. When
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.
More: [#4758](https://github.com/horizon-rust/horizon/issues/4758)
</details>
#### Controllers: tested on real pads, and in the launcher
@@ -568,6 +566,8 @@ Make controller support solid: tested with real PlayStation and Xbox pads and a
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.
More: [#10298](https://github.com/horizon-rust/horizon/issues/10298) · [#8678](https://github.com/horizon-rust/horizon/issues/8678)
</details>
#### The full debug screen
@@ -668,6 +668,16 @@ A shared resource store keyed by the preparer's digest, no prepare intermediates
</details>
#### A launcher screen readers can read
Screen readers read the launcher's buttons, lists and settings, and every part of it works from the keyboard.
<details><summary>Technical</summary>
Issue [#4537](https://github.com/horizon-rust/horizon/issues/4537): the launcher is GPUI (not Electron), so it exposes nothing to UI Automation, NSAccessibility or AT-SPI yet. Needs an accessibility tree from GPUI (an AccessKit bridge) and a role and label on each widget.
</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.
@@ -690,6 +700,18 @@ Issue [#1709](https://github.com/horizon-rust/horizon/issues/1709): aarch64 buil
</details>
#### The launcher builds Horizon on other systems
On a system Horizon has no download for, such as FreeBSD, the launcher offers to build the game on your computer, as it already does for native mods.
<details><summary>Technical</summary>
Issue [#4354](https://github.com/horizon-rust/horizon/issues/4354): when no release asset matches the platform, the launcher fetches the tagged source and runs the same pinned-toolchain `cargo build --locked` its native-mod builds use, with a warning about the time it takes. The build is unsigned, so the launcher marks the version as built locally.
More: [docs/native-mods.md](docs/native-mods.md)
</details>
#### Horizon on Windows ARM64
A native Horizon for Windows on ARM computers, such as Snapdragon laptops.
@@ -710,6 +732,8 @@ Versions of Horizon for Android, iOS and web browsers. Nothing official yet; con
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 build for the browser as long-term goals. First pieces from contributors: `horizon-dualsense` compiling on iOS ([#1813](https://github.com/horizon-rust/horizon/issues/1813)) and helper-thread limits on Android ([#1751](https://github.com/horizon-rust/horizon/issues/1751)). Preparing without Java ([#1750](https://github.com/horizon-rust/horizon/issues/1750)) matters here too. Horizon ships none of Mojang's files on any platform.
More: [#9134](https://github.com/horizon-rust/horizon/issues/9134) · [#8069](https://github.com/horizon-rust/horizon/issues/8069) · [#4153](https://github.com/horizon-rust/horizon/issues/4153)
</details>
#### Horizon on Intel Macs
@@ -752,6 +776,8 @@ Record, replay and edit videos without leaving the game: record in a keypress, r
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.
More: [#10025](https://github.com/horizon-rust/horizon/issues/10025)
</details>
#### Game menus in the launcher's style
@@ -842,6 +868,8 @@ Every download Horizon makes is checked against the project's signature, includi
Releases carry signed checksums, and the launcher and `horizon-server update` refuse anything unsigned ([#1011](https://github.com/horizon-rust/horizon/issues/1011), [#1014](https://github.com/horizon-rust/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)).
More: [#10354](https://github.com/horizon-rust/horizon/issues/10354)
</details>
## Help wanted
+99 -2
View File
@@ -1,12 +1,56 @@
# Third-party notices
## Code removed for its license
Horizon once shipped three pieces of code under MIT that came from projects whose licenses don't allow that. All
three are gone from main. They are listed here for anyone who holds an older commit, tag or release: in those
versions, that code is not covered by Horizon's MIT license. It stays under its authors' copyright and its original
license.
- **Shader packs written from Iris's source.** `crates/horizon-shaderpack` and the render stages that used it were
written from the source of [Iris](https://github.com/IrisShaders/Iris) (LGPL-3.0, copyright the Iris contributors).
They were added from 15 October 2026 (`b22583d057`), shipped in the game releases `v2026.10.16.02` to `v0.2.46`,
and were removed on 24 October 2026 ([#1561](https://github.com/horizon-rust/horizon/pull/1561)) after a developer
of Iris showed us. Shader packs came back in `crates/horizon-shaders`, first written from a description of the
pack format.
- **Math functions translated from OpenJDK.** In `crates/horizon-core/src/jmath.rs`, `fdlibm::{log, log10, cbrt,
hypot}` were translated from OpenJDK's `java/lang/FdLibm.java` and `round_f32` from its `java/lang/Math.java`
(GPL-2.0 with the Classpath Exception), and `aarch64::{sin, cos}` from HotSpot's `macroAssembler_aarch64_trig.cpp`
(GPL-2.0); copyright Oracle and/or its affiliates, and Cavium for that file. The `fdlibm` and `aarch64` functions
were added on 13 October 2026 (`558d140fca`, first released in `v2026.10.13`). `round_f32` was added on 14 October
2026 as `java_round` in `crates/horizon-client/src/mipmap.rs` (`95db0fce9d`) and moved into `jmath.rs` on 3
November ([#7770](https://github.com/horizon-rust/horizon/pull/7770)). All of them were replaced in
[#11944](https://github.com/horizon-rust/horizon/pull/11944) by code written without OpenJDK's sources, from Sun's
fdlibm C sources, the Java SE API documentation and a description of the behaviour, and checked against a JVM
([#6741](https://github.com/horizon-rust/horizon/issues/6741)).
- **Java's `HashMap` translated from OpenJDK.** `crates/horizon-world/src/java_hash_map.rs`, which reproduces the
iteration order of `java.util.HashMap`, was translated from OpenJDK's `java/util/HashMap.java` (GPL-2.0 with the
Classpath Exception; copyright Oracle and/or its affiliates). It was added on 7 October 2026 (`118e93ea51` and
`194db0dbb4`, first released in `v2026.10.12`). It was replaced in
[#12043](https://github.com/horizon-rust/horizon/pull/12043) by code written without OpenJDK's sources, from a
description of the behaviour, the Java SE API documentation and the textbook red-black tree, and checked against a
JVM ([#6741](https://github.com/horizon-rust/horizon/issues/6741)).
## Catnip / Ponder
`crates/horizon-client-render/src/fluid_render.rs` translates the still-fluid face tiling and
UV rules of Catnip's `FluidRenderHelper` from Ponder 1.0.82 for Minecraft 1.21.1
(`net.createmod.ponder:ponder-neoforge:1.0.82+mc1.21.1`). Copyright (c) 2022 The Create Team,
licensed under MIT (`LICENSE_Ponder` in that artifact); the full license heads the Rust file.
The native renderer uses Horizon's own block-sprite arrays, pose and light packing. The
development-only oracle calls the unchanged helper from the player's local artifact. No
original classes, game shaders, textures or prepared resources are distributed here.
## wgpu-hal
`third_party/wgpu-hal` is [wgpu-hal](https://github.com/gfx-rs/wgpu) 30.0.1 by the gfx-rs
developers, with upstream fixes applied
([gfx-rs/wgpu#10124](https://github.com/gfx-rs/wgpu/pull/10124),
[gfx-rs/wgpu#10714](https://github.com/gfx-rs/wgpu/pull/10714)). It is licensed under MIT or
Apache-2.0; both licenses are in that folder (`LICENSE.MIT`, `LICENSE.APACHE`).
[gfx-rs/wgpu#10714](https://github.com/gfx-rs/wgpu/pull/10714)). Its GL backend also reports
the context's sampled textures and samplers per shader stage (usually 32 on desktop GL,
where WebGL2's minimum 16 was reported before), so a pipeline layout the driver accepts is
not refused. It is licensed under MIT or Apache-2.0; both licenses are in that folder
(`LICENSE.MIT`, `LICENSE.APACHE`).
## naga
@@ -212,3 +256,56 @@ 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.
```
## Just Enough Items
The Recipes GUI sprites and settings icon under `crates/horizon-client-render/src/jei_recipes` are from
[Just Enough Items 31.17.0.108](https://github.com/mezz/JustEnoughItems/tree/0371fa6fafa14635a356019e4604f5580b26a34f)
by mezz and contributors, under the MIT license in that directory (`LICENSE.txt`).
## Flywheel native model directional shading
The two-light diffuse expression and transformed-normal policy in
`terrain.wgsl`'s native model entries follow Flywheel 1.0.6
(`internal/diffuse.glsl`, `instance/transformed.vert`, `internal/common.vert`).
No Flywheel classes, compiled shaders or original model/texture assets are bundled.
Copyright (c) 2021-2024 Jozufozu
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:
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.
## fdlibm
`crates/horizon-core/src/jmath/fdlibm.rs` and the `fdlibm_asin`, `fdlibm_atan` and
`fdlibm_atan2` functions in `crates/horizon-core/src/geometry/trig.rs` are Rust ports of
[fdlibm](https://www.netlib.org/fdlibm/) 5.3, Sun's freely distributable math library, whose
results the Java SE API specifies for `StrictMath`. `fdlibm.rs` ports `e_log.c`, `e_log10.c`,
`s_cbrt.c` and `e_hypot.c`, and, with fused multiply-adds, `s_sin.c`, `s_cos.c`, `k_sin.c`,
`k_cos.c` and `e_rem_pio2.c`; `trig.rs` ports `e_asin.c`, `s_atan.c` and `e_atan2.c`. This
notice also heads both files:
```
Copyright (C) 1993 by Sun Microsystems, Inc. All rights reserved.
Developed at SunSoft, a Sun Microsystems, Inc. business.
Permission to use, copy, modify, and distribute this
software is freely granted, provided that this notice
is preserved.
```
+5
View File
@@ -0,0 +1,5 @@
# `keys.contains(&KeyCode::ShiftLeft)` reads a mapping by its default key: ask by `Action`, so the
# mapping is named and every device that can hold it counts.
disallowed-methods = [
{ path = "horizon_client_input::held::HeldKeys::contains", reason = "use `held(Action::..)`, or `raw` for a key that is no mapping's default", allow-invalid = true },
]
+1 -1
View File
@@ -3,7 +3,7 @@ name = "cargo-horizon-mod"
version = "0.1.0"
edition.workspace = true
license.workspace = true
rust-version = "1.86"
rust-version.workspace = true
description = "cargo horizon-mod new/build/run/check: make Horizon content mods (docs/mods.md)"
# `cargo binstall --git https://github.com/horizon-rust/horizon cargo-horizon-mod` (#2120) reads this: the archive
+1 -1
View File
@@ -3,7 +3,7 @@ name = "horizon-anticheat"
version = "0.1.0"
edition.workspace = true
license.workspace = true
rust-version = "1.85"
rust-version.workspace = true
description = "The dedicated server's anticheat: its [anticheat] settings, violation levels and the checks' rules, with no game state (docs/anticheat.md)"
[dependencies]
@@ -34,7 +34,10 @@ pub fn element(v: &Element) -> Value {
.collect();
json!({"from":bits(v.from),"to":bits(v.to),"faces":faces,"rotation":v.rotation.as_ref().map(rotation),"shade":v.shade_direction_override.map(Direction::name),"light":v.light_emission})
}
#[allow(dead_code)] // Shared by fixtures which normalize different model stages.
#[allow(
dead_code,
reason = "shared by fixtures which normalize different model stages"
)]
pub fn model(v: &CuboidModel) -> Value {
let textures: BTreeMap<_, _> = v
.textures
+10 -2
View File
@@ -34,6 +34,8 @@ pub struct ModelQuad {
/// NeoForge's `render_type` names.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum RenderType {
/// A declared render type unknown to this baker; distinct from an absent default.
Unsupported,
Solid,
Cutout,
CutoutMipped,
@@ -234,7 +236,10 @@ fn bake_into(
skip_flat: bool,
output: &mut Vec<ModelQuad>,
) -> Result<()> {
let render_type = model.render_type.as_deref().and_then(RenderType::by_name);
let render_type = model
.render_type
.as_deref()
.map(|name| RenderType::by_name(name).unwrap_or(RenderType::Unsupported));
for element in model.elements() {
let element = element
.as_ref()
@@ -292,7 +297,10 @@ fn bake_obj(
transform: BlockModelTransform,
output: &mut Vec<ModelQuad>,
) -> Result<()> {
let render_type = model.render_type.as_deref().and_then(RenderType::by_name);
let render_type = model
.render_type
.as_deref()
.map(|name| RenderType::by_name(name).unwrap_or(RenderType::Unsupported));
for face in obj.faces.iter().filter(|f| !hidden.contains(&f.object)) {
let slot = match obj.materials.get(&face.material) {
Some(MtlTexture::Sprite(sprite)) => {
+9 -9
View File
@@ -179,19 +179,19 @@ impl Condition {
let fields = object(raw)?;
if fields.len() == 1 {
for name in ["OR", "AND"] {
if let Some(raw) = fields.get(name) {
if let Ok(values) = array(raw).and_then(|values| {
if let Some(raw) = fields.get(name)
&& let Ok(values) = array(raw).and_then(|values| {
values
.into_iter()
.map(Self::decode)
.collect::<Result<Vec<_>>>()
}) {
return Ok(if name == "OR" {
Self::Or(values)
} else {
Self::And(values)
});
}
})
{
return Ok(if name == "OR" {
Self::Or(values)
} else {
Self::And(values)
});
}
}
}
@@ -108,6 +108,16 @@ impl<T> CompiledChoices<T> {
multipart: self.multipart.iter().map(|c| c.map(&mut f)).collect(),
}
}
/// A variant that needs no random selection and produces exactly one model.
pub fn single_variant(&self, selected: &SelectedModels) -> Option<&T> {
let SelectedModels::Variant(index) = selected else {
return None;
};
match self.variants.get(*index)? {
Choice::Single(value) => Some(value),
Choice::Weighted { .. } => None,
}
}
/// 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`).
+55 -14
View File
@@ -917,6 +917,46 @@ impl NumericProperty {
/// (`None` removes a prototype component).
pub type Components = BTreeMap<String, Value>;
/// A stack's components read in place: its patch first, then its item's defaults. Reading one copies
/// nothing, where [`ItemModels::components`] clones the defaults for a stack with a patch (an enchanted
/// sword's are a few dozen JSON values, on every draw of it).
#[derive(Clone, Copy)]
pub struct ComponentsView<'a> {
prototype: &'a Components,
patch: &'a BTreeMap<String, Option<Value>>,
}
impl<'a> ComponentsView<'a> {
pub fn get(&self, key: &str) -> Option<&'a Value> {
match self.patch.get(key) {
Some(patched) => patched.as_ref(),
None => self.prototype.get(key),
}
}
pub fn contains_key(&self, key: &str) -> bool {
self.get(key).is_some()
}
/// Components with no patch: `components` as they are.
pub fn of(components: &'a Components) -> Self {
static NO_PATCH: BTreeMap<String, Option<Value>> = BTreeMap::new();
Self {
prototype: components,
patch: &NO_PATCH,
}
}
/// The merged map, for what needs one whole.
pub fn to_map(&self) -> Components {
let mut out = self.prototype.clone();
for (key, value) in self.patch {
match value {
Some(v) => out.insert(key.clone(), v.clone()),
None => out.remove(key),
};
}
out
}
}
/// An item stack as the resolver reads it.
#[derive(Clone, Debug, Default, PartialEq)]
pub struct StackView {
@@ -1087,14 +1127,15 @@ impl ItemModels {
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),
};
std::borrow::Cow::Owned(self.components_view(stack).to_map())
}
/// The stack's components, read in place (see [`ComponentsView`]).
pub fn components_view<'a>(&'a self, stack: &'a StackView) -> ComponentsView<'a> {
static NONE: Components = BTreeMap::new();
ComponentsView {
prototype: self.prototypes.get(&stack.item).unwrap_or(&NONE),
patch: &stack.patch,
}
std::borrow::Cow::Owned(out)
}
/// `ItemModelResolver.updateForTopItem`.
pub fn resolve(&self, stack: &StackView, ctx: &Context, env: &dyn ItemEnv) -> ResolvedItem {
@@ -1112,7 +1153,7 @@ impl ItemModels {
ctx: &Context,
env: &dyn ItemEnv,
) {
let components = self.components(stack);
let components = self.components_view(stack);
let Some(Value::String(model_id)) = components.get("minecraft:item_model") else {
return;
};
@@ -1122,7 +1163,7 @@ impl ItemModels {
let mut state = Resolve {
models: self,
stack,
components: &components,
components,
ctx,
env,
};
@@ -1140,7 +1181,7 @@ impl ItemModels {
self.properties(stack).2
}
fn properties(&self, stack: &StackView) -> (bool, bool, f32) {
match self.components(stack).get("minecraft:item_model") {
match self.components_view(stack).get("minecraft:item_model") {
Some(Value::String(id)) => identifier(id)
.ok()
.and_then(|id| self.definitions.get(&id))
@@ -1159,7 +1200,7 @@ impl ItemModels {
struct Resolve<'a> {
models: &'a ItemModels,
stack: &'a StackView,
components: &'a Components,
components: ComponentsView<'a>,
ctx: &'a Context<'a>,
env: &'a dyn ItemEnv,
}
@@ -1455,9 +1496,9 @@ impl Resolve<'_> {
fn numeric(&self, property: &NumericProperty) -> f32 {
let owner = self.ctx.owner;
match property {
NumericProperty::BundleFullness => {
self.env.bundle_fullness(self.stack, self.components)
}
NumericProperty::BundleFullness => self
.env
.bundle_fullness(self.stack, &self.components.to_map()),
NumericProperty::Compass {
wobble,
target,
@@ -63,7 +63,7 @@ pub(super) fn decode(m: &Map<String, Value>) -> Result<Value> {
/// with a matching level. An empty predicate list still needs the component.
pub(super) fn matches(
predicate: &Value,
components: &Components,
components: ComponentsView<'_>,
env: &dyn ItemEnv,
) -> Option<bool> {
let (id, expected) = predicate.as_object()?.iter().next()?;
@@ -143,13 +143,17 @@ mod tests {
json!({"minecraft:density":0}),
)]);
assert_eq!(
matches(&json!({"minecraft:enchantments":[{}]}), &components, &NoEnv),
matches(
&json!({"minecraft:enchantments":[{}]}),
ComponentsView::of(&components),
&NoEnv
),
Some(true)
);
assert_eq!(
matches(
&json!({"minecraft:enchantments":[{"enchantments":"minecraft:density"}]}),
&components,
ComponentsView::of(&components),
&NoEnv
),
Some(false)
@@ -166,12 +170,21 @@ mod tests {
"minecraft:enchantments".into(),
json!({"minecraft:density":3}),
)]);
assert_eq!(matches(&predicate, &components, &Tags), Some(true));
assert_eq!(matches(&predicate, &components, &NoEnv), Some(false));
assert_eq!(
matches(&predicate, ComponentsView::of(&components), &Tags),
Some(true)
);
assert_eq!(
matches(&predicate, ComponentsView::of(&components), &NoEnv),
Some(false)
);
components.insert(
"minecraft:enchantments".into(),
json!({"minecraft:density":2}),
);
assert_eq!(matches(&predicate, &components, &Tags), Some(false));
assert_eq!(
matches(&predicate, ComponentsView::of(&components), &Tags),
Some(false)
);
}
}
@@ -1,4 +1,5 @@
use super::*;
use serde_json::json;
#[test]
fn identifier_adds_default_namespace() {
@@ -111,3 +112,49 @@ fn custom_condition_validates_its_name_and_both_branches() {
assert!(matches!(node("example:charged", empty).unwrap(),
ItemModelNode::Condition { property: ConditionProperty::Custom(name), .. } if name == "example:charged"));
}
#[test]
fn a_patched_stack_reads_its_patch_over_its_items_defaults() {
let mut models = ItemModels::default();
models.prototypes.insert(
"minecraft:diamond_sword".into(),
Components::from_iter([
(
"minecraft:item_model".into(),
json!("minecraft:diamond_sword"),
),
("minecraft:max_damage".into(), json!(1561)),
("minecraft:rarity".into(), json!("common")),
]),
);
let mut stack = StackView::new("minecraft:diamond_sword", 1);
stack.patch.insert(
"minecraft:enchantments".into(),
Some(json!({"minecraft:sharpness": 5})),
);
stack
.patch
.insert("minecraft:rarity".into(), Some(json!("rare")));
stack.patch.insert("minecraft:max_damage".into(), None);
let view = models.components_view(&stack);
assert_eq!(view.get("minecraft:rarity"), Some(&json!("rare")));
assert_eq!(view.get("minecraft:max_damage"), None, "a removed default");
assert_eq!(
view.get("minecraft:item_model"),
Some(&json!("minecraft:diamond_sword"))
);
assert!(view.contains_key("minecraft:enchantments"));
assert!(!view.contains_key("minecraft:custom_name"));
// Every key reads as the merged map has it.
let merged = models.components(&stack);
for key in [
"minecraft:rarity",
"minecraft:max_damage",
"minecraft:item_model",
"minecraft:enchantments",
"minecraft:custom_name",
] {
assert_eq!(view.get(key), merged.get(key), "{key}");
}
assert_eq!(view.to_map(), *merged);
}
+2 -2
View File
@@ -290,12 +290,12 @@ pub fn normalize_quaternion(q: [f32; 4]) -> [f32; 4] {
[x * inv, y * inv, z * inv, w * inv]
}
/// JOML `Math.sin(float)` without fast math.
fn joml_sin(rad: f32) -> f32 {
pub fn joml_sin(rad: f32) -> f32 {
f64::from(rad).sin() as f32
}
/// JOML `Math.cosFromSin(float, float)` without fast math: the magnitude from
/// the sine, the sign from the angle's quadrant (exact zeros at right angles).
fn cos_from_sin(sin: f32, angle: f32) -> f32 {
pub fn cos_from_sin(sin: f32, angle: f32) -> f32 {
let cos = (1.0 - sin * sin).sqrt();
let tau = (std::f64::consts::PI * 2.0) as f32;
let a = angle + std::f32::consts::FRAC_PI_2;
+17
View File
@@ -777,6 +777,23 @@ mod tests {
);
}
#[test]
fn an_unknown_declared_render_type_remains_distinct_from_absent() {
let json = LIGHTS.replace("minecraft:cutout", "fixture:custom_material");
let library = library(&[("block/custom", &json)]);
let model = library.resolve("block/custom").unwrap();
let quads = crate::bake::bake_block_model(
&model,
crate::bake::BlockModelTransform::new([0; 3], false),
)
.unwrap();
assert!(!quads.is_empty());
assert!(
quads
.iter()
.all(|q| q.render_type == Some(crate::bake::RenderType::Unsupported))
);
}
#[test]
fn render_type_comes_from_the_leaf_most_model_and_loses_its_prefix() {
let library = library(&[
("block/base", r#"{"render_type":"translucent"}"#),
+1
View File
@@ -2,6 +2,7 @@
name = "horizon-bench"
version = "0.1.0"
edition = "2021"
rust-version.workspace = true
license = "MIT"
description = "The benchmark report format (docs/benchmark-report.md), shared by the game, the launcher and the upload"
+142 -18
View File
@@ -90,6 +90,9 @@ horizon-bots: load-test bots for a Minecraft Java Edition 26.3 server (offline m
--seed N default 12345
--explore-speed B blocks a second an explore bot flies; default 22 (as fast as chunks come)
--pvp B fight bots also attack each other (true or false); default true
--crits fight bots swing at a sword's full strength (every 13 ticks) and jump into every
other swing, landing it while falling: a critical hit, then a sweep from the
ground (give them swords); for PvP benches (crit and sweep particles)
--name PREFIX bot names: PREFIX0, PREFIX1...; default Bot
--brand B the client brand sent in configuration (minecraft:brand); default none
--height H blocks above spawn the bots fly at (walk, explore, idle); default 24
@@ -165,6 +168,8 @@ struct Options {
attack_after: f64,
attack_rounds: u32,
no_swing: bool,
/// --crits: fight bots alternate jumped critical hits and sweeps at a sword's cooldown.
crits: bool,
flood: usize,
no_read: bool,
no_loaded: bool,
@@ -202,6 +207,7 @@ fn options() -> Result<Options, String> {
attack_after: 5.0,
attack_rounds: u32::MAX,
no_swing: false,
crits: false,
flood: 100,
no_read: false,
no_loaded: false,
@@ -230,6 +236,10 @@ fn options() -> Result<Options, String> {
o.no_swing = true;
continue;
}
if arg == "--crits" {
o.crits = true;
continue;
}
if arg == "--no-loaded" {
o.no_loaded = true;
continue;
@@ -532,11 +542,84 @@ fn distance(a: [f64; 3], b: [f64; 3]) -> f64 {
((a[0] - b[0]).powi(2) + (a[1] - b[1]).powi(2) + (a[2] - b[2]).powi(2)).sqrt()
}
/// A sword's attack speed is 1.6 a second: full strength 20 / 1.6 = 12.5 ticks after a swing.
const SWORD_COOLDOWN_TICKS: u32 = 13;
/// `LivingEntity.getJumpPower`: a jump's first upward motion, in blocks a tick.
const JUMP_POWER: f64 = 0.42;
/// `--crits`: a fight bot that swings at a sword's full strength and jumps into every other swing, so
/// it lands while falling (`Player.attack`'s critical hit: falling, not on the ground, full strength),
/// and the next one from the ground (a sweep: on the ground, full strength, a sword).
#[derive(Default)]
struct Crits {
/// Vertical motion while in the air (`LivingEntity.travel`: minus 0.08 a tick, times 0.98).
jump: Option<f64>,
ground: f64,
/// The next swing is the jumped one.
crit_next: bool,
}
impl Crits {
fn airborne(&self) -> bool {
self.jump.is_some()
}
/// One tick: walk up to the target and, with a full cooldown, strike; jump ahead of a crit's swing.
fn tick(
&mut self,
pos: &mut [f64; 3],
target: (i32, [f64; 3]),
since: &mut u32,
) -> Option<i32> {
if let Some(v) = self.jump {
pos[1] += v;
if pos[1] <= self.ground {
pos[1] = self.ground;
self.jump = None;
} else {
self.jump = Some((v - 0.08) * 0.98);
}
} else {
self.ground = pos[1];
// A jump takes 6 ticks to its top: start it so the cooldown ends on the way down.
if self.crit_next && *since + 8 >= SWORD_COOLDOWN_TICKS {
self.jump = Some(JUMP_POWER);
}
}
let falling = self.jump.is_some_and(|v| v < 0.0);
if self.crit_next && !falling {
*since = since.saturating_add(1);
fight_step(pos, target.1);
return None;
}
let hit = fight_tick(pos, target, since, SWORD_COOLDOWN_TICKS);
if hit.is_some() {
self.crit_next = !self.crit_next;
}
hit
}
}
/// One tick of a fighting bot with a target: walk up to it, and strike once the cooldown is over.
/// Returns the entity to attack. `since_attack` counts ticks since the last swing.
fn fight_tick(pos: &mut [f64; 3], target: (i32, [f64; 3]), since_attack: &mut u32) -> Option<i32> {
fn fight_tick(
pos: &mut [f64; 3],
target: (i32, [f64; 3]),
since_attack: &mut u32,
cooldown: u32,
) -> Option<i32> {
*since_attack = since_attack.saturating_add(1);
let (id, at) = target;
fight_step(pos, at);
if *since_attack >= cooldown && distance(*pos, at) <= ATTACK_REACH {
*since_attack = 0;
return Some(id);
}
None
}
/// Walk toward `at`, stopping `CLOSE_IN` short of it.
fn fight_step(pos: &mut [f64; 3], at: [f64; 3]) {
let (dx, dz) = (at[0] - pos[0], at[2] - pos[2]);
let horizontal = dx.hypot(dz);
if horizontal > CLOSE_IN {
@@ -544,11 +627,6 @@ fn fight_tick(pos: &mut [f64; 3], target: (i32, [f64; 3]), since_attack: &mut u3
pos[0] += dx / horizontal * step;
pos[2] += dz / horizontal * step;
}
if *since_attack >= ATTACK_COOLDOWN_TICKS && distance(*pos, at) <= ATTACK_REACH {
*since_attack = 0;
return Some(id);
}
None
}
/// One tick of walking to random points around the spawn, at sprint speed (5.6 blocks a second).
@@ -616,7 +694,7 @@ fn costly(
Behaviour::FloodUse => {
// A block within reach: place stone there, or break what is there.
let at = [0, 1, 2].map(|k| block[k] + (random.unit() * 7.0) as i32 - 3);
if i % 2 == 0 {
if i.is_multiple_of(2) {
play::Serverbound::UseItemOn(play::UseItemOn {
hand: 0,
pos: at,
@@ -645,7 +723,7 @@ fn costly(
carried: None,
}),
Behaviour::FloodSuggest => {
let text = if i % 2 == 0 {
let text = if i.is_multiple_of(2) {
"/"
} else {
LONG_COMMAND.as_str()
@@ -1124,6 +1202,7 @@ fn run_bot(
spawn[1] + o.height
};
let mut since_attack = ATTACK_COOLDOWN_TICKS;
let mut crits = Crits::default();
let heading = random.unit() * std::f64::consts::TAU;
let mut target = pos;
let mut next = Instant::now();
@@ -1181,11 +1260,16 @@ fn run_bot(
Behaviour::Fight => {
let near = lock(&bot.entities).nearest(pos, o.pvp, &bots);
if let Some(near) = near {
if let Some(victim) = fight_tick(&mut pos, near, &mut since_attack) {
let hit = if o.crits {
crits.tick(&mut pos, near, &mut since_attack)
} else {
fight_tick(&mut pos, near, &mut since_attack, ATTACK_COOLDOWN_TICKS)
};
if let Some(victim) = hit {
// The client sends the attack, then swings the arm.
let sent = send(
play::Serverbound::Attack(play::Attack { id: victim }).encode(),
) && send(raw(sb::PUNCH, |w| w.varint(0)));
) && send(play::Serverbound::Punch(Empty).encode());
if sent {
bot.attacks.fetch_add(1, Ordering::Relaxed);
}
@@ -1300,9 +1384,10 @@ fn run_bot(
x: pos[0],
y: pos[1],
z: pos[2],
// Fight bots stand on the ground; the others fly.
// Fight bots stand on the ground (but in a --crits jump); the others fly.
flags: u8::from(
matches!(behaviour, Behaviour::Fight | Behaviour::Stand) || o.ground,
(matches!(behaviour, Behaviour::Fight | Behaviour::Stand) && !crits.airborne())
|| o.ground,
),
})
.encode(),
@@ -1470,10 +1555,10 @@ fn main() {
});
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}");
}
} else if let Some(path) = &o.report
&& let Err(e) = std::fs::write(path, format!("{summary:#}\n"))
{
eprintln!("horizon-bots: {path}: {e}");
}
let errors: Vec<String> = bots
.iter()
@@ -1600,7 +1685,9 @@ mod tests {
let target = (3, [1.0, 64.0, 0.0]);
let mut since = ATTACK_COOLDOWN_TICKS;
let hits: Vec<u32> = (0..40)
.filter_map(|tick| fight_tick(&mut pos, target, &mut since).map(|_| tick))
.filter_map(|tick| {
fight_tick(&mut pos, target, &mut since, ATTACK_COOLDOWN_TICKS).map(|_| tick)
})
.collect();
assert_eq!(hits[0], 0, "a bot in reach strikes at once");
assert!(
@@ -1617,7 +1704,7 @@ mod tests {
let mut since = ATTACK_COOLDOWN_TICKS;
let mut first = None;
for tick in 0..100 {
if fight_tick(&mut pos, target, &mut since).is_some() {
if fight_tick(&mut pos, target, &mut since, ATTACK_COOLDOWN_TICKS).is_some() {
first = Some(tick);
break;
}
@@ -1627,6 +1714,43 @@ mod tests {
assert!(pos[0] < 10.0 - CLOSE_IN + 1e-9);
}
#[test]
fn crits_alternate_a_falling_swing_and_a_grounded_one() {
let mut pos = [0.0, 64.0, 0.0];
let target = (3, [1.0, 64.0, 0.0]);
let (mut since, mut crits) = (SWORD_COOLDOWN_TICKS, Crits::default());
let mut last_y = pos[1];
let mut hits = Vec::new();
for tick in 0..120u32 {
let hit = crits.tick(&mut pos, target, &mut since);
if hit.is_some() {
hits.push((tick, crits_state(pos[1], last_y)));
}
last_y = pos[1];
}
assert!(hits.len() >= 6, "{hits:?}");
for (i, w) in hits.windows(2).enumerate() {
assert!(
w[1].0 - w[0].0 >= SWORD_COOLDOWN_TICKS,
"full strength each time: {hits:?}"
);
assert_ne!(w[0].1, w[1].1, "swing {i} and the next alternate: {hits:?}");
}
// A crit lands in the air, on the way down; a sweep from the ground.
assert!(hits.iter().any(|h| h.1 == "falling") && hits.iter().any(|h| h.1 == "ground"));
assert!(pos[1] >= 64.0);
}
fn crits_state(y: f64, before: f64) -> &'static str {
if y == 64.0 {
"ground"
} else if y < before {
"falling"
} else {
"rising"
}
}
#[test]
fn percentiles_use_nearest_rank() {
assert_eq!(percentile(&[], 50), 0);
+1 -1
View File
@@ -3,7 +3,7 @@ name = "horizon-chat-filter"
version = "0.1.0"
edition.workspace = true
license.workspace = true
rust-version = "1.88"
rust-version.workspace = true
description = "The dedicated server's chat filter: its [chat_filter] settings, the slur list's matcher and the mutes, with no game state (docs/chat-filter.md)"
[dependencies]
+1 -1
View File
@@ -3,7 +3,7 @@ name = "horizon-client-core"
version = "0.1.0"
edition.workspace = true
license.workspace = true
rust-version = "1.90"
rust-version.workspace = true
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]
@@ -0,0 +1,22 @@
//! NBT path parser output and byte cursor for the pinned Java oracle.
use horizon_client_core::nbt_path::NbtPath;
use std::io::{BufRead, Write};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut args = std::env::args().skip(1);
let input = std::io::BufReader::new(std::fs::File::open(args.next().unwrap())?);
let mut output = std::io::BufWriter::new(std::fs::File::create(args.next().unwrap())?);
for line in input.lines() {
let text: String = serde_json::from_str(&line?)?;
match NbtPath::parse_prefix(&text) {
Ok((path, cursor)) => {
write!(output, "OK {cursor} ")?;
for value in path.as_str().as_bytes() {
write!(output, "{value:02x}")?;
}
writeln!(output)?;
}
Err(error) => writeln!(output, "ERR {} {}", error.cursor, error.key)?,
}
}
Ok(())
}
@@ -217,9 +217,10 @@ pub fn stack(tag: &Tag) -> Option<Tag> {
Some(Tag::compound(out))
}
/// `ItemStackTemplate.CODEC`: an item id or `{id, count?, components?}`.
/// Encoding uses the primary record form, including for a bare item id.
fn template(tag: &Tag) -> Option<Tag> {
match tag {
Tag::String(_) => Some(tag.clone()),
Tag::String(_) => Some(Tag::compound([("id", tag.clone())])),
Tag::Compound(_) => {
let mut out = vec![("id", string(&text(tag.get("id"))?))];
if let Some(count) = int(tag.get("count")).filter(|c| *c != 1) {
@@ -1011,3 +1012,69 @@ mod tests {
assert_eq!(items[1].get("count"), Some(&Tag::Int(5)));
}
}
#[cfg(test)]
mod pot_template_tests {
use super::*;
use horizon_world::nbt::parse_snbt;
#[test]
fn decorated_pot_string_template_load_save_uses_item_compound() {
let parse = |s: &str| parse_snbt(s).expect("valid input NBT").0;
let input = |front: &str| {
let mut tag = parse(
r#"{id:"minecraft:decorated_pot",x:4,y:200,z:4,sherds:{back:{id:"minecraft:brick"},right:{id:"minecraft:archer_pottery_sherd"}},item:{id:"minecraft:wheat",count:3,components:{"minecraft:custom_name":"H290 item"}}}"#,
);
let Tag::Compound(root) = &mut tag else {
unreachable!()
};
let Some(Tag::Compound(sides)) = root.get_mut(&"sherds".into()) else {
unreachable!()
};
sides.insert("front".into(), parse(front));
tag
};
let other_fields = |loaded: &Tag, saved: &Tag| {
assert_eq!(saved.get("id"), loaded.get("id"));
for key in ["x", "y", "z", "item"] {
assert_eq!(saved.get(key), loaded.get(key), "{key}");
}
let loaded_sides = loaded.get("sherds").unwrap();
let saved_sides = saved.get("sherds").unwrap();
for side in ["back", "right"] {
assert_eq!(saved_sides.get(side), loaded_sides.get(side), "{side}");
}
assert_eq!(saved_sides.get("left"), None);
};
let plain = r#"{id:"minecraft:angler_pottery_sherd"}"#;
let defaults = r#"{id:"minecraft:angler_pottery_sherd",count:1,components:{}}"#;
let rich = r#"{id:"minecraft:angler_pottery_sherd",count:3,components:{"minecraft:custom_data":{h290:1},"minecraft:custom_name":"H290 sherd"}}"#;
// Actual canonical load/save entry, not the private template helper.
for (front, expected) in [(plain, plain), (defaults, plain), (rich, rich)] {
let loaded = input(front);
let original = loaded.clone();
let saved = canonical(&loaded).expect("actual decorated-pot codec");
other_fields(&loaded, &saved);
assert_eq!(
saved.get("sherds").unwrap().get("front"),
Some(&parse(expected))
);
assert_eq!(canonical(&saved).as_ref(), Some(&saved));
assert_eq!(loaded, original, "read-only canonicalization");
}
println!(
"H290 actual canonical compound/default/count/components, other sides/item/name and idempotency controls admitted"
);
let loaded = input(r#""minecraft:angler_pottery_sherd""#);
let original = loaded.clone();
let saved = canonical(&loaded).expect("actual decorated-pot codec");
other_fields(&loaded, &saved);
assert_eq!(loaded, original, "read-only canonicalization");
assert_eq!(
saved.get("sherds").unwrap().get("front"),
Some(&parse(plain)),
"string template must save the primary item compound",
);
assert_eq!(canonical(&saved).as_ref(), Some(&saved));
}
}
+34 -2
View File
@@ -111,7 +111,15 @@ impl Parser<'_> {
map.insert(key, self.value()?);
self.ws();
match self.peek() {
Some(b',') => self.i += 1,
// SnbtGrammar: an entry may be followed by a comma before the brace.
Some(b',') => {
self.i += 1;
self.ws();
if self.peek() == Some(b'}') {
self.i += 1;
return Ok(Value::Object(map));
}
}
Some(b'}') => {
self.i += 1;
return Ok(Value::Object(map));
@@ -140,7 +148,15 @@ impl Parser<'_> {
list.push(self.value()?);
self.ws();
match self.peek() {
Some(b',') => self.i += 1,
// SnbtGrammar: a list or array may end with a comma.
Some(b',') => {
self.i += 1;
self.ws();
if self.peek() == Some(b']') {
self.i += 1;
return Ok(Value::Array(list));
}
}
Some(b']') => {
self.i += 1;
return Ok(Value::Array(list));
@@ -497,6 +513,22 @@ pub fn name_text(tag: &Tag) -> Text {
#[cfg(test)]
mod tests {
use super::*;
/// SnbtGrammar: a compound or list may end with one comma; two in a row
/// stay an error.
#[test]
fn a_component_takes_one_trailing_comma() {
let parse = |s: &str| parse_snbt(s).map(|(v, _)| v);
assert_eq!(
parse("{text:\"a\",color:\"red\",}"),
parse("{text:\"a\",color:\"red\"}")
);
assert_eq!(
parse("[\"a\", {text:\"b\",} ,]"),
parse("[\"a\",{text:\"b\"}]")
);
assert!(parse("{text:\"a\",,}").is_err());
assert!(parse("[\"a\",,]").is_err());
}
#[test]
fn hex_color_follows_text_color_parse_color() {
+19 -1
View File
@@ -46,6 +46,21 @@ thread_local! {
}
/// The current language's `bidirectional`, read for every drawn string.
static BIDIRECTIONAL: AtomicBool = AtomicBool::new(false);
/// Counts the languages set (`set`): another count is another language, so text looked up
/// before it may read otherwise.
static GENERATION: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
thread_local! {
/// Counts the scopes this thread entered and left (`scoped`), which no other thread sees.
static SCOPE_GENERATION: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
}
/// Which language text looks up in now, on this thread, as a count that changes whenever that
/// can: both counts only grow, so their sum changes when either does.
pub fn generation() -> u64 {
GENERATION.load(Ordering::Relaxed) + SCOPE_GENERATION.get()
}
fn scope_changed() {
SCOPE_GENERATION.set(SCOPE_GENERATION.get() + 1);
}
/// A source of resource files: the prepared root plus packs, highest priority last.
pub trait ResourceSource {
@@ -394,7 +409,7 @@ pub fn apply_deprecated(map: &mut HashMap<String, String>, json: &[u8]) {
/// characters in strings, `=` or `=>` and `;`), which vanilla reads lang files with (expected
/// from `Language.loadFromJson`, Gson). A file that is not JSON even so adds nothing; a value
/// that is not a string, number or boolean (`GsonHelper.convertToString`) ends the file there.
fn read_lang_file(bytes: &[u8], map: &mut HashMap<String, String>) {
pub fn read_lang_file(bytes: &[u8], map: &mut HashMap<String, String>) {
use horizon_world::datafix::json::{Json, parse_lenient};
if let Ok(entries) = serde_json::from_slice::<HashMap<String, String>>(bytes) {
map.extend(entries);
@@ -524,6 +539,7 @@ pub fn available(sources: &[&dyn ResourceSource]) -> Vec<LanguageInfo> {
pub fn set(language: Language) {
BIDIRECTIONAL.store(language.bidirectional, Ordering::Relaxed);
*CURRENT.write().unwrap_or_else(|e| e.into_inner()) = Some(Arc::new(language));
GENERATION.fetch_add(1, Ordering::Relaxed);
}
pub fn current() -> Option<Arc<Language>> {
if let Some(language) = SCOPE.with(|s| s.borrow().as_ref().map(|s| s.language.clone())) {
@@ -543,6 +559,7 @@ pub fn scoped<R>(language: Language, f: impl FnOnce() -> R) -> (R, Vec<String>)
impl Drop for Restore {
fn drop(&mut self) {
SCOPE.with(|s| *s.borrow_mut() = self.0.take());
scope_changed();
}
}
let outer = SCOPE.with(|s| {
@@ -552,6 +569,7 @@ pub fn scoped<R>(language: Language, f: impl FnOnce() -> R) -> (R, Vec<String>)
})
});
let _restore = Restore(outer);
scope_changed();
let result = f();
let missed = SCOPE.with(|s| {
s.borrow_mut()
+50 -4
View File
@@ -81,7 +81,7 @@ fn elements(tag: &Tag) -> Option<Vec<Tag>> {
}
/// `DoubleTag.intValue` / `FloatTag.intValue` floor before the i32 conversion;
/// integral tags use their intValue narrowing. Byte arrays then keep the low byte.
fn int_value(t: &Tag) -> Option<i32> {
pub fn int_value(t: &Tag) -> Option<i32> {
Some(match t {
Tag::Byte(v) => i32::from(*v),
Tag::Short(v) => i32::from(*v),
@@ -726,11 +726,16 @@ fn parse_node(text: &str, i: usize, first: bool) -> Result<(Node, usize), PathEr
Some(b']') => Ok((Node::AllElements, j + 1)),
_ => {
let start = j;
if b.get(j) == Some(&b'-') {
// StringReader.readInt scans all allowed number characters
// before parsing, including malformed signs and decimal points.
while b
.get(j)
.is_some_and(|c| c.is_ascii_digit() || matches!(c, b'-' | b'.'))
{
j += 1;
}
while b.get(j).is_some_and(u8::is_ascii_digit) {
j += 1;
if start == j {
return Err(PathError::new("parsing.int.expected", vec![], start));
}
let n: i32 = text[start..j].parse().map_err(|_| {
PathError::new(
@@ -792,6 +797,13 @@ fn read_quoted(text: &str, i: usize) -> Result<(String, usize), PathError> {
let mut escaped = false;
for (o, c) in text[i + 1..].char_indices() {
if escaped {
if c != quote && c != '\\' {
return Err(PathError::new(
"parsing.quote.escape",
vec![c.to_string()],
i + 1 + o,
));
}
out.push(c);
escaped = false;
} else if c == '\\' {
@@ -840,6 +852,40 @@ fn parse_compound(text: &str, i: usize) -> Result<(Tag, usize), PathError> {
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn quoted_escapes_and_index_tokens_match_brigadier() {
let cases = [
(r#""a\q""#, "parsing.quote.escape", vec!["q"], 3),
(r#"'a\x'"#, "parsing.quote.escape", vec!["x"], 3),
("[1.0]", "parsing.int.invalid", vec!["1.0"], 1),
("[.]", "parsing.int.invalid", vec!["."], 1),
("[--1]", "parsing.int.invalid", vec!["--1"], 1),
("[2147483648]", "parsing.int.invalid", vec!["2147483648"], 1),
("[+1]", "parsing.int.expected", vec![], 1),
(r#""é\q""#, "parsing.quote.escape", vec!["q"], 4),
(r#""é"[1.0]"#, "parsing.int.invalid", vec!["1.0"], 5),
];
let mut failures = Vec::new();
for (text, key, args, cursor) in cases {
match NbtPath::parse_prefix(text) {
Err(error) if error.key == key && error.args == args && error.cursor == cursor => {}
other => failures.push(format!("{text:?}: {other:?}")),
}
}
assert!(failures.is_empty(), "{}", failures.join("\n"));
}
#[test]
fn valid_quoted_escapes_still_select_the_exact_field() {
let root = Tag::compound([("a\"b", Tag::Int(7)), ("a\\b", Tag::Int(8))]);
for (path, value) in [(r#""a\"b""#, 7), (r#""a\\b""#, 8)] {
assert_eq!(
NbtPath::parse(path).unwrap().get(&root).unwrap(),
vec![Tag::Int(value)]
);
}
}
#[test]
fn field_provenance_handles_quotes_and_filters_without_matching_lists() {
for text in [
@@ -275,6 +275,25 @@ impl Registries {
.clone_from(&e.advancement_criteria);
}
}
/// Add the damage catalog's types ([`Self::damage_types`]: content mods'
/// and the world's packs' with vanilla's) to the commands'
/// `minecraft:damage_type` ids, which the export lists for vanilla alone.
/// `ResourceArgument` looks a type up in the server's registries, so
/// `/damage` takes every type the world has. The export's tags stay. The
/// ids are replaced, not extended: `by_registry` outlives a world, and a
/// pack's type must not stay accepted in the next world opened.
pub fn sync_damage_type_ids(&mut self) {
if self.damage_types.is_empty() {
return;
}
let mut ids = self.damage_types.clone();
ids.sort();
ids.dedup();
self.by_registry
.entry("minecraft:damage_type".into())
.or_default()
.ids = ids;
}
/// The element ids of a registry (sorted).
pub fn ids(&self, registry: &str) -> &[String] {
if let Some(r) = self.by_registry.get(registry) {
@@ -312,3 +331,49 @@ impl Registries {
}
}
}
#[cfg(test)]
mod tests {
use super::Registries;
/// A content mod's damage type joins vanilla's in the commands' ids, the
/// export's tags stay, and a second world open lists each type once.
#[test]
fn damage_type_ids_take_the_catalogs_types() {
let export = Registries::from_export(&serde_json::json!({
"by_registry": {"minecraft:damage_type": {
"ids": ["minecraft:fall", "minecraft:magic"],
"tags": ["minecraft:is_fall"],
}},
}));
let mut reg = Registries {
damage_types: vec![
"minecraft:magic".into(),
"example:test".into(),
"minecraft:fall".into(),
],
..Registries::default()
};
reg.fill_from_exported(&export);
assert_eq!(
reg.ids("minecraft:damage_type"),
["minecraft:fall", "minecraft:magic"],
"the export alone lists vanilla's types"
);
for _ in 0..2 {
reg.sync_damage_type_ids();
assert_eq!(
reg.ids("minecraft:damage_type"),
["example:test", "minecraft:fall", "minecraft:magic"]
);
assert_eq!(reg.tags("minecraft:damage_type"), ["minecraft:is_fall"]);
}
// The next world, without the pack: its type is no longer accepted.
reg.damage_types = vec!["minecraft:fall".into(), "minecraft:magic".into()];
reg.sync_damage_type_ids();
assert_eq!(
reg.ids("minecraft:damage_type"),
["minecraft:fall", "minecraft:magic"]
);
}
}
+1 -1
View File
@@ -3,7 +3,7 @@ name = "horizon-client-input"
version = "0.1.0"
edition.workspace = true
license.workspace = true
rust-version = "1.90"
rust-version.workspace = true
description = "The client's input layer: key bindings and their names, mouse, controllers, clipboard and window options"
[dependencies]
+146
View File
@@ -0,0 +1,146 @@
//! Every input the game names: vanilla's key mappings (`Options.keyMappings`) and Horizon's
//! pad-only actions. Held state is asked by [`Action`] ([`crate::held::HeldKeys::held`]), not by a
//! key: code that wants a mapping says which one, and every device that can hold it counts.
use winit::keyboard::KeyCode;
macro_rules! actions {
($($variant:ident, $name:literal, $key:expr;)*) => {
/// A key mapping, or a pad-only action.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Action {
$($variant,)*
}
impl Action {
/// In `Options.keyMappings` order, then Horizon's own mappings, the pad-only actions last.
pub const ALL: &[Action] = &[$(Action::$variant,)*];
/// The saved name: vanilla's for a mapping, `pad.*` for Horizon's own.
pub fn name(self) -> &'static str {
match self {
$(Action::$variant => $name,)*
}
}
/// The default key the game's held state keeps this action under. `None` for what
/// isn't held as a key there: a mouse mapping, one with no default, a pad-only action,
/// and the debug keys, which act on the physical key (`debug_keys.rs`) and whose
/// defaults are other mappings' keys.
pub fn key(self) -> Option<KeyCode> {
match self {
$(Action::$variant => $key,)*
}
}
}
};
}
actions! {
Attack, "key.attack", None;
Use, "key.use", None;
Forward, "key.forward", Some(KeyCode::KeyW);
Left, "key.left", Some(KeyCode::KeyA);
Back, "key.back", Some(KeyCode::KeyS);
Right, "key.right", Some(KeyCode::KeyD);
Jump, "key.jump", Some(KeyCode::Space);
Sneak, "key.sneak", Some(KeyCode::ShiftLeft);
Sprint, "key.sprint", Some(KeyCode::ControlLeft);
Drop, "key.drop", Some(KeyCode::KeyQ);
Inventory, "key.inventory", Some(KeyCode::KeyE);
Chat, "key.chat", Some(KeyCode::KeyT);
Playerlist, "key.playerlist", Some(KeyCode::Tab);
PickItem, "key.pickItem", None;
Command, "key.command", Some(KeyCode::Slash);
Friends, "key.friends", Some(KeyCode::KeyO);
SocialInteractions, "key.socialInteractions", Some(KeyCode::KeyP);
ToggleGui, "key.toggleGui", Some(KeyCode::F1);
ToggleSpectatorShaderEffects, "key.toggleSpectatorShaderEffects", Some(KeyCode::F4);
Screenshot, "key.screenshot", Some(KeyCode::F2);
TogglePerspective, "key.togglePerspective", Some(KeyCode::F5);
SmoothCamera, "key.smoothCamera", None;
Fullscreen, "key.fullscreen", Some(KeyCode::F11);
SpectatorOutlines, "key.spectatorOutlines", None;
SpectatorHotbar, "key.spectatorHotbar", None;
SwapOffhand, "key.swapOffhand", Some(KeyCode::KeyF);
SaveToolbarActivator, "key.saveToolbarActivator", Some(KeyCode::KeyC);
LoadToolbarActivator, "key.loadToolbarActivator", Some(KeyCode::KeyX);
Advancements, "key.advancements", Some(KeyCode::KeyL);
QuickActions, "key.quickActions", Some(KeyCode::KeyG);
DebugOverlay, "key.debug.overlay", None;
DebugModifier, "key.debug.modifier", None;
Hotbar1, "key.hotbar.1", Some(KeyCode::Digit1);
Hotbar2, "key.hotbar.2", Some(KeyCode::Digit2);
Hotbar3, "key.hotbar.3", Some(KeyCode::Digit3);
Hotbar4, "key.hotbar.4", Some(KeyCode::Digit4);
Hotbar5, "key.hotbar.5", Some(KeyCode::Digit5);
Hotbar6, "key.hotbar.6", Some(KeyCode::Digit6);
Hotbar7, "key.hotbar.7", Some(KeyCode::Digit7);
Hotbar8, "key.hotbar.8", Some(KeyCode::Digit8);
Hotbar9, "key.hotbar.9", Some(KeyCode::Digit9);
DebugReloadChunk, "key.debug.reloadChunk", None;
DebugShowHitboxes, "key.debug.showHitboxes", None;
DebugClearChat, "key.debug.clearChat", None;
DebugCrash, "key.debug.crash", None;
DebugShowChunkBorders, "key.debug.showChunkBorders", None;
DebugShowAdvancedTooltips, "key.debug.showAdvancedTooltips", None;
DebugCopyRecreateCommand, "key.debug.copyRecreateCommand", None;
DebugSpectate, "key.debug.spectate", None;
DebugSwitchGameMode, "key.debug.switchGameMode", None;
DebugDebugOptions, "key.debug.debugOptions", None;
DebugFocusPause, "key.debug.focusPause", None;
DebugDumpDynamicTextures, "key.debug.dumpDynamicTextures", None;
DebugReloadResourcePacks, "key.debug.reloadResourcePacks", None;
DebugProfiling, "key.debug.profiling", None;
DebugCopyLocation, "key.debug.copyLocation", None;
DebugDumpVersion, "key.debug.dumpVersion", None;
DebugProfilingChart, "key.debug.profilingChart", None;
DebugFpsCharts, "key.debug.fpsCharts", None;
DebugNetworkCharts, "key.debug.networkCharts", None;
DebugLightmapTexture, "key.debug.lightmapTexture", None;
DebugImprovedTransparency, "key.debug.improvedTransparency", None;
Emote, "key.emote", Some(KeyCode::KeyB);
HotbarPrevious, "pad.hotbarPrevious", None;
HotbarNext, "pad.hotbarNext", None;
}
impl Action {
pub fn parse(name: &str) -> Option<Self> {
Self::ALL.iter().copied().find(|a| a.name() == name)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::keybinds::{self, HORIZON_MAPPINGS, KeyBinds, MAPPINGS, all_mappings};
#[test]
fn every_mapping_is_an_action_in_order() {
let names: Vec<&str> = Action::ALL.iter().map(|a| a.name()).collect();
let mappings: Vec<&str> = MAPPINGS.iter().map(|m| m.0).collect();
assert_eq!(names[..mappings.len()], mappings[..]);
let horizon: Vec<&str> = HORIZON_MAPPINGS.iter().map(|m| m.0).collect();
let end = mappings.len() + horizon.len();
assert_eq!(names[mappings.len()..end], horizon[..]);
for action in &names[end..] {
assert!(action.starts_with("pad."), "{action}");
}
}
#[test]
fn an_actions_key_is_its_mappings_default_key() {
for (name, _, category) in all_mappings() {
let action = Action::parse(name).unwrap();
let want = if *category == "debug" {
None
} else {
keybinds::key_code(KeyBinds::default_of(name))
};
assert_eq!(action.key(), want, "{name}");
}
}
#[test]
fn every_bindable_action_is_an_action() {
for (name, _) in crate::gamepad::ACTIONS {
assert!(Action::parse(name).is_some(), "{name}");
}
}
}
+353 -38
View File
@@ -1,11 +1,17 @@
//! Controllers: Horizon's own feature (vanilla 26.3 has no gamepad input).
//! The default layout is Bedrock Edition's; see docs/controllers.md.
//! Its default buttons are Horizon's own; analog walking and toggle sneak follow Bedrock Edition on
//! consoles. See docs/controllers.md.
//!
//! This module is the backend-free part: positional buttons, the action
//! bindings, deadzones and the look curve, button edges from successive pad
//! states, and `horizon-controller.txt`, kept next to `options.txt` so that
//! file stays exactly vanilla's. The game reads the pads with gilrs.
use std::{collections::BTreeMap, fs, path::Path};
use std::{
collections::BTreeMap,
fs,
path::Path,
time::{Duration, Instant},
};
pub const FILE: &str = "horizon-controller.txt";
@@ -296,7 +302,7 @@ pub const HOTBAR_NEXT: &str = "pad.hotbarNext";
/// always pause / back and isn't listed.
pub const ACTIONS: &[(&str, Option<Button>)] = &[
("key.jump", Some(Button::South)),
("key.drop", Some(Button::East)),
("key.drop", Some(Button::DPadDown)),
("key.swapOffhand", Some(Button::West)),
("key.inventory", Some(Button::North)),
(HOTBAR_PREVIOUS, Some(Button::LeftBumper)),
@@ -305,17 +311,31 @@ pub const ACTIONS: &[(&str, Option<Button>)] = &[
("key.attack", Some(Button::RightTrigger)),
("key.sprint", Some(Button::LeftStick)),
("key.sneak", Some(Button::RightStick)),
("key.togglePerspective", Some(Button::DPadUp)),
("key.togglePerspective", Some(Button::Select)),
("key.pickItem", Some(Button::DPadLeft)),
("key.chat", Some(Button::DPadRight)),
("key.advancements", Some(Button::DPadDown)),
("key.playerlist", Some(Button::Select)),
("key.chat", Some(Button::DPadUp)),
("key.advancements", None),
("key.playerlist", None),
("key.command", None),
("key.screenshot", None),
("key.toggleGui", None),
("key.smoothCamera", None),
// Horizon's emote wheel: no button is free by default; the stick's cursor picks.
("key.emote", None),
("key.fullscreen", None),
("key.socialInteractions", None),
("key.quickActions", None),
("key.saveToolbarActivator", None),
("key.loadToolbarActivator", None),
("key.hotbar.1", None),
("key.hotbar.2", None),
("key.hotbar.3", None),
("key.hotbar.4", None),
("key.hotbar.5", None),
("key.hotbar.6", None),
("key.hotbar.7", None),
("key.hotbar.8", None),
("key.hotbar.9", None),
];
/// How a bindable action reaches the game: one of its own handlers, shared
@@ -330,6 +350,10 @@ pub enum Route {
Sneak,
Screenshot,
SmoothCamera,
Fullscreen,
/// Does nothing by itself: what reads it asks whether the button is held
/// (`Minecraft.handleKeybinds`' toolbar keys, held with a hotbar key).
Held,
Key(winit::keyboard::KeyCode),
Mouse(winit::event::MouseButton),
}
@@ -342,6 +366,8 @@ pub fn route(action: &str) -> Option<Route> {
"key.sneak" => Some(Route::Sneak),
"key.screenshot" => Some(Route::Screenshot),
"key.smoothCamera" => Some(Route::SmoothCamera),
"key.fullscreen" => Some(Route::Fullscreen),
"key.saveToolbarActivator" | "key.loadToolbarActivator" => Some(Route::Held),
_ => {
let default = crate::keybinds::KeyBinds::default_of(action);
crate::keybinds::mouse_button(default)
@@ -361,6 +387,46 @@ pub fn action_caption(action: &str) -> &str {
}
}
/// How long a held drop button throws single items before it throws the rest of the stack.
pub const DROP_STACK_HOLD: Duration = Duration::from_secs(1);
/// What a held drop button does now, past its press.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DropStep {
/// Held key.drop (a repeat reaches `KeyMapping.click` as a press).
One,
/// Control + key.drop: a pad has no Control to hold, so holding on is how it asks.
Stack,
}
/// A held drop button's repeats: a tap throws one item, as Q does; held, it repeats at the pad's
/// repeat delay and interval; and held for [`DROP_STACK_HOLD`] it throws what's left.
#[derive(Clone, Copy, Debug)]
pub struct HeldDrop {
since: Instant,
next: Instant,
}
impl HeldDrop {
pub fn new(now: Instant, delay: Duration) -> Self {
Self {
since: now,
next: now + delay,
}
}
/// The step due at `now`, if any; at most one per call.
pub fn due(&mut self, now: Instant, interval: Duration) -> Option<DropStep> {
if now < self.next {
return None;
}
self.next = now + interval;
Some(if now.duration_since(self.since) >= DROP_STACK_HOLD {
DropStep::Stack
} else {
DropStep::One
})
}
}
/// Which button pictures prompts use.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum GlyphStyle {
@@ -404,18 +470,57 @@ impl GlyphStyle {
GlyphStyle::PlayStation,
GlyphStyle::Nintendo,
];
/// The style for a pad by its USB vendor (Sony, Nintendo; Microsoft,
/// Valve's Steam Deck and Steam Input's virtual pad, and everything else
/// look like Xbox).
pub fn of_vendor(vendor: Option<u16>) -> Self {
match vendor {
Some(0x054c) => GlyphStyle::PlayStation,
Some(0x057e) => GlyphStyle::Nintendo,
/// The style for a pad by its USB ids: Sony's and Nintendo's own, and a few officially licensed
/// third-party pads made for their consoles. Microsoft, Valve's Steam Deck and Steam Input's
/// virtual pad, and everything else look like Xbox.
pub fn of_device(vendor: Option<u16>, product: Option<u16>) -> Self {
match (vendor, product) {
(Some(0x054c), _) => GlyphStyle::PlayStation,
(Some(0x057e), _) => GlyphStyle::Nintendo,
// HORIPAD 4 FPS and FPS Plus; Razer Raiju; Nacon Revolution Pro.
(Some(0x0f0d), Some(0x0055 | 0x0066))
| (Some(0x1532), Some(0x1000))
| (Some(0x146b), Some(0x0d01)) => GlyphStyle::PlayStation,
// HORIPAD for Nintendo Switch; PowerA Wired Plus; PDP Faceoff Wired Pro.
(Some(0x0f0d), Some(0x00c1))
| (Some(0x20d6), Some(0xa711))
| (Some(0x0e6f), Some(0x0180)) => GlyphStyle::Nintendo,
_ => GlyphStyle::Xbox,
}
}
}
/// How much the gameplay guide says.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum GuideDetail {
#[default]
Full,
/// Only what can be done to what's in front of the player or under the crosshair.
Basic,
Off,
}
impl GuideDetail {
pub const ALL: [GuideDetail; 3] = [GuideDetail::Full, GuideDetail::Basic, GuideDetail::Off];
/// The name in `horizon-controller.txt`.
pub fn name(self) -> &'static str {
match self {
GuideDetail::Full => "full",
GuideDetail::Basic => "basic",
GuideDetail::Off => "off",
}
}
/// Reads a name, or the `true` and `false` this option first saved. Anything else is full.
pub fn parse(value: &str) -> Self {
match value {
"basic" => GuideDetail::Basic,
"off" | "false" => GuideDetail::Off,
_ => GuideDetail::Full,
}
}
}
/// The on-screen keyboard A opens on a text field in focus mode.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum TextKeyboard {
@@ -463,6 +568,8 @@ pub fn steam_keyboard(choice: TextKeyboard) -> bool {
pub struct PadSettings {
pub enabled: bool,
pub vibration: bool,
/// The pad keeps driving the game while its window has no focus.
pub play_in_background: bool,
/// A DualSense's triggers push back (Horizon's; docs/controllers.md).
pub adaptive_triggers: bool,
pub lightbar: Lightbar,
@@ -482,6 +589,8 @@ pub struct PadSettings {
/// by default on the Switch only.
pub nintendo_letters: bool,
pub keyboard: TextKeyboard,
/// The gameplay guide: what the buttons do now, in the HUD's corners while the pad plays.
pub guide_detail: GuideDetail,
/// Menu pointer multiplier, scroll lines/second, and repeat times in seconds.
pub pointer_speed: f32,
pub scroll_speed: f32,
@@ -495,6 +604,7 @@ impl Default for PadSettings {
Self {
enabled: true,
vibration: true,
play_in_background: false,
adaptive_triggers: true,
lightbar: Lightbar::Health,
look_sensitivity: 0.5,
@@ -507,6 +617,7 @@ impl Default for PadSettings {
glyphs: GlyphStyle::Auto,
nintendo_letters: cfg!(target_os = "horizon"),
keyboard: TextKeyboard::Auto,
guide_detail: GuideDetail::Full,
pointer_speed: 1.0,
scroll_speed: 12.0,
repeat_delay: 0.35,
@@ -561,6 +672,7 @@ impl PadSettings {
match key.trim() {
"enabled" => s.enabled = flag(s.enabled),
"vibration" => s.vibration = flag(s.vibration),
"playInBackground" => s.play_in_background = flag(s.play_in_background),
"adaptiveTriggers" => s.adaptive_triggers = flag(s.adaptive_triggers),
"lightbar" => s.lightbar = Lightbar::parse(value),
"lookSensitivity" => s.look_sensitivity = unit(s.look_sensitivity),
@@ -575,6 +687,7 @@ impl PadSettings {
"glyphs" => s.glyphs = GlyphStyle::parse(value),
"nintendoLetters" => s.nintendo_letters = flag(s.nintendo_letters),
"keyboard" => s.keyboard = TextKeyboard::parse(value),
"gameplayGuide" => s.guide_detail = GuideDetail::parse(value),
"pointerSpeed" => s.pointer_speed = bounded(s.pointer_speed, 0.25, 3.0),
"scrollSpeed" => s.scroll_speed = bounded(s.scroll_speed, 1.0, 36.0),
"repeatDelay" => s.repeat_delay = bounded(s.repeat_delay, 0.1, 1.0),
@@ -593,6 +706,24 @@ impl PadSettings {
}
}
}
// A file from before these defaults: its player's buttons stay where they were.
if !text.is_empty() && !text.lines().any(|line| line.starts_with("bindLayout:")) {
for (action, button) in [
("key.drop", Button::East),
("key.togglePerspective", Button::DPadUp),
("key.chat", Button::DPadRight),
("key.playerlist", Button::Select),
("key.advancements", Button::DPadDown),
] {
// A button saved by hand stays, even one that's now this action's default.
if !text
.lines()
.any(|line| line.starts_with(&format!("bind_{action}:")))
{
s.bindings.entry(action).or_insert(Some(button));
}
}
}
s
}
pub fn serialize(&self) -> String {
@@ -614,7 +745,8 @@ impl PadSettings {
self.keyboard.name(),
);
text.push_str(&format!(
"pointerSpeed:{}\nscrollSpeed:{}\nrepeatDelay:{}\nrepeatInterval:{}\n",
"gameplayGuide:{}\npointerSpeed:{}\nscrollSpeed:{}\nrepeatDelay:{}\nrepeatInterval:{}\n",
self.guide_detail.name(),
self.pointer_speed, self.scroll_speed, self.repeat_delay, self.repeat_interval,
));
for (action, button) in &self.bindings {
@@ -623,6 +755,11 @@ impl PadSettings {
button.map_or("none", Button::name)
));
}
text.push_str(&format!(
"playInBackground:{}\n",
bool_text(self.play_in_background)
));
text.push_str("bindLayout:1\n");
text
}
pub fn default_of(action: &str) -> Option<Button> {
@@ -693,12 +830,36 @@ pub fn stick_axes(
{
// Default map: RightStickX is the stick's Y, LeftZ its X; RightStickY
// is the right trigger and must not drive look.
(left, [left_z, right[0]])
// gilrs turns over only the axes it calls Y, and this Y is on one it doesn't.
(left, [left_z, -right[0]])
} else {
(left, right)
}
}
/// The tilt from which a stick holds a key in [`eight_direction_keys`]: holding one is full speed,
/// so the small tilts past the deadzone stay still.
const KEY_TILT: f32 = 0.3;
/// tan(22.5 degrees): where one of the 8 sectors meets the next.
const SECTOR_EDGE: f32 = 0.414_213_57;
/// The movement keys (forward, backward, left, right) a move vector (`[left, forward]`) holds: the
/// one of the 8 headings it is nearest, so it is at most 22.5 degrees off. An edge goes to the
/// straight heading.
pub fn eight_direction_keys([left, forward]: [f32; 2]) -> [bool; 4] {
if length([left, forward]) < KEY_TILT {
return [false; 4];
}
let along = forward.abs() > SECTOR_EDGE * left.abs();
let across = left.abs() > SECTOR_EDGE * forward.abs();
[
along && forward > 0.0,
along && forward < 0.0,
across && left > 0.0,
across && left < 0.0,
]
}
/// A radial deadzone: inside it the stick is centred; beyond it the rest of
/// the travel is rescaled to 0..1 in the same direction, so diagonals aren't
/// clipped and the first movement past the deadzone is small.
@@ -747,15 +908,6 @@ pub fn look_delta(
)
}
/// The hotbar slot after a bumper: one step with wrap-around.
pub fn hotbar_step(selected: usize, forward: bool) -> usize {
if forward {
(selected + 1) % 9
} else {
(selected + 8) % 9
}
}
/// Menu cursor speed in GUI pixels per second at full tilt.
pub const CURSOR_SPEED: f32 = 300.0;
@@ -1462,6 +1614,52 @@ mod tests {
}
}
#[test]
fn a_held_drop_repeats_then_throws_the_stack() {
let t = Instant::now();
let at = |ms: u64| t + Duration::from_millis(ms);
let (delay, interval) = (Duration::from_millis(350), Duration::from_millis(90));
let mut held = HeldDrop::new(t, delay);
assert_eq!(held.due(at(100), interval), None, "a tap is one item");
assert_eq!(held.due(at(349), interval), None);
assert_eq!(held.due(at(350), interval), Some(DropStep::One));
assert_eq!(held.due(at(400), interval), None, "not before the interval");
assert_eq!(held.due(at(440), interval), Some(DropStep::One));
assert_eq!(held.due(at(999), interval), Some(DropStep::One));
assert_eq!(held.due(at(1000), interval), None);
assert_eq!(held.due(at(1100), interval), Some(DropStep::Stack));
assert_eq!(held.due(at(1200), interval), Some(DropStep::Stack));
assert_eq!(held.due(at(1201), interval), None);
}
#[test]
fn the_later_actions_have_no_default_button_and_leave_saved_bindings() {
let defaults = PadSettings::default();
for action in [
"key.fullscreen",
"key.socialInteractions",
"key.quickActions",
"key.saveToolbarActivator",
"key.loadToolbarActivator",
"key.hotbar.1",
"key.hotbar.9",
] {
assert_eq!(defaults.binding(action), None, "{action}");
}
let read = PadSettings::parse(
"bindLayout:1
bind_key.jump:east
bind_key.hotbar.2:north
",
);
assert_eq!(read.binding("key.jump"), Some(Button::East));
assert_eq!(read.binding("key.inventory"), Some(Button::North));
assert_eq!(
read.actions_of(Button::North),
["key.inventory", "key.hotbar.2"]
);
}
#[test]
fn navigation_preferences_migrate_and_stay_with_each_player() {
let dir = std::env::temp_dir().join(format!(
@@ -1581,7 +1779,7 @@ mod tests {
}
#[test]
fn defaults_follow_bedrock() {
fn every_action_has_its_default() {
let s = PadSettings::default();
assert_eq!(s.actions_of(Button::South), ["key.jump"]);
assert_eq!(s.actions_of(Button::RightTrigger), ["key.attack"]);
@@ -1589,16 +1787,55 @@ mod tests {
assert_eq!(s.actions_of(Button::North), ["key.inventory"]);
assert_eq!(s.actions_of(Button::RightBumper), [HOTBAR_NEXT]);
assert!(s.actions_of(Button::Start).is_empty());
// Every default action names a vanilla mapping or a pad action.
// Every default action names a key mapping or a pad action.
for (action, _) in ACTIONS {
assert!(
action.starts_with("pad.")
|| crate::keybinds::MAPPINGS.iter().any(|m| m.0 == *action),
|| crate::keybinds::all_mappings().any(|m| m.0 == *action),
"{action}"
);
}
}
#[test]
fn playing_in_the_background_is_off_until_saved_on() {
assert!(!PadSettings::parse("vibration:false\n").play_in_background);
let mut on = PadSettings::default();
assert!(!on.play_in_background);
on.play_in_background = true;
assert!(PadSettings::parse(&on.serialize()).play_in_background);
}
#[test]
fn a_file_from_before_these_defaults_keeps_its_buttons() {
// Saved settings that never bound D-pad down: it stays on the advancements.
let s = PadSettings::parse("vibration:false\nbind_key.chat:up\n");
assert_eq!(s.binding("key.advancements"), Some(Button::DPadDown));
assert_eq!(s.binding("key.chat"), Some(Button::DPadUp));
assert_eq!(s.binding("key.drop"), Some(Button::East));
assert_eq!(s.binding("key.togglePerspective"), Some(Button::DPadUp));
assert_eq!(s.binding("key.playerlist"), Some(Button::Select));
assert!(!s.conflicts("key.advancements"));
// Advancements moved by hand stays moved, and the file stays this way from then on.
let s = PadSettings::parse("bind_key.advancements:left\nbind_key.drop:down\n");
assert_eq!(s.binding("key.advancements"), Some(Button::DPadLeft));
assert_eq!(s.binding("key.drop"), Some(Button::DPadDown));
assert_eq!(PadSettings::parse(&s.serialize()), s);
}
#[test]
fn a_new_player_drops_on_d_pad_down_and_chats_on_d_pad_up() {
for s in [PadSettings::default(), PadSettings::parse("")] {
assert_eq!(s.binding("key.drop"), Some(Button::DPadDown));
assert_eq!(s.binding("key.chat"), Some(Button::DPadUp));
assert_eq!(s.binding("key.togglePerspective"), Some(Button::Select));
assert_eq!(s.binding("key.playerlist"), None);
assert_eq!(s.binding("key.advancements"), None);
}
let s = PadSettings::parse(&PadSettings::default().serialize());
assert_eq!(s, PadSettings::default());
}
#[test]
fn settings_round_trip_and_store_only_changes() {
let mut s = PadSettings::default();
@@ -1610,6 +1847,7 @@ mod tests {
s.look_sensitivity = 0.75;
s.glyphs = GlyphStyle::PlayStation;
s.nintendo_letters = !s.nintendo_letters;
s.guide_detail = GuideDetail::Basic;
let text = s.serialize();
assert!(text.contains(&format!("nintendoLetters:{}\n", s.nintendo_letters)));
assert!(text.contains("bind_key.jump:east\n"));
@@ -1620,8 +1858,9 @@ mod tests {
assert_eq!(back.binding("key.drop"), None);
assert_eq!(back.actions_of(Button::East), ["key.jump"]);
// Garbage keeps the defaults.
let odd =
PadSettings::parse("lookSensitivity:NaN\nbind_key.nope:south\nbind_key.jump:x9\n");
let odd = PadSettings::parse(
"lookSensitivity:NaN\nbind_key.nope:south\nbind_key.jump:x9\nbindLayout:1\n",
);
assert_eq!(odd, PadSettings::default());
}
@@ -1636,7 +1875,11 @@ mod tests {
false,
);
assert_eq!(left, [0.2, 0.3]);
assert_eq!(right, [-0.5, 0.7], "X from LeftZ, Y from RightStickX");
assert_eq!(
right,
[-0.5, -0.7],
"X from LeftZ, Y from RightStickX, turned over"
);
let kept = stick_axes(
[0.2, 0.3],
[0.7, -0.9],
@@ -1678,6 +1921,63 @@ mod tests {
assert_eq!(Button::North.by_letter().label(GlyphStyle::Nintendo), "Y");
}
#[test]
fn a_stick_holds_the_keys_of_its_nearest_heading() {
// Forward, backward, left, right, by the heading counted from forward toward left.
let headings = [
[true, false, false, false],
[true, false, true, false],
[false, false, true, false],
[false, true, true, false],
[false, true, false, false],
[false, true, false, true],
[false, false, false, true],
[true, false, false, true],
];
for tenth in 0..3600 {
let degrees = tenth as f32 / 10.0;
if degrees % 45.0 == 22.5 {
continue;
}
for tilt in [0.31, 0.5, 1.0] {
let (s, c) = degrees.to_radians().sin_cos();
let keys = eight_direction_keys([tilt * s, tilt * c]);
let sector = ((degrees / 45.0).round() as usize) % 8;
assert_eq!(keys, headings[sector], "{degrees} at {tilt}");
let [left, forward] =
crate::input::keyboard_direction(keys[0], keys[1], keys[2], keys[3]);
let off = (left.atan2(forward).to_degrees() - degrees).rem_euclid(360.0);
assert!(off.min(360.0 - off) <= 22.5 + 1e-3, "{degrees}: {off}");
}
}
}
#[test]
fn a_sector_edge_goes_to_the_straight_heading() {
assert_eq!(
eight_direction_keys([0.414_213_57, 1.0]),
[true, false, false, false]
);
assert_eq!(
eight_direction_keys([0.4143, 1.0]),
[true, false, true, false]
);
assert_eq!(
eight_direction_keys([1.0, 0.414_213_57]),
[false, false, true, false]
);
}
#[test]
fn a_small_tilt_holds_no_key() {
assert_eq!(eight_direction_keys([0.0, 0.0]), [false; 4]);
assert_eq!(eight_direction_keys([0.2, 0.2]), [false; 4]);
assert_eq!(
eight_direction_keys([0.0, 0.31]),
[true, false, false, false]
);
}
#[test]
fn radial_deadzone_rescales() {
assert_eq!(radial([0.1, 0.05], 0.15), [0.0, 0.0]);
@@ -2208,12 +2508,27 @@ mod tests {
}
#[test]
fn hotbar_wraps_and_glyphs_follow_the_vendor() {
assert_eq!(hotbar_step(8, true), 0);
assert_eq!(hotbar_step(0, false), 8);
assert_eq!(hotbar_step(3, true), 4);
assert_eq!(GlyphStyle::of_vendor(Some(0x054c)), GlyphStyle::PlayStation);
assert_eq!(GlyphStyle::of_vendor(Some(0x28de)), GlyphStyle::Xbox);
assert_eq!(GlyphStyle::of_vendor(None), GlyphStyle::Xbox);
fn glyphs_follow_the_vendor() {
let of = |vendor, product| GlyphStyle::of_device(Some(vendor), Some(product));
// Any product of the consoles' makers.
assert_eq!(of(0x054c, 0x0ce6), GlyphStyle::PlayStation);
assert_eq!(of(0x054c, 0x0001), GlyphStyle::PlayStation);
assert_eq!(of(0x057e, 0x2009), GlyphStyle::Nintendo);
// A pad made for a console by another maker, and that maker's others.
assert_eq!(of(0x0f0d, 0x0066), GlyphStyle::PlayStation);
assert_eq!(of(0x0f0d, 0x00c1), GlyphStyle::Nintendo);
assert_eq!(of(0x0f0d, 0x0067), GlyphStyle::Xbox);
assert_eq!(of(0x20d6, 0xa711), GlyphStyle::Nintendo);
// Valve's virtual pad, Microsoft's, nothing known, nothing reported.
assert_eq!(of(0x28de, 0x11ff), GlyphStyle::Xbox);
assert_eq!(of(0x045e, 0x0b12), GlyphStyle::Xbox);
assert_eq!(of(0x1234, 0x5678), GlyphStyle::Xbox);
assert_eq!(GlyphStyle::of_device(None, None), GlyphStyle::Xbox);
// A vendor alone is enough only for the consoles' makers.
assert_eq!(
GlyphStyle::of_device(Some(0x054c), None),
GlyphStyle::PlayStation
);
assert_eq!(GlyphStyle::of_device(Some(0x0f0d), None), GlyphStyle::Xbox);
}
}
@@ -0,0 +1,962 @@
//! The gameplay guide: what the buttons do right now, in the corners of the HUD while a pad plays
//! and of an inventory it drives (Horizon's; vanilla has no gamepad input). The game reads the facts
//! from its own hit results and capabilities (`game/pad/guide.rs`); this turns them into rows, and
//! places the rows around what was drawn.
use crate::gamepad::{Button, GuideDetail, PadSettings, Rect};
/// What a use does, by what takes it.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Verb {
Use,
Open,
Ride,
Trade,
Feed,
Place,
Eat,
Drink,
Shoot,
Load,
Throw,
Raise,
Equip,
Fish,
Zoom,
Boost,
}
impl Verb {
fn caption(self) -> &'static str {
match self {
Verb::Use => "horizon.controller.guide.use",
Verb::Open => "horizon.controller.guide.open",
Verb::Ride => "horizon.controller.guide.ride",
Verb::Trade => "horizon.controller.guide.trade",
Verb::Feed => "horizon.controller.guide.feed",
Verb::Place => "horizon.controller.guide.place",
Verb::Eat => "horizon.controller.guide.eat",
Verb::Drink => "horizon.controller.guide.drink",
Verb::Shoot => "horizon.controller.guide.shoot",
Verb::Load => "horizon.controller.guide.load",
Verb::Throw => "horizon.controller.guide.throw",
Verb::Raise => "horizon.controller.guide.raise",
Verb::Equip => "horizon.controller.guide.equip",
Verb::Fish => "horizon.controller.guide.fish",
Verb::Zoom => "horizon.controller.guide.zoom",
Verb::Boost => "horizon.controller.guide.boost",
}
}
}
/// `Minecraft.hitResult`, with what each button would do to it.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Target {
#[default]
None,
Block {
breakable: bool,
/// The block takes the use before the held item gets its turn.
takes: Option<Verb>,
},
Entity {
attackable: bool,
takes: Option<Verb>,
},
}
/// An item in use, by what letting go of it does.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Active {
Shoot,
Throw,
Stop,
}
/// What the guide knows of the player this moment.
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct Facts {
pub spectator: bool,
pub target: Target,
/// What `Item.use` does on the target or in the air: the main hand's, else the off hand's.
pub item_use: Option<Verb>,
/// Pick Block finds something it can select or, in creative, add.
pub can_pick: bool,
pub using: Option<Active>,
/// `LocalPlayer.isHandsBusy`: paddling a boat.
pub hands_busy: bool,
pub riding: bool,
/// The vehicle jumps when told (a saddled horse).
pub vehicle_jumps: bool,
pub on_ground: bool,
pub in_water: bool,
pub flying: bool,
pub may_fly: bool,
pub gliding: bool,
/// In the air with a glider on: a jump would start gliding.
pub can_glide: bool,
/// Sneaking that the button latched, so the next press stands up.
pub sneak_latched: bool,
pub can_sprint: bool,
pub main_hand_item: bool,
pub off_hand_item: bool,
}
/// One button's line: the button and the translation key of what it does.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Row {
/// Shown before `button`: held with it, or its other half (LB and RB for pages).
pub with: Option<Button>,
pub button: Button,
pub caption: &'static str,
}
impl Row {
fn new(button: Button, caption: &'static str) -> Self {
Row {
with: None,
button,
caption,
}
}
fn pair(with: Button, button: Button, caption: &'static str) -> Self {
Row {
with: Some(with),
button,
caption,
}
}
}
/// The two columns, most useful row first.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Guide {
/// Top left: movement and the player's own actions.
pub left: Vec<Row>,
/// Top right: break, use and pick block.
pub right: Vec<Row>,
}
impl Guide {
/// The rows this detail keeps. Basic is the right column: what the buttons do to what's in
/// front of the player, or the plain face buttons in an inventory.
pub fn at_detail(self, detail: GuideDetail) -> Self {
match detail {
GuideDetail::Full => self,
GuideDetail::Basic => Guide {
left: Vec::new(),
right: self.right,
},
GuideDetail::Off => Guide {
left: Vec::new(),
right: Vec::new(),
},
}
}
}
/// The rows for these facts on these bindings. An unbound action has no row, and neither has one
/// on Start, which is always pause.
pub fn resolve(facts: &Facts, settings: &PadSettings) -> Guide {
let row = |action: &str, caption: Option<&'static str>| {
let button = settings
.binding(action)
.filter(|button| *button != Button::Start)?;
Some(Row::new(button, caption?))
};
let player = !facts.spectator;
let any_item = facts.main_hand_item || facts.off_hand_item;
Guide {
left: [
row("key.jump", jump_caption(facts)),
row("key.sneak", sneak_caption(facts)),
row("key.sprint", facts.can_sprint.then_some("key.sprint")),
row("key.inventory", player.then_some("key.inventory")),
row(
"key.drop",
(player && facts.main_hand_item).then_some("horizon.controller.guide.drop"),
),
row(
"key.swapOffhand",
(player && any_item).then_some("horizon.controller.guide.swap"),
),
]
.into_iter()
.flatten()
.collect(),
right: [
row("key.attack", attack_caption(facts)),
row("key.use", use_caption(facts)),
row("key.pickItem", pick_caption(facts)),
]
.into_iter()
.flatten()
.collect(),
}
}
fn jump_caption(f: &Facts) -> Option<&'static str> {
if f.riding {
f.vehicle_jumps.then_some("key.jump")
} else if f.flying {
Some("horizon.controller.guide.fly_up")
} else if f.gliding {
None
} else if f.in_water {
Some("horizon.controller.guide.swim_up")
} else if f.on_ground {
Some("key.jump")
} else if f.can_glide {
Some("horizon.controller.guide.glide")
} else {
f.may_fly.then_some("horizon.controller.guide.fly")
}
}
fn sneak_caption(f: &Facts) -> Option<&'static str> {
if f.riding {
Some("horizon.controller.guide.dismount")
} else if f.flying {
Some("horizon.controller.guide.fly_down")
} else if f.gliding {
None
} else if f.in_water && !f.on_ground {
Some("horizon.controller.guide.swim_down")
} else if f.sneak_latched {
Some("horizon.controller.guide.stand")
} else {
Some("key.sneak")
}
}
/// `Minecraft.handleKeybinds` spends attack clicks while an item is in use, and
/// `startAttack` does nothing with busy hands.
fn attack_caption(f: &Facts) -> Option<&'static str> {
if f.using.is_some() || f.hands_busy {
return None;
}
match f.target {
Target::Entity {
attackable: true, ..
} if f.spectator => Some("horizon.controller.guide.spectate"),
Target::Entity {
attackable: true, ..
} => Some("horizon.controller.guide.attack"),
Target::Block {
breakable: true, ..
} => Some("horizon.controller.guide.break"),
_ => None,
}
}
/// `Minecraft.startUseItem`: what is aimed at first, then the item.
fn use_caption(f: &Facts) -> Option<&'static str> {
match f.using {
Some(Active::Shoot) => return Some("horizon.controller.guide.release_shoot"),
Some(Active::Throw) => return Some("horizon.controller.guide.release_throw"),
Some(Active::Stop) => return Some("horizon.controller.guide.release_stop"),
None => {}
}
if f.hands_busy {
return None;
}
let takes = match f.target {
Target::Block { takes, .. } | Target::Entity { takes, .. } => takes,
Target::None => None,
};
takes.or(f.item_use).map(Verb::caption)
}
fn pick_caption(f: &Facts) -> Option<&'static str> {
if f.spectator {
// key.spectatorHotbar is this button: it opens the teleport menu.
return Some("horizon.controller.guide.spectator_menu");
}
(f.can_pick && f.using.is_none()).then_some("key.pickItem")
}
/// What the crosshair is on in an inventory.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Under {
#[default]
Nothing,
/// A slot. `same` is the carried stack being its kind; `result` a crafting result, which is
/// only ever taken whole.
Slot {
item: bool,
same: bool,
result: bool,
/// The carried stack can go here.
accepts: bool,
},
Recipe {
direct_craft: bool,
alternatives: bool,
},
/// One of the recipe book's other controls.
Control,
}
/// What an inventory's rows are resolved from.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct MenuFacts {
pub under: Under,
pub carrying: bool,
/// The recipe book is showing: the bumpers turn its pages, the triggers its categories.
pub book: bool,
/// The bumpers switch tabs.
pub tabs: bool,
}
/// An inventory's rows, by what they ask of the player: at the bottom right one face button, at the
/// bottom left anything else (a trigger held with a button, the bumpers, View, a stick click). What
/// the crosshair is on comes first in its column, the screen's own rows after, except Close, which
/// stays lowest so the rest don't move it. Menu buttons aren't rebindable, so these name them.
pub fn resolve_menu(f: &MenuFacts) -> Guide {
use Button::{East, LeftBumper, LeftStick, LeftTrigger, North, RightBumper, RightTrigger};
use Button::{Select, South, West};
const TAKE: &str = "horizon.controller.guide.take";
const MOVE: &str = "horizon.controller.guide.quick_move";
let under = match f.under {
// A result takes only into empty hands or the same stack.
Under::Slot {
item: true,
result: true,
same,
..
} if !f.carrying || same => vec![Row::new(South, TAKE), Row::new(North, MOVE)],
Under::Slot { result: true, .. } => Vec::new(),
Under::Slot { accepts: false, .. } if f.carrying => Vec::new(),
Under::Slot { item, same, .. } if f.carrying => vec![
Row::new(
South,
if item && !same {
"horizon.controller.guide.swap_stack"
} else {
"horizon.controller.guide.put"
},
),
Row::new(West, "horizon.controller.guide.put_one"),
],
Under::Slot { item: true, .. } => vec![
Row::new(South, TAKE),
Row::new(West, "horizon.controller.guide.take_half"),
Row::new(North, MOVE),
Row::new(Select, "horizon.controller.guide.drop"),
Row::new(LeftStick, "horizon.controller.guide.drop_stack"),
],
Under::Nothing | Under::Slot { .. } => Vec::new(),
Under::Recipe {
direct_craft,
alternatives,
} => [
Some(Row::new(South, "horizon.controller.guide.place_recipe")),
direct_craft.then_some(Row::pair(
LeftTrigger,
South,
"horizon.controller.direct_craft",
)),
Some(Row::new(North, "horizon.controller.guide.place_recipe_all")),
alternatives.then_some(Row::new(West, "horizon.controller.guide.alternatives")),
]
.into_iter()
.flatten()
.collect(),
Under::Control => vec![Row::new(South, "horizon.controller.guide.select")],
};
let screen = [
f.book.then_some(Row::pair(
LeftBumper,
RightBumper,
"horizon.controller.guide.pages",
)),
f.book.then_some(Row::pair(
LeftTrigger,
RightTrigger,
"horizon.controller.guide.categories",
)),
(f.tabs && !f.book).then_some(Row::pair(
LeftBumper,
RightBumper,
"horizon.controller.guide.tabs",
)),
];
let close = Row::new(East, "horizon.controller.guide.close");
let rows = std::iter::once(close)
.chain(under)
.chain(screen.into_iter().flatten());
let (right, left) =
rows.partition(|r| r.with.is_none() && matches!(r.button, South | East | West | North));
Guide { left, right }
}
/// A row's height and the view's margin, in GUI pixels.
pub const ROW_HEIGHT: f32 = 11.0;
const MARGIN: f32 = 2.0;
fn overlap(a: &Rect, b: &Rect) -> bool {
a[0] < b[0] + b[2] && b[0] < a[0] + a[2] && a[1] < b[1] + b[3] && b[1] < a[1] + a[3]
}
/// Where a column's rows go, down from a top corner of the `[width, height]` view: under whatever
/// a row would overlap in `taken`, or nowhere when that leaves the view's upper three quarters (the
/// row is dropped: the rows come in the order they matter). The placed rows join `taken`.
pub fn place_column(
view: [f32; 2],
left: bool,
widths: &[f32],
taken: &mut Vec<Rect>,
) -> Vec<Option<Rect>> {
place(view, left, false, widths, taken)
}
/// `place_column`'s rows up from a bottom corner, over what they would overlap, within the view's
/// lower quarter: the first row is the lowest.
pub fn place_column_up(
view: [f32; 2],
left: bool,
widths: &[f32],
taken: &mut Vec<Rect>,
) -> Vec<Option<Rect>> {
place(view, left, true, widths, taken)
}
fn place(
view: [f32; 2],
left: bool,
up: bool,
widths: &[f32],
taken: &mut Vec<Rect>,
) -> Vec<Option<Rect>> {
let mut y = if up {
view[1] - MARGIN - ROW_HEIGHT
} else {
MARGIN
};
widths
.iter()
.map(|&width| {
if width > view[0] - 2.0 * MARGIN {
return None;
}
let x = if left {
MARGIN
} else {
view[0] - MARGIN - width
};
let mut at = y;
loop {
let outside = if up {
at < view[1] * 0.75
} else {
at + ROW_HEIGHT > view[1] * 0.75
};
if outside {
return None;
}
let rect = [x, at, width, ROW_HEIGHT];
let hit = taken.iter().filter(|t| overlap(&rect, t));
let past = if up {
hit.map(|t| t[1]).fold(f32::INFINITY, f32::min)
} else {
hit.map(|t| t[1] + t[3]).fold(f32::NEG_INFINITY, f32::max)
};
if past.is_infinite() {
taken.push(rect);
y = if up {
at - ROW_HEIGHT - 1.0
} else {
at + ROW_HEIGHT + 1.0
};
return Some(rect);
}
at = if up {
past.floor() - ROW_HEIGHT - 1.0
} else {
past.ceil() + 1.0
};
}
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn walking() -> Facts {
Facts {
on_ground: true,
can_sprint: true,
main_hand_item: true,
..Facts::default()
}
}
fn on_block(f: Facts, breakable: bool, takes: Option<Verb>) -> Facts {
Facts {
target: Target::Block { breakable, takes },
..f
}
}
fn rows(rows: &[Row]) -> Vec<(Button, &'static str)> {
rows.iter().map(|r| (r.button, r.caption)).collect()
}
fn captions(rows: &[Row]) -> Vec<&'static str> {
rows.iter().map(|r| r.caption).collect()
}
#[test]
fn survival_on_a_block_shows_break_use_and_pick_on_the_default_buttons() {
let facts = Facts {
can_pick: true,
item_use: Some(Verb::Place),
..on_block(walking(), true, None)
};
let guide = resolve(&facts, &PadSettings::default());
assert_eq!(
rows(&guide.right),
[
(Button::RightTrigger, "horizon.controller.guide.break"),
(Button::LeftTrigger, "horizon.controller.guide.place"),
(Button::DPadLeft, "key.pickItem"),
]
);
assert_eq!(
rows(&guide.left),
[
(Button::South, "key.jump"),
(Button::RightStick, "key.sneak"),
(Button::LeftStick, "key.sprint"),
(Button::North, "key.inventory"),
(Button::DPadDown, "horizon.controller.guide.drop"),
(Button::West, "horizon.controller.guide.swap"),
]
);
}
#[test]
fn a_rebound_action_moves_its_row_and_an_unbound_one_loses_it() {
let mut settings = PadSettings::default();
settings.bind("key.attack", Some(Button::North));
settings.bind("key.pickItem", None);
settings.bind("key.jump", Some(Button::Start));
let facts = Facts {
can_pick: true,
..on_block(walking(), true, None)
};
let guide = resolve(&facts, &settings);
assert_eq!(
rows(&guide.right),
[(Button::North, "horizon.controller.guide.break")]
);
// Start only ever pauses.
assert!(!captions(&guide.left).contains(&"key.jump"));
}
#[test]
fn what_a_block_or_entity_takes_comes_before_the_held_item() {
let settings = PadSettings::default();
let chest = Facts {
item_use: Some(Verb::Place),
..on_block(walking(), true, Some(Verb::Open))
};
assert_eq!(
captions(&resolve(&chest, &settings).right),
[
"horizon.controller.guide.break",
"horizon.controller.guide.open"
]
);
let villager = Facts {
item_use: Some(Verb::Eat),
target: Target::Entity {
attackable: true,
takes: Some(Verb::Trade),
},
..walking()
};
assert_eq!(
captions(&resolve(&villager, &settings).right),
[
"horizon.controller.guide.attack",
"horizon.controller.guide.trade"
]
);
// A mob the item means nothing to: the item is used on.
let cow = Facts {
target: Target::Entity {
attackable: true,
takes: None,
},
..villager
};
assert_eq!(
captions(&resolve(&cow, &settings).right)[1],
"horizon.controller.guide.eat"
);
}
#[test]
fn nothing_to_do_leaves_the_right_column_empty() {
let settings = PadSettings::default();
assert!(resolve(&walking(), &settings).right.is_empty());
// An unbreakable block too (bedrock, a creative sword, adventure without can_break).
assert!(
resolve(&on_block(walking(), false, None), &settings)
.right
.is_empty()
);
}
#[test]
fn an_item_in_use_leaves_only_letting_go() {
let facts = Facts {
using: Some(Active::Shoot),
can_pick: true,
..on_block(walking(), true, Some(Verb::Open))
};
assert_eq!(
captions(&resolve(&facts, &PadSettings::default()).right),
["horizon.controller.guide.release_shoot"]
);
}
#[test]
fn busy_hands_neither_attack_nor_use() {
let facts = Facts {
hands_busy: true,
riding: true,
item_use: Some(Verb::Eat),
..on_block(Facts::default(), true, None)
};
assert!(resolve(&facts, &PadSettings::default()).right.is_empty());
}
#[test]
fn movement_follows_what_the_player_is_doing() {
let settings = PadSettings::default();
let first_two = |f: Facts| {
let left = resolve(&f, &settings).left;
captions(&left).into_iter().take(2).collect::<Vec<_>>()
};
assert_eq!(
first_two(Facts {
flying: true,
may_fly: true,
..Facts::default()
}),
[
"horizon.controller.guide.fly_up",
"horizon.controller.guide.fly_down"
]
);
assert_eq!(
first_two(Facts {
in_water: true,
..Facts::default()
}),
[
"horizon.controller.guide.swim_up",
"horizon.controller.guide.swim_down"
]
);
// In the air, only a glider or creative flight makes a jump do anything.
assert_eq!(first_two(Facts::default()), ["key.sneak", "key.inventory"]);
assert_eq!(
first_two(Facts {
can_glide: true,
..Facts::default()
})[0],
"horizon.controller.guide.glide"
);
assert_eq!(
first_two(Facts {
sneak_latched: true,
on_ground: true,
..Facts::default()
})[1],
"horizon.controller.guide.stand"
);
// A boat can't jump; a saddled horse can; both dismount.
assert_eq!(
first_two(Facts {
riding: true,
on_ground: true,
..Facts::default()
})[0],
"horizon.controller.guide.dismount"
);
assert_eq!(
first_two(Facts {
riding: true,
vehicle_jumps: true,
..Facts::default()
}),
["key.jump", "horizon.controller.guide.dismount"]
);
// Gliding: neither jump nor sneak does anything.
assert_eq!(
first_two(Facts {
gliding: true,
..Facts::default()
}),
["key.inventory"]
);
}
#[test]
fn a_spectator_only_flies_opens_spectates_and_has_its_menu() {
let facts = Facts {
spectator: true,
flying: true,
may_fly: true,
main_hand_item: true,
target: Target::Entity {
attackable: true,
takes: None,
},
..Facts::default()
};
let guide = resolve(&facts, &PadSettings::default());
assert_eq!(
captions(&guide.right),
[
"horizon.controller.guide.spectate",
"horizon.controller.guide.spectator_menu"
]
);
assert_eq!(
captions(&guide.left),
[
"horizon.controller.guide.fly_up",
"horizon.controller.guide.fly_down"
]
);
}
#[test]
fn rows_stack_from_their_corner() {
let mut taken = Vec::new();
let left = place_column([200.0, 200.0], true, &[50.0, 30.0], &mut taken);
assert_eq!(left[0], Some([2.0, 2.0, 50.0, 11.0]));
assert_eq!(left[1], Some([2.0, 14.0, 30.0, 11.0]));
let right = place_column([200.0, 200.0], false, &[40.0], &mut taken);
assert_eq!(right[0], Some([158.0, 2.0, 40.0, 11.0]));
}
#[test]
fn a_row_goes_under_what_it_would_overlap() {
// The effect icons in the top right corner.
let mut taken = vec![[150.0, 1.0, 50.0, 24.0]];
let right = place_column([200.0, 200.0], false, &[40.0, 40.0], &mut taken);
assert_eq!(right[0], Some([158.0, 26.0, 40.0, 11.0]));
assert_eq!(right[1], Some([158.0, 38.0, 40.0, 11.0]));
}
#[test]
fn the_columns_of_a_narrow_view_never_overlap_and_late_rows_drop() {
// One quarter of a small window, a boss bar across its top: the rows have 31..67 left.
let view = [160.0, 90.0];
let bar = [0.0, 12.0, 160.0, 19.0];
let mut taken = vec![bar];
let right = place_column(view, false, &[90.0, 90.0, 90.0], &mut taken);
let left = place_column(view, true, &[90.0; 6], &mut taken);
let placed: Vec<Rect> = right.iter().chain(&left).flatten().copied().collect();
for (i, a) in placed.iter().enumerate() {
assert!(a[1] + a[3] <= 67.5 && a[0] >= 0.0 && a[0] + a[2] <= 160.0);
assert!(!overlap(a, &bar));
for b in &placed[i + 1..] {
assert!(!overlap(a, b), "{a:?} overlaps {b:?}");
}
}
// Three rows' worth of room, and the right column came first.
assert_eq!(right.iter().flatten().count(), 3);
assert_eq!(left.iter().flatten().count(), 0);
}
#[test]
fn rows_stack_up_from_a_bottom_corner_and_drop_above_its_quarter() {
let view = [200.0, 100.0];
let mut taken = vec![[0.0, 80.0, 60.0, 5.0]];
let left = place_column_up(view, true, &[50.0, 50.0], &mut taken);
assert_eq!(left[0], Some([2.0, 87.0, 50.0, 11.0]));
// Over the panel at 80 would be 68, under the lower quarter's 75.
assert_eq!(left[1], None);
let right = place_column_up(view, false, &[40.0], &mut taken);
assert_eq!(right[0], Some([158.0, 87.0, 40.0, 11.0]));
}
fn short(rows: &[Row]) -> Vec<&str> {
rows.iter()
.map(|r| r.caption.rsplit('.').next().unwrap_or(""))
.collect()
}
#[test]
fn an_inventorys_face_buttons_go_right_and_the_rest_left() {
let on = |under, carrying| {
resolve_menu(&MenuFacts {
under,
carrying,
..MenuFacts::default()
})
};
let slot = |item, same, result| Under::Slot {
item,
same,
result,
accepts: true,
};
let close_only = ["close"];
let empty: [&str; 0] = [];
for quiet in [Under::Nothing, slot(false, false, false)] {
let guide = on(quiet, false);
assert_eq!(short(&guide.right), close_only);
assert_eq!(short(&guide.left), empty);
}
let item = on(slot(true, false, false), false);
assert_eq!(
short(&item.right),
["close", "take", "take_half", "quick_move"]
);
assert_eq!(short(&item.left), ["drop", "drop_stack"]);
assert_eq!(
short(&on(slot(false, false, false), true).right),
["close", "put", "put_one"]
);
assert_eq!(
short(&on(slot(true, false, false), true).right),
["close", "swap_stack", "put_one"]
);
assert_eq!(
short(&on(slot(true, true, false), true).right),
["close", "put", "put_one"]
);
assert_eq!(
short(&on(slot(true, false, true), false).right),
["close", "take", "quick_move"]
);
let recipe = on(
Under::Recipe {
direct_craft: true,
alternatives: true,
},
false,
);
assert_eq!(
short(&recipe.right),
["close", "place_recipe", "place_recipe_all", "alternatives"]
);
assert_eq!(recipe.left.len(), 1);
assert_eq!(recipe.left[0].with, Some(Button::LeftTrigger));
assert_eq!(recipe.left[0].button, Button::South);
let plain = on(
Under::Recipe {
direct_craft: false,
alternatives: false,
},
false,
);
assert_eq!(
short(&plain.right),
["close", "place_recipe", "place_recipe_all"]
);
assert!(plain.left.is_empty());
}
#[test]
fn an_inventorys_screen_rows_follow_its_book_and_tabs() {
let left = |book, tabs| {
resolve_menu(&MenuFacts {
book,
tabs,
..MenuFacts::default()
})
.left
};
assert!(left(false, false).is_empty());
assert_eq!(short(&left(true, false)), ["pages", "categories"]);
assert_eq!(short(&left(false, true)), ["tabs"]);
}
#[test]
fn a_row_wider_than_the_view_is_dropped() {
let mut taken = Vec::new();
assert_eq!(
place_column([50.0, 200.0], true, &[60.0, 20.0], &mut taken),
[None, Some([2.0, 2.0, 20.0, 11.0])]
);
}
}
#[cfg(test)]
mod detail_tests {
use super::*;
fn rows() -> Guide {
Guide {
left: vec![Row::new(Button::South, "left")],
right: vec![Row::new(Button::East, "right")],
}
}
#[test]
fn full_keeps_both_columns() {
let guide = rows().at_detail(GuideDetail::Full);
assert_eq!((guide.left.len(), guide.right.len()), (1, 1));
}
#[test]
fn basic_keeps_the_right_column() {
let guide = rows().at_detail(GuideDetail::Basic);
assert_eq!((guide.left.len(), guide.right.len()), (0, 1));
}
#[test]
fn off_keeps_nothing() {
let guide = rows().at_detail(GuideDetail::Off);
assert_eq!((guide.left.len(), guide.right.len()), (0, 0));
}
#[test]
fn a_file_saved_with_the_old_switch_keeps_its_meaning() {
assert_eq!(GuideDetail::parse("true"), GuideDetail::Full);
assert_eq!(GuideDetail::parse("false"), GuideDetail::Off);
assert_eq!(GuideDetail::parse("basic"), GuideDetail::Basic);
}
}
#[cfg(test)]
mod slot_tests {
use super::*;
/// The rows for the slot: South or West, whichever column they're in.
fn rows(item: bool, same: bool, result: bool, accepts: bool, carrying: bool) -> usize {
let guide = resolve_menu(&MenuFacts {
under: Under::Slot {
item,
same,
result,
accepts,
},
carrying,
..MenuFacts::default()
});
[guide.left, guide.right]
.iter()
.flatten()
.filter(|row| matches!(row.button, Button::South | Button::West))
.count()
}
#[test]
fn a_slot_that_refuses_the_carried_stack_offers_nothing() {
assert_eq!(rows(false, false, false, false, true), 0);
assert_eq!(rows(true, true, false, false, true), 0);
assert!(rows(false, false, false, true, true) > 0);
}
#[test]
fn a_result_is_taken_only_into_empty_hands_or_the_same_stack() {
assert!(rows(true, false, true, true, false) > 0);
assert!(rows(true, true, true, true, true) > 0);
assert_eq!(rows(true, false, true, true, true), 0);
}
}
+32 -5
View File
@@ -3,15 +3,18 @@
//! device letting go (splitscreen moving the keyboard to another player, say)
//! can't release what another still presses.
use crate::actions::Action;
use std::collections::HashMap;
use winit::keyboard::KeyCode;
/// Where a press came from.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Source {
/// The keyboard and mouse, and the scripting socket acting as them.
/// The keyboard and mouse.
Keyboard,
Pad,
/// `horizon-ctl`'s `key` and `mouse` and the agent commands that walk and click.
Agent,
}
impl Source {
@@ -19,6 +22,7 @@ impl Source {
match self {
Source::Keyboard => 1,
Source::Pad => 2,
Source::Agent => 4,
}
}
}
@@ -29,9 +33,20 @@ pub struct HeldKeys {
}
impl HeldKeys {
pub fn contains(&self, key: &KeyCode) -> bool {
/// Whether anything holds this mapping: every device, whatever the mapping is bound to.
pub fn held(&self, action: Action) -> bool {
action.key().is_some_and(|key| self.raw(&key))
}
/// Whether a key that is no mapping's default is held (Right Shift, the arrows, Alt, the system
/// key), or the key a caller already holds as a key: most code wants [`HeldKeys::held`].
pub fn raw(&self, key: &KeyCode) -> bool {
self.held.contains_key(key)
}
/// The old way of asking, by a mapping's default key; clippy refuses it (`clippy.toml`). It
/// stays so that a change written against it says what to use instead.
pub fn contains(&self, key: &KeyCode) -> bool {
self.raw(key)
}
/// `source` holds `key`. True when nothing held it before.
pub fn press(&mut self, key: KeyCode, source: Source) -> bool {
let sources = self.held.entry(key).or_insert(0);
@@ -72,7 +87,7 @@ impl HeldKeys {
/// Held, by the keyboard unless a device already holds it: for callers
/// that don't say where the press came from.
pub fn insert(&mut self, key: KeyCode) -> bool {
if self.contains(&key) {
if self.raw(&key) {
return false;
}
self.press(key, Source::Keyboard)
@@ -132,13 +147,25 @@ mod tests {
assert!(!keys.press(KeyCode::Space, Source::Keyboard));
assert!(keys.press(KeyCode::KeyW, Source::Keyboard));
assert_eq!(keys.release_source(Source::Keyboard), vec![KeyCode::KeyW]);
assert!(keys.contains(&KeyCode::Space) && !keys.contains(&KeyCode::KeyW));
assert!(keys.held(Action::Jump) && !keys.held(Action::Forward));
assert!(keys.held_by(&KeyCode::Space, Source::Pad));
assert!(!keys.held_by(&KeyCode::Space, Source::Keyboard));
assert!(keys.release(&KeyCode::Space, Source::Pad));
assert!(keys.is_empty());
}
#[test]
fn a_script_holds_a_key_by_itself() {
let mut keys = HeldKeys::default();
keys.press(KeyCode::KeyW, Source::Agent);
keys.press(KeyCode::KeyW, Source::Keyboard);
assert!(!keys.release(&KeyCode::KeyW, Source::Keyboard));
assert!(keys.held(Action::Forward));
assert!(keys.release_source(Source::Pad).is_empty());
assert_eq!(keys.release_source(Source::Agent), vec![KeyCode::KeyW]);
assert!(!keys.held(Action::Forward));
}
#[test]
fn untagged_inserts_and_removes_act_as_before() {
let mut keys = HeldKeys::default();
@@ -149,7 +176,7 @@ mod tests {
keys.remove(&KeyCode::ShiftLeft),
"remove lets go for everyone"
);
assert!(!keys.contains(&KeyCode::ShiftLeft));
assert!(!keys.held(Action::Sneak));
assert!(keys.release(&KeyCode::KeyA, Source::Keyboard), "never held");
}
}
@@ -51,11 +51,6 @@ impl InteractionInput {
pub fn press(&mut self, button: Button) {
self.clicks[button as usize] += 1;
}
/// Whether a click waits for the next tick.
pub fn has_clicks(&self) -> bool {
self.clicks.iter().any(|&c| c > 0)
}
pub fn tick(&mut self, world: &mut impl InteractionWorld, attack_down: bool, use_down: bool) {
let [attack, use_item, pick] = std::mem::take(&mut self.clicks);
world.pick_hit_result();
+59 -16
View File
@@ -23,8 +23,6 @@ pub const MAPPINGS: &[(&str, &str, &str)] = &[
("key.pickItem", "key.mouse.middle", "gameplay"),
("key.command", "key.keyboard.slash", "multiplayer"),
("key.friends", "key.keyboard.o", "multiplayer"),
// Horizon's emote wheel (docs/cosmetics.md), on a key vanilla leaves free.
("key.emote", "key.keyboard.b", "multiplayer"),
("key.socialInteractions", "key.keyboard.p", "multiplayer"),
("key.toggleGui", "key.keyboard.f1", "misc"),
(
@@ -76,6 +74,17 @@ pub const MAPPINGS: &[(&str, &str, &str)] = &[
("key.debug.lightmapTexture", "key.keyboard.4", "debug"),
("key.debug.improvedTransparency", "key.keyboard.x", "debug"),
];
/// Horizon's own key mappings: bound in Controls like the vanilla ones, but kept in
/// `horizon-extras.txt` (`emoteKey`), never in `options.txt`, which stays exactly vanilla's
/// (`MAPPINGS` is what the options parity harness compares with the pinned client).
pub const HORIZON_MAPPINGS: &[(&str, &str, &str)] = &[
// The emote wheel (docs/cosmetics.md), on a key vanilla leaves free.
("key.emote", "key.keyboard.b", "multiplayer"),
];
/// Every mapping the game binds: vanilla's in `Options.keyMappings` order, then Horizon's.
pub fn all_mappings() -> impl Iterator<Item = &'static (&'static str, &'static str, &'static str)> {
MAPPINGS.iter().chain(HORIZON_MAPPINGS)
}
/// Mappings Options builds as `ToggleKeyMapping` (hold or toggle by option).
pub fn is_toggle(name: &str) -> bool {
matches!(name, "key.sneak" | "key.sprint" | "key.use" | "key.attack")
@@ -314,8 +323,7 @@ pub struct KeyBinds {
impl Default for KeyBinds {
fn default() -> Self {
Self {
keys: MAPPINGS
.iter()
keys: all_mappings()
.map(|(n, d, _)| (*n, (*d).to_owned()))
.collect(),
}
@@ -341,8 +349,7 @@ impl KeyBinds {
.map_or("key.keyboard.unknown", String::as_str)
}
pub fn default_of(name: &str) -> &'static str {
MAPPINGS
.iter()
all_mappings()
.find(|m| m.0 == name)
.map_or("key.keyboard.unknown", |m| m.1)
}
@@ -350,16 +357,24 @@ impl KeyBinds {
self.get(name) == Self::default_of(name)
}
pub fn set(&mut self, name: &str, key: &str) {
if let Some((n, _, _)) = MAPPINGS.iter().find(|m| m.0 == name) {
if let Some((n, _, _)) = all_mappings().find(|m| m.0 == name) {
self.keys.insert(n, key.to_owned());
}
}
/// The bindings `options.txt` holds: vanilla's mappings only.
pub fn saved(&self) -> Vec<(&'static str, String)> {
MAPPINGS
.iter()
.map(|(n, _, _)| (*n, self.get(n).to_owned()))
.collect()
}
/// Horizon's own bindings, for `horizon-extras.txt`.
pub fn horizon_saved(&self) -> Vec<(&'static str, String)> {
HORIZON_MAPPINGS
.iter()
.map(|(n, _, _)| (*n, self.get(n).to_owned()))
.collect()
}
pub fn reset_all(&mut self) {
*self = Self::default();
}
@@ -369,8 +384,7 @@ impl KeyBinds {
if key == "key.keyboard.unknown" {
return Vec::new();
}
MAPPINGS
.iter()
all_mappings()
.filter(|(n, _, _)| {
*n != name && self.get(n) == key && (!self.is_default(n) || !self.is_default(name))
})
@@ -384,8 +398,7 @@ impl KeyBinds {
/// Every mapping a physical input triggers, in list order: like
/// `KeyMapping.set`/`click`, one key drives all the mappings bound to it.
pub fn mappings_for<'a>(&'a self, key: &'a str) -> impl Iterator<Item = &'static str> + 'a {
MAPPINGS
.iter()
all_mappings()
.map(|m| m.0)
.filter(move |n| self.get(n) == key)
}
@@ -395,8 +408,7 @@ impl KeyBinds {
/// key and the binding itself); their defaults (A, S, D, T, L, 1 to 4...)
/// would otherwise turn the key back into its old gameplay meaning.
fn translated_mappings<'a>(&'a self, key: &'a str) -> impl Iterator<Item = &'static str> + 'a {
MAPPINGS
.iter()
all_mappings()
.filter(|m| m.2 != "debug")
.map(|m| m.0)
.filter(move |n| self.get(n) == key)
@@ -404,9 +416,7 @@ impl KeyBinds {
/// Whether a mapping (other than a debug key) whose default is `key` was
/// bound elsewhere.
fn default_moved(&self, key: &str) -> bool {
MAPPINGS
.iter()
.any(|(n, d, c)| *d == key && *c != "debug" && !self.is_default(n))
all_mappings().any(|(n, d, c)| *d == key && *c != "debug" && !self.is_default(n))
}
/// Translate a physical key into the default key of the mapping it is
/// bound to, so game code written against vanilla defaults follows the
@@ -442,6 +452,18 @@ impl KeyBinds {
}
vec![Ok(key)]
}
/// `Minecraft.handleKeybinds`: key.pickItem and key.spectatorHotbar share the middle button by
/// default and are separate mappings, so a press of what `input` is bound to acts only as the
/// one this player can use: the spectator hotbar for a spectator, pick block otherwise. (Pick
/// block does nothing a spectator would see.)
pub fn middle_acts(&self, input: &str, spectator: bool) -> bool {
let mapping = if spectator {
"key.spectatorHotbar"
} else {
"key.pickItem"
};
self.get(mapping) == input
}
/// Translate a mouse button the same way (keys bound to mouse buttons).
pub fn translate_mouse(&self, button: MouseButton) -> Option<Result<KeyCode, MouseButton>> {
self.translate_mouse_all(button).into_iter().next()
@@ -505,6 +527,27 @@ impl KeyBinds {
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mappings_sharing_the_middle_button_act_as_themselves() {
let middle = "key.mouse.middle";
let z = "key.keyboard.z";
let mut keys = KeyBinds::default();
assert!(keys.middle_acts(middle, false) && keys.middle_acts(middle, true));
// The spectator hotbar on Z: the middle button only picks, and Z never picks.
keys.set("key.spectatorHotbar", z);
assert!(keys.middle_acts(middle, false));
assert!(!keys.middle_acts(middle, true));
assert!(keys.middle_acts(z, true));
assert!(!keys.middle_acts(z, false));
// Pick block on Z instead: the other way round.
keys.reset_all();
keys.set("key.pickItem", z);
assert!(keys.middle_acts(z, false));
assert!(!keys.middle_acts(z, true));
assert!(keys.middle_acts(middle, true));
assert!(!keys.middle_acts(middle, false));
}
#[test]
fn container_mouse_hotkeys_follow_slot_bindings_and_pick_item_priority() {
for button in [MouseButton::Back, MouseButton::Forward, MouseButton::Middle] {
+2
View File
@@ -3,8 +3,10 @@
//! options. horizon-client re-exports each module under its old path
//! (`horizon_client::keybinds`, ...).
#![forbid(unsafe_code)]
pub mod actions;
pub mod clipboard;
pub mod gamepad;
pub mod gameplay_guide;
pub mod held;
pub mod input;
pub mod interaction_input;
+57 -15
View File
@@ -395,16 +395,14 @@ impl FullscreenState {
let mode_change = !in_video_screen && self.take_mode_change();
match request {
Some(false) => window.set_fullscreen(None),
Some(true) => window.set_fullscreen(Some(fullscreen_request(
Some(true) => enter_fullscreen(
window,
exclusive,
preferred.as_ref(),
))),
None if reapply || mode_change => window.set_fullscreen(Some(fullscreen_request(
fullscreen_request(window, exclusive, preferred.as_ref()),
),
None if reapply || mode_change => enter_fullscreen(
window,
exclusive,
preferred.as_ref(),
))),
fullscreen_request(window, exclusive, preferred.as_ref()),
),
None => {}
}
}
@@ -583,6 +581,34 @@ fn monitor_current_mode(
.max_by_key(|m| (m.refresh_rate_millihertz(), m.bit_depth()))
}
/// `window.set_fullscreen(Some(fullscreen))`. On Windows a maximized window leaves its maximized
/// state first: winit keeps a maximized window's client area inside the work area, so a
/// borderless fullscreen kept the taskbar over its bottom and pushed its top off screen (#9984).
pub fn enter_fullscreen(window: &winit::window::Window, fullscreen: winit::window::Fullscreen) {
#[cfg(target_os = "windows")]
if window.is_maximized() {
window.set_maximized(false);
}
window.set_fullscreen(Some(fullscreen));
}
/// Borderless fullscreen on the window's own monitor (the primary one when it has none).
pub fn borderless_fullscreen(window: &winit::window::Window) -> winit::window::Fullscreen {
winit::window::Fullscreen::Borderless(
window
.current_monitor()
.or_else(|| window.primary_monitor()),
)
}
/// A window created fullscreen that the system also marked maximized enters it again
/// unmaximized ([`enter_fullscreen`]); only Windows does that.
pub fn settle_created_fullscreen(window: &winit::window::Window, fullscreen: bool) {
if cfg!(target_os = "windows") && fullscreen && window.is_maximized() {
enter_fullscreen(window, borderless_fullscreen(window));
}
}
/// `Window.applyFullscreen`: the fullscreen the window asks for. Exclusive fullscreen takes
/// `Monitor.getPreferredVideoMode` (the preferred mode if the monitor has it, else the desktop
/// mode); a monitor with no usable mode falls back to borderless, as vanilla does.
@@ -592,13 +618,13 @@ pub fn fullscreen_request(
preferred: Option<&VideoMode>,
) -> winit::window::Fullscreen {
use winit::window::Fullscreen;
if !exclusive {
return Fullscreen::Borderless(None);
}
let Some(monitor) = window
let monitor = window
.current_monitor()
.or_else(|| window.primary_monitor())
else {
.or_else(|| window.primary_monitor());
if !exclusive {
return Fullscreen::Borderless(monitor);
}
let Some(monitor) = monitor else {
return Fullscreen::Borderless(None);
};
let wanted = preferred.and_then(|p| {
@@ -608,7 +634,7 @@ pub fn fullscreen_request(
});
match wanted.or_else(|| monitor_current_mode(&monitor)) {
Some(mode) => Fullscreen::Exclusive(mode),
None => Fullscreen::Borderless(None),
None => Fullscreen::Borderless(Some(monitor)),
}
}
@@ -628,6 +654,22 @@ pub fn icon() -> Option<winit::window::Icon> {
winit::window::Icon::from_rgba(bytes, info.width, info.height).ok()
}
/// Apply Horizon's icon to every window. Windows has a separate taskbar icon slot from the small
/// window icon; requiring the decoded icon there prevents a generic taskbar icon from appearing.
pub fn with_horizon_icon(
attributes: winit::window::WindowAttributes,
) -> winit::window::WindowAttributes {
let icon = icon();
#[cfg(target_os = "windows")]
let icon = Some(icon.expect("Horizon's icon must be available on Windows"));
let attributes = attributes.with_window_icon(icon.clone());
#[cfg(target_os = "windows")]
let attributes =
winit::platform::windows::WindowAttributesExtWindows::with_taskbar_icon(attributes, icon);
attributes
}
#[cfg(test)]
mod tests {
use super::*;
+13 -5
View File
@@ -6,7 +6,7 @@ autotests = false
version = "0.1.0"
edition.workspace = true
license.workspace = true
rust-version = "1.90"
rust-version.workspace = true
description = "Reusable client systems, control tool and parity probes"
[lib]
@@ -42,7 +42,8 @@ horizon-chat-filter = { path = "../horizon-chat-filter" }
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" }
# Play with Friends: invite codes and their tunnel; the `friends` feature.
horizon-friends = { path = "../horizon-friends", optional = true }
# The narrator (GameNarrator): text-to-speech of chat, system messages and screens.
horizon-narrator = { path = "../horizon-narrator" }
# Horizon mods: sandboxed WebAssembly components, off unless installed and turned on (docs/mods.md).
@@ -73,9 +74,10 @@ mio = { version = "1.2.4", features = ["os-poll", "net"] }
xxhash-rust = { version = "0.8.19", features = ["xxh3"] }
horizon-theme = { path = "../horizon-theme" }
# 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"] }
# the anonymous "I'm playing" ping the game sends itself (`heartbeat.rs`), the `heartbeat` feature.
horizon-telemetry = { path = "../horizon-telemetry", default-features = false }
horizon-bench = { path = "../horizon-bench" }
crc32fast = "1"
zip = { version = "9.1.3", default-features = false, features = ["deflate"] }
# Controllers (evdev + libudev on Linux, IOKit on macOS, Windows.Gaming.Input); the `controllers` feature.
gilrs = { version = "0.11", optional = true }
@@ -112,12 +114,18 @@ objc2-foundation = { version = "0.3", default-features = false, features = ["std
wgpu = { version = "30.0.1", default-features = false, features = ["gles"] }
[features]
default = ["audio", "controllers"]
default = ["audio", "controllers", "discord", "friends", "heartbeat"]
# 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 = ["horizon-client-render/audio"]
# Controllers: gilrs, and the DualSense's own output. Without it only `horizon-ctl pad`'s virtual pad.
controllers = ["dep:gilrs", "dep:horizon-dualsense", "horizon-client-input/controllers"]
# "Playing Horizon" on the player's Discord profile (`discord_presence`).
discord = []
# Play with Friends, joining side (`net.rs`): without it no invite code joins.
friends = ["dep:horizon-friends"]
# The anonymous "I'm playing" ping (`heartbeat.rs`, docs/telemetry.md).
heartbeat = ["horizon-telemetry/live"]
# Records the call sites of mob random draws (`mob_ai_probe` traces).
rng-trace = ["horizon-core/rng-trace"]
# Compatibility with earlier experiment commands: main already uses mimalloc.
+2 -2
View File
@@ -3,7 +3,7 @@ name = "horizon-client-render"
version = "0.1.0"
edition.workspace = true
license.workspace = true
rust-version = "1.90"
rust-version.workspace = true
description = "The client's world data, rendering and screens: chunks, meshing, light, streaming, the renderer, the HUD, menus and settings; no gameplay"
[dependencies]
@@ -26,7 +26,7 @@ horizon-optifine = { path = "../horizon-optifine" }
horizon-protocol = { path = "../horizon-protocol" }
horizon-shader-api = { path = "../horizon-shader-api" }
horizon-shaders = { path = "../horizon-shaders" }
horizon-telemetry = { path = "../horizon-telemetry", default-features = false, features = ["live"] }
horizon-telemetry = { path = "../horizon-telemetry", default-features = false }
horizon-theme = { path = "../horizon-theme" }
horizon-threads = { path = "../horizon-threads" }
horizon-world = { path = "../horizon-world" }
@@ -0,0 +1,55 @@
//! Headless production brewing-fuel component fixture.
use horizon_client_render::{
brewing::{Brewing, BrewingStand, brewing_fuel},
providers::Providers,
};
use horizon_items::{
item_data::decode_components,
items::{ItemStack, Items},
};
use horizon_world::loot::PackOverlay;
use std::{
io::{BufRead, Write},
path::Path,
};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<_> = std::env::args().collect();
let root = Path::new(&args[1]);
let items = Items::catalog(root, &PackOverlay::NONE)?;
let providers = Providers::load(root);
let brewing = Brewing {
recipes: vec![],
reagents: Default::default(),
inputs: Default::default(),
};
let mut out = std::io::BufWriter::new(std::fs::File::create(&args[3])?);
for line in std::io::BufReader::new(std::fs::File::open(&args[2])?).lines() {
let input: serde_json::Value = serde_json::from_str(&line?)?;
let mut stack = ItemStack::new(
input["item"].as_str().unwrap(),
input["count"].as_u64().unwrap() as u32,
);
let components = horizon_client_core::scoreboard::json_to_tag(&input["components"]);
let (_, extra) = decode_components(Some(&components));
stack.extra = extra;
let accepted = brewing_fuel(&items, &stack).is_some();
let mut stand = BrewingStand::new();
// A newly constructed vanilla block entity has no saved total yet.
stand.total_fuel = 0;
stand.items[4] = Some(stack);
for _ in 0..2 {
stand.tick(&brewing, &items, &providers);
writeln!(
out,
"{} {} {} {} {}",
accepted,
stand.fuel,
stand.total_fuel,
stand.speed.to_bits(),
stand.items[4].as_ref().map_or(0, |s| s.count)
)?;
}
}
Ok(())
}
@@ -66,6 +66,8 @@ fn fixture(window: Arc<Window>) -> Result<(), Box<dyn std::error::Error>> {
false,
false,
true,
false,
false,
))?;
assert!(!renderer.pack_entity_shadows(), "no selected pack");
renderer.set_shader_pack(0, None, &[]).unwrap();
@@ -224,6 +224,8 @@ fn replay(window: Arc<Window>) -> Result<(), Box<dyn std::error::Error>> {
settings.translucent_batching,
false,
false,
false,
false,
))?;
drop(pixels);
render.pixels.pack();
@@ -243,6 +245,7 @@ fn replay(window: Arc<Window>) -> Result<(), Box<dyn std::error::Error>> {
settings.fxaa,
settings.taa,
settings.upscale,
0.85,
);
let mut notes = Vec::new();
let mut note = |what: &str, result: horizon_client_render::Result<()>| {
@@ -126,6 +126,8 @@ fn fixture(window: Arc<Window>) -> Result<(), Box<dyn std::error::Error>> {
false,
false,
true,
false,
false,
))?;
renderer.resize(320, 180);
assert!(
@@ -348,6 +350,7 @@ fn fixture_frame(hud: &HudBatch) -> Frame<'_> {
skybox: false,
end_sky: false,
end_sky_sprite: None,
boss_world_fog: false,
environment_fog: None,
water_fog: [0.0; 3],
fog: None,
@@ -368,6 +371,7 @@ fn fixture_frame(hud: &HudBatch) -> Frame<'_> {
visible_static_block_entity_sections: &[],
static_block_entity_stats: Default::default(),
entity_motion: &[],
entity_motion_posed: &[],
entity_spans: &[],
skin: None,
glowing: &[],
+3 -12
View File
@@ -6,13 +6,7 @@ use horizon_world::{
worldgen::{tags::TagDefinitions, terrain::TerrainBlockStates},
};
use serde_json::Value;
use std::{
collections::BTreeSet,
fs::File,
io::{self, BufReader},
path::Path,
sync::Arc,
};
use std::{collections::BTreeSet, io, path::Path, sync::Arc};
pub const MATERIALS: [&str; 6] = ["bedrock", "dirt", "stone", "sand", "oak_planks", "bricks"];
#[derive(Clone, Copy)]
@@ -190,15 +184,12 @@ fn invalid(message: &str) -> io::Error {
}
impl Assets {
pub fn load(root: &Path) -> Result<Self> {
let manifest: Value = serde_json::from_reader(BufReader::new(File::open(
root.join("horizon-resources.json"),
)?))?;
let manifest: Value = horizon_world::json_file::read(&root.join("horizon-resources.json"))?;
if manifest["data_version"].as_i64() != Some(i64::from(DATA_VERSION)) {
return Err(invalid("resources do not match Horizon's pinned data version").into());
}
let (registry, flags) = horizon_world::level::data::load_registry_with_flags(root)?;
let bundle: Value =
serde_json::from_reader(BufReader::new(File::open(root.join("block-tags.json"))?))?;
let bundle: Value = horizon_world::json_file::read(&root.join("block-tags.json"))?;
let mut definitions = TagDefinitions::default();
for (name, text) in bundle
.as_object()
+535 -71
View File
@@ -2,7 +2,9 @@
//! small mixer standing in for OpenAL: one channel per source with its gain,
//! pitch, linear distance attenuation (`AL_LINEAR_DISTANCE`, reference 0,
//! rolloff 1), looping, relative flag and source state, Ogg/Vorbis decoding
//! (streams on a worker thread) and OpenAL Soft's stereo panning.
//! (streams on a worker thread) and OpenAL Soft's mixing: mono sources panned
//! and attenuated, multi-channel buffers (stereo music and records) played
//! direct as OpenAL does.
use crate::Result;
use crate::sound_engine::{
Attenuation, Backend, Bound, ChannelId, InstanceId, PlayResult, SoundEngine, SoundInstance,
@@ -29,7 +31,7 @@ enum State {
}
/// An OpenAL source.
struct Channel {
samples: Option<(Arc<Vec<f32>>, u32)>,
samples: Option<(Arc<Vec<f32>>, u32, u16)>,
position: f64,
state: State,
gain: f32,
@@ -45,6 +47,53 @@ struct Channel {
world: u32,
/// The sound file it started and when, to tell another player's copy of it.
started: Option<(String, std::time::Instant)>,
/// Its sound event plays clearly through a mod's muffle (`Audio::muffle`).
unmuffled: bool,
}
/// The cutoff of the muffle's low-pass at full strength: the world heard through ringing ears.
const MUFFLED_CUTOFF: f32 = 300.0;
/// The cutoff it sweeps down from as its strength rises.
const OPEN_CUTOFF: f32 = 20_000.0;
/// How long the muffle takes to glide to a new level: a game tick's step (50 ms) would click.
const MUFFLE_GLIDE: f32 = 0.02;
/// A mod's muffle (`Audio::muffle`) in the mixer: the other sounds are mixed apart, low-passed by
/// two one-pole stages and turned down, then added to the sounds it leaves clear. While it is off
/// (never set, or faded out) the sounds mix straight into the output, as without it.
#[derive(Default)]
struct Muffle {
/// `[amount, lowpass]`, each 0 to 1, as the game's tick last set them.
target: [f32; 2],
/// Where the output is now, gliding to `target`.
current: [f32; 2],
/// The low-pass stages' state for the left and right output channels.
stages: [[f32; 2]; 2],
/// The muffled sounds of one buffer, reused.
bus: Vec<f32>,
}
impl Muffle {
fn active(&self) -> bool {
self.target != [0.0; 2] || self.current.iter().any(|v| *v > 1e-4)
}
/// Adds `bus` (the muffled sounds) to `data`, low-passed and turned down.
fn apply(&mut self, bus: &[f32], data: &mut [f32], channels: usize, rate: u32) {
let rate = rate as f32;
let glide = 1.0 - (-1.0 / (MUFFLE_GLIDE * rate)).exp();
for (frame, muffled) in data.chunks_mut(channels).zip(bus.chunks(channels)) {
for (current, target) in self.current.iter_mut().zip(self.target) {
*current += (target - *current) * glide;
}
let [amount, lowpass] = self.current;
let gain = 1.0 - amount;
let cutoff = OPEN_CUTOFF * (MUFFLED_CUTOFF / OPEN_CUTOFF).powf(lowpass);
let a = 1.0 - (-std::f32::consts::TAU * cutoff / rate).exp();
for ((out, s), stage) in frame.iter_mut().zip(muffled).zip(&mut self.stages) {
stage[0] += (s - stage[0]) * a;
stage[1] += (stage[0] - stage[1]) * a;
// Dry at no strength, so a muffle that only turns sounds down leaves them as they were.
*out += (s + (stage[1] - s) * lowpass) * gain;
}
}
}
}
#[derive(Default)]
struct Mixer {
@@ -63,6 +112,8 @@ struct Mixer {
cameras: Vec<([f64; 3], Basis, u32)>,
/// Per-source gain/pan/step cache for one buffer, reused across callbacks.
voices: Vec<(ChannelId, [f32; 3], f64)>,
/// A mod's muffle of the sounds it does not exempt.
muffle: Muffle,
limiter_peak: f32,
/// The output device the stream was opened on (`Library.currentDeviceName`).
#[cfg(feature = "audio")]
@@ -98,6 +149,21 @@ pub struct Heard {
pub range: f32,
pub looping: bool,
}
/// A mod's muffle (`Audio::muffle`): its full strength, and the ticks it fades over and has left.
#[derive(Clone, Copy, Debug, PartialEq)]
struct MuffleFade {
amount: f32,
lowpass: f32,
ticks: u32,
left: u32,
}
impl MuffleFade {
/// `[amount, lowpass]` now: full when set, falling in a straight line to none.
fn level(&self) -> [f32; 2] {
let left = self.left as f32 / self.ticks as f32;
[self.amount * left, self.lowpass * left]
}
}
/// The mixer side of the engine (`Backend`).
pub struct MixerBackend {
mixer: Arc<Mutex<Mixer>>,
@@ -135,8 +201,8 @@ impl Drop for MixerBackend {
static SHARED_CHANNELS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
/// Two players' copies of one sound: started within this of each other.
const SAME_SOUND: std::time::Duration = std::time::Duration::from_millis(150);
/// Decoded samples and their rate (`None`: the file did not load).
type Samples = Option<(Arc<Vec<f32>>, u32)>;
/// Decoded interleaved samples, their rate and channel count (`None`: the file did not load).
type Samples = Option<(Arc<Vec<f32>>, u32, u16)>;
/// Decoded static sounds (`SoundBufferLibrary`'s buffers), dropped least
/// recently used first past `CACHE_BYTES`: kept forever, every sound heard in
/// a session stayed (up to 1.4 GiB of samples, villages playing hundreds).
@@ -177,7 +243,7 @@ impl SoundCache {
fn size(samples: &Samples) -> usize {
samples
.as_ref()
.map_or(0, |(s, _)| s.len() * std::mem::size_of::<f32>())
.map_or(0, |(s, _, _)| s.len() * std::mem::size_of::<f32>())
}
}
/// A static sound to decode for a channel waiting on it.
@@ -245,6 +311,8 @@ pub struct Audio {
pub stopped: std::collections::BTreeMap<String, u32>,
/// The volume sliders' `SoundPreviewHandler`.
preview: crate::sound_preview::SoundPreview,
/// A mod's muffle fading out (`muffle`), advanced by `tick`.
muffle: Option<MuffleFade>,
/// `SoundEngine.lastSeenDevices`: the system default output last seen.
#[cfg(feature = "audio")]
default_seen: Option<String>,
@@ -259,8 +327,42 @@ pub struct Audio {
/// Ticks since the output devices were last looked at (`DeviceTracker.tick`).
#[cfg(feature = "audio")]
device_ticks: u32,
/// The look at the output devices running on its own thread, until a tick
/// takes its answer: listing them can take 100 ms (#11490).
#[cfg(feature = "audio")]
device_probe: Option<PendingProbe>,
}
fn decode(path: &Path) -> Option<(Vec<f32>, u32)> {
/// What one look at the output devices found: the system default, the
/// fingerprint, and the device list when those say it may have changed.
#[cfg(feature = "audio")]
struct DeviceProbe {
default: Option<String>,
cards: Option<String>,
devices: Option<Vec<String>>,
}
#[cfg(feature = "audio")]
type PendingProbe = Arc<Mutex<Option<DeviceProbe>>>;
/// Looks at the output devices as `should_change_device` did on the render
/// thread: the device list only when the default or the fingerprint moved.
#[cfg(feature = "audio")]
fn probe_devices(default_seen: Option<String>, cards_seen: Option<String>) -> DeviceProbe {
let default = default_output_name();
let cards = card_fingerprint();
// Listing the devices probes every one (on Linux every built-in PCM,
// printing ALSA's errors for the plugins this system lacks; on macOS
// every Core Audio device, about 90 ms with twenty of them; on Windows
// every endpoint, with no fingerprint to skip it); only the fingerprint's
// devices can change the list, and it is cheap.
let devices = (default != default_seen
|| cards_changed(cards_seen.as_deref(), cards.as_deref()))
.then(output_devices);
DeviceProbe {
default,
cards,
devices,
}
}
fn decode(path: &Path) -> Option<(Vec<f32>, u32, u16)> {
use symphonia::core::{
audio::SampleBuffer, codecs::DecoderOptions, formats::FormatOptions, io::MediaSourceStream,
meta::MetadataOptions, probe::Hint,
@@ -284,7 +386,8 @@ fn decode(path: &Path) -> Option<(Vec<f32>, u32)> {
let mut decoder = symphonia::default::get_codecs()
.make(&track.codec_params, &DecoderOptions::default())
.ok()?;
let mut mono = Vec::new();
let mut samples = Vec::new();
let mut channels = 0u16;
while let Ok(packet) = format.next_packet() {
if packet.track_id() != id {
continue;
@@ -293,14 +396,34 @@ fn decode(path: &Path) -> Option<(Vec<f32>, u32)> {
continue;
};
let spec = *buffer.spec();
let channels = spec.channels.count().max(1);
let mut samples = SampleBuffer::<f32>::new(buffer.capacity() as u64, spec);
samples.copy_interleaved_ref(buffer);
for frame in samples.samples().chunks(channels) {
mono.push(frame.iter().sum::<f32>() / channels as f32);
let frame_channels = spec.channels.count().max(1);
let mut decoded = SampleBuffer::<f32>::new(buffer.capacity() as u64, spec);
decoded.copy_interleaved_ref(buffer);
if channels == 0 {
channels = frame_channels.min(2) as u16;
}
if frame_channels <= 2 {
samples.extend_from_slice(decoded.samples());
} else {
// OpenAL plays multi-channel buffers direct; a stereo output hears the odd/even split.
for frame in decoded.samples().chunks(frame_channels) {
let (mut left, mut right) = (0.0, 0.0);
let (mut lefts, mut rights) = (0.0f32, 0.0f32);
for (i, sample) in frame.iter().enumerate() {
if i % 2 == 0 {
left += sample;
lefts += 1.0;
} else {
right += sample;
rights += 1.0;
}
}
samples.push(left / lefts.max(1.0));
samples.push(right / rights.max(1.0));
}
}
}
Some((mono, rate))
(channels != 0).then_some((samples, rate, channels))
}
/// A sound file without its variant number (`mob/creeper/say2.ogg` is `mob/creeper/say`): each
/// player's game picks its own variant of the same event.
@@ -382,6 +505,7 @@ impl Backend for MixerBackend {
owner: self.owner,
world: self.world,
started: None,
unmuffled: false,
},
);
Some(id)
@@ -409,6 +533,9 @@ impl Backend for MixerBackend {
fn set_relative(&mut self, channel: ChannelId, relative: bool) {
self.with(channel, |c| c.relative = relative);
}
fn set_unmuffled(&mut self, channel: ChannelId, unmuffled: bool) {
self.with(channel, |c| c.unmuffled = unmuffled);
}
/// `getCompleteBuffer` (cached, else decoded on the decoder thread) or
/// `getStream` (decoded on a thread of its own), then attach and
/// `alSourcePlay`. A sound that cannot load leaves its source initial,
@@ -427,7 +554,7 @@ impl Backend for MixerBackend {
// A streamed sound loops by restarting its stream.
let mixer = self.mixer.clone();
std::thread::spawn(move || {
let samples = decode(&file).map(|(s, r)| (Arc::new(s), r));
let samples = decode(&file).map(|(s, r, c)| (Arc::new(s), r, c));
attach(&mixer, channel, samples, looping);
});
return;
@@ -448,7 +575,7 @@ impl Backend for MixerBackend {
// Queued twice before the first decode finished.
let cached = cache.lock().ok().and_then(|mut c| c.get(&job.key));
let samples = cached.unwrap_or_else(|| {
let samples = decode(&job.file).map(|(s, r)| (Arc::new(s), r));
let samples = decode(&job.file).map(|(s, r, c)| (Arc::new(s), r, c));
if let Ok(mut c) = cache.lock() {
c.insert(job.key, samples.clone());
}
@@ -584,9 +711,13 @@ fn mix(mixer: &mut Mixer, data: &mut [f32], channels: usize, rate: u32) {
let mut voices = std::mem::take(&mut mixer.voices);
voices.clear();
for (id, c) in &mixer.channels {
let Some((_, source_rate)) = &c.samples else {
let Some((_, source_rate, source_channels)) = &c.samples else {
continue;
};
// OpenAL plays a multi-channel buffer direct: no panning and no distance attenuation
// (`CalcVoiceParams`' non-spatialized path, vanilla's HRTF-off stereo context). Vanilla's
// music and records are stereo. A mono buffer is the positional one.
let direct = *source_channels > 1;
if c.state != State::Playing {
continue;
}
@@ -611,38 +742,61 @@ fn mix(mixer: &mut Mixer, data: &mut [f32], channels: usize, rate: u32) {
let d = if c.relative { at } else { away(listener) };
let distance = (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt() as f32;
let gain = if camera.is_some() {
c.gain.clamp(0.0, 1.0) * distance_gain(c.linear, distance)
c.gain.clamp(0.0, 1.0)
* if direct {
1.0
} else {
distance_gain(c.linear, distance)
}
} else {
0.0
};
// Relative sources are already in OpenAL's listener space (-Z ahead).
let [mut left, mut right] = if c.relative {
stereo_gains(d[0] as f32, d[1] as f32, -d[2] as f32)
let [left, right] = if direct {
[gain, gain]
} else {
stereo_gains(dot(d, right), dot(d, up), dot(d, forward))
// Relative sources are already in OpenAL's listener space (-Z ahead).
let [mut left, mut right] = if c.relative {
stereo_gains(d[0] as f32, d[1] as f32, -d[2] as f32)
} else {
stereo_gains(dot(d, right), dot(d, up), dot(d, forward))
};
if shared {
// Several players on the speakers: half the stereo width, as left and right are
// nobody's in particular.
let middle = (left + right) / 2.0;
left = middle + (left - middle) / 2.0;
right = middle + (right - middle) / 2.0;
}
[left * gain, right * gain]
};
if shared {
// Several players on the speakers: half the stereo width, as left and right are
// nobody's in particular.
let middle = (left + right) / 2.0;
left = middle + (left - middle) / 2.0;
right = middle + (right - middle) / 2.0;
}
let [left, right] = [left, right].map(|pan| gain * pan);
let step = f64::from(*source_rate) / f64::from(rate) * f64::from(c.pitch.max(0.0));
voices.push((*id, [left, right, gain], step));
}
// A mod's muffle: the sounds it doesn't exempt mix into a bus of their own first.
let muffling = mixer.muffle.active();
let mut bus = std::mem::take(&mut mixer.muffle.bus);
if muffling {
bus.clear();
bus.resize(data.len(), 0.0);
}
for (id, [left, right, gain], step) in voices.iter().copied() {
let Some(c) = mixer.channels.get_mut(&id) else {
continue;
};
let Some((samples, _)) = c.samples.clone() else {
let Some((samples, _, source_channels)) = c.samples.clone() else {
continue;
};
for frame in data.chunks_mut(channels) {
let source_channels = source_channels as usize;
let frames = samples.len() / source_channels;
let out: &mut [f32] = if muffling && !c.unmuffled {
&mut bus
} else {
&mut *data
};
for frame in out.chunks_mut(channels) {
let mut i = c.position as usize;
if i + 1 >= samples.len() {
if c.looping && samples.len() > 1 {
if i + 1 >= frames {
if c.looping && frames > 1 {
c.position = 0.0;
i = 0;
} else {
@@ -651,18 +805,37 @@ fn mix(mixer: &mut Mixer, data: &mut [f32], channels: usize, rate: u32) {
}
}
let t = (c.position - i as f64) as f32;
let s = samples[i] * (1.0 - t) + samples[i + 1] * t;
if channels >= 2 {
frame[0] += s * left;
frame[1] += s * right;
if source_channels > 1 {
let l = samples[i * 2] * (1.0 - t) + samples[(i + 1) * 2] * t;
let r = samples[i * 2 + 1] * (1.0 - t) + samples[(i + 1) * 2 + 1] * t;
if channels >= 2 {
frame[0] += l * left;
frame[1] += r * right;
} else {
frame[0] += (l + r) * 0.5 * gain;
}
} else {
// A mono output has no panning: the source's own gain.
frame[0] += s * gain;
let s = samples[i] * (1.0 - t) + samples[i + 1] * t;
if channels >= 2 {
frame[0] += s * left;
frame[1] += s * right;
} else {
// A mono output has no panning: the source's own gain.
frame[0] += s * gain;
}
}
c.position += step;
}
}
mixer.voices = voices;
if muffling {
mixer.muffle.apply(&bus, data, channels, rate);
} else {
// Off: the next muffle starts from silence, not from the last one's filter.
mixer.muffle.current = [0.0; 2];
mixer.muffle.stages = [[0.0; 2]; 2];
}
mixer.muffle.bus = bus;
// Limit the whole buffer together, preserving waveform and stereo balance
// instead of flattening each overloaded sample; release over 200 ms
// (OpenAL Soft's output limiter, `ALC_OUTPUT_LIMITER_SOFT`, releases
@@ -807,27 +980,50 @@ fn cards_changed(seen: Option<&str>, cards: Option<&str>) -> bool {
}
}
/// The buffer size to request for `open_device`. On Linux the ALSA backend's
/// `BufferSize::Default` means a ~100 ms buffer, so sound effects come out
/// about 100 ms late: request a short fixed buffer (1024 frames) by default.
/// An environment override `HORIZON_AUDIO_BUFFER` can specify `default` or a
/// custom frame count (e.g. 2048) when the default short buffer underruns
/// (e.g. PipeWire min-quantum >= 1024).
#[cfg(feature = "audio")]
fn target_buffer_size(
range: (u32, u32),
var: impl Fn(&str) -> Option<std::ffi::OsString>,
) -> Option<cpal::BufferSize> {
if let Some(val) = var("HORIZON_AUDIO_BUFFER").and_then(|v| v.into_string().ok()) {
if val == "default" {
return Some(cpal::BufferSize::Default);
}
if let Ok(frames) = val.parse::<u32>() {
return Some(cpal::BufferSize::Fixed(frames.clamp(range.0, range.1)));
}
}
#[cfg(target_os = "linux")]
{
Some(cpal::BufferSize::Fixed(1024u32.clamp(range.0, range.1)))
}
#[cfg(not(target_os = "linux"))]
{
None
}
}
/// The cpal `StreamConfig` to open an output stream with. On Linux the ALSA
/// backend's `BufferSize::Default` means a ~100 ms buffer, so sound effects
/// come out about 100 ms late: request a short fixed buffer there. Other
/// platforms keep cpal's default behaviour.
#[cfg(feature = "audio")]
fn audio_stream_config(config: &cpal::SupportedStreamConfig) -> cpal::StreamConfig {
#[cfg(target_os = "linux")]
{
let range = match config.buffer_size() {
cpal::SupportedBufferSize::Range { min, max } => (*min, *max),
cpal::SupportedBufferSize::Unknown => (256, 4096),
};
let target = 1024u32.clamp(range.0, range.1);
let mut sc = config.config();
sc.buffer_size = cpal::BufferSize::Fixed(target);
sc
}
#[cfg(not(target_os = "linux"))]
{
config.config()
let mut sc = config.config();
let range = match config.buffer_size() {
cpal::SupportedBufferSize::Range { min, max } => (*min, *max),
cpal::SupportedBufferSize::Unknown => (256, 4096),
};
if let Some(bs) = target_buffer_size(range, |name| std::env::var_os(name)) {
sc.buffer_size = bs;
}
sc
}
#[cfg(feature = "audio")]
@@ -965,12 +1161,13 @@ impl Audio {
listener: [0.0; 3],
listener_yaw: 0.0,
master: 1.0,
random: LegacyRandom::new(crate::sound_engine::unique_seed()),
random: crate::random_source::create(),
source_override: None,
capture: None,
heard: Default::default(),
stopped: Default::default(),
preview: Default::default(),
muffle: None,
#[cfg(feature = "audio")]
default_seen: if live { default_output_name() } else { None },
#[cfg(feature = "audio")]
@@ -979,6 +1176,8 @@ impl Audio {
devices_seen: devices,
#[cfg(feature = "audio")]
device_ticks: 0,
#[cfg(feature = "audio")]
device_probe: None,
}
}
#[cfg(not(feature = "audio"))]
@@ -1124,12 +1323,13 @@ impl Audio {
listener: [0.0; 3],
listener_yaw: 0.0,
master: self.master,
random: LegacyRandom::new(crate::sound_engine::unique_seed()),
random: crate::random_source::create(),
source_override: None,
capture: None,
heard: Default::default(),
stopped: Default::default(),
preview: Default::default(),
muffle: None,
// The device belongs to the first player's output, which looks after it.
#[cfg(feature = "audio")]
default_seen: self.default_seen.clone(),
@@ -1139,6 +1339,8 @@ impl Audio {
devices_seen: self.devices_seen.clone(),
#[cfg(feature = "audio")]
device_ticks: 0,
#[cfg(feature = "audio")]
device_probe: None,
}
}
/// Sync the volume options, refreshing the categories that changed
@@ -1160,7 +1362,8 @@ impl Audio {
/// `SoundEngine.shouldChangeDevice`: the open device was lost, the system
/// default changed (with no device chosen in the options), or the chosen
/// device came back; the output is then opened again. The devices are looked
/// at once a second.
/// at once a second, on a thread of their own: a later tick reads what it
/// found.
#[cfg(feature = "audio")]
fn should_change_device(&mut self) -> bool {
// A splitscreen share plays through the first player's output: it leaves the device,
@@ -1174,29 +1377,54 @@ impl Audio {
};
if lost {
log::info!("Horizon: audio device was lost");
self.device_probe = None;
self.default_seen = default_output_name();
self.cards_seen = card_fingerprint();
self.devices_seen = output_devices();
return true;
}
if let Some(pending) = &self.device_probe {
let Some(probe) = pending.lock().ok().and_then(|mut found| found.take()) else {
return false;
};
self.device_probe = None;
return self.devices_changed(probe, current);
}
self.device_ticks += 1;
if self._stream.is_none() || self.device_ticks < 20 {
return false;
}
self.device_ticks = 0;
let default = default_output_name();
let cards = card_fingerprint();
// Listing the devices probes every one (on Linux every built-in PCM,
// printing ALSA's errors for the plugins this system lacks; on macOS
// every Core Audio device, about 90 ms with twenty of them); only the
// fingerprint's devices can change the list, and it is cheap.
if default == self.default_seen
&& !cards_changed(self.cards_seen.as_deref(), cards.as_deref())
{
return false;
let pending = PendingProbe::default();
let out = Arc::clone(&pending);
let seen = (self.default_seen.clone(), self.cards_seen.clone());
let started = std::thread::Builder::new()
.name("audio-devices".into())
.spawn(move || {
let probe = probe_devices(seen.0, seen.1);
if let Ok(mut found) = out.lock() {
*found = Some(probe);
}
});
match started {
Ok(_) => self.device_probe = Some(pending),
Err(e) => log::warn!("Horizon: could not look at the audio devices: {e}"),
}
false
}
/// The rest of `shouldChangeDevice` with what a look at the devices found,
/// `current` being the device playing now.
#[cfg(feature = "audio")]
fn devices_changed(&mut self, probe: DeviceProbe, current: String) -> bool {
let DeviceProbe {
default,
cards,
devices,
} = probe;
let Some(devices) = devices else {
return false;
};
self.cards_seen = cards;
let devices = output_devices();
// Only a change in the device lists is looked at, as in vanilla: a
// preferred device that is listed but cannot open is not retried.
if default == self.default_seen && devices == self.devices_seen {
@@ -1234,6 +1462,50 @@ impl Audio {
}
self.sync_volumes();
self.engine.tick(paused, ctx, &mut self.backend);
// A paused game holds a mod's muffle where it is, as it holds the sounds.
if !paused && let Some(fade) = &mut self.muffle {
fade.left = fade.left.saturating_sub(1);
let level = fade.level();
if fade.left == 0 {
self.muffle = None;
self.engine.set_unmuffled(Vec::new(), &mut self.backend);
}
self.set_muffle_level(level);
}
}
/// A mod's muffle (`native_muffle_sounds`): `amount` (0 to 1) of every sound's volume taken
/// away and `lowpass` (0 to 1) dulling it, from untouched to a 300 Hz low-pass, fading in a
/// straight line to none over `ticks` client ticks; the sound events in `exempt` (namespaced,
/// `mymod:ear.ringing`) play clearly through it, those already playing too. It replaces any
/// muffle in place; `ticks` 0 or no strength ends it. The output's, so in splitscreen every
/// player hears it. False for an amount or strength outside 0 to 1.
pub fn muffle(&mut self, amount: f32, lowpass: f32, ticks: u32, exempt: &[&str]) -> bool {
if !(0.0..=1.0).contains(&amount) || !(0.0..=1.0).contains(&lowpass) {
return false;
}
let on = ticks > 0 && (amount > 0.0 || lowpass > 0.0);
let exempt = if on {
exempt
.iter()
.map(|e| crate::sound_events::normalize(e))
.collect()
} else {
Vec::new()
};
self.engine.set_unmuffled(exempt, &mut self.backend);
self.muffle = on.then_some(MuffleFade {
amount,
lowpass,
ticks,
left: ticks,
});
self.set_muffle_level(self.muffle.map_or([0.0; 2], |fade| fade.level()));
true
}
fn set_muffle_level(&mut self, level: [f32; 2]) {
if let Ok(mut m) = self.backend.mixer.lock() {
m.muffle.target = level;
}
}
/// The `uiSoundReserve` extra: positional sounds leave
/// `sound_engine::HEARD_RESERVE` static channels free (off: vanilla's pool).
@@ -1346,7 +1618,7 @@ impl Audio {
"minecraft:ui.button.click",
1.0,
0.25,
SoundEngine::unseeded(),
crate::random_source::create(),
),
false,
);
@@ -1488,7 +1760,7 @@ impl Audio {
f64::from(pos[2]) + 0.5,
];
let event = crate::sound_events::normalize(event);
let instance = SoundInstance::for_jukebox_song(&event, at, SoundEngine::unseeded());
let instance = SoundInstance::for_jukebox_song(&event, at, crate::random_source::create());
self.play_instance(instance, true).0
}
/// Stop and drop a held instance.
@@ -1687,7 +1959,7 @@ mod tests {
fn channel(samples: Vec<f32>, linear: Option<f32>, at: [f32; 3], looping: bool) -> Channel {
Channel {
samples: Some((Arc::new(samples), 48_000)),
samples: Some((Arc::new(samples), 48_000, 1)),
position: 0.0,
state: State::Playing,
gain: 1.0,
@@ -1699,14 +1971,27 @@ mod tests {
owner: 0,
world: 0,
started: None,
unmuffled: false,
}
}
/// A channel whose buffer is interleaved stereo.
fn stereo_channel(
samples: Vec<f32>,
linear: Option<f32>,
at: [f32; 3],
looping: bool,
) -> Channel {
let mut c = channel(Vec::new(), linear, at, looping);
c.samples = Some((Arc::new(samples), 48_000, 2));
c
}
#[test]
fn decoded_sounds_stay_within_the_budget_least_recently_used_out() {
let mut cache = SoundCache::default();
let quarter = CACHE_BYTES / 4 / std::mem::size_of::<f32>();
let sound = || Some((Arc::new(vec![0.0f32; quarter]), 44100));
let sound = || Some((Arc::new(vec![0.0f32; quarter]), 44100, 1));
for key in ["a", "b", "c", "d"] {
cache.insert(key.into(), sound());
}
@@ -1744,6 +2029,42 @@ mod tests {
);
}
#[test]
#[cfg(feature = "audio")]
fn buffer_size_override_and_defaults() {
let env = |pairs: &'static [(&'static str, &'static str)]| {
move |name: &str| {
pairs
.iter()
.find(|(k, _)| *k == name)
.map(|(_, v)| std::ffi::OsString::from(*v))
}
};
let range = (256, 4096);
assert_eq!(
target_buffer_size(range, env(&[("HORIZON_AUDIO_BUFFER", "default")])),
Some(cpal::BufferSize::Default)
);
assert_eq!(
target_buffer_size(range, env(&[("HORIZON_AUDIO_BUFFER", "2048")])),
Some(cpal::BufferSize::Fixed(2048))
);
assert_eq!(
target_buffer_size(range, env(&[("HORIZON_AUDIO_BUFFER", "100")])),
Some(cpal::BufferSize::Fixed(256))
);
assert_eq!(
target_buffer_size(range, env(&[("HORIZON_AUDIO_BUFFER", "10000")])),
Some(cpal::BufferSize::Fixed(4096))
);
#[cfg(target_os = "linux")]
assert_eq!(
target_buffer_size(range, env(&[])),
Some(cpal::BufferSize::Fixed(1024))
);
#[cfg(not(target_os = "linux"))]
assert_eq!(target_buffer_size(range, env(&[])), None);
}
#[test]
fn linear_attenuation_reaches_zero_at_the_max_distance() {
assert_eq!(distance_gain(None, 100.0), 1.0);
assert_eq!(distance_gain(Some(16.0), 0.0), 1.0);
@@ -1833,6 +2154,32 @@ mod tests {
}
}
#[test]
fn stereo_sounds_play_direct_without_panning_or_distance() {
// OpenAL Soft plays a multi-channel buffer direct (its non-spatialized path), so vanilla's
// stereo music and records keep their own left/right mix at full volume whichever way the
// camera faces and however far it is. Only mono buffers are panned and attenuated.
let wave = [0.1, -0.2, 0.3, -0.4, 0.5, -0.6]; // three stereo frames
let expected = [wave[0], wave[1], wave[2], wave[3]];
for (yaw, at, linear, listener) in [
(0.0, [100.0, 0.0, 0.0], Some(16.0), [0.0; 3]),
(180.0, [0.0, 0.0, -100.0], Some(16.0), [0.0; 3]),
(90.0, [2.0, 1.0, -3.0], Some(16.0), [5.0, 60.0, 5.0]),
(0.0, [0.0, 0.0, 0.0], None, [0.0; 3]),
] {
let mut m = Mixer {
listener,
listener_yaw: yaw,
..Mixer::default()
};
m.channels
.insert(1, stereo_channel(wave.to_vec(), linear, at, false));
let mut data = [0.0; 4];
mix(&mut m, &mut data, 2, 48_000);
assert_eq!(data, expected, "yaw {yaw}, source {at:?}");
}
}
#[test]
fn panning_follows_the_camera_rotation() {
let gains = |yaw: f32, pitch: f32, at: [f32; 3]| {
@@ -2045,6 +2392,123 @@ mod tests {
assert_eq!(data, [0.5; 2]);
}
/// One second of a `hz` sine at 48 kHz, amplitude 0.3 (looped, it repeats seamlessly).
fn sine(hz: f32) -> Vec<f32> {
(0..48_000)
.map(|i| 0.3 * (std::f32::consts::TAU * hz * i as f32 / 48_000.0).sin())
.collect()
}
fn rms(data: &[f32]) -> f32 {
(data.iter().map(|s| s * s).sum::<f32>() / data.len() as f32).sqrt()
}
/// `seconds` of the mixer's stereo output, in the 10 ms buffers an output stream asks for.
fn listen(m: &mut Mixer, seconds: f32) -> Vec<f32> {
let mut out = Vec::new();
let mut buffer = [0.0; 960];
for _ in 0..(seconds * 100.0).round() as usize {
mix(m, &mut buffer, 2, 48_000);
out.extend_from_slice(&buffer);
}
out
}
#[test]
fn a_muffle_dulls_and_quietens_every_sound_but_the_exempt_ones() {
// A looping tone heard through a muffle of `[amount, lowpass]`, once it has settled.
let heard = |hz: f32, muffle: [f32; 2], unmuffled: bool| {
let mut m = Mixer::default();
let mut c = channel(sine(hz), None, [0.0, 0.0, 2.0], true);
c.unmuffled = unmuffled;
m.channels.insert(1, c);
m.muffle.target = muffle;
listen(&mut m, 0.2);
listen(&mut m, 0.2)
};
let plain = |hz: f32| rms(&heard(hz, [0.0; 2], false));
// Fully low-passed, a 6 kHz whine all but goes and a 100 Hz hum keeps most of its level.
assert!(rms(&heard(6000.0, [0.0, 1.0], false)) < plain(6000.0) * 0.05);
assert!(rms(&heard(100.0, [0.0, 1.0], false)) > plain(100.0) * 0.8);
// Half the volume taken and no low-pass: half the level, the tone itself untouched.
let ducked = rms(&heard(6000.0, [0.5, 0.0], false)) / plain(6000.0);
assert!((ducked - 0.5).abs() < 1e-3, "{ducked}");
// An exempt sound plays through as if there were no muffle, sample for sample.
assert_eq!(
heard(6000.0, [0.5, 1.0], true),
heard(6000.0, [0.0; 2], false)
);
}
#[test]
fn a_faded_muffle_leaves_the_sound_as_it_was() {
let mixer = || {
let mut m = Mixer::default();
m.channels
.insert(1, channel(sine(3000.0), None, [2.0, 0.0, 1.0], true));
m
};
let (mut muffled, mut plain) = (mixer(), mixer());
muffled.muffle.target = [0.7, 1.0];
assert_ne!(listen(&mut muffled, 0.1), listen(&mut plain, 0.1));
muffled.muffle.target = [0.0; 2];
listen(&mut muffled, 0.5);
listen(&mut plain, 0.5);
assert_eq!(listen(&mut muffled, 0.1), listen(&mut plain, 0.1));
}
#[test]
fn a_mods_muffle_fades_with_the_game_ticks_and_spares_its_exempt_sounds() {
let mut audio = Audio::silent(Path::new("/nonexistent/horizon-test-assets"));
audio.engine.registry.add_file(
"minecraft",
br#"{"ambient.cave":{"sounds":["ambient/cave/cave1"]}}"#,
&|_, _| true,
);
audio.engine.registry.add_file(
"hztest",
br#"{"ear.ringing":{"sounds":["ear/ringing"]}}"#,
&|_, _| true,
);
// The engine makes its channels; with no output, none loads a file.
audio.engine.loaded = true;
let marks = |audio: &Audio| {
let m = audio.backend.mixer.lock().unwrap();
let mut marks: Vec<_> = m
.channels
.iter()
.map(|(&id, c)| (id, c.unmuffled))
.collect();
marks.sort();
marks
};
let level = |audio: &Audio| audio.backend.mixer.lock().unwrap().muffle.target;
let close =
|a: [f32; 2], b: [f32; 2]| (a[0] - b[0]).abs() < 1e-6 && (a[1] - b[1]).abs() < 1e-6;
// Two sounds already playing, and one exempt sound started under the muffle.
audio.play("ambient.cave", None, 1.0, 1.0);
audio.play("hztest:ear.ringing", None, 1.0, 1.0);
assert!(audio.muffle(0.8, 1.0, 4, &["hztest:ear.ringing"]));
audio.play("hztest:ear.ringing", None, 1.0, 1.0);
assert_eq!(marks(&audio), [(1, false), (2, true), (3, true)]);
assert!(close(level(&audio), [0.8, 1.0]));
let ticks = TickContext::default();
audio.tick(false, &ticks);
assert!(close(level(&audio), [0.6, 0.75]), "{:?}", level(&audio));
// A paused game holds it.
audio.tick(true, &ticks);
assert!(close(level(&audio), [0.6, 0.75]), "{:?}", level(&audio));
for _ in 0..3 {
audio.tick(false, &ticks);
}
assert_eq!(level(&audio), [0.0; 2]);
// Over: its exemptions go with it.
assert_eq!(marks(&audio), [(1, false), (2, false), (3, false)]);
// Out of range changes nothing.
assert!(!audio.muffle(1.5, 0.0, 10, &[]));
assert_eq!(level(&audio), [0.0; 2]);
}
#[test]
fn output_devices_are_looked_at_again_only_when_the_cards_change() {
// No fingerprint (not Linux, or the cards could not be read): the list
+1 -1
View File
@@ -288,7 +288,7 @@ impl Biomes {
.collect();
files.sort_by_cached_key(|f| format!("{}.json", f.name));
for file in files {
let v: Value = serde_json::from_slice(file.bytes()?)?;
let v: Value = horizon_world::json_file::parse(&file.path, file.bytes()?)?;
list.push(BiomeInfo::from_json(
format!("minecraft:{}", file.name),
&v,
@@ -77,6 +77,9 @@ pub struct BlockEntities {
/// The fluid tanks of mod blocks' block entities, by block state id;
/// built with `states`, like the containers.
mod_fluids: FxHashMap<u32, Arc<ModFluidTanks>>,
/// State-aware native factory ownership. Shared across level snapshots;
/// an empty slice means the declarations have not yet been compiled.
stateful_entities: Arc<[bool]>,
/// The energy buffers of mod blocks' block entities, by block state id;
/// built with `states`, like the containers.
mod_energy: FxHashMap<u32, Arc<ModEnergyBuffer>>,
@@ -209,6 +212,21 @@ impl BlockEntities {
pub fn mod_fluids(&self, state: u32) -> Option<&Arc<ModFluidTanks>> {
self.mod_fluids.get(&state)
}
pub fn set_stateful_entities(&mut self, states: Vec<bool>) {
self.stateful_entities = states.into();
}
pub fn has_stateful_entity_table(&self) -> bool {
!self.stateful_entities.is_empty()
}
pub fn stateful_entities(&self) -> Arc<[bool]> {
self.stateful_entities.clone()
}
pub fn stateful_entity(&self, state: u32) -> bool {
self.stateful_entities
.get(state as usize)
.copied()
.unwrap_or(false)
}
pub fn set_mod_energy(&mut self, energy: FxHashMap<u32, Arc<ModEnergyBuffer>>) {
self.mod_energy = energy;
}
@@ -538,6 +556,11 @@ impl BlockEntities {
});
Arc::clone(&self.tickers)
}
/// How many block entities tick (`Level.blockEntityTickers`, the slots still live), as of
/// the last [`Self::tickers`]: what a diagnostics mod counts.
pub fn ticking_len(&self) -> usize {
self.live.len()
}
/// Whether a slot from `tickers` still ticks.
pub fn is_live(&self, (pos, slot): (Pos, u32)) -> bool {
self.live.get(&pos) == Some(&slot) && self.map.contains_key(&pos)
@@ -3,20 +3,22 @@
//! decorated pots, conduits, enchanting-table and lectern books and copper
//! golem statues, End portals and End gateways. Models are vanilla's baked `LayerDefinitions` layers
//! (`entity-models.json`); transforms, animation and textures follow each
//! vanilla renderer. Faces are lit like the `entity` shader: the lightmap at
//! the block plus `minecraft_mix_light` with the level's two diffuse lights.
//! vanilla renderer. Faces carry their tint and world normal: the entity
//! shader (and a shader pack) applies `minecraft_mix_light` from the normal, as
//! vanilla's does.
use crate::{
Result,
bidi::visual,
block_entity_anim::{self, EnchantingBook},
breaking::entity_geometry,
fast_hash::FxHashMap,
font::Glyph,
font::{Font, Glyph},
gfx::mark_oit,
hud::Rect,
mesher::{CULL_BACK, Vertex},
models::load_visibility,
signs::{self, SignData, SignText},
text::{Style, Text},
world::World,
};
use glam::{DVec3, Mat4, Quat, Vec3};
@@ -128,6 +130,14 @@ fn pack_color(rgb: u32, alpha: u32, brightness: f32) -> u32 {
let c = |shift: u32| (((rgb >> shift) & 255) as f32 * brightness) as u32;
alpha << 24 | c(0) << 16 | c(8) << 8 | c(16)
}
/// A world normal as `Vertex::normal` holds it: RGBA8_SNORM, `w` positive so
/// the entity shader and a shader pack both shade by it. Block-entity models
/// carry no normals of their own (vanilla computes each face's from its
/// polygon, then `minecraft_mix_light` uses it in the shader).
fn pack_normal(normal: Vec3) -> [i8; 4] {
let c = |v: f32| (v.clamp(-1.0, 1.0) * 127.0).round() as i8;
[c(normal.x), c(normal.y), c(normal.z), 127]
}
/// Everything a draw call needs besides the geometry.
#[derive(Clone, Copy)]
@@ -165,6 +175,7 @@ pub fn draw_part(
light,
lights,
glow,
..
} = paint;
for poly in &part.polygons {
let pos = poly.map(|v| m.transform_point3(Vec3::new(v[0], v[1], v[2]) / 16.0));
@@ -177,11 +188,21 @@ pub fn draw_part(
} else {
normal
};
let color = pack_color(tint, 255, diffuse(normal, lights));
// The colour is the tint alone when the caller shades by the level's
// lights: the entity shader applies `minecraft_mix_light` from the
// normal, as vanilla does, so a shader pack receives the unshaded
// colour Iris gives it (a baked shade would be applied a second time
// by the pack's lighting). Other lights (an item preview's
// `Lighting.Entry`) are not the shader's to know, so those stay baked
// into the colour, as before.
let baked = !(lights == diffuse_lights(false) || lights == diffuse_lights(true));
let brightness = if baked { diffuse(normal, lights) } else { 1.0 };
let color = pack_color(tint, 255, brightness);
let normal = if baked { [0; 4] } else { pack_normal(normal) };
for (r, light) in std::iter::once((r, light)).chain(glow.map(|g| (g, FULL_BRIGHT))) {
for (v, position) in poly.iter().zip(pos) {
out.push(Vertex {
normal: [0; 4],
normal,
pos: position.to_array(),
uv: [r.u0 + (r.u1 - r.u0) * v[3], r.v0 + (r.v1 - r.v0) * v[4]],
layer: 0,
@@ -600,30 +621,26 @@ fn frac(x: f32) -> f32 {
x - x.floor()
}
/// `StringSplitter.splitLines(...).get(0)` for plain text: the first line,
/// wrapped at the last space that fits or mid-word when none does.
fn first_line(line: &str, max_width: f32, advance: &dyn Fn(char) -> f32) -> String {
/// `StringSplitter.findLineBreak`: first line, with styled advances.
fn first_line(line: &Text, max_width: f32, advance: &dyn Fn(char, &Style) -> f32) -> Text {
let mut width = 0.0;
let mut visible = false;
let mut last_space = None;
for (i, c) in line.char_indices() {
for (i, (c, style)) in line.chars().enumerate() {
if c == '\n' {
return line[..i].to_owned();
return line.slice(0, i);
}
if c == ' ' {
last_space = Some(i);
}
width += advance(c);
if width > max_width {
let end = match last_space {
Some(s) if s > 0 => s,
_ if i > 0 => i,
// The first char alone is too wide: take that one char.
_ => c.len_utf8(),
};
return line[..end].to_owned();
let step = advance(c, style);
width += step;
if visible && width > max_width.max(1.0) {
return line.slice(0, last_space.unwrap_or(i));
}
visible |= step != 0.0;
}
line.to_owned()
line.clone()
}
/// `AbstractSignRenderer.isOutlineVisible`: the camera within 16 blocks of
@@ -641,7 +658,7 @@ fn sign_text_near(pos: [i32; 3], camera: DVec3) -> bool {
/// `AbstractSignRenderer.submitSignText`: four centred lines, glowing text
/// full-bright with an 8x outline in the dark colour. The font colours each
/// glyph by its style (`Font.StringRenderOutput`): a line's own colour wins,
/// else the sign's. A line is one call per run of one colour.
/// else the sign's. Keep style boundaries for glyphs and decoration effects.
fn sign_text_calls(
scene: &Scene,
data: &SignData,
@@ -653,27 +670,43 @@ fn sign_text_calls(
let line_height = data.line_height();
let midpoint = (4.0 * line_height / 2.0).floor();
let dark = 0xFF00_0000 | text.dark_color();
let (color, outline, light) = if text.glowing {
let (color, outline, light, tier) = if text.glowing {
let c = text.text_color();
(
0xFF00_0000 | c,
if c == 0 || near { dark } else { 0 },
240 | 240 << 8,
crate::mesher::LOW,
)
} else {
(dark, 0, vertex_light(packed))
(dark, 0, vertex_light(packed), 0)
};
let advance = |c: char, style: &Style| {
let g = scene
.font
.map_or_else(|| (scene.glyph)(c), |f| f.glyph_styled(c, style));
g.advance + if style.is_bold() { g.offset } else { 0.0 }
};
let advance = |c: char| (scene.glyph)(c).advance;
let mut calls: Vec<TextCall> = Vec::with_capacity(text.lines.len());
for i in 0..text.lines.len() {
// The line that fits, in the order it is drawn (getVisualOrder).
let line = text.line_text(i);
let line = line.resolved();
let flat: String = line.chars().map(|(c, _)| c).collect();
let fits = first_line(&flat, data.max_line_width(), &advance);
let shown = line.slice(0, fits.chars().count());
let shown = if let Some(font) = scene.font {
font.split_formatted(&line, data.max_line_width())
.into_iter()
.next()
.unwrap_or_default()
.resolved()
.into_owned()
} else {
first_line(&line.resolved(), data.max_line_width(), &advance)
};
let shown = visual(&shown);
let width = shown.chars().map(|(c, _)| advance(c)).sum::<f32>().ceil();
let width = shown
.chars()
.map(|(c, s)| advance(c, s))
.sum::<f32>()
.ceil();
let x = -((width as i32) / 2) as f32;
let y = i as f32 * line_height - midpoint;
let first = calls.len();
@@ -683,19 +716,22 @@ fn sign_text_calls(
.style
.color
.map_or(color, |rgb| 0xFF00_0000 | (rgb & 0xFF_FFFF));
match calls[first..].last_mut() {
Some(last) if last.color == run => last.text.push_str(&span.text),
_ => calls.push(TextCall {
matrix: m,
x: x + pen,
y,
text: span.text.clone(),
color: run,
outline,
light,
}),
}
pen += span.text.chars().map(advance).sum::<f32>();
calls.push(TextCall {
matrix: m,
x: x + pen,
y,
text: span.text.clone(),
style: span.style.clone(),
color: run,
outline,
light,
tier,
});
pen += span
.text
.chars()
.map(|c| advance(c, &span.style))
.sum::<f32>();
}
if calls.len() == first {
// A line with no text is still a call, of no glyphs.
@@ -704,15 +740,28 @@ fn sign_text_calls(
x,
y,
text: String::new(),
style: Style::default(),
color,
outline,
light,
tier,
});
}
}
calls
}
fn sign_text_animated(data: &SignData) -> bool {
[&data.front, &data.back].into_iter().any(|side| {
(0..side.lines.len()).any(|i| {
side.line_text(i)
.resolved()
.chars()
.any(|(c, s)| c != ' ' && s.obfuscated == Some(true))
})
})
}
/// How far the text of an outlined call sits in front of its outline, in
/// text units (`Font.DisplayMode.POLYGON_OFFSET`, see `draw_text`).
const TEXT_OVER_OUTLINE: f32 = 0.05;
@@ -726,72 +775,131 @@ const TEXT_OVER_OUTLINE: f32 = 0.05;
/// reader instead (a twentieth of a font pixel, invisible) rather than
/// z-fighting with the outline under it.
fn draw_text(scene: &Scene, call: &TextCall, out: &mut Vec<Vertex>) {
text_vertices(scene.glyph, call, out);
let glyph = |c| {
let Some(font) = scene.font else {
return (scene.glyph)(c);
};
let g = font.glyph_styled(c, &call.style);
if call.style.obfuscated == Some(true) && c != ' ' {
font.random_glyph(&call.style, g.advance)
} else {
g
}
};
text_vertices(&glyph, scene.font.map(Font::white), call, out);
}
/// `draw_text` with the font's glyph cells and advances from `glyph`: the
/// `BakedSheetGlyph.renderChar` / `Font.PreparedTextBuilder.accept`: the
/// text of `call` as glyph quads in its matrix's plane, 1 unit a font pixel,
/// y down as the font's, with its outline when it has one. A native mod's
/// block entity draws its text this way (`native_draw_text`).
pub fn text_vertices(glyph: &dyn Fn(char) -> Glyph, call: &TextCall, out: &mut Vec<Vertex>) {
let mut emit = |dx: f32, dy: f32, z: f32, argb: u32| {
let mut x = call.x + dx;
let y0 = call.y + dy;
for ch in call.text.chars() {
pub fn text_vertices(
glyph: &dyn Fn(char) -> Glyph,
white: Option<Rect>,
call: &TextCall,
out: &mut Vec<Vertex>,
) {
let bold = call.style.is_bold();
let mut quad = |corners: [(f32, f32); 4], cell: Rect, z: f32, argb: u32| {
let uvs = [
[cell.u0, cell.v1],
[cell.u1, cell.v1],
[cell.u1, cell.v0],
[cell.u0, cell.v0],
];
let color = 0xFF00_0000 | (argb & 255) << 16 | (argb >> 8 & 255) << 8 | (argb >> 16 & 255);
for ((x, y), uv) in corners.into_iter().zip(uvs) {
out.push(Vertex {
normal: [0; 4],
pos: call.matrix.transform_point3(Vec3::new(x, y, z)).to_array(),
uv,
layer: u32::from(call.tier) << 16,
color,
light: call.light,
glint_uv: [0.0; 2],
});
}
};
let mut emit = |dx: f32, dy: f32, z: f32, argb: u32, outline: bool| {
let mut pen = call.x;
for (i, ch) in call.text.chars().enumerate() {
let g = glyph(ch);
let x = pen + dx * g.offset;
let y = call.y + dy * g.offset;
let advance = g.advance + if bold { g.offset } else { 0.0 };
if let Some(cell) = g.rect {
// BakedSheetGlyph: the glyph's own box from the pen, not a
// fixed 8x8 cell (a Unifont glyph is 0.5 units per texel).
let (x0, x1) = (x + g.left, x + g.right);
let (top, bottom) = (y0 + g.top, y0 + g.bottom);
let corners = [(x0, bottom), (x1, bottom), (x1, top), (x0, top)];
let uvs = [
[cell.u0, cell.v1],
[cell.u1, cell.v1],
[cell.u1, cell.v0],
[cell.u0, cell.v0],
];
let c =
0xFF00_0000 | (argb & 255) << 16 | (argb >> 8 & 255) << 8 | (argb >> 16 & 255);
for ((cx, cy), uv) in corners.into_iter().zip(uvs) {
out.push(Vertex {
normal: [0; 4],
pos: call
.matrix
.transform_point3(Vec3::new(cx, cy, z))
.to_array(),
uv,
layer: 0,
color: c,
light: call.light,
glint_uv: [0.0, 0.0],
});
let thick = if bold { 0.1 } else { 0.0 };
let shear = |v| {
if call.style.is_italic() {
1.0 - 0.25 * v
} else {
0.0
}
};
for copy in 0..=usize::from(bold) {
let x0 = x + copy as f32 * g.offset + g.left - thick;
let x1 = x + copy as f32 * g.offset + g.right + thick;
let top = y + g.top - thick;
let bottom = y + g.bottom + thick;
quad(
[
(x0 + shear(g.bottom), bottom),
(x1 + shear(g.bottom), bottom),
(x1 + shear(g.top), top),
(x0 + shear(g.top), top),
],
cell,
z + copy as f32 * 0.001,
argb,
);
}
}
x += g.advance;
// `Font.prepare8xTextOutline` discards effects: only the front
// text gets underline/strikethrough, including over spaces.
if !outline && let Some(white) = white {
let x0 = if i == 0 { x - 1.0 } else { x };
for (enabled, top) in [
(call.style.strikethrough, 3.5),
(call.style.underlined, 8.0),
] {
if enabled == Some(true) {
quad(
[
(x0, y + top + 1.0),
(x + advance, y + top + 1.0),
(x + advance, y + top),
(x0, y + top),
],
white,
z + 0.01,
argb,
);
}
}
}
pen += advance;
}
};
if call.outline != 0 {
for (dx, dy) in [
(-1.0, -1.0),
(0.0, -1.0),
(1.0, -1.0),
(-1.0, 0.0),
(1.0, 0.0),
(-1.0, 1.0),
(0.0, 1.0),
(1.0, 1.0),
] {
emit(dx, dy, 0.0, call.outline);
for dx in -1..=1 {
for dy in -1..=1 {
if dx != 0 || dy != 0 {
emit(dx as f32, dy as f32, 0.0, call.outline, true);
}
}
}
}
let z = if call.outline != 0 {
TEXT_OVER_OUTLINE
} else {
0.0
};
emit(0.0, 0.0, z, call.color);
emit(
0.0,
0.0,
if call.outline != 0 {
TEXT_OVER_OUTLINE
} else {
0.0
},
call.color,
false,
);
}
/// Block-entity fields the level ticks, at one position (vanilla's own field
@@ -824,12 +932,16 @@ pub struct TextCall {
pub x: f32,
pub y: f32,
pub text: String,
pub style: Style,
/// ARGB.
pub color: u32,
/// ARGB outline colour, 0 for none.
pub outline: u32,
/// Horizon packed light (sky*16 | block*16 << 8).
pub light: u32,
/// HDR emission tier of its glyphs (`mesher::hdr_tier`), 0 for none: glowing sign text
/// shines in HDR output and in shader packs (`horizonTier`), as full-bright text does in SDR.
pub tier: u8,
}
/// Inputs from the game for one frame of block-entity rendering.
@@ -868,6 +980,8 @@ pub struct Scene<'a> {
pub skull: &'a dyn Fn([i32; 3]) -> Option<String>,
/// Font glyph: atlas cell (none for blank glyphs such as the space), box and advance.
pub glyph: &'a dyn Fn(char) -> Glyph,
/// Font providers, effects and random glyphs for styled sign text.
pub font: Option<&'a Font>,
/// Changes whenever `glyph` may answer differently (`Font::epoch`): sign
/// text laid out under another value is laid out again.
pub glyphs: u64,
@@ -1029,6 +1143,7 @@ struct SignTextEntry {
/// Game time it was last drawn.
used: i64,
vertices: Vec<Vertex>,
animated: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct StaticKey {
@@ -2335,9 +2450,11 @@ impl BlockEntityRenderer {
let entry = match signs.entry(pos) {
std::collections::hash_map::Entry::Occupied(mut occupied) => {
let entry = occupied.get_mut();
let changed = entry.data != *data;
if (entry.state, entry.packed, entry.near, entry.glyphs)
!= (state, packed, near, glyphs)
|| entry.data != *data
|| changed
|| entry.animated
{
entry.vertices.clear();
self.render_one(
@@ -2352,7 +2469,10 @@ impl BlockEntityRenderer {
&mut entry.vertices,
);
entry.state = state;
entry.data.clone_from(data);
if changed {
entry.animated = sign_text_animated(data);
entry.data.clone_from(data);
}
entry.packed = packed;
entry.near = near;
entry.glyphs = glyphs;
@@ -2373,6 +2493,7 @@ impl BlockEntityRenderer {
&mut vertices,
);
vacant.insert(SignTextEntry {
animated: sign_text_animated(data),
state,
data: data.clone(),
packed,
@@ -3407,7 +3528,9 @@ impl BeamPart {
(at(b, self.end), [s.u0, v1]),
] {
out.push(Vertex {
normal: [0; 4],
// The beacon beam render types keep the default back-face culling: seen
// from inside a beam, its walls must not show.
normal: CULL_BACK,
pos: p.to_array(),
uv,
layer: 6,
@@ -3610,6 +3733,7 @@ mod tests {
skull: &|_| None,
glyph: &|_| Glyph::default(),
glyphs: 0,
font: None,
dynamic_visible: None,
};
// Compare the cached world path with the original model renderer,
@@ -3772,6 +3896,105 @@ mod tests {
// Straight down: ambient only.
assert!((diffuse(-Vec3::Y, lights) - 0.4).abs() < 1e-6);
}
/// A face's colour carries its tint alone and its normal carries the
/// shading: the entity shader applies `minecraft_mix_light` from it, as a
/// shader pack's lighting does, so a baked shade would be applied twice.
#[test]
fn a_face_keeps_its_tint_and_normal_for_the_shader_to_shade() {
let quad = |y: f32, z0: f32, z1: f32| {
[
[0.0, y, z0, 0.0, 0.0],
[0.0, y, z1, 0.0, 1.0],
[16.0, y, z1, 1.0, 1.0],
[16.0, y, z0, 1.0, 0.0],
]
};
// Opposite windings: the first faces up, the second down.
let part = ModelPart {
pose: [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0],
polygons: vec![quad(0.0, 0.0, 16.0), quad(16.0, 16.0, 0.0)],
children: vec![],
};
let mut out = Vec::new();
draw_part(
&part,
"root",
Mat4::IDENTITY,
&|_, _| {},
Paint {
sprite: Rect {
u0: 0.0,
v0: 0.0,
u1: 1.0,
v1: 1.0,
w: 16,
h: 16,
},
tint: 0x123456,
light: 0,
lights: diffuse_lights(false),
glow: None,
},
&mut out,
);
assert_eq!(out.len(), 8);
// The same tint on both faces: no directional shade baked in (`pack_color`
// stores the tint in the vertex colour's BGR byte order).
let tint = pack_color(0x12_3456, 255, 1.0);
assert!(out.iter().all(|v| v.color == tint));
let (up, down) = (&out[0], &out[4]);
assert_eq!(up.normal, [0, 127, 0, 127]);
assert_eq!(down.normal, [0, -127, 0, 127]);
// The shader still gets the difference the old baked colours held.
let lights = diffuse_lights(false);
let up_normal = Vec3::new(0.0, f32::from(up.normal[1]) / 127.0, 0.0);
let down_normal = Vec3::new(0.0, f32::from(down.normal[1]) / 127.0, 0.0);
assert!(diffuse(up_normal, lights) > diffuse(down_normal, lights));
}
/// Lights the entity shader does not know (an item preview's
/// `Lighting.Entry`) stay baked into the colour, and `normal.w` 0 tells
/// the shader to keep it: `game/item_draw` passes those for block-entity
/// items in inventories and hands.
#[test]
fn lights_other_than_the_levels_stay_baked() {
let part = ModelPart {
pose: [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0],
polygons: vec![[
[0.0, 0.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 16.0, 0.0, 1.0],
[16.0, 0.0, 16.0, 1.0, 1.0],
[16.0, 0.0, 0.0, 1.0, 0.0],
]],
children: vec![],
};
let lights = (Vec3::X, Vec3::Z);
let mut out = Vec::new();
draw_part(
&part,
"root",
Mat4::IDENTITY,
&|_, _| {},
Paint {
sprite: Rect {
u0: 0.0,
v0: 0.0,
u1: 1.0,
v1: 1.0,
w: 16,
h: 16,
},
tint: 0x12_3456,
light: 0,
lights,
glow: None,
},
&mut out,
);
let baked = pack_color(0x12_3456, 255, diffuse(Vec3::Y, lights));
assert!(out.iter().all(|v| v.color == baked));
assert!(out.iter().all(|v| v.normal == [0; 4]));
assert_ne!(baked, pack_color(0x12_3456, 255, 1.0));
}
#[test]
fn part_poses_compose_like_model_part() {
let part = ModelPart {
@@ -3905,6 +4128,7 @@ mod tests {
skull: &|_| None,
glyph: &glyph,
glyphs: 0,
font: None,
dynamic_visible: None,
};
let mut call = TextCall {
@@ -3912,9 +4136,11 @@ mod tests {
x: 0.0,
y: 0.0,
text: "Hi".into(),
style: Style::default(),
color: 0xFFFF_FF00,
outline: 0xFF33_3300,
light: 240 | 240 << 8,
tier: 0,
};
let mut out = Vec::new();
draw_text(&scene, &call, &mut out);
@@ -3978,6 +4204,7 @@ mod tests {
skull: &|_| None,
glyph: &glyph,
glyphs: 0,
font: None,
dynamic_visible: None,
};
let call = TextCall {
@@ -3985,9 +4212,11 @@ mod tests {
x: 10.0,
y: 2.0,
text: "ab".into(),
style: Style::default(),
color: 0xFF00_0000,
outline: 0,
light: 240 | 240 << 8,
tier: 0,
};
let mut out = Vec::new();
draw_text(&scene, &call, &mut out);
@@ -4007,6 +4236,83 @@ mod tests {
assert_eq!(boxes, [[10.0, 14.0, 2.0, 10.0], [15.0, 19.0, 2.0, 10.0]]);
}
#[test]
fn styled_sign_quads_follow_vanilla_glyph_and_effect_bounds() {
let white = Rect {
u0: 0.0,
v0: 0.0,
u1: 0.1,
v1: 0.1,
w: 1,
h: 1,
};
let glyph = |c| Glyph {
rect: (c != ' ').then_some(white),
left: 0.0,
right: 4.0,
top: 0.0,
bottom: 8.0,
advance: 5.0,
offset: 0.5,
};
let mut call = TextCall {
matrix: Mat4::IDENTITY,
x: 0.0,
y: 0.0,
text: "A ".into(),
color: 0xff123456,
outline: 0,
light: 240,
tier: crate::mesher::LOW,
style: Style {
bold: Some(true),
italic: Some(true),
underlined: Some(true),
strikethrough: Some(true),
..Default::default()
},
};
let mut out = Vec::new();
text_vertices(&glyph, Some(white), &call, &mut out);
// BakedSheetGlyph: bold expands by 0.1, italic shears by 1 - y/4,
// the second bold glyph is 0.5 along and 0.001 in front (Unihex).
assert_eq!(out.len(), 24);
for (got, expected) in out[0].pos.iter().zip([-1.1, 8.1, 0.0]) {
assert!((got - expected).abs() < 1e-6);
}
assert!((out[4].pos[0] - out[0].pos[0] - 0.5).abs() < 1e-6);
assert_eq!(out[4].pos[2], 0.001);
assert_eq!(out[8].pos, [-1.0, 4.5, 0.01]);
assert_eq!(out[12].pos, [-1.0, 9.0, 0.01]);
assert_eq!(out[21].pos, [11.0, 9.0, 0.01]); // underline crosses the space
assert!(out.iter().all(|v| v.color == 0xff563412 && v.light == 240));
call.outline = 0xff010203;
out.clear();
text_vertices(&glyph, Some(white), &call, &mut out);
// Eight outlines contain glyphs only; effects appear once, in front.
assert_eq!(out.len(), 8 * 8 + 24);
assert!((out[0].pos[0] - -1.6).abs() < 1e-6);
assert!(out[64..].iter().all(|v| v.pos[2] >= TEXT_OVER_OUTLINE));
// The glow tier reaches bold copies, outlines and decoration quads.
assert!(out.iter().all(|v| v.layer >> 16 == 1));
call.tier = 0;
out.clear();
text_vertices(&glyph, Some(white), &call, &mut out);
assert!(out.iter().all(|v| v.layer >> 16 == 0));
}
#[test]
fn styled_sign_clipping_counts_bold_width_and_preserves_resets() {
let line = Text::legacy("§lAB§r東Z");
let advance = |_: char, s: &Style| 5.0 + if s.is_bold() { 0.5 } else { 0.0 };
let shown = first_line(&line, 16.0, &advance);
assert_eq!(shown.plain(), "AB東");
assert_eq!(
shown.chars().map(|(_, s)| s.is_bold()).collect::<Vec<_>>(),
[true, true, false]
);
}
/// The front text calls of a sign whose every glyph advances 6.
fn front_calls(data: &SignData, near: bool) -> Vec<(String, f32, f32, u32, u32)> {
let world = World::new(BlockStateId(0), 1);
@@ -4035,6 +4341,7 @@ mod tests {
skull: &|_| None,
glyph: &glyph,
glyphs: 0,
font: None,
dynamic_visible: None,
};
sign_text_calls(&scene, data, &data.front, Mat4::IDENTITY, 0, near)
@@ -4174,7 +4481,9 @@ mod tests {
..changed.clone()
};
let wide = std::cell::Cell::new(6.0);
let glyph_reads = std::cell::Cell::new(0);
let glyph = |c: char| {
glyph_reads.set(glyph_reads.get() + 1);
let u = f32::from(c as u8 % 16) / 16.0;
let rect = Rect {
u0: u,
@@ -4217,6 +4526,7 @@ mod tests {
skull: &|_| None,
glyph: &glyph,
glyphs: 0,
font: None,
dynamic_visible: None,
};
let check = |scene: &Scene, pos: [i32; 3], state: u32| {
@@ -4260,6 +4570,23 @@ mod tests {
wide.set(5.0);
scene.glyphs = 1;
check(&scene, pos, 1);
// With the same game time and data, plain text reuses its vertices,
// but obfuscated text must ask the font for fresh glyphs each frame.
let mut vertices = Vec::new();
let reads = glyph_reads.get();
renderer.render(&scene, [(pos, 1)].iter(), &mut vertices);
assert_eq!(glyph_reads.get(), reads);
let animated = SignData {
front: text("§kSecret", false),
..Default::default()
};
let animated_sign = |_| Some(&animated);
scene.sign = &animated_sign;
renderer.render(&scene, [(pos, 0)].iter(), &mut vertices);
let reads = glyph_reads.get();
renderer.render(&scene, [(pos, 0)].iter(), &mut vertices);
assert!(glyph_reads.get() > reads);
scene.sign = &first_sign;
// Signs out of view for a while are dropped.
scene.game_time = 2 * SIGN_TEXT_KEEP;
check(&scene, pos, 0);
@@ -4267,16 +4594,45 @@ mod tests {
}
#[test]
fn first_line_wraps_on_bytes_without_splitting_a_char() {
let advance = |c: char| if c == ' ' { 4.0 } else { 6.0 };
assert_eq!(first_line("ab cd ef", 13.0, &advance), "ab");
assert_eq!(first_line("abcdef", 13.0, &advance), "ab");
assert_eq!(first_line("\u{6771}", 3.0, &advance), "\u{6771}");
assert_eq!(first_line("ab\ncd", 100.0, &advance), "ab");
let advance = |c: char, _: &Style| if c == ' ' { 4.0 } else { 6.0 };
assert_eq!(
first_line(&Text::literal("ab cd ef"), 13.0, &advance).plain(),
"ab"
);
assert_eq!(
first_line(&Text::literal("abcdef"), 13.0, &advance).plain(),
"ab"
);
assert_eq!(
first_line(&Text::literal("\u{6771}"), 3.0, &advance).plain(),
"\u{6771}"
);
assert_eq!(
first_line(&Text::literal("ab\ncd"), 100.0, &advance).plain(),
"ab"
);
}
/// `rendertype_end_portal` culls back faces, and `AbstractEndPortalRenderer`
/// submits the unit cube's two Y faces. Both are wound outward, so of each
/// face only the side looking at the camera draws: the portal shows one
/// surface from above and one from below, never both at once.
/// `RenderPipelines.BEACON_BEAM_OPAQUE` / `_TRANSLUCENT` keep the default
/// back-face culling, so a camera inside a beam sees no wall of it.
#[test]
fn beacon_beam_faces_cull_their_backs() {
let rect = Rect {
u0: 0.0,
v0: 0.0,
u1: 1.0,
v1: 1.0,
w: 16,
h: 16,
};
let mut out = Vec::new();
beacon_beam(Vec3::ZERO, &[(0xFFFFFF, 4)], 0.0, 1.0, rect, &mut out);
assert_eq!(out.len(), 32, "solid and glow, four quads each");
assert!(out.iter().all(|v| v.normal == CULL_BACK));
}
#[test]
fn the_end_portal_cube_is_one_sided() {
let mut out = Vec::new();
@@ -4358,6 +4714,7 @@ mod tests {
skull: &|_| None,
glyph: &|_| Glyph::default(),
glyphs: 0,
font: None,
dynamic_visible: None,
};
let mut verts = Vec::new();
@@ -4444,6 +4801,7 @@ mod tests {
skull: &|_| None,
glyph: &|_| Glyph::default(),
glyphs: 0,
font: None,
dynamic_visible: None,
};
// A closed single chest of `block` drawn by `renderer`: positions and UVs.
@@ -358,6 +358,7 @@ fn batched_callbacks_preserve_entity_ranges_and_allow_immediate_culling() {
skull: &|_| None,
glyph: &|_| Glyph::default(),
glyphs: 0,
font: None,
dynamic_visible: None,
};
// Reverse local-position order to check that cache indexing does not
@@ -447,6 +448,7 @@ fn static_cache_qualifies_local_positions_by_section_and_drops_old_entries() {
skull: &|_| None,
glyph: &|_| Glyph::default(),
glyphs: 0,
font: None,
dynamic_visible: None,
};
let options = CacheOptions {
@@ -502,6 +504,7 @@ fn gpu_static_cache_uses_section_local_chests_and_keeps_visibility_separate() {
skull: &|_| None,
glyph: &|_| Glyph::default(),
glyphs: 0,
font: None,
dynamic_visible: None,
};
let blocks = [([100, 70, -32], 0)];
@@ -689,6 +692,7 @@ fn sign_text_cache_can_be_disabled_and_reenabled_without_changing_output() {
skull: &|_| None,
glyph: &glyph,
glyphs: 0,
font: None,
dynamic_visible: None,
};
let blocks = [([0, 64, 0], 0)];
@@ -743,6 +747,7 @@ fn shulker_cache_tracks_settled_poses_and_does_not_freeze_opening_animation() {
skull: &|_| None,
glyph: &|_| Glyph::default(),
glyphs: 0,
font: None,
dynamic_visible: None,
};
let blocks = [([0, 64, 0], 0)];
@@ -845,6 +850,7 @@ fn shulker_cache_covers_all_colors_and_facing_directions() {
skull: &|_| None,
glyph: &|_| Glyph::default(),
glyphs: 0,
font: None,
dynamic_visible: None,
};
let options = CacheOptions {
@@ -900,6 +906,7 @@ fn cached_bells_keep_shaking_until_the_animation_ends() {
skull: &|_| None,
glyph: &|_| Glyph::default(),
glyphs: 0,
font: None,
dynamic_visible: None,
};
let blocks = [([0, 64, 0], 0)];
@@ -962,6 +969,7 @@ fn cached_closed_chests_match_the_direct_model_and_rebuild_when_opening() {
skull: &|_| None,
glyph: &|_| Glyph::default(),
glyphs: 0,
font: None,
dynamic_visible: None,
};
let blocks = [([0, 64, 0], 0)];
@@ -993,7 +1001,9 @@ fn cached_closed_chests_match_the_direct_model_and_rebuild_when_opening() {
scene.nether = true;
let nether_closed = render_cached(&scene);
assert_same_vertices(&nether_closed, &render_direct(&renderer, &scene, &blocks));
assert_vertices_differ(&closed_again, &nether_closed);
// The nether is the entity shader's flag now (`entity_light`'s `l1` and
// `globals.time.y`), not a baked colour, so the cached geometry serves both.
assert_same_vertices(&closed_again, &nether_closed);
}
#[test]
@@ -1058,6 +1068,7 @@ fn closed_chest_cache_covers_all_materials_and_atlas_reloads() {
skull: &|_| None,
glyph: &|_| Glyph::default(),
glyphs: 0,
font: None,
dynamic_visible: None,
};
let pos = [0, 64, 0];
@@ -1147,6 +1158,7 @@ fn double_chest_cache_bypasses_both_halves_while_either_lid_is_open() {
skull: &|_| None,
glyph: &|_| Glyph::default(),
glyphs: 0,
font: None,
dynamic_visible: None,
};
let pair = [([0, 64, 0], 0), ([1, 64, 0], 1)];
@@ -1223,6 +1235,7 @@ fn decorated_pot_cache_tracks_sherds_and_bypasses_wobble_animation() {
skull: &|_| None,
glyph: &|_| Glyph::default(),
glyphs: 0,
font: None,
dynamic_visible: None,
};
let blocks = [([0, 64, 0], 0)];
@@ -1302,6 +1315,7 @@ fn banner_cache_keeps_the_pole_static_while_the_flag_and_patterns_update() {
skull: &|_| None,
glyph: &|_| Glyph::default(),
glyphs: 0,
font: None,
dynamic_visible: None,
};
let blocks = [([0, 64, 0], 0)];
@@ -1369,6 +1383,7 @@ fn cached_wall_banner_poles_use_the_wall_model() {
skull: &|_| None,
glyph: &|_| Glyph::default(),
glyphs: 0,
font: None,
dynamic_visible: None,
};
let pos = [0, 64, 0];
@@ -1417,6 +1432,7 @@ fn copper_statue_cache_rebuilds_when_its_pose_state_changes() {
skull: &|_| None,
glyph: &|_| Glyph::default(),
glyphs: 0,
font: None,
dynamic_visible: None,
};
let options = CacheOptions {
@@ -1523,6 +1539,7 @@ fn copper_statue_cache_covers_weathering_waxing_and_all_poses() {
skull: &|_| None,
glyph: &|_| Glyph::default(),
glyphs: 0,
font: None,
dynamic_visible: None,
};
let options = CacheOptions {
@@ -0,0 +1,249 @@
//! Every block state's facts and looks by its id, for a client mod drawing blocks or a map off the main thread
//! (`native_block_states`; docs/native-mods.md): what the game knows of each state, what its model draws on each side
//! and the texture behind each of those sprites. A copy, so the game's own tables can change shape under it.
use crate::{
block_sprites::{BlockSprites, Lookup, SpriteRef},
streaming::WorkerContext,
};
use horizon_items::blocks::{BlockFacts, MAP_COLORS, RenderShape};
use horizon_world::{
block_collision::BlockCollisionRegistry, fluid::FluidState, registry::BlockStateId,
};
use std::{collections::HashMap, sync::Arc};
/// What one block state is, as a renderer or a map reads it.
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub struct BlockStateFacts {
/// Its block, without properties (`minecraft:grass_block`).
pub name: Arc<str>,
/// `BlockState.isAir`: air, cave air or void air.
pub air: bool,
/// No model draws it (render shape `INVISIBLE`): air, barriers, light, and water and lava, drawn as fluids.
pub invisible: bool,
/// `BlockState.getFluidState`: the fluid in it, a waterlogged block's and kelp's included; empty for none.
pub fluid: FluidState,
/// Its `waterlogged` property is true.
pub waterlogged: bool,
/// The light it gives, 0 to 15.
pub emission: u8,
/// `BlockState.getShape`, the outline the cursor picks, as block-local boxes
/// `[min_x, min_y, min_z, max_x, max_y, max_z]`: `[0, 0, 0, 1, 1, 1]` for a whole block, none for air. Without
/// the random offset of flowers and grass.
pub outline: Arc<[[f64; 6]]>,
/// `MapColor.col` (`0xRRGGBB`), its colour on a vanilla map; none for `MapColor.NONE`, which maps leave out.
pub map_color: Option<u32>,
/// It can collide at all: false for a state whose collision shape is always empty (air, flowers, water).
pub collides: bool,
}
// A mod builds its tables from these on a worker thread.
const _: () = {
const fn shareable<T: Send + Sync + 'static>() {}
shareable::<BlockStates>()
};
/// Every block state by its id: its facts, and what its model draws with the resources loaded when it was made.
#[derive(Clone)]
pub struct BlockStates {
states: Arc<[BlockStateFacts]>,
context: Arc<WorkerContext>,
/// Texture names by sprite layer, across every array.
textures: Arc<HashMap<u32, String>>,
split: u32,
}
impl BlockStates {
/// The states of `context`'s registry, with their `facts` and `collision`, and the sprites of `context`'s
/// resources at `split` layers to an array (`Renderer::sprite_split`).
pub fn new(
facts: &BlockFacts,
collision: &BlockCollisionRegistry<'_>,
context: Arc<WorkerContext>,
split: u32,
) -> Self {
let registry = context.assets.registry;
let states = facts
.states
.iter()
.enumerate()
.map(|(i, f)| {
let id = BlockStateId(i as u32);
BlockStateFacts {
name: f.name.clone(),
air: facts.is_air(id),
invisible: f.render_shape == RenderShape::Invisible,
fluid: registry
.state(id)
.and_then(|state| FluidState::from_block(state).ok())
.unwrap_or(FluidState::EMPTY),
waterlogged: f.waterlogged,
emission: f.emission,
outline: f.outline.clone(),
map_color: MAP_COLORS
.get(usize::from(f.map_color))
.copied()
.filter(|_| f.map_color != 0),
collides: collision.may_collide(id),
}
})
.collect();
// Vanilla's names go without their namespace, as `native_draw_cube` takes them.
let textures = context
.render
.sprite_layers
.iter()
.map(|(name, layer)| (*layer, name.trim_start_matches("minecraft:").to_owned()))
.collect();
Self {
states,
context,
textures: Arc::new(textures),
split: split.max(1),
}
}
/// How many states there are: their ids run from 0 to one less.
pub fn len(&self) -> usize {
self.states.len()
}
pub fn is_empty(&self) -> bool {
self.states.is_empty()
}
/// `state`'s facts; none for an id past the last state.
pub fn get(&self, state: BlockStateId) -> Option<&BlockStateFacts> {
self.states.get(state.0 as usize)
}
/// Every state with its id, in id order.
pub fn iter(&self) -> impl Iterator<Item = (BlockStateId, &BlockStateFacts)> {
self.states
.iter()
.enumerate()
.map(|(i, facts)| (BlockStateId(i as u32), facts))
}
/// What `state` draws on each side, as `native_block_sprites` says for a block by name; none for an id past the
/// last state.
pub fn sprites(&self, state: BlockStateId) -> Option<BlockSprites> {
self.get(state)?;
Some(Lookup::new(&self.context, self.split).state(state))
}
/// The texture `sprite` shows, by the name `native_texture_sprites` takes (`block/grass_block_top`; a mod's with
/// its namespace, `mymod:block/gear`).
pub fn texture(&self, sprite: SpriteRef) -> Option<&str> {
let layer = sprite
.array
.checked_mul(self.split)?
.checked_add(sprite.layer)?;
self.textures.get(&layer).map(String::as_str)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
assets::Assets, biome::Biomes, block_sprites::parse_state, light::LightTables,
mesher::MeshTables,
};
use horizon_assets::model::Direction;
use horizon_world::fluid::FluidKind;
use std::{path::PathBuf, sync::OnceLock};
/// Far fewer layers to an array than a GPU takes, so most sprites lie past array 0.
const SMALL_SPLIT: u32 = 64;
/// The game's own states from real prepared resources, loaded once for every test here.
fn states() -> &'static BlockStates {
static STATES: OnceLock<BlockStates> = OnceLock::new();
STATES.get_or_init(|| {
let root = PathBuf::from(
std::env::var_os("HORIZON_TEST_RESOURCES").expect("HORIZON_TEST_RESOURCES"),
);
let assets = Arc::new(Assets::load(&root).unwrap());
let render = Arc::new(assets.reload(&root).unwrap());
let facts = BlockFacts::load(&root, assets.registry).unwrap();
let collision = crate::collisions::load(&root, assets.registry).unwrap();
let visibility = crate::models::load_visibility(&root, assets.registry).unwrap();
let context = Arc::new(WorkerContext {
light: Arc::new(LightTables::load(&root, assets.registry, &facts).unwrap()),
tables: Arc::new(MeshTables::new(&assets, &render, &facts, visibility).unwrap()),
assets: assets.clone(),
render,
biomes: Arc::new(Biomes::load(&root).unwrap()),
});
BlockStates::new(&facts, &collision, context, SMALL_SPLIT)
})
}
fn facts(text: &str) -> &'static BlockStateFacts {
let states = states();
let id = parse_state(states.context.assets.registry, text).expect(text);
states.get(id).unwrap()
}
#[test]
#[ignore = "requires prepared resources in HORIZON_TEST_RESOURCES"]
fn a_grass_block_is_a_whole_solid_grass_coloured_block() {
let grass = facts("grass_block");
assert_eq!(&*grass.name, "minecraft:grass_block");
assert!(!grass.air && !grass.invisible && !grass.waterlogged);
assert_eq!(grass.fluid.kind(), FluidKind::Empty);
assert_eq!(grass.emission, 0);
assert_eq!(&*grass.outline, [[0.0, 0.0, 0.0, 1.0, 1.0, 1.0]]);
assert_eq!(grass.map_color, Some(0x7F_B238), "MapColor.GRASS");
assert!(grass.collides);
}
#[test]
#[ignore = "requires prepared resources in HORIZON_TEST_RESOURCES"]
fn air_and_light_are_not_drawn() {
let air = facts("air");
assert!(air.air && air.invisible && !air.collides);
assert!(air.outline.is_empty());
assert_eq!(air.map_color, None);
let light = facts("light[level=15]");
assert!(!light.air && light.invisible);
assert_eq!(light.emission, 15);
}
#[test]
#[ignore = "requires prepared resources in HORIZON_TEST_RESOURCES"]
fn a_slabs_outline_is_its_half() {
assert_eq!(
&*facts("stone_slab[type=bottom]").outline,
[[0.0, 0.0, 0.0, 1.0, 0.5, 1.0]]
);
assert_eq!(
&*facts("stone_slab[type=top]").outline,
[[0.0, 0.5, 0.0, 1.0, 1.0, 1.0]]
);
let wet = facts("stone_slab[type=bottom,waterlogged=true]");
assert!(wet.waterlogged && wet.collides);
assert_eq!(wet.fluid.kind(), FluidKind::Water);
assert!(wet.fluid.is_source());
}
#[test]
#[ignore = "requires prepared resources in HORIZON_TEST_RESOURCES"]
fn a_face_names_its_texture() {
let states = states();
let id = parse_state(states.context.assets.registry, "grass_block").unwrap();
let grass = states.sprites(id).unwrap();
let top = grass.sides[Direction::Up as usize][0].sprite;
assert_eq!(states.texture(top), Some("block/grass_block_top"));
let north = &grass.sides[Direction::North as usize];
assert_eq!(
states.texture(north[0].sprite),
Some("block/grass_block_side")
);
assert_eq!(
states.texture(north[1].sprite),
Some("block/grass_block_side_overlay")
);
}
}
+7 -7
View File
@@ -114,12 +114,8 @@ mod tests {
ambient_occlusion: true,
particle: None,
cube: None,
faces: vec![BakedFace {
layer: 0,
opaque: true,
translucent: false,
fusion: None,
quad: ModelQuad {
faces: vec![BakedFace::new(
ModelQuad {
positions: [
[0.0, 0.5, 0.0],
[0.0, 0.5, 1.0],
@@ -142,7 +138,11 @@ mod tests {
render_type: None,
shade: true,
},
}],
0,
true,
false,
None,
)],
}
}
#[test]
+58 -1
View File
@@ -387,8 +387,21 @@ impl BrewingStand {
out.brewed = true;
}
}
/// The `minecraft:brewing_fuel` providers of an item: (uses, speed_multiplier).
/// `ItemStack.get(BREWING_FUEL)`: the stack's providers (uses, speed_multiplier),
/// respecting component patches before falling back to the item's defaults.
pub fn brewing_fuel(items: &Items, stack: &ItemStack) -> Option<(Value, Value)> {
if let Some(data) = stack.data() {
if data.other("!minecraft:brewing_fuel").is_some() {
return None;
}
if let Some(own) = data.other("minecraft:brewing_fuel") {
let component = horizon_client_core::scoreboard::tag_to_json(own);
return Some((
component["uses"].clone(),
component["speed_multiplier"].clone(),
));
}
}
items.get(&stack.item).and_then(|i| i.brewing_fuel.clone())
}
@@ -396,6 +409,50 @@ pub fn brewing_fuel(items: &Items, stack: &ItemStack) -> Option<(Value, Value)>
mod tests {
use super::*;
#[test]
fn stack_fuel_controls_brew_duration_and_consumption() {
let items = Items::for_tests(Default::default());
let brewing = Brewing {
reagents: ["minecraft:nether_wart".to_owned()].into_iter().collect(),
inputs: HashSet::new(),
recipes: vec![BrewingRecipe {
id: "awkward".into(),
input: PotionIngredient {
items: ["minecraft:potion".to_owned()].into_iter().collect(),
potions: Some(["minecraft:water".to_owned()].into_iter().collect()),
},
reagent: PotionIngredient {
items: ["minecraft:nether_wart".to_owned()].into_iter().collect(),
potions: None,
},
output: ItemStack::new("minecraft:potion", 1).with_potion("minecraft:awkward"),
}],
};
let mut fuel = ItemStack::new("minecraft:stone", 2);
fuel.data_mut().set_loot_component(
"minecraft:brewing_fuel",
&serde_json::json!({"uses": 7, "speed_multiplier": 2.5}),
);
let mut stand = BrewingStand::new();
stand.items[0] = Some(ItemStack::new("minecraft:potion", 1).with_potion("minecraft:water"));
stand.items[3] = Some(ItemStack::new("minecraft:nether_wart", 1));
stand.items[4] = Some(fuel);
let providers = Providers::empty();
stand.tick(&brewing, &items, &providers);
assert_eq!((stand.brew_time, stand.fuel, stand.total_fuel), (160, 6, 7));
assert_eq!(stand.items[4].as_ref().unwrap().count, 1);
for _ in 1..160 {
assert!(!stand.tick(&brewing, &items, &providers).brewed);
}
assert!(stand.tick(&brewing, &items, &providers).brewed);
assert_eq!(
stand.items[0].as_ref().unwrap().potion(),
Some("minecraft:awkward")
);
assert_eq!(stand.fuel, 6);
assert_eq!(stand.items[4].as_ref().unwrap().count, 1);
}
#[test]
fn potion_colours_and_names() {
// Swiftness: speed colour 3402751 = 0x33EBFF.
+90 -4
View File
@@ -1188,11 +1188,27 @@ impl SuggestLayout {
};
}
/// A suggestion accepted by Tab or a click (`SuggestionsList.useSuggestion`).
#[derive(Debug, PartialEq, Eq)]
pub struct SuggestionCompletion {
/// The suggestion list's original input, also when cycling with Tab.
pub original: String,
/// The replaced range in `original`, as Java UTF-16 start/end offsets.
pub range: (usize, usize),
pub text: String,
/// The resulting input, after the text field's length limit.
pub input: String,
}
/// The open chat screen: vanilla's `ChatScreen` with its `EditBox` and
/// `CommandSuggestions` (argument colouring, usage lines, parse errors,
/// the suggestion list, Tab completion, history).
pub struct ChatScreen {
pub input: TextField,
/// `ChatScreen.isDraft`: the box opened with the draft saved last time and
/// has not been edited since. A draft draws grey italic, and the first
/// Backspace clears the whole box instead of one character.
is_draft: bool,
width: i32,
height: i32,
history_buffer: String,
@@ -1207,6 +1223,7 @@ pub struct ChatScreen {
/// the command up to the cursor.
request: Option<(i32, String)>,
suggestions: Option<SuggestionList>,
completion: Option<SuggestionCompletion>,
usage: Vec<Text>,
usage_position: i32,
usage_width: i32,
@@ -1300,11 +1317,13 @@ fn truncate_utf16(s: &str, max_units: usize) -> String {
s.to_owned()
}
impl ChatScreen {
/// `ChatScreen.init`: the input at the bottom, focused, holding `initial`.
/// `ChatScreen.init`: the input at the bottom, focused, holding `initial`,
/// which is a draft when `is_draft` (`ChatComponent.createScreen`).
pub fn new(
m: &Measure,
c: &Commands,
initial: &str,
is_draft: bool,
history_len: usize,
width: i32,
height: i32,
@@ -1317,6 +1336,7 @@ impl ChatScreen {
input.set_value(m, initial);
let mut s = Self {
input,
is_draft,
width,
height,
history_buffer: String::new(),
@@ -1326,6 +1346,7 @@ impl ChatScreen {
awaiting: None,
request: None,
suggestions: None,
completion: None,
usage: Vec::new(),
usage_position: 0,
usage_width: 0,
@@ -1353,6 +1374,7 @@ impl ChatScreen {
input.set_value(m, command);
let mut s = Self {
input,
is_draft: false,
width,
height,
history_buffer: String::new(),
@@ -1362,6 +1384,7 @@ impl ChatScreen {
awaiting: None,
request: None,
suggestions: None,
completion: None,
usage: Vec::new(),
usage_position: 0,
usage_width: 0,
@@ -1377,12 +1400,18 @@ impl ChatScreen {
pub fn value(&self) -> &str {
&self.input.value
}
/// Take the most recently accepted suggestion, at most once. Call after
/// each key or click to observe every acceptance, including Tab cycling.
pub fn take_suggestion_completion(&mut self) -> Option<SuggestionCompletion> {
self.completion.take()
}
/// Replace the input for a suggest-command link and refresh completions.
pub fn suggest_command(&mut self, m: &Measure, c: &Commands, command: &str) {
self.input.set_value(m, command);
self.edited(m, c);
}
/// `ChatScreen.resize`: a new screen with the current text.
/// `ChatScreen.resize`: a new screen with the current text, still a draft
/// while it has not been edited (`ChatScreen.resize` keeps the field).
pub fn resize(&mut self, m: &Measure, c: &Commands, width: i32, height: i32) {
if (width, height) != (self.width, self.height) {
let value = self.input.value.clone();
@@ -1390,7 +1419,16 @@ impl ChatScreen {
let abilities = self.abilities;
let message = std::mem::take(&mut self.input.message);
*self = if self.layout == SuggestLayout::CHAT {
Self::new(m, c, &value, self.history_pos, width, height, abilities)
Self::new(
m,
c,
&value,
self.is_draft,
self.history_pos,
width,
height,
abilities,
)
} else {
Self::command_block(m, c, &value, width, height)
};
@@ -1402,6 +1440,7 @@ impl ChatScreen {
fn edited(&mut self, m: &Measure, c: &Commands) {
self.allow_suggestions = true;
self.update_command_info(m, c);
self.is_draft = false;
}
/// Ctrl+A/C/V/X in the input box.
pub fn shortcut(
@@ -1415,6 +1454,24 @@ impl ChatScreen {
self.edited(m, c);
}
}
/// Edit the existing input and refresh its command responder when it changes.
pub fn edit_input(&mut self, m: &Measure, c: &Commands, f: impl FnOnce(&mut TextField)) {
let before = (
self.input.value.clone(),
self.input.cursor,
self.input.highlight,
);
f(&mut self.input);
if before
!= (
self.input.value.clone(),
self.input.cursor,
self.input.highlight,
)
{
self.edited(m, c);
}
}
/// Typed text (`charTyped` per character).
pub fn type_text(&mut self, m: &Measure, c: &Commands, text: &str) {
for ch in text.chars() {
@@ -1714,6 +1771,17 @@ impl ChatScreen {
self.select(l, l.current as isize);
self.keep_suggestions = false;
l.tab_cycles = true;
let units = |byte: usize| {
l.original[..byte.min(l.original.len())]
.encode_utf16()
.count()
};
self.completion = Some(SuggestionCompletion {
original: l.original.clone(),
range: (units(s.range.0), units(s.range.1)),
text: s.text,
input: self.input.value.clone(),
});
}
/// `ChatScreen.keyPressed`.
pub fn key(
@@ -1729,6 +1797,14 @@ impl ChatScreen {
if self.suggestions_key(m, c, key, shift) {
return ChatAction::None;
}
// ChatScreen.keyPressed: while the box holds an unedited draft, the
// first Backspace clears all of it rather than one character.
if self.is_draft && key == K::Backspace {
self.input.set_value(m, "");
self.is_draft = false;
self.edited(m, c);
return ChatAction::None;
}
match key {
K::Escape => return ChatAction::Close,
K::Enter | K::NumpadEnter => {
@@ -1862,8 +1938,13 @@ impl ChatScreen {
l.offset = ((l.offset as f64 - amount.clamp(-1.0, 1.0)) as i32).clamp(0, max) as usize;
true
}
/// `CommandSuggestions.formatText`: argument colours over the input.
/// `CommandSuggestions.formatText`: argument colours over the input. The
/// draft formatter (`ChatScreen.formatChat`) is registered before this one
/// and wins while the draft is unedited: the whole text is grey italic.
fn format(&self, value: &str, offset: usize) -> Text {
if self.is_draft {
return Text::literal(value).with_color(text::GRAY).italic();
}
let gray = Style::color(text::GRAY);
let Some(parse) = &self.parse else {
return Text::literal(value);
@@ -1990,6 +2071,11 @@ impl ChatScreen {
for i in 0..limit {
let y = ry + 12 * i as i32;
d::fill(out, rx, y, rx + rw, y + 12, self.layout.fill);
// A row for the pad: nothing else can pick a suggestion without Tab.
crate::widgets::target_as(
[rx as f32, y as f32, rw as f32, 12.0],
crate::widgets::Kind::Row,
);
if mouse.0 > rx && mouse.0 < rx + rw && mouse.1 > y && mouse.1 < y + 12 {
if moved {
self.select(l, (i + l.offset) as isize);
+193 -7
View File
@@ -7,6 +7,85 @@ fn full() -> ChatAbilities {
ChatAbilities::new(ChatVisibility::Full)
}
#[test]
fn accepted_suggestions_report_utf16_ranges_and_tab_cycles() {
use winit::keyboard::KeyCode as K;
let font = font();
let m = Measure(&font);
let registries = Registries {
players: vec!["Steve".into(), "Steven".into()],
..Registries::default()
};
let c = Commands {
dispatcher: crate::command::vanilla::game_dispatcher(),
registries: &registries,
};
let mut screen = ChatScreen::new(&m, &c, "", false, 0, 640, 360, full());
screen.type_text(&m, &c, "😀 hi st");
assert!(screen.take_suggestion_completion().is_none());
screen.key(&m, &c, &[], K::Tab, false, false);
assert!(
screen.take_suggestion_completion().is_none(),
"opening is not accepting"
);
for (shift, text) in [(false, "Steve"), (false, "Steven"), (true, "Steve")] {
screen.key(&m, &c, &[], K::Tab, shift, false);
assert_eq!(screen.value(), format!("😀 hi {text}"));
assert_eq!(
screen.take_suggestion_completion(),
Some(SuggestionCompletion {
original: "😀 hi st".into(),
range: (6, 8),
text: text.into(),
input: format!("😀 hi {text}"),
})
);
assert!(
screen.take_suggestion_completion().is_none(),
"reported once"
);
}
screen.key(&m, &c, &[], K::ArrowDown, false, false);
assert!(
screen.take_suggestion_completion().is_none(),
"selection is not accepting"
);
screen.key(&m, &c, &[], K::Escape, false, false);
assert!(screen.take_suggestion_completion().is_none());
}
#[test]
fn clicked_suggestion_reports_the_actual_length_limited_input() {
use winit::keyboard::KeyCode as K;
let font = font();
let m = Measure(&font);
let registries = Registries {
players: vec!["Steve".into()],
..Registries::default()
};
let c = Commands {
dispatcher: crate::command::vanilla::game_dispatcher(),
registries: &registries,
};
let mut screen = ChatScreen::new(&m, &c, "", false, 0, 640, 360, full());
screen.input.max_length = 6;
screen.type_text(&m, &c, "hi st");
screen.key(&m, &c, &[], K::Tab, false, false);
let [x, y, _, _] = screen.suggestions.as_ref().unwrap().rect;
assert!(screen.click(&m, &c, f64::from(x + 1), f64::from(y + 1)));
assert_eq!(screen.value(), "hi Ste");
assert_eq!(
screen.take_suggestion_completion(),
Some(SuggestionCompletion {
original: "hi st".into(),
range: (3, 5),
text: "Steve".into(),
input: "hi Ste".into(),
})
);
assert!(screen.take_suggestion_completion().is_none());
}
fn font() -> Font {
// The missing-glyph metrics let these tests run without game assets.
Font::load(
@@ -201,7 +280,7 @@ fn locate_coordinates_fill_chat_with_teleport_without_submitting() {
assert_eq!(link.component()["click_event"]["command"], expected);
let mut chat = Chat::default();
add(&mut chat, &font, Text::literal("Found at ").append(link));
let mut screen = ChatScreen::new(&m, &commands, "draft", 0, 640, 360, full());
let mut screen = ChatScreen::new(&m, &commands, "draft", false, 0, 640, 360, full());
let x = 4.0 + f64::from(font.width("Found at "));
assert!(!screen.click(&m, &commands, x, 315.0));
let Some(ClickEvent::SuggestCommand(command)) = chat.click_at(&font, 360, x, 315.0) else {
@@ -242,6 +321,7 @@ fn history_arrows_recall_messages_and_restore_the_draft() {
&m,
&commands,
"unfinished draft",
false,
history.len(),
640,
360,
@@ -263,6 +343,93 @@ fn history_arrows_recall_messages_and_restore_the_draft() {
}
}
/// `ChatScreen.isDraft`: the box opens on the draft kept from last time
/// (`ChatComponent.createScreen` passes `true`, which `ChatScreen.init`
/// stores), so the text draws grey italic through `ChatScreen.formatChat`
/// and the first Backspace clears the whole draft instead of one character.
/// Editing the draft (`onEdited`) ends both.
#[test]
fn a_restored_draft_is_grey_italic_and_one_backspace_clears_it() {
use winit::keyboard::KeyCode::Backspace;
let font = font();
let m = Measure(&font);
let registries = Registries::default();
let c = Commands {
dispatcher: crate::command::vanilla::game_dispatcher(),
registries: &registries,
};
// The input's own text as the game draws it: x 4, y height - 12.
let input_text = |screen: &mut ChatScreen| -> Text {
screen
.draw(&m, (0, 0), 0, i32::MIN)
.into_iter()
.find_map(|draw| match draw {
Draw::Text {
text, x: 4, y: 348, ..
} => Some(text),
_ => None,
})
.expect("the input draws its text")
};
let draft_style = |t: &Text| {
t.spans
.iter()
.all(|s| s.style.color == Some(text::GRAY) && s.style.is_italic())
};
let mut screen = ChatScreen::new(&m, &c, "half-written", true, 0, 640, 360, full());
assert_eq!(screen.value(), "half-written");
assert!(
draft_style(&input_text(&mut screen)),
"the restored draft draws grey italic"
);
// Typing into the draft (`onEdited`) ends the draft style, and Backspace
// eats one character again.
screen.type_text(&m, &c, "!");
assert_eq!(screen.value(), "half-written!");
let typed = input_text(&mut screen);
assert!(
!typed.spans.iter().any(|s| s.style.is_italic()),
"an edited draft is not grey italic"
);
assert_eq!(
screen.key(&m, &c, &[], Backspace, false, false),
ChatAction::None
);
assert_eq!(screen.value(), "half-written");
// A draft left alone clears whole on its first Backspace.
let mut screen = ChatScreen::new(&m, &c, "half-written", true, 0, 640, 360, full());
assert_eq!(
screen.key(&m, &c, &[], Backspace, false, false),
ChatAction::None
);
assert_eq!(screen.value(), "", "the first Backspace clears the draft");
// The flag is gone with it: typing is ordinary text, and Backspace eats
// one character.
screen.type_text(&m, &c, "hi");
assert_eq!(screen.value(), "hi");
let typed = input_text(&mut screen);
assert!(!typed.spans.iter().any(|s| s.style.is_italic()));
assert_eq!(
screen.key(&m, &c, &[], Backspace, false, false),
ChatAction::None
);
assert_eq!(screen.value(), "h");
// A box opened on ordinary text deletes one character at a time.
let mut screen = ChatScreen::new(&m, &c, "half-written", false, 0, 640, 360, full());
assert!(!draft_style(&input_text(&mut screen)));
assert_eq!(
screen.key(&m, &c, &[], Backspace, false, false),
ChatAction::None
);
assert_eq!(screen.value(), "half-writte");
// A command draft is grey italic as a whole: `formatChat` is registered
// before `CommandSuggestions.formatText`, so it wins over the argument
// colours.
let mut screen = ChatScreen::new(&m, &c, "/give @s stone", true, 0, 640, 360, full());
assert!(draft_style(&input_text(&mut screen)));
}
#[test]
fn mod_commands_are_coloured_and_complete_like_vanilla_ones() {
let font = font();
@@ -283,7 +450,7 @@ fn mod_commands_are_coloured_and_complete_like_vanilla_ones() {
dispatcher,
registries: &registries,
};
let mut screen = ChatScreen::new(&m, &c, "", 0, 640, 360, full());
let mut screen = ChatScreen::new(&m, &c, "", false, 0, 640, 360, full());
screen.type_text(&m, &c, line);
screen
};
@@ -333,7 +500,7 @@ fn chat_completes_the_servers_custom_completions_with_the_players() {
dispatcher,
registries,
};
let mut screen = ChatScreen::new(&m, &c, "", 0, 640, 360, full());
let mut screen = ChatScreen::new(&m, &c, "", false, 0, 640, 360, full());
screen.type_text(&m, &c, line);
// Chat shows no list by itself: Tab shows it, Tab again uses it.
for _ in 0..2 {
@@ -381,7 +548,7 @@ fn ask_server_nodes_wait_for_the_servers_completions() {
let tab = |screen: &mut ChatScreen| {
screen.key(&m, &c, &[], winit::keyboard::KeyCode::Tab, false, false);
};
let mut screen = ChatScreen::new(&m, &c, "", 0, 640, 360, full());
let mut screen = ChatScreen::new(&m, &c, "", false, 0, 640, 360, full());
screen.type_text(&m, &c, "/bossbar get pt");
let (request, sent) = screen.take_request().expect("the server is asked");
assert_eq!(sent, "/bossbar get pt");
@@ -395,15 +562,25 @@ fn ask_server_nodes_wait_for_the_servers_completions() {
tab(&mut screen);
assert_eq!(screen.value(), "/bossbar get pt");
screen.server_suggestions(&m, &c, request, (13, 2), reply("pt6:check"));
assert!(screen.take_suggestion_completion().is_none());
tab(&mut screen);
assert_eq!(screen.value(), "/bossbar get pt6:check");
assert_eq!(
screen.take_suggestion_completion(),
Some(SuggestionCompletion {
original: "/bossbar get pt".into(),
range: (13, 15),
text: "pt6:check".into(),
input: "/bossbar get pt6:check".into(),
})
);
// Without the server's provider the local list is done at once.
let local = crate::command::vanilla::game_dispatcher();
let c = Commands {
dispatcher: local,
registries: &registries,
};
let mut screen = ChatScreen::new(&m, &c, "", 0, 640, 360, full());
let mut screen = ChatScreen::new(&m, &c, "", false, 0, 640, 360, full());
screen.type_text(&m, &c, "/bossbar get pt");
assert_eq!(screen.take_request(), None);
}
@@ -473,7 +650,7 @@ fn the_chat_option_decides_what_may_be_sent() {
assert_eq!(abilities.allows("hi"), chat, "{visibility:?}");
assert_eq!(abilities.allows("/time set day"), command, "{visibility:?}");
for (line, allowed) in [("hi", chat), ("/time set day", command)] {
let mut screen = ChatScreen::new(&m, &c, "", 0, 640, 360, abilities);
let mut screen = ChatScreen::new(&m, &c, "", false, 0, 640, 360, abilities);
screen.type_text(&m, &c, line);
match enter(&mut screen) {
ChatAction::Submit(sent) => {
@@ -499,7 +676,16 @@ fn restricted_input_says_why_it_will_not_send() {
registries: &registries,
};
let usage = |visibility, line: &str| {
let mut screen = ChatScreen::new(&m, &c, "", 0, 640, 360, ChatAbilities::new(visibility));
let mut screen = ChatScreen::new(
&m,
&c,
"",
false,
0,
640,
360,
ChatAbilities::new(visibility),
);
screen.type_text(&m, &c, line);
screen.usage.iter().map(|t| t.plain()).collect::<Vec<_>>()
};
+43
View File
@@ -225,6 +225,8 @@ pub const BIOME_STRATEGY: Strategy = Strategy::biomes(8);
/// (what vanilla servers and clients send), more when content mods add states
/// past 65,536 (docs/modpacks.md). Set once, before the first chunk.
static BLOCK_BITS: std::sync::atomic::AtomicU8 = std::sync::atomic::AtomicU8::new(16);
/// States in the registry, set with [`BLOCK_BITS`].
static BLOCK_STATES: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(u32::MAX);
/// Block states are `PalettedContainer`s over the registry: vanilla's 16-bit
/// global palette, wider with enough content mods.
@@ -232,6 +234,35 @@ pub fn block_strategy() -> Strategy {
Strategy::blocks(BLOCK_BITS.load(std::sync::atomic::Ordering::Relaxed))
}
/// Whether the registry has this state. Every id is known until the registry is loaded.
pub fn known_block_state(id: u32) -> bool {
id < BLOCK_STATES.load(std::sync::atomic::Ordering::Relaxed)
}
/// A server's block container in our palette width. Ids past our registry (a
/// server or host with mods we lack) read as air: every per-state table is
/// indexed by them, and a palette's unused entries are scanned too.
pub fn known_blocks_only(blocks: PalettedContainer) -> PalettedContainer {
let ours = block_strategy();
let unknown = |id: u32| !known_block_state(id) || u64::from(id) >> ours.global_bits != 0;
let any_unknown = if blocks.is_global() {
blocks.has(unknown)
} else {
blocks.maybe_has(unknown)
};
if !any_unknown && blocks.strategy().global_bits == ours.global_bits {
return blocks;
}
let mut values = [0u32; 4096];
blocks.unpack(&mut values);
for value in &mut values {
if unknown(*value) {
*value = 0;
}
}
PalettedContainer::from_values(ours, &values)
}
/// Frees `columns` (`count` of them) on a short-lived thread of its own
/// when there are many: a teleport or a dimension change drops a few
/// thousand columns at once, and freeing their sections took the frame
@@ -250,6 +281,10 @@ pub fn free_columns<T: Send + 'static>(columns: T, count: usize) {
pub fn set_block_states(states: usize) {
let bits = horizon_core::paletted::ceil_log2(states).max(16);
BLOCK_BITS.store(bits, std::sync::atomic::Ordering::Relaxed);
BLOCK_STATES.store(
u32::try_from(states).unwrap_or(u32::MAX),
std::sync::atomic::Ordering::Relaxed,
);
forget_shared();
}
@@ -585,6 +620,14 @@ impl Chunk {
entities: Vec::new(),
}
}
/// The original `structures.starts`, including starts with unknown pieces.
pub fn saved_structure_starts(&self) -> Option<&horizon_world::nbt::Compound> {
let structures = self.saved.as_ref()?.root.get(&"structures".into())?;
match structures.get("starts")? {
horizon_world::nbt::Tag::Compound(starts) => Some(starts),
_ => None,
}
}
/// The minimum block Y of the column.
#[inline]
pub fn min_y(&self) -> i32 {
@@ -11,7 +11,7 @@ use horizon_world::{
use std::{
collections::HashMap,
sync::{
Arc,
Arc, Mutex, OnceLock, PoisonError,
atomic::{AtomicU64, Ordering},
},
};
@@ -59,6 +59,9 @@ pub enum BlockEntity {
CreakingHeart(Box<CreakingHeart>),
/// `BannerBlockEntity`: the pattern layers.
Banner(Banner),
/// `CopperGolemStatueBlockEntity`: its own components, which hold the
/// custom name of the golem it was (`createStatue`).
CopperGolemStatue(Compound),
/// `CrafterBlockEntity`.
Crafter(Crafter),
/// `SpawnerBlockEntity` (`crate::mobs::spawner`).
@@ -108,6 +111,9 @@ pub enum BlockEntity {
pub struct ModBlockEntity {
fields: ModBlockEntityFields,
revision: ModRevision,
// A client update can consume transient effects without changing saved
// fields. Most entities never use this; do not allocate their queue.
client_update_fields: OnceLock<Mutex<Compound>>,
}
/// What a `ModBlockEntity` holds: its type id (`mymod:machine`), its saved
/// compound without `id`, `x`, `y`, `z` and `Items`, its item slots
@@ -138,11 +144,55 @@ impl ModBlockEntity {
born: MOD_BLOCK_ENTITIES_MADE.fetch_add(1, Ordering::Relaxed),
changes: 0,
},
client_update_fields: OnceLock::new(),
}
}
pub fn revision(&self) -> ModRevision {
self.revision
}
/// Replace the owner's pending, one-shot client update fields. These
/// fields are not saved, cloned, compared or retained for drawing, and
/// changing them does not change this entity's revision. The owner must
/// explicitly take and merge them into its update tag. Calling this does
/// not schedule a block update.
///
/// An empty batch clears a pending batch without allocating an idle queue.
pub fn set_client_update_fields(&mut self, fields: Compound) {
if fields.is_empty() {
self.clear_client_update_fields();
return;
}
*self
.client_update_fields
.get_or_init(|| Mutex::new(Compound::new()))
.lock()
.unwrap_or_else(PoisonError::into_inner) = fields;
}
/// Discard the owner's pending client update fields, without changing
/// saved data, revision or a queue that has never been used.
pub fn clear_client_update_fields(&mut self) {
if let Some(fields) = self.client_update_fields.get() {
fields
.lock()
.unwrap_or_else(PoisonError::into_inner)
.clear();
}
}
/// Atomically take the owner's pending fields at update-tag construction.
/// A loaded chunk and a block change use this same boundary. Construct a
/// tag once, then send that tag to every recipient; another construction
/// sees an empty batch. A save or a clone does not consume this batch.
///
/// This is safe for an update-tag hook receiving only a shared reference,
/// including a saved chunk encoded by a worker thread. If a previous lock
/// holder panicked, preserve its pending data instead of dropping it.
pub fn take_client_update_fields(&self) -> Compound {
self.client_update_fields
.get()
.map_or_else(Compound::new, |fields| {
std::mem::take(&mut *fields.lock().unwrap_or_else(PoisonError::into_inner))
})
}
/// A client's copy loads the tag the server sent (`loadWithComponents`
/// in `ClientPacketListener.handleBlockEntityData`): `tag`'s fields
/// replace its own of the same names. One it already has as sent is
@@ -706,6 +756,85 @@ pub struct CommandBlock {
mod tests {
use super::*;
#[test]
fn transient_client_update_fields_are_lazy_replaceable_and_drained_once() {
let mut machine = ModBlockEntity::new(ModBlockEntityFields {
id: "test:machine".into(),
nbt: Compound::from([("progress".into(), Tag::Int(7))]),
..Default::default()
});
let revision = machine.revision();
let saved = machine.fields.clone();
let debug = format!("{machine:?}");
machine.clear_client_update_fields();
machine.set_client_update_fields(Compound::new());
assert!(machine.take_client_update_fields().is_empty());
assert!(machine.client_update_fields.get().is_none());
machine.set_client_update_fields(Compound::from([("discarded".into(), Tag::Int(1))]));
let expected = Compound::from([("effects".into(), Tag::List(vec![Tag::Int(2)]))]);
machine.set_client_update_fields(expected.clone());
assert_eq!(machine.take_client_update_fields(), expected);
assert!(machine.take_client_update_fields().is_empty());
machine.set_client_update_fields(expected.clone());
let copy = machine.clone();
assert_eq!(machine, copy, "transient fields are not retained contents");
assert_ne!(machine.revision(), copy.revision());
assert!(copy.client_update_fields.get().is_none());
assert!(copy.take_client_update_fields().is_empty());
assert_eq!(machine.take_client_update_fields(), expected);
machine.set_client_update_fields(expected);
machine.clear_client_update_fields();
assert!(machine.take_client_update_fields().is_empty());
assert_eq!(machine.fields, saved);
assert_eq!(machine.revision(), revision);
assert_eq!(format!("{machine:?}"), debug);
}
#[test]
fn concurrent_client_update_constructions_consume_one_batch() {
let mut machine = ModBlockEntity::default();
let expected = Compound::from([("effects".into(), Tag::Int(23))]);
machine.set_client_update_fields(expected.clone());
let received = std::thread::scope(|scope| {
let handles = (0..16)
.map(|_| scope.spawn(|| machine.take_client_update_fields()))
.collect::<Vec<_>>();
handles
.into_iter()
.map(|h| h.join().unwrap())
.collect::<Vec<_>>()
});
let nonempty = received
.into_iter()
.filter(|c| !c.is_empty())
.collect::<Vec<_>>();
assert_eq!(nonempty, [expected]);
}
#[test]
fn a_poisoned_client_update_lock_preserves_its_pending_batch() {
let mut machine = ModBlockEntity::default();
let expected = Compound::from([("effects".into(), Tag::Int(23))]);
machine.set_client_update_fields(expected.clone());
std::thread::scope(|scope| {
assert!(
scope
.spawn(|| {
let _held = machine.client_update_fields.get().unwrap().lock().unwrap();
panic!("interrupt before consuming the queued batch");
})
.join()
.is_err()
);
});
assert_eq!(machine.take_client_update_fields(), expected);
assert!(machine.take_client_update_fields().is_empty());
machine.set_client_update_fields(expected.clone());
assert_eq!(machine.take_client_update_fields(), expected);
}
#[test]
fn a_mod_block_entitys_revision_moves_with_each_change_and_only_then() {
let mut machine = ModBlockEntity::new(ModBlockEntityFields {
@@ -0,0 +1,143 @@
//! The files of the resources the game loaded, resource packs' over vanilla's, for a client mod reading textures and
//! other resources on any thread (`native_client_resources`; docs/native-mods.md).
use horizon_core::prepared_files;
use std::path::PathBuf;
/// A texture's pixels: `width` by `height` RGBA, rows top first, as its PNG stores them (gamma-encoded, alpha not
/// premultiplied). An animated texture is its frames one under another, as in its file.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TexturePixels {
pub width: u32,
pub height: u32,
pub rgba: Vec<u8>,
}
// A mod reads textures on a worker thread.
const _: () = {
const fn shareable<T: Send + Sync + 'static>() {}
shareable::<ClientResources>()
};
/// The resources the game loaded, as one tree: vanilla's, with the resource packs turned on (a higher pack over a
/// lower one) and the content mods' files over it. A pack turned on or off makes a new tree: ask for it again then.
/// It reads the files on disk at each call, not a copy: while a reload rebuilds them, a read can miss a file or get
/// the new pack's before `native_block_sprites_version()` changes.
#[derive(Clone, Debug)]
pub struct ClientResources {
root: PathBuf,
}
impl ClientResources {
/// The tree at `root` (the game's `options.resources`).
pub fn new(root: PathBuf) -> Self {
Self { root }
}
/// The bytes of the resource at `path` in the tree (`assets/minecraft/textures/entity/cow/cow_temperate.png`).
/// None when there is no such file, or for a path that isn't plain names under `assets/`.
pub fn read(&self, path: &str) -> Option<Vec<u8>> {
let plain = |p: &str| !p.is_empty() && p != "." && p != ".." && !p.contains('\\');
if !path.starts_with("assets/") || !path.split('/').all(plain) {
return None;
}
prepared_files::read(&self.root.join(path)).ok()
}
/// A texture's pixels by name (`block/stone`, `entity/cow/cow_temperate`, a mod's `mymod:item/gear`): the PNG
/// under `assets/<namespace>/textures/`, decoded. None when there is no such PNG or it doesn't decode.
pub fn texture(&self, name: &str) -> Option<TexturePixels> {
let path = crate::atlas_sources::texture_path(&self.root, name, "textures");
let (width, height, rgba) =
crate::hud::decode_bytes(&prepared_files::read(&path).ok()?).ok()?;
Some(TexturePixels {
width,
height,
rgba,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::packs::{build_overlay, discover};
use std::{fs, path::Path};
const COW: &str = "assets/minecraft/textures/entity/cow/cow_temperate.png";
const RED: [u8; 4] = [255, 0, 0, 255];
const BLUE: [u8; 4] = [0, 0, 255, 255];
fn one_pixel_png(rgba: [u8; 4]) -> Vec<u8> {
let mut out = Vec::new();
let mut encoder = png::Encoder::new(&mut out, 1, 1);
encoder.set_color(png::ColorType::Rgba);
encoder.set_depth(png::BitDepth::Eight);
encoder
.write_header()
.unwrap()
.write_image_data(&rgba)
.unwrap();
out
}
fn write(path: &Path, bytes: &[u8]) {
fs::create_dir_all(path.parent().unwrap()).unwrap();
fs::write(path, bytes).unwrap();
}
#[test]
fn a_resource_pack_overrides_a_texture() {
let base =
std::env::temp_dir().join(format!("horizon-client-resources-{}", std::process::id()));
let _ = fs::remove_dir_all(&base);
let vanilla = base.join("vanilla");
write(&vanilla.join(COW), &one_pixel_png(RED));
let pack = base.join("packs/Blue cows");
write(
&pack.join("pack.mcmeta"),
br#"{"pack":{"description":"blue cows","pack_format":97}}"#,
);
write(&pack.join(COW), &one_pixel_png(BLUE));
let overlay = base.join("overlay");
build_overlay(&vanilla, &discover(&vanilla, &base.join("packs")), &overlay).unwrap();
let (plain, packed) = (ClientResources::new(vanilla), ClientResources::new(overlay));
assert_eq!(plain.read(COW), Some(one_pixel_png(RED)));
assert_eq!(packed.read(COW), Some(one_pixel_png(BLUE)));
assert_eq!(
packed.texture("entity/cow/cow_temperate"),
Some(TexturePixels {
width: 1,
height: 1,
rgba: BLUE.to_vec(),
})
);
let _ = fs::remove_dir_all(base);
}
#[test]
fn only_plain_paths_under_assets_are_read() {
let base = std::env::temp_dir().join(format!(
"horizon-client-resources-paths-{}",
std::process::id()
));
let _ = fs::remove_dir_all(&base);
let root = base.join("root");
write(&root.join(COW), &one_pixel_png(RED));
write(&root.join("horizon-resources.json"), b"{}");
write(&base.join("secret.txt"), b"secret");
let resources = ClientResources::new(root);
assert!(resources.read(COW).is_some());
for refused in [
"horizon-resources.json",
"assets/../horizon-resources.json",
"assets/../../secret.txt",
"assets//minecraft/textures/entity/cow/cow_temperate.png",
"/assets/minecraft/textures/entity/cow/cow_temperate.png",
] {
assert_eq!(resources.read(refused), None, "{refused}");
}
let _ = fs::remove_dir_all(base);
}
}
+37 -3
View File
@@ -208,7 +208,7 @@ where
Ok(resolve_world_movement(
self,
MovementRequest {
entity: EntityIdentity(0),
entity: context.entity().unwrap_or(EntityIdentity(0)),
context,
bounds,
movement,
@@ -243,11 +243,24 @@ impl SceneCollisions<'_, '_> {
max_step: f32,
on_ground: bool,
border: Option<(&BorderGeometry, [f64; 2])>,
) -> Result<Vec3> {
self.resolve_in_border_with(bounds, movement, max_step, on_ground, border, &[])
}
/// [`Self::resolve_in_border`], also stopped by `entities`: the boxes
/// `EntityGetter.getEntityCollisions` gave the moving entity.
pub fn resolve_in_border_with(
self,
bounds: Aabb,
movement: Vec3,
max_step: f32,
on_ground: bool,
border: Option<(&BorderGeometry, [f64; 2])>,
entities: &[Aabb],
) -> Result<Vec3> {
let context = self.context(bounds);
EntityCollisions {
blocks: self,
entities: &[],
entities,
border,
}
.resolve_with_context(bounds, movement, max_step, on_ground, context)
@@ -266,7 +279,7 @@ impl SceneCollisions<'_, '_> {
Ok(resolve_world_movement(
self,
MovementRequest {
entity: EntityIdentity(0),
entity: context.entity().unwrap_or(EntityIdentity(0)),
context,
bounds,
movement,
@@ -287,6 +300,7 @@ impl SceneCollisions<'_, '_> {
CollisionContext::for_entity(
CollisionEntity {
identity: EntityIdentity(0),
kind: horizon_world::collision_context::EntityContextKind::Living,
y,
descending: false,
main_hand: Some(CollisionItem::EMPTY),
@@ -322,6 +336,26 @@ where
{
no_collision_in(world, bounds, true)
}
/// `CollisionGetter.noCollision(entity, bounds)`: retain the real entity snapshot.
pub fn no_collision_with_context<W, E>(
world: &mut W,
bounds: Aabb,
context: CollisionContext<'_>,
) -> bool
where
W: BlockCollisionWorld<Error = E> + EntityMovementWorld<Error = E>,
{
no_world_collision(
world,
CollisionCheckRequest {
entity: context.entity().unwrap_or(EntityIdentity(0)),
context,
bounds,
candidate_budget: 1_000_000,
},
)
.unwrap_or(false)
}
fn no_collision_in<W, E>(world: &mut W, bounds: Aabb, fluids: bool) -> bool
where
W: BlockCollisionWorld<Error = E> + EntityMovementWorld<Error = E>,
@@ -395,6 +395,9 @@ impl BlockEntity {
if name == "minecraft:daylight_detector" {
return Some(Self::DaylightDetector);
}
if name.ends_with("copper_golem_statue") {
return Some(Self::CopperGolemStatue(Default::default()));
}
if name == "minecraft:beacon" {
return Some(Self::Beacon(Box::new(Beacon {
last_check_y: i32::MIN / 2,
@@ -460,6 +463,7 @@ impl BlockEntity {
Self::CommandBlock(_) => crate::command_block::Mode::of_block(name).is_some(),
Self::Skull(_) => is_skull(name),
Self::Banner(_) => name.ends_with("_banner"),
Self::CopperGolemStatue(_) => name.ends_with("copper_golem_statue"),
Self::Shelf(_) => name == "minecraft:shelf" || name.ends_with("_shelf"),
Self::Mod(_) => !name.starts_with("minecraft:"),
Self::Beehive(_) => matches!(name, "minecraft:beehive" | "minecraft:bee_nest"),
@@ -510,6 +514,7 @@ impl BlockEntity {
Self::Lectern(_) => "minecraft:lectern",
Self::Sculk(s) => s.id(),
Self::Banner(_) => "minecraft:banner",
Self::CopperGolemStatue(_) => "minecraft:copper_golem_statue",
Self::Crafter(_) => "minecraft:crafter",
Self::CreakingHeart(_) => "minecraft:creaking_heart",
Self::Spawner(_) => "minecraft:mob_spawner",
@@ -558,7 +563,7 @@ impl BlockEntity {
Self::Mod(m) => m.items.iter().flatten().cloned().collect(),
Self::Lectern(l) => l.book.iter().cloned().collect(),
Self::Sculk(_) => Vec::new(),
Self::Banner(_) => Vec::new(),
Self::Banner(_) | Self::CopperGolemStatue(_) => Vec::new(),
Self::Crafter(c) => c.items.iter().flatten().cloned().collect(),
Self::Campfire(c) => c.items.iter().flatten().cloned().collect(),
Self::ChiseledBookshelf(b) => b.items.iter().flatten().cloned().collect(),
@@ -575,6 +580,7 @@ impl BlockEntity {
| Self::Lectern(_)
| Self::Sculk(_)
| Self::Banner(_)
| Self::CopperGolemStatue(_)
| Self::EndGateway(_)
| Self::Skull(_)
| Self::PotentSulfur(_)
@@ -609,6 +615,7 @@ impl BlockEntity {
| Self::Lectern(_)
| Self::Sculk(_)
| Self::Banner(_)
| Self::CopperGolemStatue(_)
| Self::EndGateway(_)
| Self::Skull(_)
| Self::PotentSulfur(_)
@@ -65,6 +65,51 @@ pub struct Setting {
pub item: Item,
pub text: &'static str,
}
/// What the chosen value does, for a setting whose values each do something different.
fn choice_description(opt: Opt, pad: &PadSettings) -> Option<&'static str> {
Some(match opt {
Opt::PadGameplayGuide => match pad.guide_detail {
gamepad::GuideDetail::Full => {
"Shows what your buttons do right now, in play and in inventories."
}
gamepad::GuideDetail::Basic => {
"Shows only what you can do with what you're looking at, and the main inventory buttons."
}
gamepad::GuideDetail::Off => "Doesn't show what your buttons do.",
},
Opt::PadGlyphs => match pad.glyphs {
GlyphStyle::Auto => "Shows the buttons of the controller you're using.",
GlyphStyle::Xbox => "Always shows Xbox buttons.",
GlyphStyle::PlayStation => "Always shows PlayStation buttons.",
GlyphStyle::Nintendo => "Always shows Nintendo buttons.",
},
Opt::PadNintendoButtons => {
if pad.nintendo_letters {
"A, on the right, confirms and jumps and B goes back, as on a Switch."
} else {
"B, at the bottom, confirms and jumps and A goes back, as on other controllers."
}
}
Opt::PadKeyboard => match pad.keyboard {
gamepad::TextKeyboard::Auto => {
"Opens Steam's keyboard on a Steam Deck under Steam, and Horizon's otherwise."
}
gamepad::TextKeyboard::Horizon => "Always opens Horizon's keyboard.",
gamepad::TextKeyboard::Steam => "Always opens Steam's keyboard.",
},
Opt::PadLightbar => match pad.lightbar {
gamepad::Lightbar::Health => {
"A DualSense's light shows your health, from green through amber to red."
}
gamepad::Lightbar::Dimension => {
"A DualSense's light changes with the dimension you're in."
}
gamepad::Lightbar::Off => "A DualSense keeps its own light.",
},
_ => return None,
})
}
fn option(opt: Opt, text: &'static str) -> Setting {
Setting {
item: Item::Option(opt),
@@ -97,10 +142,16 @@ pub fn rows() -> Vec<(Page, Vec<(&'static str, Vec<Setting>)>)> {
vec![
(
"Controller",
vec![option(
Opt::PadEnabled,
"Off, the game ignores every controller for this player.",
)],
vec![
option(
Opt::PadEnabled,
"Off, the game ignores every controller for this player.",
),
option(
Opt::PadPlayInBackground,
"The controller keeps playing while this window isn't focused, and losing focus won't pause.",
),
],
),
(
"Look",
@@ -133,6 +184,19 @@ pub fn rows() -> Vec<(Page, Vec<(&'static str, Vec<Setting>)>)> {
),
],
),
(
"Play",
vec![
option(
Opt::PadGameplayGuide,
"Show the buttons for what you can do, while you play with a controller.",
),
option(
Opt::PadTriggerThreshold,
"How far a trigger is pulled before it counts as pressed.",
),
],
),
(
"Menus",
vec![
@@ -204,6 +268,10 @@ pub fn rows() -> Vec<(Page, Vec<(&'static str, Vec<Setting>)>)> {
Opt::PadGlyphs,
"Follow the controller, or always show one family's buttons.",
),
option(
Opt::PadNintendoButtons,
"Whether a Nintendo controller's A and B work by where they sit or by their letters.",
),
info(Info::Using, "The buttons menus show right now."),
],
),
@@ -272,6 +340,7 @@ impl Item {
match self {
Item::Option(Opt::PadEnabled) => Some(c.enabled),
Item::Option(Opt::PadVibration) => Some(c.vibration),
Item::Option(Opt::PadPlayInBackground) => Some(c.play_in_background),
Item::Option(Opt::PadAdaptiveTriggers) => Some(c.adaptive_triggers),
Item::Option(Opt::PadInvertX) => Some(c.invert_x),
Item::Option(Opt::PadInvertY) => Some(c.invert_y),
@@ -565,7 +634,7 @@ impl ControllerUi {
&cols,
&lines,
self.scroll.amount,
Page::name,
|page| lang::get(page.name()),
|batch, setting: Setting, y| {
let item = setting.item;
let [rx, ry, rw, rh] = cols.row(y);
@@ -597,6 +666,7 @@ impl ControllerUi {
hover,
focused,
held: self.dragging == Some(item),
key: None,
};
shell::row(batch, atlas, &cols, y, &view);
}
@@ -811,8 +881,12 @@ impl ControllerUi {
/// What a row does. A binding names the controller's own buttons, and what else its button
/// does when it shares it.
fn description(&self, setting: Setting, s: &Settings) -> String {
let Item::Bind(action) = setting.item else {
return lang::get(setting.text);
let action = match setting.item {
Item::Bind(action) => action,
Item::Option(opt) => {
return lang::get(choice_description(opt, &s.controller).unwrap_or(setting.text));
}
_ => return lang::get(setting.text),
};
let pad = &s.controller;
let style = self.context.style;
@@ -1258,6 +1332,7 @@ mod tests {
for opt in [
Opt::PadEnabled,
Opt::PadVibration,
Opt::PadPlayInBackground,
Opt::PadAdaptiveTriggers,
Opt::PadLightbar,
Opt::PadLookSensitivity,
@@ -1266,7 +1341,9 @@ mod tests {
Opt::PadInvertY,
Opt::PadMoveDeadzone,
Opt::PadLookDeadzone,
Opt::PadTriggerThreshold,
Opt::PadGlyphs,
Opt::PadNintendoButtons,
Opt::PadKeyboard,
Opt::PadPointerSpeed,
Opt::PadScrollSpeed,
@@ -1364,6 +1441,78 @@ mod tests {
assert!((s.controller.move_deadzone - before - 0.01).abs() < 0.001);
}
/// The Device page's Nintendo Buttons row sits with the glyphs, steps between its two
/// values rather than ticking a box, and what it sets is what the controller file keeps.
#[test]
fn the_nintendo_buttons_row_flips_letters_and_the_file_keeps_it() {
let (page, group) = rows()
.into_iter()
.flat_map(|(page, groups)| groups.into_iter().map(move |g| (page, g)))
.find(|(_, (_, rows))| {
rows.iter()
.any(|r| r.item == Item::Option(Opt::PadNintendoButtons))
})
.unwrap();
assert_eq!((page, group.0), (Page::Device, "Glyphs"));
let mut s = Settings::default();
let item = Item::Option(Opt::PadNintendoButtons);
assert!(item.steps(&s));
s.controller.nintendo_letters = false;
let mut ui = ControllerUi {
focus: Some(Target::Item(item)),
..ControllerUi::default()
};
// Vanilla's caption and value template; Horizon's own words come bundled.
struct Vanilla;
impl lang::ResourceSource for Vanilla {
fn read(&self, path: &str) -> Option<Vec<u8>> {
(path == "assets/minecraft/lang/en_us.json")
.then(|| br#"{"options.generic_value": "%s: %s"}"#.to_vec())
}
}
let value = |ui: &ControllerUi, s: &Settings| {
lang::scoped(lang::Language::load(&[&Vanilla], "en_us"), || {
ui.parts(item, s)
})
.0
};
assert_eq!(value(&ui, &s).0, "Nintendo Buttons");
assert_eq!(value(&ui, &s).1.as_deref(), Some("By Position"));
let actions = key(&mut ui, &mut s, K::Enter);
assert!(actions.contains(&Action::Changed), "{actions:?}");
assert!(s.controller.nintendo_letters);
assert_eq!(value(&ui, &s).1.as_deref(), Some("By Letter"));
assert!(PadSettings::parse(&s.controller.serialize()).nintendo_letters);
key(&mut ui, &mut s, K::Enter);
assert!(!s.controller.nintendo_letters);
assert!(!PadSettings::parse(&s.controller.serialize()).nintendo_letters);
}
/// What the screen draws at the bottom of the page changes with the choice, in every one of
/// these settings.
#[test]
fn each_choice_describes_itself() {
let ui = ControllerUi::default();
let mut s = Settings::default();
for (opt, choices) in [
(Opt::PadGameplayGuide, 3),
(Opt::PadGlyphs, 4),
(Opt::PadNintendoButtons, 2),
(Opt::PadKeyboard, 3),
(Opt::PadLightbar, 3),
] {
let setting = option(opt, "the fixed sentence");
let mut seen = Vec::new();
for _ in 0..choices {
let text = ui.description(setting, &s);
assert_ne!(text, "the fixed sentence", "{opt:?}");
assert!(!seen.contains(&text), "{opt:?} repeats {text}");
seen.push(text);
opt.cycle(&mut s, false, FB);
}
}
}
#[test]
fn every_text_is_in_the_language_table() {
let (_, missed) = lang::scoped(lang::Language::load(&[], "de_de"), || {
@@ -1377,6 +1526,19 @@ mod tests {
}
}
}
let mut settings = Settings::default();
for opt in [
Opt::PadGameplayGuide,
Opt::PadGlyphs,
Opt::PadNintendoButtons,
Opt::PadKeyboard,
Opt::PadLightbar,
] {
for _ in 0..4 {
lang::get(choice_description(opt, &settings.controller).unwrap());
opt.cycle(&mut settings, false, FB);
}
}
for text in [
"Controller",
"Player profile",
+22 -1
View File
@@ -10,6 +10,7 @@ use std::sync::Mutex;
use std::sync::atomic::{AtomicBool, Ordering};
use chrono::{DateTime, Local};
use horizon_telemetry::system::machine_arch;
/// What the game is doing, set as it goes: graphics adapter, settings, packs, world.
static DETAILS: Mutex<Vec<(&'static str, String)>> = Mutex::new(Vec::new());
@@ -242,9 +243,18 @@ fn read(path: &str) -> Option<String> {
std::fs::read_to_string(path).ok()
}
/// The build's architecture, and the machine's after "on" when it emulates the build.
fn arch_label(build: &str, machine: &str) -> String {
if build == machine {
build.to_owned()
} else {
format!("{build} on {machine}")
}
}
/// `PRETTY_NAME` of /etc/os-release and the kernel, or the system's own version string.
fn operating_system() -> String {
let arch = std::env::consts::ARCH;
let arch = arch_label(std::env::consts::ARCH, machine_arch());
let version = if cfg!(target_os = "linux") {
let name = read("/etc/os-release").and_then(|t| {
t.lines()
@@ -683,6 +693,17 @@ pub fn debug_crash_after() -> Option<std::time::Duration> {
mod tests {
use super::*;
#[test]
fn an_emulated_build_names_both_architectures() {
// A Windows ARM64 PC playing the x64 build: its reports say so (#7513 read "x86_64").
assert_eq!(arch_label("x86_64", "aarch64"), "x86_64 on aarch64");
assert_eq!(arch_label("aarch64", "aarch64"), "aarch64");
assert_eq!(arch_label("x86_64", "x86_64"), "x86_64");
// On the machine running the tests: native builds name one architecture.
let label = arch_label(std::env::consts::ARCH, machine_arch());
assert!(label.starts_with(std::env::consts::ARCH));
}
#[test]
fn a_release_reports_its_built_in_version() {
// release.yml builds with HORIZON_VERSION (docs/versioning.md): `v0.2.3`, wherever it is installed.
@@ -0,0 +1,243 @@
//! OptiFine custom skies (`horizon_optifine::sky`) as the game draws them: a pack's layers with
//! their decoded textures, read once with the pack, and each frame's choice of layers, their
//! brightness and rotation ([`CustomSky::draws`]), which `gfx::custom_sky` draws over the
//! vanilla sky. A pack without `optifine/sky` gives an empty [`CustomSky`] and the sky stays
//! vanilla.
use std::path::Path;
use glam::{Mat4, Vec3};
use horizon_optifine::sky::{Blend, Conditions, Skies, SkyLayer, Transition};
/// A layer's texture, RGBA8.
pub struct Image {
pub width: u32,
pub height: u32,
pub rgba: Vec<u8>,
}
/// The pack's layers, by world, with one texture per layer (`images[i]` for the `i`th layer
/// over all worlds, in `worlds` order).
#[derive(Default)]
pub struct CustomSky {
worlds: Vec<(i32, Vec<SkyLayer>)>,
pub images: Vec<Image>,
}
/// One layer of a frame: which texture, with which blend, how bright and turned how.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Draw {
pub image: u16,
pub blend: Blend,
pub brightness: f32,
/// Camera-relative world from the skybox's space.
pub rotation: Mat4,
}
/// The eased brightness of each layer, kept by the game between frames.
#[derive(Default)]
pub struct State {
world: Option<i32>,
eased: Vec<Transition>,
}
/// Textures past this many texels in all are left out (a skybox is a few megapixels at most).
const TEXEL_BUDGET: u64 = 64 << 20;
impl CustomSky {
/// The custom skies of the overlay at `root`. A layer whose texture does not decode is
/// dropped with a warning.
pub fn load(root: &Path) -> Self {
let skies = Skies::scan(root);
let mut out = Self::default();
let mut texels = 0u64;
for (world, sky) in skies.worlds {
let mut layers = Vec::new();
for layer in sky.layers {
match crate::hud::decode(&root.join(&layer.source)) {
Ok((width, height, rgba))
if texels + u64::from(width) * u64::from(height) <= TEXEL_BUDGET =>
{
texels += u64::from(width) * u64::from(height);
out.images.push(Image {
width,
height,
rgba,
});
layers.push(layer);
}
Ok(_) => log::warn!("{}: past the custom sky's texture budget", layer.source),
Err(e) => log::warn!("{}: {e:#}", layer.source),
}
}
if !layers.is_empty() {
out.worlds.push((world, layers));
}
}
out
}
pub fn is_empty(&self) -> bool {
self.worlds.is_empty()
}
/// Whether `world` has layers (`0` the Overworld, `1` the End).
pub fn has(&self, world: i32) -> bool {
self.worlds.iter().any(|(w, _)| *w == world)
}
/// This frame's layers of `world` into `out` (cleared first), in drawing order: those whose
/// conditions hold, eased over their `transition` by `seconds` since the last frame, at the
/// brightness of their fade times. `sun_turns` is the sun's angle over 360° (`0` at noon).
pub fn draws(
&self,
world: i32,
conditions: &Conditions,
sun_turns: f32,
seconds: f32,
state: &mut State,
out: &mut Vec<Draw>,
) {
out.clear();
let mut first = 0;
let mut found = None;
for (w, layers) in &self.worlds {
if *w == world {
found = Some(layers);
break;
}
first += layers.len();
}
let Some(layers) = found else {
state.world = None;
return;
};
// A new world (or pack) starts each layer where its conditions put it: no fade in.
let snap = state.world != Some(world) || state.eased.len() != layers.len();
if snap {
state.world = Some(world);
state.eased.clear();
state.eased.resize(layers.len(), Transition::default());
}
for (i, (layer, eased)) in layers.iter().zip(&mut state.eased).enumerate() {
let target = if layer.shows(conditions) { 1.0 } else { 0.0 };
let shown = if snap {
eased.value = target;
target
} else {
eased.step(target, seconds, layer.transition)
};
let brightness = shown * layer.brightness(conditions.day_time);
if brightness <= 0.0 {
continue;
}
let turns = layer.turns(conditions.day_time, sun_turns);
let rotation = if turns == 0.0 {
Mat4::IDENTITY
} else {
Mat4::from_axis_angle(Vec3::from_array(layer.axis), turns * std::f32::consts::TAU)
};
out.push(Draw {
image: (first + i) as u16,
blend: layer.blend,
brightness,
rotation,
});
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use horizon_optifine::sky::Now;
fn pack(files: &[(&str, &str)]) -> (std::path::PathBuf, CustomSky) {
let root = std::env::temp_dir().join(format!(
"horizon-custom-sky-{}-{}",
std::process::id(),
files.len()
));
let _ = std::fs::remove_dir_all(&root);
let dir = root.join("assets/minecraft/optifine/sky/world0");
std::fs::create_dir_all(&dir).unwrap();
let png = {
let mut bytes = Vec::new();
let mut encoder = png::Encoder::new(&mut bytes, 6, 4);
encoder.set_color(png::ColorType::Rgba);
encoder.set_depth(png::BitDepth::Eight);
let mut writer = encoder.write_header().unwrap();
writer
.write_image_data(&[255, 0, 0, 255].repeat(24))
.unwrap();
writer.finish().unwrap();
bytes
};
for (name, text) in files {
std::fs::write(dir.join(format!("{name}.properties")), text).unwrap();
std::fs::write(dir.join(format!("{name}.png")), &png).unwrap();
}
let sky = CustomSky::load(&root);
(root, sky)
}
fn noon(biome: &str) -> Conditions<'_> {
Conditions {
day_time: 6000,
weather: Now::Clear,
biome,
y: 64,
}
}
#[test]
fn layers_ease_in_at_a_biome_border_but_not_on_arrival() {
let (root, sky) = pack(&[
("sky1", "biomes=desert\ntransition=2"),
("sky2", "blend=alpha\nrotate=false"),
]);
let _ = std::fs::remove_dir_all(root);
assert!(sky.has(0) && !sky.has(1));
assert_eq!(sky.images[0].width, 6);
let (mut state, mut out) = (State::default(), Vec::new());
sky.draws(
0,
&noon("minecraft:desert"),
0.0,
0.05,
&mut state,
&mut out,
);
assert_eq!(
out.iter().map(|d| d.brightness).collect::<Vec<_>>(),
[1.0, 1.0]
);
// Out of the desert: the first layer fades over its two seconds.
sky.draws(0, &noon("minecraft:plains"), 0.0, 0.5, &mut state, &mut out);
assert_eq!(out[0].brightness, 0.75);
assert_eq!(out[1].blend, Blend::Alpha);
sky.draws(0, &noon("minecraft:plains"), 0.0, 2.0, &mut state, &mut out);
assert_eq!(out.len(), 1, "a dark layer is not drawn");
assert_eq!(out[0].image, 1);
// Into the End, which has no layers, and back: no fade.
sky.draws(1, &noon("minecraft:desert"), 0.0, 0.0, &mut state, &mut out);
assert!(out.is_empty());
sky.draws(0, &noon("minecraft:desert"), 0.0, 0.0, &mut state, &mut out);
assert_eq!(out.len(), 2);
}
#[test]
fn a_layer_turns_west_like_the_sun() {
let (root, sky) = pack(&[("sky1", "")]);
let _ = std::fs::remove_dir_all(root);
let (mut state, mut out) = (State::default(), Vec::new());
// A quarter turn after noon (sunset): the skybox's zenith has gone down to the west.
let c = Conditions {
day_time: 12000,
..noon("minecraft:plains")
};
sky.draws(0, &c, 0.25, 0.0, &mut state, &mut out);
let up = out[0].rotation.transform_vector3(Vec3::Y);
assert!((up - Vec3::NEG_X).length() < 1e-5, "{up}");
}
}
+255 -15
View File
@@ -141,7 +141,8 @@ impl DataPack {
pub fn discover(world: &Path) -> Vec<DataPack> {
discover_folder(&world.join("datapacks"))
}
/// Packs in Create World's temporary pack folder, before a world exists.
/// `FolderRepositorySource.loadPacks`: packs in Create World's temporary
/// folder or a world's data-pack directory, with required folder metadata.
pub fn discover_folder(dir: &Path) -> Vec<DataPack> {
let Ok(entries) = std::fs::read_dir(dir) else {
return Vec::new();
@@ -150,7 +151,7 @@ pub fn discover_folder(dir: &Path) -> Vec<DataPack> {
for e in entries.flatten() {
let name = e.file_name().to_string_lossy().into_owned();
let path = e.path();
if path.is_dir() && path.join("pack.mcmeta").exists() {
if path.is_dir() && folder_metadata_present(&path) {
packs.push(DataPack {
id: format!("file/{name}"),
source: Source::Dir(path),
@@ -168,6 +169,22 @@ pub fn discover_folder(dir: &Path) -> Vec<DataPack> {
packs.sort_by(|a, b| a.id.cmp(&b.id));
packs
}
/// `Pack.readPackMetadata`: even its fallback codec requires a pack section
/// and description. Keep this folder gate separate from format compatibility
/// and the existing interpretation of the description component.
fn folder_metadata_present(path: &Path) -> bool {
let Some(meta) = std::fs::read(path.join("pack.mcmeta"))
.ok()
.and_then(|bytes| crate::packs::parse_meta(&bytes))
else {
return false;
};
meta.as_object()
.and_then(|root| root.get("pack"))
.and_then(serde_json::Value::as_object)
.and_then(|pack| pack.get("description"))
.is_some_and(|description| !description.is_null())
}
/// The resources root's built-in feature packs (`ServerPacksSource`
/// scanning the game's `data/minecraft/datapacks`): every folder with a
/// `pack.mcmeta`, a feature pack (`PackSource.FEATURE`). Off until a world
@@ -261,6 +278,43 @@ pub fn screen_packs(dir: &Path, resources: &Path) -> Vec<crate::pack_screen::Pac
}
infos
}
/// The folder of a game folder whose packs every new world there starts with: a modpack's profile
/// keeps the packs of its data-only jars here (`horizon-port-pack`). Each world gets its own copy,
/// as Create World's packs do, so a world keeps working when the folder changes. Vanilla has no
/// such folder, and without one nothing here happens.
pub const NEW_WORLD_PACKS: &str = "new-world-datapacks";
/// Create World's temporary pack folder holding `game_dir`'s [`NEW_WORLD_PACKS`], and their ids,
/// to show them selected in the Data Packs screen. `None` when there are none.
pub fn seed_create_world(game_dir: &Path) -> Option<(PathBuf, Vec<String>)> {
let from = game_dir.join(NEW_WORLD_PACKS);
let ids: Vec<String> = discover_folder(&from).into_iter().map(|p| p.id).collect();
if ids.is_empty() {
return None;
}
let seeded = temp_folder()
.map_err(|e| e.to_string())
.and_then(|dir| copy(&from, &dir).map(|()| dir));
match seeded {
Ok(dir) => Some((dir, ids)),
Err(e) => {
log::warn!("Horizon: data packs for new worlds: {e}");
None
}
}
}
/// A world about to be created without Create World (`--world`, the control socket) gets
/// `game_dir`'s [`NEW_WORLD_PACKS`]. The game turns on every pack it finds in a world.
pub fn install_new_world_packs(game_dir: &Path, world: &Path) -> Result<(), String> {
let from = game_dir.join(NEW_WORLD_PACKS);
let exists = |name: &str| world.join(name).exists();
if !from.is_dir() || exists("level.dat") || exists("level.dat_old") {
return Ok(());
}
copy(&from, &world.join("datapacks"))
}
/// A new empty folder for Create World's packs
/// (`Files.createTempDirectory("mcworld-")`).
pub fn temp_folder() -> std::io::Result<PathBuf> {
@@ -608,7 +662,7 @@ pub struct DataStore {
/// `item_modifier` files, for `/item modify`.
pub item_modifiers: BTreeMap<String, serde_json::Value>,
/// The content tables' raw files from the packs after the built-in one:
/// `recipe/`, `recipe_filter/`, `loot_table/`, `advancement/`, `dialog/`, `damage_type/`, `data_maps/` and `tags/…/` files, by
/// `recipe/`, `recipe_filter/`, `loot_table/`, `advancement/`, `dialog/`, `damage_type/`, `data_maps/`, `enchantment/` and `tags/…/` files, by
/// `<kind-dir>/<ns>:<path>`, each id's files in pack order. The tables
/// merge them over what they read from the resource root.
pub files: BTreeMap<String, Vec<Vec<u8>>>,
@@ -617,7 +671,7 @@ pub struct DataStore {
}
/// `<kind-dir>/<ns>:<path>` for a pack file the content tables read:
/// `recipe/`, `recipe_filter/`, `loot_table/`, `advancement/`, `trade_set/`,
/// `villager_trade/`, `dialog/`, `damage_type/`, `data_maps/` or `tags/<sub>/`, where the id keeps the sub
/// `villager_trade/`, `dialog/`, `damage_type/`, `data_maps/`, `enchantment/` or `tags/<sub>/`, where the id keeps the sub
/// path so a nested tag kind stays addressable.
fn content_id(file: &str) -> Option<String> {
let rest = file.strip_prefix("data/")?;
@@ -625,7 +679,7 @@ fn content_id(file: &str) -> Option<String> {
let (kind, rest) = rest.split_once('/')?;
let kind = match kind {
"recipe" | "recipe_filter" | "loot_table" | "advancement" | "trade_set"
| "villager_trade" | "dialog" | "damage_type" | "data_maps" => kind,
| "villager_trade" | "dialog" | "damage_type" | "data_maps" | "enchantment" => kind,
// A tag id is `{ns}:{path-under-tags}` (`tags/worldgen/biome/
// test:sub/mytag`), the whole path after the namespace, so nested
// registries keep their directory (`TagLoader`'s id shape).
@@ -659,6 +713,7 @@ impl DataStore {
"damage_type",
"data_maps",
"dialog",
"enchantment",
"function",
"item_modifier",
"loot_table",
@@ -714,15 +769,55 @@ impl DataStore {
}
s
}
/// `FunctionManager.getTag`: the functions a tag expands to (nested tags
/// included, each function once).
/// `ServerFunctionLibrary.getTag`: only valid function tags expand
/// (nested tags included, each function once).
pub fn tag_functions(&self, id: &str) -> Vec<String> {
let mut out = Vec::new();
let mut seen_fns = std::collections::HashSet::new();
let mut seen_tags = std::collections::HashSet::new();
self.expand(id, &mut out, &mut seen_fns, &mut seen_tags);
let invalid = self.invalid_function_tags(id);
self.expand(id, &mut out, &mut seen_fns, &mut seen_tags, &invalid);
out
}
/// `TagLoader.tryBuildTag`: missing required members invalidate their tag and
/// required parents. Optional invalid children contribute nothing.
/// Resolve against the current accepted function map, without caching a
/// candidate store or changing the existing expansion's cycle handling.
fn invalid_function_tags<'a>(&'a self, id: &'a str) -> std::collections::HashSet<&'a str> {
let mut invalid = std::collections::HashSet::new();
let mut parents: std::collections::HashMap<&str, Vec<&str>> =
std::collections::HashMap::new();
let mut seen = std::collections::HashSet::new();
let mut pending = vec![id];
while let Some(tag) = pending.pop() {
if !seen.insert(tag) {
continue;
}
let Some(entries) = self.function_tags.get(tag) else {
invalid.insert(tag);
continue;
};
for (entry, required) in entries {
if let Some(child) = entry.strip_prefix('#') {
pending.push(child);
if *required {
parents.entry(child).or_default().push(tag);
}
} else if *required && !self.functions.contains_key(entry) {
invalid.insert(tag);
}
}
}
pending.extend(invalid.iter().copied());
while let Some(child) = pending.pop() {
for &parent in parents.get(child).into_iter().flatten() {
if invalid.insert(parent) {
pending.push(parent);
}
}
}
invalid
}
/// Depth-first walk keeping first-seen order. Each tag is expanded at most
/// once, so reference cycles end and nesting depth is not capped.
fn expand<'a>(
@@ -731,18 +826,19 @@ impl DataStore {
out: &mut Vec<String>,
seen_fns: &mut std::collections::HashSet<&'a str>,
seen_tags: &mut std::collections::HashSet<&'a str>,
invalid: &std::collections::HashSet<&'a str>,
) {
if !seen_tags.insert(id) {
if invalid.contains(id) || !seen_tags.insert(id) {
return;
}
let Some(entries) = self.function_tags.get(id) else {
return;
};
for (e, required) in entries {
for (e, _) in entries {
match e.strip_prefix('#') {
Some(inner) => self.expand(inner, out, seen_fns, seen_tags),
Some(inner) => self.expand(inner, out, seen_fns, seen_tags, invalid),
None => {
if (self.functions.contains_key(e) || *required) && seen_fns.insert(e) {
if self.functions.contains_key(e) && seen_fns.insert(e) {
out.push(e.clone());
}
}
@@ -896,6 +992,13 @@ pub fn worldgen_overrides(
let mut warnings = Vec::new();
for pack in packs {
for file in pack.list_kinds(&overrides::PACK_KINDS) {
if let Some(id) = overrides::template_id(&file) {
match pack.read(&file) {
Some(nbt) => out.set_template(&id, nbt),
None => warnings.push(format!("{}: {file}: unreadable", pack.id)),
}
continue;
}
let read = || String::from_utf8(pack.read(&file)?).ok();
match out.add_file(&pack.id, &file, read) {
PackFile::Read | PackFile::Other => {}
@@ -917,6 +1020,97 @@ pub fn worldgen_overrides(
mod tests {
use super::*;
#[test]
fn folder_metadata_is_required_before_contents_are_discovered() {
let root =
std::env::temp_dir().join(format!("horizon-folder-metadata-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&root);
for (name, metadata) in [
("broken", Some("{")),
("root_empty", Some("{}")),
("root_array", Some("[]")),
("pack_empty", Some(r#"{"pack":{}}"#)),
("pack_string", Some(r#"{"pack":"bad"}"#)),
("missing", None),
(
"full",
Some(
r#"{"pack":{"description":"full","pack_format":121,"min_format":[121,0],"max_format":[121,0]}}"#,
),
),
(
"description",
Some(r#"{"pack":{"description":"description only"}}"#),
),
("numeric", Some(r#"{"pack":{"description":42}}"#)),
("null", Some(r#"{"pack":{"description":null}}"#)),
("bom", Some("\u{feff}{\"pack\":{\"description\":\"BOM\"}}")),
(
"comment",
Some("{\"pack\":{\"description\":\"comment\"}}\n// trailing"),
),
] {
let dir = root.join(name);
std::fs::create_dir_all(dir.join("data/h146/function")).unwrap();
std::fs::write(
dir.join(format!("data/h146/function/{name}.mcfunction")),
"scoreboard players set admitted h146 1\n",
)
.unwrap();
if let Some(metadata) = metadata {
std::fs::write(dir.join("pack.mcmeta"), metadata).unwrap();
}
}
let packs = discover_folder(&root);
assert_eq!(
packs.iter().map(|p| p.id.as_str()).collect::<Vec<_>>(),
[
"file/bom",
"file/comment",
"file/description",
"file/full",
"file/numeric"
]
);
let store = DataStore::load(&packs.iter().collect::<Vec<_>>());
assert_eq!(
store
.functions
.keys()
.map(String::as_str)
.collect::<Vec<_>>(),
[
"h146:bom",
"h146:comment",
"h146:description",
"h146:full",
"h146:numeric"
]
);
let numeric = packs.iter().find(|p| p.id == "file/numeric").unwrap();
let raw: serde_json::Value =
serde_json::from_slice(&numeric.read("pack.mcmeta").unwrap()).unwrap();
assert_eq!(raw["pack"]["description"], 42);
// Builtin and feature construction still have their independent paths.
let resources = root.join("resources");
let feature = resources.join("data/minecraft/datapacks/feature");
std::fs::create_dir_all(&feature).unwrap();
std::fs::write(
feature.join("pack.mcmeta"),
r#"{"pack":{"description":"feature"}}"#,
)
.unwrap();
assert_eq!(
feature_packs(&resources)
.iter()
.map(|p| p.id.as_str())
.collect::<Vec<_>>(),
["feature"]
);
assert_eq!(DataPack::builtin(&resources).id, "vanilla");
std::fs::remove_dir_all(root).unwrap();
}
#[test]
fn data_map_files_are_content_files_keyed_by_map_namespace_and_registry() {
assert_eq!(
@@ -944,6 +1138,35 @@ mod tests {
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn a_game_folder_s_new_world_packs_go_into_new_worlds_only() {
let root = std::env::temp_dir().join(format!("hz-new-world-packs-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&root);
let (game, world, old) = (root.join("game"), root.join("new"), root.join("old"));
assert!(seed_create_world(&game).is_none());
assert_eq!(install_new_world_packs(&game, &world), Ok(()));
assert!(!world.join("datapacks").exists());
for pack in ["void", "islands"] {
let dir = game.join(NEW_WORLD_PACKS).join(pack);
std::fs::create_dir_all(dir.join("data/v/function")).unwrap();
std::fs::write(dir.join("pack.mcmeta"), "{}").unwrap();
std::fs::write(dir.join("data/v/function/a.mcfunction"), "say a").unwrap();
}
let (temp, ids) = seed_create_world(&game).unwrap();
assert_eq!(ids, ["file/islands", "file/void"]);
assert!(temp.join("void/data/v/function/a.mcfunction").is_file());
let _ = std::fs::remove_dir_all(&temp);
install_new_world_packs(&game, &world).unwrap();
let ids: Vec<String> = discover(&world).into_iter().map(|p| p.id).collect();
assert_eq!(ids, ["file/islands", "file/void"]);
// A world that exists already keeps the packs it has.
std::fs::create_dir_all(&old).unwrap();
std::fs::write(old.join("level.dat"), "").unwrap();
install_new_world_packs(&game, &old).unwrap();
assert!(discover(&old).is_empty());
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn enabled_packs_override_worldgen_in_order() {
let dir = std::env::temp_dir().join(format!("hz-datapack-worldgen-{}", std::process::id()));
@@ -954,8 +1177,9 @@ mod tests {
std::fs::write(path, t).unwrap();
};
for pack in ["a", "b", "off"] {
write(pack, "pack.mcmeta", "{}");
write(pack, "pack.mcmeta", r#"{"pack":{"description":"fixture"}}"#);
write(pack, "data/minecraft/worldgen/noise/cave.json", pack);
write(pack, "data/p/structure/room/a.nbt", pack);
write(
pack,
"data/minecraft/tags/worldgen/biome/is_hill.json",
@@ -992,6 +1216,8 @@ mod tests {
o.entries("noises").collect::<Vec<_>>(),
[("minecraft:cave", "a")]
);
assert_eq!(o.templates().len(), 1);
assert_eq!(&*o.templates()["p:room/a"], b"a");
assert_eq!(
o.entries("biomes").collect::<Vec<_>>(),
[("minecraft:plains", "{}")]
@@ -1122,6 +1348,20 @@ mod tests {
#[test]
fn tag_resolution_dedupes_fast_ends_cycles_and_nests_deeply() {
let mut store = DataStore::default();
// Every referenced function exists; missing required ones invalidate a tag.
for id in (0..25_000)
.map(|i| format!("t:f{i}"))
.chain(["t:x", "t:y", "t:deep"].map(str::to_owned))
{
store.functions.insert(
id.clone(),
Function {
id,
lines: Vec::new(),
parameters: Vec::new(),
},
);
}
let big: Vec<(String, bool)> = (0..50_000)
.map(|i| (format!("t:f{}", i % 25_000), true))
.collect();
@@ -1206,13 +1446,13 @@ mod tests {
write(
"fns",
"pack.mcmeta",
r#"{"pack":{"min_format":121,"max_format":121}}"#,
r#"{"pack":{"description":"fixture","min_format":121,"max_format":121}}"#,
);
write("fns", "data/x/function/f.mcfunction", "say hi");
write(
"off",
"pack.mcmeta",
r#"{"pack":{"min_format":121,"max_format":121}}"#,
r#"{"pack":{"description":"fixture","min_format":121,"max_format":121}}"#,
);
std::fs::create_dir_all(&dir).unwrap();
let mut zip = zip::ZipWriter::new(std::fs::File::create(dir.join("old.zip")).unwrap());
@@ -98,14 +98,42 @@ pub struct Source {
/// The [`MAX`] sources nearest `camera` within reach, as the shaders read them: camera-relative
/// position and level. Overwrites `out`.
pub fn select(sources: &[Source], camera: DVec3, out: &mut Vec<[f32; 4]>) {
select_iter(sources.iter().copied(), camera, out);
}
/// Streaming selection with the same stable distance order as [`select`]. Only the best
/// [`MAX`] sources are kept, and each source's distance is evaluated once.
pub fn select_iter(
sources: impl IntoIterator<Item = Source>,
camera: DVec3,
out: &mut Vec<[f32; 4]>,
) {
out.clear();
let key = |s: &Source| s.at.distance_squared(camera);
let mut near: Vec<&Source> = sources
.iter()
.filter(|s| s.level > 0 && key(s) <= REACH * REACH)
.collect();
near.sort_by(|a, b| key(a).total_cmp(&key(b)));
out.extend(near.into_iter().take(MAX).map(|s| {
let mut near = [(
0.0_f64,
Source {
at: DVec3::ZERO,
level: 0,
},
); MAX];
let mut len = 0;
for source in sources {
let distance = source.at.distance_squared(camera);
// The original <= test also rejects NaN and infinite distances.
if source.level == 0 || !(0.0..=REACH * REACH).contains(&distance) {
continue;
}
let at = near[..len].partition_point(|(d, _)| d.total_cmp(&distance).is_le());
if at == MAX {
continue;
}
// Insert after equal distances: the first MAX sources in input order win ties.
let end = len.min(MAX - 1);
near.copy_within(at..end, at + 1);
near[at] = (distance, source);
len = (len + 1).min(MAX);
}
out.extend(near[..len].iter().map(|(_, s)| {
let p = (s.at - camera).as_vec3();
[p.x, p.y, p.z, f32::from(s.level.min(15))]
}));
@@ -179,6 +207,104 @@ mod tests {
assert!(out.is_empty());
}
#[test]
fn streaming_selection_matches_stable_full_sort() {
let camera = DVec3::new(30_000_000.0, -64.0, -30_000_000.0);
let mut seed = 0x8469_d28b_314a_723fu64;
let mut sources = Vec::new();
for i in 0..2048 {
let offset = std::array::from_fn::<_, 3, _>(|_| {
seed ^= seed << 13;
seed ^= seed >> 7;
seed ^= seed << 17;
(seed % 241) as f64 - 120.0
});
sources.push(Source {
at: camera + DVec3::from(offset),
level: (i % 22) as u8,
});
}
sources.extend([
Source {
at: camera + DVec3::X * 96.0,
level: 14,
},
Source {
at: camera - DVec3::X * 96.0,
level: 15,
},
Source {
at: camera + DVec3::X * 96.0001,
level: 15,
},
Source {
at: DVec3::splat(f64::NAN),
level: 15,
},
Source {
at: DVec3::splat(f64::INFINITY),
level: 15,
},
]);
let reference = |input: &[Source], camera: DVec3| {
let mut selected: Vec<_> = input
.iter()
.filter(|s| s.level > 0 && s.at.distance_squared(camera) <= 96.0 * 96.0)
.collect();
selected.sort_by(|a, b| {
a.at.distance_squared(camera)
.total_cmp(&b.at.distance_squared(camera))
});
selected
.into_iter()
.take(16)
.map(|s| {
let p = (s.at - camera).as_vec3();
[p.x, p.y, p.z, f32::from(s.level.min(15))]
})
.collect::<Vec<_>>()
};
let mut out = vec![[999.0; 4]; 20];
for input in [sources.clone(), sources.into_iter().rev().collect()] {
for n in [0, 1, 15, 16, 17, 40, input.len()] {
select_iter(input[..n].iter().copied(), camera, &mut out);
assert_eq!(out, reference(&input[..n], camera));
}
select_iter(input.iter().copied(), DVec3::splat(f64::NAN), &mut out);
assert!(out.is_empty());
}
}
#[test]
fn equal_distances_keep_first_sources_and_reach_is_inclusive() {
// Every source ties: neither a different position nor a different light level
// may win the sixteenth slot just because it was inserted later.
let sources: Vec<_> = (0..40)
.map(|i| Source {
at: if i % 2 == 0 {
DVec3::X * 96.0
} else {
-DVec3::X * 96.0
},
level: (i % 15 + 1) as u8,
})
.collect();
let mut out = Vec::new();
select_iter(sources, DVec3::ZERO, &mut out);
assert_eq!(out.len(), 16);
for (i, light) in out.iter().enumerate() {
assert_eq!(
*light,
[
if i % 2 == 0 { 96.0 } else { -96.0 },
0.0,
0.0,
(i % 15 + 1) as f32
]
);
}
}
#[test]
fn light_falls_off_a_level_per_block() {
let torch = [[0.0, 0.0, 0.0, 14.0]];
+170 -31
View File
@@ -14,6 +14,8 @@ pub struct EffectInstance {
pub amplifier: i32,
pub ambient: bool,
pub visible: bool,
/// `MobEffectInstance.showIcon`: whether the HUD and inventory list it.
pub show_icon: bool,
pub hidden: Option<Box<EffectInstance>>,
}
impl EffectInstance {
@@ -24,9 +26,18 @@ impl EffectInstance {
amplifier: amplifier.clamp(0, 255),
ambient: false,
visible: true,
show_icon: true,
hidden: None,
}
}
/// `new MobEffectInstance(effect, duration, amplifier, ambient, visible)`:
/// the icon shows exactly when the particles do.
pub fn with_flags(mut self, ambient: bool, visible: bool) -> Self {
self.ambient = ambient;
self.visible = visible;
self.show_icon = visible;
self
}
fn infinite(&self) -> bool {
self.duration == -1
}
@@ -71,6 +82,10 @@ impl EffectInstance {
self.visible = take.visible;
changed = true;
}
if take.show_icon != self.show_icon {
self.show_icon = take.show_icon;
changed = true;
}
changed
}
/// `MobEffectInstance.tickClient`: a client counts its effects down
@@ -98,6 +113,7 @@ impl EffectInstance {
self.amplifier = h.amplifier;
self.ambient = h.ambient;
self.visible = h.visible;
self.show_icon = h.show_icon;
self.hidden = h.hidden;
}
}
@@ -116,8 +132,9 @@ pub enum EffectAction {
}
/// `shouldApplyEffectTickThisTick` + `applyEffectTick` for the periodic effects.
fn periodic(id: &str, tick: i32, amp: i32) -> Option<EffectAction> {
// Java's `>>` and `<<` take the shift distance modulo 32 (`wrapping_*`).
let every = |base: i32| {
let interval = base >> amp;
let interval = base.wrapping_shr(amp as u32);
interval <= 0 || tick % interval == 0
};
match id {
@@ -126,10 +143,12 @@ fn periodic(id: &str, tick: i32, amp: i32) -> Option<EffectAction> {
"wither" if every(40) => Some(EffectAction::Hurt("minecraft:wither", 1.0)),
"hunger" => Some(EffectAction::Exhaust(0.005 * (amp + 1) as f32)),
"saturation" if tick >= 1 => Some(EffectAction::Eat(amp + 1, 1.0)),
"instant_health" if tick >= 1 => Some(EffectAction::Heal((4 << amp.min(30)) as f32)),
"instant_health" if tick >= 1 => Some(EffectAction::Heal(
4i32.wrapping_shl(amp as u32).max(0) as f32,
)),
"instant_damage" if tick >= 1 => Some(EffectAction::Hurt(
"minecraft:magic",
(6 << amp.min(30)) as f32,
6i32.wrapping_shl(amp as u32) as f32,
)),
_ => None,
}
@@ -139,11 +158,11 @@ fn periodic(id: &str, tick: i32, amp: i32) -> Option<EffectAction> {
pub fn instant(id: &str, amp: i32, scale: f64) -> Option<EffectAction> {
match id {
"instant_health" => Some(EffectAction::Heal(
(scale * f64::from(4 << amp.min(30)) + 0.5) as i32 as f32,
(scale * f64::from(4i32.wrapping_shl(amp as u32)) + 0.5) as i32 as f32,
)),
"instant_damage" => Some(EffectAction::Hurt(
"minecraft:magic",
(scale * f64::from(6 << amp.min(30)) + 0.5) as i32 as f32,
(scale * f64::from(6i32.wrapping_shl(amp as u32)) + 0.5) as i32 as f32,
)),
"saturation" => Some(EffectAction::Eat(amp + 1, 1.0)),
_ => None,
@@ -168,6 +187,57 @@ pub enum OmenTick {
/// It added this effect, then returned false.
Add(EffectInstance),
}
/// A saved player's effect: `MobEffectInstance.Details` under its `id`.
#[derive(Clone, Debug, PartialEq)]
pub struct SavedEffect {
pub id: String,
pub duration: i32,
pub amplifier: i32,
pub ambient: bool,
/// `show_particles`.
pub particles: bool,
/// `show_icon`.
pub icon: bool,
/// `hidden_effect`: the weaker, longer instance of the same effect that
/// comes back when this one ends.
pub hidden: Option<Box<SavedEffect>>,
}
impl SavedEffect {
/// Ambient off, particles and icon shown, nothing hidden: the codec's
/// defaults.
pub fn new(id: impl Into<String>, duration: i32, amplifier: i32) -> Self {
Self {
id: id.into(),
duration,
amplifier,
ambient: false,
particles: true,
icon: true,
hidden: None,
}
}
fn of(e: &EffectInstance) -> Self {
Self {
id: potions::namespaced(e.id).to_owned(),
duration: e.duration,
amplifier: e.amplifier,
ambient: e.ambient,
particles: e.visible,
icon: e.show_icon,
hidden: e.hidden.as_deref().map(|h| Box::new(Self::of(h))),
}
}
/// `new MobEffectInstance(effect, details)`: the hidden chain comes back
/// with it.
fn instance(&self, id: &'static str) -> EffectInstance {
let mut e = EffectInstance::new(id, self.duration, self.amplifier);
e.ambient = self.ambient;
e.visible = self.particles;
e.show_icon = self.icon;
e.hidden = self.hidden.as_deref().map(|h| Box::new(h.instance(id)));
e
}
}
#[derive(Clone, Debug, Default)]
pub struct PlayerEffects {
pub list: Vec<EffectInstance>,
@@ -200,39 +270,25 @@ impl PlayerEffects {
pub fn clear(&mut self) {
self.list.clear();
}
/// The effects of a saved player (`Player.active_effects`: id,
/// duration, amplifier, ambient, particles visible).
pub fn from_saved(saved: &[(String, i32, i32, bool, bool)]) -> Self {
/// The effects of a saved player (`Player.active_effects`), each with
/// the instances it hides.
pub fn from_saved(saved: &[SavedEffect]) -> Self {
let mut effects = Self::default();
for (id, duration, amplifier, ambient, particles) in saved {
if let Some(def) = potions::effect(id) {
let mut e = EffectInstance::new(def.id, *duration, *amplifier);
e.ambient = *ambient;
e.visible = *particles;
effects.add(e);
for s in saved {
if let Some(def) = potions::effect(&s.id) {
effects.add(s.instance(def.id));
}
}
effects
}
/// `Player.active_effects` for the save.
pub fn saved(&self) -> Vec<(String, i32, i32, bool, bool)> {
self.list
.iter()
.map(|e| {
(
potions::namespaced(e.id).to_owned(),
e.duration,
e.amplifier,
e.ambient,
e.visible,
)
})
.collect()
pub fn saved(&self) -> Vec<SavedEffect> {
self.list.iter().map(SavedEffect::of).collect()
}
/// ClientboundUpdateMobEffectPacket's flags without blending: ambient,
/// particles visible and the icon shown (always, for these effects).
/// particles visible and the icon shown.
pub fn flags(e: &EffectInstance) -> i8 {
i8::from(e.ambient) | i8::from(e.visible) << 1 | 4
i8::from(e.ambient) | i8::from(e.visible) << 1 | i8::from(e.show_icon) << 2
}
/// `LivingEntity.tickEffects` (server part) with the omens' effect ticks:
/// `omen` answers `BadOmenMobEffect` (every tick), `RaidOmenMobEffect`
@@ -494,7 +550,75 @@ pub fn effect_modifier(effect: &str) -> Option<(&'static str, &'static str, f64,
#[cfg(test)]
mod tests {
use super::*;
/// `HealOrHarmMobEffect`: Java's `<<` shifts by the amplifier modulo
/// 32 and wraps, so amplifier 32 is amplifier 0 again; a ticked heal is
/// at least 0 (#11386).
#[test]
fn heal_and_harm_shift_like_java() {
let magic = "minecraft:magic";
assert_eq!(
instant("instant_health", 32, 1.0),
Some(EffectAction::Heal(4.0))
);
assert_eq!(
instant("instant_damage", 33, 1.0),
Some(EffectAction::Hurt(magic, 12.0))
);
assert_eq!(
instant("instant_health", 1, 0.5),
Some(EffectAction::Heal(4.0))
);
assert_eq!(
periodic("instant_health", 1, 32),
Some(EffectAction::Heal(4.0))
);
// 4 << 29 is i32::MIN: the heal stops at 0. 6 << 30 wraps to i32::MIN.
assert_eq!(
periodic("instant_health", 1, 29),
Some(EffectAction::Heal(0.0))
);
assert_eq!(
periodic("instant_damage", 1, 30),
Some(EffectAction::Hurt(magic, i32::MIN as f32))
);
// Regeneration: 50 >> 32 is 50 again, not 0 (which would heal each tick).
assert_eq!(periodic("regeneration", 49, 32), None);
assert_eq!(
periodic("regeneration", 50, 32),
Some(EffectAction::Heal(1.0))
);
}
/// Warden darkness (`new MobEffectInstance(DARKNESS, 260, 0, false,
/// false)`) shows neither particles nor its icon; a renewal that shows the
/// icon turns it back on (`MobEffectInstance.update`).
#[test]
fn an_effect_without_particles_hides_its_icon() {
let mut fx = PlayerEffects::default();
fx.add(EffectInstance::new("darkness", 260, 0).with_flags(false, false));
let darkness = fx.get("darkness").unwrap();
// ClientboundUpdateMobEffectPacket: no ambient, particles or icon bit.
assert_eq!(PlayerEffects::flags(darkness), 0);
assert!(!crate::hud_overlays::EffectIcon::from(darkness).show_icon);
assert!(fx.add(EffectInstance::new("darkness", 100, 0)));
assert_eq!(PlayerEffects::flags(fx.get("darkness").unwrap()), 2 | 4);
// Details keeps show_icon apart from show_particles.
let mut speed = EffectInstance::new("speed", 100, 0);
speed.show_icon = false;
let mut fx = PlayerEffects::default();
fx.add(speed);
let saved = fx.saved();
assert_eq!(
saved,
[SavedEffect {
icon: false,
..SavedEffect::new("minecraft:speed", 100, 0)
}]
);
let loaded = PlayerEffects::from_saved(&saved);
assert_eq!(PlayerEffects::flags(loaded.get("speed").unwrap()), 2);
}
#[test]
fn adding_counts_a_change_only_when_the_effect_changed() {
let mut fx = PlayerEffects::default();
@@ -531,6 +655,21 @@ mod tests {
assert_eq!(fx.get("speed").unwrap().duration, 49);
assert_eq!(fx.get("raid_omen").unwrap().amplifier, 1);
}
#[test]
fn a_reloaded_effect_falls_back_to_the_one_it_hid() {
// /effect give poison 6 s level II over poison 30 s level I, saved
// and loaded: when level II runs out, level I resumes with what is
// left of its 30 s.
let mut fx = PlayerEffects::default();
fx.add(EffectInstance::new("poison", 600, 0));
fx.add(EffectInstance::new("poison", 120, 1));
let mut loaded = PlayerEffects::from_saved(&fx.saved());
for _ in 0..120 {
loaded.tick();
}
let poison = loaded.get("poison").expect("level I resumed");
assert_eq!((poison.amplifier, poison.duration), (0, 480));
}
/// The only test here that registers mod effects (once per process).
#[test]
fn a_mod_effect_saves_under_its_namespace() {
@@ -546,11 +685,11 @@ mod tests {
1,
));
let saved = fx.saved();
assert_eq!(saved, [("ruby:glow".to_owned(), 600, 1, false, true)]);
assert_eq!(saved, [SavedEffect::new("ruby:glow", 600, 1)]);
assert_eq!(PlayerEffects::from_saved(&saved).list, fx.list);
// Without its mod the saved effect is dropped, as an unknown id is.
assert!(
PlayerEffects::from_saved(&[("gone:glow".into(), 600, 1, false, true)])
PlayerEffects::from_saved(&[SavedEffect::new("gone:glow", 600, 1)])
.list
.is_empty()
);
@@ -3,7 +3,7 @@
//! celestial angles and star brightness for the overworld day cycle.
use crate::Result;
use serde_json::Value;
use std::{collections::HashMap, fs, path::Path};
use std::{collections::HashMap, path::Path};
#[derive(Clone, Copy, Debug)]
enum Easing {
@@ -151,13 +151,13 @@ impl Environment {
/// A dimension's base attributes, optionally driven by the day timeline.
pub fn load_dimension(root: &Path, dimension: &str, day_cycle: bool) -> Result<Self> {
let timeline: Value = if day_cycle {
serde_json::from_slice(&fs::read(root.join("data/minecraft/timeline/day.json"))?)?
horizon_world::json_file::read(&root.join("data/minecraft/timeline/day.json"))?
} else {
serde_json::json!({"period_ticks": 24000, "tracks": {}})
};
let dimension: Value = serde_json::from_slice(&fs::read(
root.join(format!("data/minecraft/dimension_type/{dimension}.json")),
)?)?;
let dimension: Value = horizon_world::json_file::read(
&root.join(format!("data/minecraft/dimension_type/{dimension}.json")),
)?;
let mut tracks = HashMap::new();
for (name, track) in timeline["tracks"].as_object().ok_or("invalid timeline")? {
let easing = match &track["ease"] {
@@ -399,8 +399,11 @@ pub fn apply_weather(sky: &mut Sky, rain: f32, thunder: f32) {
pub fn sky_darken(environment: &Environment, day_time: i64, rain: f32, thunder: f32) -> i32 {
let mut sky = environment.sample(day_time, None, None);
apply_weather(&mut sky, rain, thunder);
// Level.updateSkyBrightness: darkening from the sky light level.
(15.0 - 15.0 * sky.sky_light_level).round() as i32
// Level.updateSkyBrightness: `(int)(15.0F - skyLightLevel)`, the float
// truncated, not rounded (#11323): the darkening steps up only once the
// light has fallen a whole level. `sky_light_level` is the attribute as a
// fraction of 15, so the product is its raw value in `f32`.
(15.0 - 15.0 * sky.sky_light_level) as i32
}
#[cfg(test)]
+66 -28
View File
@@ -10,6 +10,9 @@ pub const FILE: &str = "horizon-extras.txt";
pub const SORT_KEY: &str = "horizon.sort";
/// The 3D skin layers line, here and in the `options.txt` that held it first.
const SKIN_LAYERS_KEY: &str = "threeDSkinLayers";
/// The emote wheel's key, here and in the `key_key.emote` line #10092 wrote to `options.txt`.
const EMOTE_KEY: &str = "emoteKey";
const OLD_EMOTE_LINE: &str = "key_key.emote";
/// The minimap's sizes in GUI pixels, and its zooms in blocks per pixel.
pub const MINIMAP_SIZES: [(u8, &str); 3] = [(64, "Small"), (96, "Medium"), (128, "Large")];
@@ -126,6 +129,10 @@ pub struct Extras {
pub sort: bool,
/// A vanilla key name: sorts the part under the cursor.
pub sort_key: String,
/// The emote wheel's key (`key.emote`), as the game last saved it. The live binding is
/// `Settings::keys`, bound in Controls with the vanilla ones; it is only kept in this file
/// so that `options.txt` stays vanilla's (`Settings::load` hands it over, `save` takes it back).
pub emote_key: String,
/// A new chat line slides in from the left while it fades in (`Chat::layout`).
pub chat_animation: bool,
/// The indicator bar at the left of a chat line (on by default, as vanilla).
@@ -164,10 +171,10 @@ pub struct Extras {
/// Cosmetics (`horizon_cosmetics`, off unless enabled), the lines `horizon_cosmetics::prefs`
/// shares with the launcher's Wardrobe: Show my cosmetics (the account's, sent to the service;
/// on by default), how much of other players' to show (no line until chosen:
/// `effective_others`), and their auras and trails (off by default).
/// `effective_others`), and whether their trails and auras are hidden (off by default).
pub cosmetics_mine: bool,
pub cosmetics_others: Option<horizon_cosmetics::prefs::Others>,
pub cosmetics_auras: bool,
pub cosmetics_hide_fx: bool,
/// My cosmetics in single player too (`cosmeticsSolo`, off by default): the window's own server
/// names the player by their offline profile, which the service doesn't know, so their own
/// cosmetics and name tag style show only with this on.
@@ -220,6 +227,7 @@ impl Default for Extras {
mouse_tweaks_wheel: true,
sort: false,
sort_key: "key.keyboard.unknown".into(),
emote_key: crate::keybinds::KeyBinds::default_of("key.emote").into(),
chat_animation: false,
chat_indicator: true,
no_ai: false,
@@ -230,12 +238,12 @@ impl Default for Extras {
splitscreen_two: 0,
splitscreen_three: 0,
splitscreen_names: true,
wheel_cycles_settings: true,
wheel_cycles_settings: false,
music_off: Vec::new(),
music_page: false,
cosmetics_mine: true,
cosmetics_others: None,
cosmetics_auras: false,
cosmetics_hide_fx: false,
cosmetics_solo: false,
cosmetics_owner: false,
horizon_marks: true,
@@ -324,6 +332,7 @@ impl Extras {
"mouseTweaksWheel" => e.mouse_tweaks_wheel = flag(e.mouse_tweaks_wheel),
"sort" => e.sort = flag(e.sort),
"sortKey" if value.starts_with("key.") => e.sort_key = value.to_owned(),
EMOTE_KEY if value.starts_with("key.") => e.emote_key = value.to_owned(),
"chatAnimation" => e.chat_animation = flag(e.chat_animation),
"chatIndicator" => e.chat_indicator = flag(e.chat_indicator),
horizon_core::no_ai::KEY => e.no_ai = flag(e.no_ai),
@@ -334,7 +343,11 @@ impl Extras {
"splitscreenTwo" => e.splitscreen_two = value.parse().unwrap_or(0).min(2),
"splitscreenThree" => e.splitscreen_three = value.parse().unwrap_or(0).min(2),
"splitscreenNames" => e.splitscreen_names = flag(e.splitscreen_names),
"wheelCyclesSettings" => e.wheel_cycles_settings = flag(e.wheel_cycles_settings),
// A new name: `wheelCyclesSettings` was written as true by every
// install while it was the default, so it says nothing of a choice.
"wheelCyclesListSettings" => {
e.wheel_cycles_settings = flag(e.wheel_cycles_settings)
}
"musicOff" => {
e.music_off = value
.split(',')
@@ -349,7 +362,7 @@ impl Extras {
e.cosmetics_others =
horizon_cosmetics::prefs::Others::parse(value).or(e.cosmetics_others)
}
"cosmeticsAuras" => e.cosmetics_auras = flag(e.cosmetics_auras),
"cosmeticsHideFx" => e.cosmetics_hide_fx = flag(e.cosmetics_hide_fx),
"cosmeticsSolo" => e.cosmetics_solo = flag(e.cosmetics_solo),
horizon_cosmetics::prefs::MARKS => e.horizon_marks = flag(e.horizon_marks),
horizon_cosmetics::prefs::LISTED => e.horizon_listed = flag(e.horizon_listed),
@@ -363,7 +376,7 @@ impl Extras {
}
pub fn serialize(&self) -> String {
let mut text = format!(
"packBrowser:{}\npregen:{}\nfullbright:{}\ndynamicLights:{}\nzoom:{}\nzoomKey:{}\nzoomLevel:{}\nzoomSmooth:{}\nzoomSensitivity:{}\nlodZoomDetail:{}\nfps:{}\nfpsCorner:{}\nfpsLows:{}\nkeystrokes:{}\nkeystrokesCorner:{}\nkeystrokesCps:{}\nmovedToMods:{}\nminimap:{}\nminimapCorner:{}\nminimapSize:{}\nminimapShape:{}\nminimapZoom:{}\nminimapRotate:{}\nautoReconnect:{}\nuiSoundReserve:{}\nownNameTag:{}\nhideBlockParticles:{}\nmouseTweaks:{}\nmouseTweaksWheel:{}\nsort:{}\nsortKey:{}\nchatAnimation:{}\nchatIndicator:{}\n{}:{}\nagentControl:{}\nreproKeys:{}\ndiscordPresence:{}\nsplitscreen:{}\nsplitscreenTwo:{}\nsplitscreenThree:{}\nsplitscreenNames:{}\nwheelCyclesSettings:{}\nmusicOff:{}\nmusicPage:{}\ncosmeticsMine:{}\n{}cosmeticsAuras:{}\ncosmeticsSolo:{}\n{}:{}\n{}:{}\n{}:{}\n",
"packBrowser:{}\npregen:{}\nfullbright:{}\ndynamicLights:{}\nzoom:{}\nzoomKey:{}\nzoomLevel:{}\nzoomSmooth:{}\nzoomSensitivity:{}\nlodZoomDetail:{}\nfps:{}\nfpsCorner:{}\nfpsLows:{}\nkeystrokes:{}\nkeystrokesCorner:{}\nkeystrokesCps:{}\nmovedToMods:{}\nminimap:{}\nminimapCorner:{}\nminimapSize:{}\nminimapShape:{}\nminimapZoom:{}\nminimapRotate:{}\nautoReconnect:{}\nuiSoundReserve:{}\nownNameTag:{}\nhideBlockParticles:{}\nmouseTweaks:{}\nmouseTweaksWheel:{}\nsort:{}\nsortKey:{}\nchatAnimation:{}\nchatIndicator:{}\n{}:{}\nagentControl:{}\nreproKeys:{}\ndiscordPresence:{}\nsplitscreen:{}\nsplitscreenTwo:{}\nsplitscreenThree:{}\nsplitscreenNames:{}\nwheelCyclesListSettings:{}\nmusicOff:{}\nmusicPage:{}\ncosmeticsMine:{}\n{}cosmeticsHideFx:{}\ncosmeticsSolo:{}\n{}:{}\n{}:{}\n{}:{}\n",
bool_text(self.pack_browser),
bool_text(self.pregen),
bool_text(self.fullbright),
@@ -417,7 +430,7 @@ impl Extras {
// Written once the player chose.
self.cosmetics_others
.map_or(String::new(), |o| format!("cosmeticsOthers:{}\n", o.name())),
bool_text(self.cosmetics_auras),
bool_text(self.cosmetics_hide_fx),
bool_text(self.cosmetics_solo),
horizon_cosmetics::prefs::MARKS,
bool_text(self.horizon_marks),
@@ -426,32 +439,44 @@ impl Extras {
SKIN_LAYERS_KEY,
bool_text(self.three_d_skin_layers),
);
text.push_str(&format!("{EMOTE_KEY}:{}\n", self.emote_key));
self.pvp.serialize(&mut text);
text
}
/// Adopts the settings `options.txt` held before they moved here, while
/// this file (`own`) has no line of its own: 3D skin layers was an
/// `options.txt` line. Vanilla's `Options.save` drops lines it doesn't
/// this file (`own`) has no line of its own: 3D skin layers and the emote key were
/// `options.txt` lines. Vanilla's `Options.save` drops lines it doesn't
/// know, so it is read there once, the first start after the update.
pub fn read_old_options(&mut self, own: &str, options_txt: &str) {
fn line(text: &str) -> Option<&str> {
fn line<'a>(text: &'a str, name: &str) -> Option<&'a str> {
text.lines().find_map(|l| {
l.split_once(':')
.filter(|(key, _)| *key == SKIN_LAYERS_KEY)
.filter(|(key, _)| *key == name)
.map(|(_, value)| value)
})
}
if line(own).is_none()
&& let Some(value) = line(options_txt)
if line(own, SKIN_LAYERS_KEY).is_none()
&& let Some(value) = line(options_txt, SKIN_LAYERS_KEY)
{
self.three_d_skin_layers = value == "true";
}
// The emote key was a `key_key.emote` line of options.txt (#10092): a rebound key is kept.
if line(own, EMOTE_KEY).is_none()
&& let Some(value) = line(options_txt, OLD_EMOTE_LINE).map(str::trim)
&& value.starts_with("key.")
&& (value == "key.keyboard.unknown"
|| crate::keybinds::key_code(value).is_some()
|| crate::keybinds::mouse_button(value).is_some())
{
self.emote_key = value.to_owned();
}
}
/// Binds the extra key `name` (`SORT_KEY`); false for
/// another name (a vanilla mapping).
pub fn set_key(&mut self, name: &str, value: String) -> bool {
match name {
SORT_KEY => self.sort_key = value,
crate::pvp::HUD_EDITOR_KEY => self.pvp.editor_key = value,
_ => return false,
}
true
@@ -598,7 +623,7 @@ pub enum Item {
/// Shown only while cosmetics are enabled (`horizon_cosmetics::enabled`).
CosmeticsMine,
CosmeticsOthers,
CosmeticsAuras,
CosmeticsHideFx,
CosmeticsSolo,
HorizonMarks,
HorizonMe,
@@ -638,7 +663,7 @@ pub fn rows() -> Vec<Result<Item, &'static str>> {
Ok(CosmeticsMine),
Ok(CosmeticsSolo),
Ok(CosmeticsOthers),
Ok(CosmeticsAuras),
Ok(CosmeticsHideFx),
Ok(HorizonMarks),
Ok(HorizonMe),
]);
@@ -672,7 +697,7 @@ impl Item {
Item::Pregen => "horizon.extras.pregen",
Item::CosmeticsMine => "horizon.extras.cosmetics_mine",
Item::CosmeticsOthers => "horizon.extras.cosmetics_others",
Item::CosmeticsAuras => "horizon.extras.cosmetics_auras",
Item::CosmeticsHideFx => "horizon.extras.cosmetics_hide_fx",
Item::CosmeticsSolo => "horizon.extras.cosmetics_solo",
Item::HorizonMarks => "horizon.extras.horizon_marks",
Item::HorizonMe => "horizon.extras.horizon_me",
@@ -700,12 +725,12 @@ impl Item {
Item::Splitscreen => "horizon.extras.splitscreen.tooltip",
Item::ChatAnimation => "horizon.extras.chat_animation.tooltip",
Item::ChatIndicator => "horizon.extras.chat_indicator.tooltip",
Item::WheelCyclesSettings => "horizon.extras.wheel_cycles_settings.tooltip",
Item::WheelCyclesSettings => "horizon.extras.wheel_cycles_list_settings.tooltip",
Item::PackBrowser => "horizon.extras.pack_browser.tooltip",
Item::Pregen => "horizon.extras.pregen.tooltip",
Item::CosmeticsMine => "horizon.extras.cosmetics_mine.tooltip",
Item::CosmeticsOthers => "horizon.extras.cosmetics_others.tooltip",
Item::CosmeticsAuras => "horizon.extras.cosmetics_auras.tooltip",
Item::CosmeticsHideFx => "horizon.extras.cosmetics_hide_fx.tooltip",
Item::CosmeticsSolo => "horizon.extras.cosmetics_solo.tooltip",
Item::HorizonMarks => "horizon.extras.horizon_marks.tooltip",
Item::HorizonMe => "horizon.extras.horizon_me.tooltip",
@@ -747,7 +772,7 @@ impl Item {
Item::Pregen => "Chunk pregeneration",
Item::CosmeticsMine => "My cosmetics",
Item::CosmeticsOthers => "Other players' cosmetics",
Item::CosmeticsAuras => "Others' auras and trails",
Item::CosmeticsHideFx => "Hide other players' trails and auras",
Item::CosmeticsSolo => "In single player",
Item::HorizonMarks => "Horizon logos",
Item::HorizonMe => "Show me as Horizon",
@@ -766,7 +791,7 @@ impl Item {
Fullbright | DynamicLights | OwnNameTag | HideBlockParticles | MouseTweaks | Sort
| AutoReconnect | NoAi | AgentControl | ReproKeys | DiscordPresence | Splitscreen
| ChatAnimation | WheelCyclesSettings | MusicPage | PackBrowser | CosmeticsMine
| CosmeticsOthers | CosmeticsAuras | CosmeticsSolo | HorizonMarks | HorizonMe
| CosmeticsOthers | CosmeticsHideFx | CosmeticsSolo | HorizonMarks | HorizonMe
| Pregen => {
return None;
}
@@ -834,7 +859,9 @@ impl Item {
Item::CosmeticsOthers => {
"What other Horizon players wear: everything, only their name tag styles, or nothing. Hidden unless you own cosmetics, until you choose. Hidden, the game never asks what they wear."
}
Item::CosmeticsAuras => "Auras and trails other players wear. Off by default.",
Item::CosmeticsHideFx => {
"Other players' trails and auras stay hidden; their other cosmetics still show. Off by default."
}
Item::CosmeticsSolo => {
"Show your cosmetics and your name tag's badge in your single-player worlds too (your name tag with Own name tag). Off: only on servers."
}
@@ -872,7 +899,7 @@ impl Item {
| Item::PackBrowser => Some(Cost::None),
Item::CosmeticsMine
| Item::CosmeticsOthers
| Item::CosmeticsAuras
| Item::CosmeticsHideFx
| Item::CosmeticsSolo
| Item::HorizonMarks
| Item::HorizonMe => Some(Cost::Low),
@@ -902,7 +929,7 @@ impl Item {
Item::WheelCyclesSettings => e.wheel_cycles_settings,
Item::PackBrowser => e.pack_browser,
Item::Pregen => e.pregen,
Item::CosmeticsAuras => e.cosmetics_auras,
Item::CosmeticsHideFx => e.cosmetics_hide_fx,
Item::CosmeticsSolo => e.cosmetics_solo,
Item::HorizonMarks => e.horizon_marks,
Item::HorizonMe => e.horizon_listed,
@@ -967,7 +994,7 @@ impl Item {
let o = e.effective_others();
e.cosmetics_others = Some(if back { o.next().next() } else { o.next() });
}
Item::CosmeticsAuras => e.cosmetics_auras = !e.cosmetics_auras,
Item::CosmeticsHideFx => e.cosmetics_hide_fx = !e.cosmetics_hide_fx,
Item::CosmeticsSolo => e.cosmetics_solo = !e.cosmetics_solo,
Item::HorizonMarks => e.horizon_marks = !e.horizon_marks,
Item::HorizonMe => e.horizon_listed = !e.horizon_listed,
@@ -979,6 +1006,7 @@ impl Item {
pub fn key_name(self) -> Option<&'static str> {
match self {
Item::SortKey => Some(SORT_KEY),
Item::Pvp(crate::pvp::Opt::HudEditorKey) => Some(crate::pvp::HUD_EDITOR_KEY),
_ => None,
}
}
@@ -996,7 +1024,7 @@ mod tests {
..Extras::default()
};
assert_eq!(
(e.effective_others(), e.cosmetics_auras, e.cosmetics_mine),
(e.effective_others(), e.cosmetics_hide_fx, e.cosmetics_mine),
(Others::Shown, false, true)
);
Item::CosmeticsOthers.cycle(&mut e, false);
@@ -1055,6 +1083,16 @@ mod tests {
assert_eq!(hidden.effective_others(), Others::Hidden);
}
#[test]
fn a_saved_wheel_setting_from_before_the_default_changed_is_dropped() {
assert!(!Extras::parse("wheelCyclesSettings:true\n").wheel_cycles_settings);
let on = Extras {
wheel_cycles_settings: true,
..Extras::default()
};
assert!(Extras::parse(&on.serialize()).wheel_cycles_settings);
}
#[test]
fn everything_is_off_by_default_and_round_trips() {
let d = Extras::default();
@@ -1063,7 +1101,7 @@ mod tests {
assert!(!d.mouse_tweaks && !d.sort && !d.own_name_tag && !d.ui_sound_reserve);
assert!(!d.hide_block_particles && !d.pregen);
assert!(!d.music_page && d.music_off.is_empty());
assert!(d.discord_presence && d.wheel_cycles_settings);
assert!(d.discord_presence && !d.wheel_cycles_settings);
assert_eq!(Extras::parse(""), d);
assert_eq!(Extras::parse(&d.serialize()), d);
let mut e = d.clone();
@@ -1107,7 +1145,7 @@ mod tests {
Item::Splitscreen.cycle(&mut e, false);
assert_eq!(Item::Splitscreen.switch(&e), Some(true));
Item::WheelCyclesSettings.cycle(&mut e, false);
assert_eq!(Item::WheelCyclesSettings.switch(&e), Some(false));
assert_eq!(Item::WheelCyclesSettings.switch(&e), Some(true));
e.music_off = vec![
"minecraft:music/game/calm1".into(),
"minecraft:music.menu".into(),
@@ -0,0 +1,268 @@
//! The Horizon Extras page, on the settings shell (`settings_shell`): which rows it has and what
//! pressing them does. Each one's value and what it does stay in `extras::Item`.
use crate::{
extras::{self, Item},
lang,
options_ui::{Event, Page},
settings::Settings,
settings_shell::{self as shell, Group, Output, Row, Screen, Sheet, Value},
};
const TITLE: &str = "horizon.pages.extras";
const ABOUT: &str = "horizon.pages.extras.tooltip";
/// The group of the mod that replaced an extra.
const MODS: &str = "horizon.pages.mods";
const ZOOM: &str = "horizon.extras.zoom";
const ZOOM_INSTALL: &str = "horizon.extras.zoom_mod_install";
const MOD_SETTINGS: &str = "horizon.mods.settings";
/// Its descriptions run long, as tooltips they were: most need more than two lines.
const NOTE_LINES: usize = 5;
/// The official native mod that took over from the Zoom extra (#8713).
const ZOOM_ID: &str = "zoom";
/// A row of the page.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum ExtraRow {
Item(Item),
/// The Zoom mod, at this place among the installed ones, on or off.
Zoom(usize),
/// Its own settings page.
ZoomSettings(usize),
/// It isn't installed: where to get it. A note, not a button: Options > Mods already has the
/// one way into the Mods page (`every_setting_and_page_is_reached_from_one_place`).
ZoomInstall,
}
fn item_row(item: Item, s: &Settings) -> Row<ExtraRow> {
let e = &s.extras;
let value = match (item.key_name(), item.switch(e)) {
(Some(key), _) => Value::Key(key, item.value(e)),
(None, Some(on)) => Value::Switch(on),
(None, None) => Value::Choice(item.value(e)),
};
// A setting of a switch that is off does nothing, so it is greyed out.
let live = item
.parent()
.is_none_or(|parent| parent.switch(e) != Some(false));
Row::new(ExtraRow::Item(item), item.caption_text(), value)
.about(item.description_text())
.live(live)
}
/// Zoom, the FPS counter, Keystrokes and the Minimap are official mods now; Extras keeps a way to
/// the one players look for here.
fn zoom_rows(s: &Settings) -> Vec<Row<ExtraRow>> {
let installed = &s.mods.installed;
let found = installed
.iter()
.position(|m| m.id == ZOOM_ID && m.native.is_some());
let Some(i) = found else {
let note = Row::new(ExtraRow::ZoomInstall, lang::get(ZOOM_INSTALL), Value::Note);
return vec![note.live(false)];
};
let about = installed[i].problem.as_ref();
let about = about.unwrap_or(&installed[i].description).clone();
let on = Value::Switch(s.mods.is_on(i));
let mut rows = vec![Row::new(ExtraRow::Zoom(i), lang::get(ZOOM), on).about(about)];
let page = installed[i].native.as_ref();
if page.is_some_and(|page| !page.keys.is_empty() || !page.buttons.is_empty()) {
let name = lang::get(MOD_SETTINGS);
rows.push(Row::new(ExtraRow::ZoomSettings(i), name, Value::Press));
}
rows
}
/// The page itself holds nothing: everything it shows is in the settings.
#[derive(Default)]
pub(crate) struct ExtrasScreen;
impl Screen for ExtrasScreen {
type Page = ();
type Row = ExtraRow;
fn title(&self) -> &'static str {
TITLE
}
fn note_lines(&self) -> usize {
NOTE_LINES
}
/// The rows read the extras, the mods and whether cosmetics are offered.
fn stamp(&self, s: &Settings) -> u64 {
shell::stamp_of(&(&s.extras, &s.mods, horizon_cosmetics::enabled()))
}
fn sheets(&self, s: &Settings) -> Vec<Sheet<(), ExtraRow>> {
let mut groups = vec![Group {
name: MODS,
rows: zoom_rows(s),
}];
for row in extras::rows() {
match row {
Err(name) => groups.push(Group {
name,
rows: Vec::new(),
}),
Ok(item) => {
if let Some(group) = groups.last_mut() {
group.rows.push(item_row(item, s));
}
}
}
}
vec![Sheet {
id: (),
name: lang::get(TITLE),
about: lang::get(ABOUT),
groups,
}]
}
fn press(&mut self, s: &mut Settings, row: ExtraRow, back: bool) -> Vec<Output> {
match row {
ExtraRow::Item(item) => match item.key_name() {
Some(key) => vec![Output::BindKey(key)],
None => {
item.cycle(&mut s.extras, back);
vec![Output::Event(Event::Changed)]
}
},
ExtraRow::Zoom(i) => {
s.mods.toggle(i);
vec![Output::Event(Event::Changed)]
}
ExtraRow::ZoomSettings(i) => vec![
Output::Event(Event::ButtonClick),
Output::Push(Page::NativeMod(i)),
Output::Event(Event::NativeSettings(i)),
],
ExtraRow::ZoomInstall => Vec::new(),
}
}
/// With the wheel extra on, the wheel over an item changes it, without a click; the mod's rows
/// let the list scroll.
fn wheel_step(&mut self, s: &mut Settings, row: ExtraRow, back: bool) -> Option<Vec<Output>> {
let ExtraRow::Item(item) = row else {
return None;
};
item.cycle(&mut s.extras, back);
Some(vec![Output::Event(Event::Changed)])
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::settings_shell::{Hosted, Shell};
const SIZE: (f32, f32) = (854.0, 480.0);
fn rows_of(s: &Settings) -> Vec<Row<ExtraRow>> {
let sheets = ExtrasScreen.sheets(s);
let groups = sheets.into_iter().flat_map(|sheet| sheet.groups);
groups.flat_map(|group| group.rows).collect()
}
/// Greyed exactly while its own switch is off, so it takes no press then.
#[test]
fn a_setting_of_a_switch_that_is_off_is_greyed_and_takes_no_press() {
let mut s = Settings::default();
let live = |s: &Settings| {
let wheel = ExtraRow::Item(Item::MouseTweaksWheel);
rows_of(s).iter().find(|r| r.id == wheel).unwrap().live
};
s.extras.mouse_tweaks = false;
assert!(!live(&s));
let mut shell = Shell::<ExtrasScreen>::default();
let row = ExtraRow::Item(Item::MouseTweaksWheel);
let at = shell.point_at(&s, SIZE, row);
let before = s.extras.mouse_tweaks_wheel;
assert!(
shell
.click(&mut s, at, SIZE, false, false, false)
.is_empty()
);
assert_eq!(s.extras.mouse_tweaks_wheel, before);
s.extras.mouse_tweaks = true;
assert!(live(&s));
assert!(
!shell
.click(&mut s, at, SIZE, false, false, false)
.is_empty()
);
assert_ne!(s.extras.mouse_tweaks_wheel, before);
}
#[test]
fn the_three_way_cosmetics_row_turns_both_ways() {
use horizon_cosmetics::prefs::Others;
let mut s = Settings::default();
let mut screen = ExtrasScreen;
let row = ExtraRow::Item(Item::CosmeticsOthers);
// Not chosen, nothing owned: it shows Hidden, and forward wraps to Shown.
assert_eq!(s.extras.effective_others(), Others::Hidden);
screen.press(&mut s, row, false);
assert_eq!(s.extras.cosmetics_others, Some(Others::Shown));
screen.press(&mut s, row, true);
assert_eq!(s.extras.cosmetics_others, Some(Others::Hidden));
screen.press(&mut s, row, true);
assert_eq!(s.extras.cosmetics_others, Some(Others::NameTags));
}
/// The sort key binds instead of cycling; the wheel leaves it be.
#[test]
fn the_key_row_waits_for_a_key() {
let mut s = Settings::default();
let mut screen = ExtrasScreen;
let row = ExtraRow::Item(Item::SortKey);
let key = s.extras.sort_key.clone();
assert_eq!(
screen.press(&mut s, row, false),
vec![Output::BindKey(extras::SORT_KEY)]
);
screen.wheel_step(&mut s, row, true);
assert_eq!(s.extras.sort_key, key);
}
#[test]
fn the_wheel_flips_a_switch_and_leaves_the_mod_to_scroll() {
let mut s = Settings::default();
let mut screen = ExtrasScreen;
let row = ExtraRow::Item(Item::Sort);
assert_eq!(
screen.wheel_step(&mut s, row, true),
Some(vec![Output::Event(Event::Changed)])
);
assert!(s.extras.sort);
assert_eq!(screen.wheel_step(&mut s, ExtraRow::Zoom(0), true), None);
}
/// A page with no page list has Done under the search field: Down goes to the first row
/// instead, from the keys and, as the frame declares it, from a controller's focus.
#[test]
fn down_from_the_search_field_lands_on_the_first_row() {
use crate::{hud::HudBatch, settings_shell::Columns, ui};
use winit::keyboard::KeyCode as K;
let atlas = crate::hud::HudAtlas::build(std::path::Path::new("/nonexistent")).unwrap();
let mut s = Settings::default();
let mut shell = Shell::<ExtrasScreen>::default();
let mut batch = HudBatch::new([1708, 960]);
shell.draw(&s, &mut batch, &atlas, (0.0, 0.0), false, None);
crate::widgets::end_frame(1);
let size = (batch.width, batch.height);
let cols = Columns::of(size.0, size.1).with_note_lines(NOTE_LINES);
// The Mods group's note can't take the focus: the first item can.
let first = ExtraRow::Item(Item::OwnNameTag);
let at = shell.point_at(&s, size, first);
let [x, y, w, h] = crate::widgets::linked_step(cols.search(), true).unwrap();
assert!(ui::hovered(at, x, y, w, h));
assert_eq!(crate::widgets::linked_step(cols.search(), false), None);
let [x, y, w, h] = cols.search();
shell.click(
&mut s,
(x + w / 2.0, y + h / 2.0),
size,
false,
false,
false,
);
shell.key(&mut s, K::ArrowDown, None, false, size);
assert_eq!(shell.focused_row(), Some(first));
}
}
@@ -0,0 +1,210 @@
// Still-fluid tiling follows Catnip/Ponder 1.0.82 FluidRenderHelper.renderTiledFace.
// Copyright (c) 2022 The Create Team
//
// 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:
//
// 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.
use horizon_core::geometry::Direction;
/// Controls a still-fluid box. The tint is stack ARGB, independent of biome tint.
#[derive(Clone, Copy, Debug)]
pub struct NativeFluidRenderOptions {
pub argb: Option<u32>,
pub render_bottom: bool,
/// TextureAtlasSprite.uvShrinkRatio: 4 / the largest stitched atlas dimension.
/// Explicit because Horizon's block sprites occupy arrays rather than a stitched atlas.
pub uv_shrink_ratio: f32,
}
/// A corner before the caller's pose, with UVs within the still sprite, and its outward normal.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct FluidVertex {
pub pos: [f32; 3],
pub uv: [f32; 2],
pub normal: [i8; 3],
}
/// Preserve sky light and increase block light to the fluid's luminosity (native light packing).
pub fn fluid_light(light: u32, luminosity: u8) -> u32 {
(light & 0xf0) | (((light >> 8 & 0xf0).max(u32::from(luminosity.min(15)) * 16)) << 8)
}
/// Emit whole tiled quads, or refuse invalid/unreasonably large geometry before emitting anything.
/// Negative coordinates and exact integer boundaries retain the source's local UV fractions.
pub fn fluid_box(
bounds: [f32; 6],
options: NativeFluidRenderOptions,
mut emit: impl FnMut(FluidVertex),
) -> bool {
if !options.uv_shrink_ratio.is_finite()
|| !(0.0..=1.0).contains(&options.uv_shrink_ratio)
|| bounds
.iter()
.any(|x| !x.is_finite() || x.abs() > 1_048_576.0)
|| (0..3).any(|i| bounds[i] > bounds[i + 3])
{
return false;
}
let tiles =
std::array::from_fn::<_, 3, _>(|i| (bounds[i + 3].ceil() - bounds[i].floor()) as u64);
let quads = tiles[0] * tiles[1] * 2
+ tiles[1] * tiles[2] * 2
+ tiles[0] * tiles[2] * if options.render_bottom { 2 } else { 1 };
if quads > 65_536 {
return false;
}
let [x0, y0, z0, x1, y1, z1] = bounds;
for (side, face) in [
(Direction::Down, [x0, z0, x1, z1, y0]),
(Direction::Up, [x0, z0, x1, z1, y1]),
(Direction::North, [x0, y0, x1, y1, z0]),
(Direction::South, [x0, y0, x1, y1, z1]),
(Direction::West, [z0, y0, z1, y1, x0]),
(Direction::East, [z0, y0, z1, y1, x1]),
] {
if side != Direction::Down || options.render_bottom {
tiled_face(&mut emit, side, face, options.uv_shrink_ratio);
}
}
true
}
fn tiled_face(out: &mut impl FnMut(FluidVertex), side: Direction, face: [f32; 5], ratio: f32) {
let [a0, b0, a1, b1, plane] = face;
let positive = matches!(side, Direction::Up | Direction::South | Direction::East);
let shrink = ratio * 0.25;
let lerp = |v| v + shrink * (0.5 - v);
let mut a = a0;
while a < a1 {
let end_a = (a.floor() + 1.0).min(a1);
let (u0, u1) = if matches!(side, Direction::North | Direction::East) {
(end_a.ceil() - end_a, end_a.ceil() - a)
} else {
(a - a.floor(), end_a - a.floor())
};
let (u0, u1) = (lerp(u0), lerp(u1));
let mut b = b0;
while b < b1 {
let end_b = (b.floor() + 1.0).min(b1);
let (v0, v1) = if side == Direction::Up {
(b - b.floor(), end_b - b.floor())
} else {
(end_b.ceil() - end_b, end_b.ceil() - b)
};
let (v0, v1) = (lerp(v0), lerp(v1));
let positions = match side {
Direction::West | Direction::East => {
let (near, far) = if positive { (end_a, a) } else { (a, end_a) };
[
[plane, end_b, near],
[plane, b, near],
[plane, b, far],
[plane, end_b, far],
]
}
Direction::North | Direction::South => {
let (near, far) = if positive { (a, end_a) } else { (end_a, a) };
[
[near, end_b, plane],
[near, b, plane],
[far, b, plane],
[far, end_b, plane],
]
}
Direction::Down | Direction::Up => {
let (near, far) = if positive { (b, end_b) } else { (end_b, b) };
[
[a, plane, near],
[a, plane, far],
[end_a, plane, far],
[end_a, plane, near],
]
}
};
let normal = match side {
Direction::Down => [0, -1, 0],
Direction::Up => [0, 1, 0],
Direction::North => [0, 0, -1],
Direction::South => [0, 0, 1],
Direction::West => [-1, 0, 0],
Direction::East => [1, 0, 0],
};
for (pos, uv) in positions
.into_iter()
.zip([[u0, v0], [u0, v1], [u1, v1], [u1, v0]])
{
out(FluidVertex { pos, uv, normal });
}
b = end_b;
}
a = end_a;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn source_tiling_is_outward_and_does_not_inset_geometry() {
let bounds = [-1.25, 0.125, -0.25, 1.5, 1.125, 0.75];
let mut result = Vec::new();
assert!(fluid_box(
bounds,
NativeFluidRenderOptions {
argb: None,
render_bottom: false,
uv_shrink_ratio: 4.0 / 2048.0,
},
|v| result.push(v),
));
assert_eq!(result.len(), (4 * 2 * 2 + 2 * 2 * 2 + 4 * 2) * 4);
for quad in result.as_chunks::<4>().0 {
let [a, b, c, _] = quad.map(|v| glam::Vec3::from_array(v.pos));
assert!(
(b - a)
.cross(c - a)
.dot(glam::Vec3::from_array(quad[0].normal.map(f32::from)))
> 0.0
);
assert!(
quad.iter()
.all(|v| (0..3).all(|i| v.pos[i] >= bounds[i] && v.pos[i] <= bounds[i + 3]))
);
}
assert!(
result
.iter()
.any(|v| v.pos == [bounds[0], bounds[4], bounds[2]])
);
assert!(!fluid_box(
[0.0, 0.0, 0.0, 1_000.0, 1_000.0, 1_000.0],
NativeFluidRenderOptions {
argb: None,
render_bottom: true,
uv_shrink_ratio: 0.0,
},
|_| panic!("invalid geometry must fail before emitting"),
));
}
#[test]
fn luminosity_changes_only_block_light() {
assert_eq!(fluid_light(0x20a0, 15), 0xf0a0);
assert_eq!(fluid_light(0x25a5, 15), 0xf0a0);
assert_eq!(fluid_light(0xe030, 3), 0xe030);
}
}
+76 -16
View File
@@ -9,6 +9,7 @@ use crate::{
environment::Sky,
world::World,
};
use horizon_core::random::{LegacyRandom, RandomSource};
use std::path::Path;
/// `FogType` of the camera position.
@@ -64,7 +65,7 @@ impl FogTables {
}
/// Fog state that evolves over time.
#[derive(Clone, Copy, Debug, Default)]
#[derive(Clone, Debug)]
pub struct FogState {
/// `LocalPlayer.waterVisionTime` (0..600).
pub water_vision_time: i32,
@@ -72,24 +73,28 @@ pub struct FogState {
pub rain_fog_multiplier: f32,
/// `LightmapRenderStateExtractor.blockLightFlicker`.
pub block_light_flicker: f32,
flicker_seed: u64,
/// `LightmapRenderStateExtractor.random`: `RandomSource.create()`.
flicker_random: LegacyRandom,
}
impl Default for FogState {
fn default() -> Self {
Self {
water_vision_time: 0,
rain_fog_multiplier: 0.0,
block_light_flicker: 0.0,
flicker_random: crate::random_source::create(),
}
}
}
impl FogState {
fn next_float(&mut self) -> f32 {
// splitmix64: the vanilla source is unseeded `RandomSource.create()`.
self.flicker_seed = self.flicker_seed.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = self.flicker_seed;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
((z ^ (z >> 31)) >> 40) as f32 / (1u32 << 24) as f32
}
/// `LightmapRenderStateExtractor.tick`: the torch light flickers.
pub fn tick_flicker(&mut self) {
let r = &mut self.flicker_random;
let (a, b, c, d) = (
self.next_float(),
self.next_float(),
self.next_float(),
self.next_float(),
r.next_float(),
r.next_float(),
r.next_float(),
r.next_float(),
);
self.block_light_flicker += (a - b) * c * d * 0.1;
self.block_light_flicker *= 0.9;
@@ -413,6 +418,19 @@ pub fn clear_distance_fog(fog: &mut FogData, input: &FogInputs) {
(fog.environmental_start, fog.environmental_end) = (1e9, 2e9);
}
}
/// What `AtmosphericFogEnvironment.updateRainFogState` eases the rain fog toward: the rain, where
/// the sky reaches the eye, halved in a biome without precipitation.
pub fn rain_fog_target(rain: f32, sky_light: u8, biome_rains: bool) -> f32 {
let sky = ((f32::from(sky_light) - 8.0) / 7.0).clamp(0.0, 1.0);
rain * sky * if biome_rains { 1.0 } else { 0.5 }
}
/// Whether the frame draws no sky, only the clear to the fog colour: `LevelRenderer.addSkyPass`
/// leaves the sky out in lava and powder snow and while blindness or darkness is on (`Camera`'s
/// `doesMobEffectBlockSky`), and `GameRenderer.renderLevel` gives `LevelRenderer.render`
/// `shouldRenderSky` false while a boss bar makes world fog (the Ender Dragon's).
pub fn sky_pass_skipped(fog_type: FogType, effect_blocks_sky: bool, boss_world_fog: bool) -> bool {
matches!(fog_type, FogType::Lava | FogType::PowderSnow) || effect_blocks_sky || boss_world_fog
}
/// `AtmosphericFogEnvironment.updateRainFogState`.
pub fn update_rain_fog(
state: &mut FogState,
@@ -421,10 +439,14 @@ pub fn update_rain_fog(
biome_rains: bool,
delta_ticks: f32,
) {
let sky = ((f32::from(sky_light) - 8.0) / 7.0).clamp(0.0, 1.0);
let target = rain * sky * if biome_rains { 1.0 } else { 0.5 };
let target = rain_fog_target(rain, sky_light, biome_rains);
state.rain_fog_multiplier += (target - state.rain_fog_multiplier) * delta_ticks * 0.2;
}
/// The rain fog at its target at once: a frozen world under `HORIZON_STILL`, where the rain sits
/// at its level and easing would wait for ticks that do not come.
pub fn settle_rain_fog(state: &mut FogState, rain: f32, sky_light: u8, biome_rains: bool) {
state.rain_fog_multiplier = rain_fog_target(rain, sky_light, biome_rains);
}
fn rgb(c: u32) -> [f32; 3] {
[
(c >> 16 & 255) as f32 / 255.0,
@@ -480,6 +502,20 @@ mod tests {
assert_eq!(fog(surface - 0.01, -90.0), FogType::Atmospheric);
}
#[test]
fn the_dragon_fights_world_fog_leaves_out_the_sky() {
use FogType::*;
// GameRenderer.renderLevel: no sky pass under a createWorldFog boss bar, in clear air
// as in water.
assert!(sky_pass_skipped(Atmospheric, false, true));
assert!(sky_pass_skipped(Water, false, true));
// LevelRenderer.addSkyPass's own conditions.
assert!(sky_pass_skipped(Lava, false, false));
assert!(sky_pass_skipped(PowderSnow, false, false));
assert!(sky_pass_skipped(Atmospheric, true, false));
assert!(!sky_pass_skipped(Atmospheric, false, false));
assert!(!sky_pass_skipped(Water, false, false));
}
#[test]
fn boss_world_fog_closes_in() {
// AtmosphericFogEnvironment.setupFog under a createWorldFog boss bar.
let sky = Sky {
@@ -705,6 +741,30 @@ mod tests {
assert_eq!(clear.environmental_end, settled.environmental_end);
assert_eq!(clear.color, settled.color);
}
/// Settling puts the rain fog at its target exactly, where a tick of easing moves it a fifth
/// of the way, as before.
#[test]
fn settled_rain_fog_is_its_target_and_easing_is_unchanged() {
assert_eq!(
rain_fog_target(1.0, 15, false),
0.5,
"a dry biome halves it"
);
assert_eq!(rain_fog_target(1.0, 8, true), 0.0, "no sky, no rain fog");
let start = FogState {
rain_fog_multiplier: 0.1,
..FogState::default()
};
let mut settled = start.clone();
settle_rain_fog(&mut settled, 0.5, 15, true);
assert_eq!(settled.rain_fog_multiplier, 0.5);
let mut eased = start;
update_rain_fog(&mut eased, 0.5, 15, true, 1.0);
assert_eq!(
eased.rain_fog_multiplier.to_bits(),
(0.1f32 + (0.5f32 - 0.1) * 1.0 * 0.2).to_bits()
);
}
#[test]
fn rain_fog_pulls_fog_in() {
let sky = Sky {
+135 -32
View File
@@ -126,11 +126,16 @@ struct Cache {
random: horizon_core::random::LegacyRandom,
}
pub struct Font {
/// The atlas rects of the sheets under `textures/font`.
/// The atlas rects of every texture, by resource name: a bitmap provider's
/// `file` may name any texture, not only one under `textures/font`.
sheets: HashMap<String, Rect>,
missing: Glyph,
white: Rect,
root: PathBuf,
/// The game's own resources, below a pack overlay: a font file the overlay
/// lacks (a pack adds providers over vanilla's, or the overlay left a
/// directory out) is read from here, as vanilla's resource stack does.
fallback: Option<PathBuf>,
/// Atlas size and the reserved dynamic region (y, height).
atlas: (u32, u32),
region: (u32, u32),
@@ -199,6 +204,13 @@ fn parse_hex(text: &[u8], out: &mut HashMap<u32, HexGlyph>) {
out.insert(codepoint, HexGlyph { rows, left, right });
}
}
/// A resource file's bytes, from `root` or, when the overlay lacks it, from
/// the game's own resources below (`defaults`).
fn read_resource(root: &Path, defaults: Option<&Path>, path: &str) -> Option<Vec<u8>> {
std::fs::read(root.join(path))
.ok()
.or_else(|| defaults.and_then(|d| std::fs::read(d.join(path)).ok()))
}
fn read_json(root: &Path, path: &str) -> Option<serde_json::Value> {
serde_json::from_slice(&std::fs::read(root.join(path)).ok()?).ok()
}
@@ -281,6 +293,7 @@ fn hex_advance(h: &HexGlyph) -> f32 {
/// pixels, copied into the atlas when first drawn.
fn load_bitmap(
root: &Path,
defaults: Option<&Path>,
p: &serde_json::Value,
sheets: &HashMap<String, Rect>,
pixels: &[u8],
@@ -290,22 +303,36 @@ fn load_bitmap(
let mut out = HashMap::new();
let file = p["file"].as_str().unwrap_or("");
let (ns, name) = file.split_once(':').unwrap_or(("minecraft", file));
let in_atlas = (ns == "minecraft")
.then(|| sheets.get(name.trim_end_matches(".png")).copied())
.flatten();
let name = name.trim_end_matches(".png");
// The atlas names the default namespace's textures by their path and every
// other namespace's as `namespace:path` (HudAtlas::build).
let atlas_name = if ns == "minecraft" {
name.to_owned()
} else {
format!("{ns}:{name}")
};
let in_atlas = sheets.get(&atlas_name).copied();
// The sheet: its size, and its pixels when they are not in the atlas.
let (sw, sh, sheet) = match in_atlas {
Some(rect) => {
let (w, h) = rect.texels(atlas);
(w, h, None)
}
None => match crate::hud::decode(&root.join(resource_path(file, "textures", ""))) {
Ok((w, h, rgba)) => (w, h, Some(Arc::new((w, rgba)))),
Err(_) => {
log::warn!("font sheet {file} could not be read");
return out;
None => {
let path = resource_path(file, "textures", "");
let sheet = crate::hud::decode(&root.join(&path)).or_else(|first| {
defaults
.and_then(|d| crate::hud::decode(&d.join(&path)).ok())
.ok_or(first)
});
match sheet {
Ok((w, h, rgba)) => (w, h, Some(Arc::new((w, rgba)))),
Err(_) => {
log::warn!("font sheet {file} could not be read");
return out;
}
}
},
}
};
let rows: Vec<Vec<char>> = p["chars"]
.as_array()
@@ -382,11 +409,13 @@ fn load_bitmap(
}
/// `TrueTypeGlyphProviderDefinition.load`: a TrueType file (not CFF) under
/// `font/`, its Unicode characters but the `skip` ones.
fn load_true_type(root: &Path, p: &serde_json::Value) -> Option<TrueType> {
fn load_true_type(root: &Path, defaults: Option<&Path>, p: &serde_json::Value) -> Option<TrueType> {
let file = p["file"].as_str()?;
let data = std::fs::read(root.join(resource_path(file, "font", "")))
.map_err(|e| log::warn!("font file {file}: {e}"))
.ok()?;
let path = resource_path(file, "font", "");
let Some(data) = read_resource(root, defaults, &path) else {
log::warn!("font file {file} could not be read");
return None;
};
// FT_Get_Font_Format must say TrueType: CFF outlines ("OTTO") are refused.
if data.starts_with(b"OTTO") {
log::warn!("font file {file} is not in TTF format");
@@ -486,13 +515,16 @@ impl TrueType {
)
}
}
fn load_hex(root: &Path, p: &serde_json::Value) -> HashMap<u32, HexGlyph> {
fn load_hex(root: &Path, defaults: Option<&Path>, p: &serde_json::Value) -> HashMap<u32, HexGlyph> {
let mut glyphs = HashMap::new();
let Some(file) = p["hex_file"].as_str() else {
return glyphs;
};
let path = root.join(resource_path(file, "", "").replace("//", "/"));
let Ok(zip) = Zip::open(&path) else {
let path = resource_path(file, "", "").replace("//", "/");
let Some(zip) = Zip::open(&root.join(&path))
.ok()
.or_else(|| defaults.and_then(|d| Zip::open(&d.join(&path)).ok()))
else {
return glyphs;
};
let names: Vec<String> = zip
@@ -603,7 +635,24 @@ impl Font {
region: (u32, u32),
white: Rect,
) -> Self {
Self::load_with(root, sprites, pixels, atlas, region, white, false)
Self::load_with(root, None, sprites, pixels, atlas, region, white, false)
}
/// [`Font::load`] over a pack overlay: a sheet, TrueType file or Unihex
/// file the overlay lacks is read from `defaults`, the game's own
/// resources, as vanilla's resource stack does. (Definitions stay the
/// overlay's: a pack's filter may hide one from a lower pack.)
#[allow(clippy::too_many_arguments)]
pub fn load_over(
root: &Path,
defaults: &Path,
sprites: &HashMap<String, Rect>,
pixels: &[u8],
atlas: (u32, u32),
region: (u32, u32),
white: Rect,
) -> Self {
let fallback = (root != defaults).then_some(defaults);
Self::load_with(root, fallback, sprites, pixels, atlas, region, white, false)
}
/// A server measures text but never draws it. Decode bitmap sheets only
/// to obtain advances, then release their pixels; never rasterize glyphs.
@@ -616,10 +665,21 @@ impl Font {
w: 1,
h: 1,
};
Self::load_with(root, &HashMap::new(), &[], (1, 1), (0, 0), white, true)
Self::load_with(
root,
None,
&HashMap::new(),
&[],
(1, 1),
(0, 0),
white,
true,
)
}
#[allow(clippy::too_many_arguments)]
fn load_with(
root: &Path,
fallback: Option<&Path>,
sprites: &HashMap<String, Rect>,
pixels: &[u8],
atlas: (u32, u32),
@@ -627,11 +687,7 @@ impl Font {
white: Rect,
metrics_only: bool,
) -> Self {
let sheets = sprites
.iter()
.filter(|(k, _)| k.starts_with("font/"))
.map(|(k, v)| (k.clone(), *v))
.collect();
let sheets = sprites.clone();
let font = Self {
sheets,
missing: Glyph {
@@ -645,6 +701,7 @@ impl Font {
},
white,
root: root.to_owned(),
fallback: fallback.map(|p| p.to_owned()),
atlas,
region,
metrics_only,
@@ -656,11 +713,8 @@ impl Font {
pending: Vec::new(),
full: false,
},
random: horizon_core::random::LegacyRandom::new(
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |d| d.as_nanos() as i64),
),
// `Font.random`: `RandomSource.create()`.
random: crate::random_source::create(),
}),
generation: std::cell::Cell::new(next_generation()),
uniform: false,
@@ -698,6 +752,7 @@ impl Font {
Filter::of(p),
load_bitmap(
&self.root,
self.fallback.as_deref(),
p,
&self.sheets,
pixels,
@@ -708,7 +763,7 @@ impl Font {
"unihex" => Some(Provider::Unihex(Filter::of(p), p.clone(), None)),
"ttf" => Some(Provider::TrueType(
Filter::of(p),
Box::new(load_true_type(&self.root, p)?),
Box::new(load_true_type(&self.root, self.fallback.as_deref(), p)?),
)),
_ => None,
})
@@ -868,7 +923,8 @@ impl Font {
}
}
Provider::Unihex(filter, def, glyphs) if filter.passes(uniform, japanese) => {
let glyphs = glyphs.get_or_insert_with(|| load_hex(&self.root, def));
let glyphs = glyphs
.get_or_insert_with(|| load_hex(&self.root, self.fallback.as_deref(), def));
if let Some(h) = glyphs.get(&u32::from(c)).copied()
&& !(non_fishy && fishy_advance(hex_advance(&h)))
{
@@ -1026,7 +1082,8 @@ impl Font {
}
}
Provider::Unihex(filter, def, glyphs) if filter.passes(uniform, japanese) => {
let glyphs = glyphs.get_or_insert_with(|| load_hex(&self.root, def));
let glyphs = glyphs
.get_or_insert_with(|| load_hex(&self.root, self.fallback.as_deref(), def));
if let Some(h) = glyphs.get(&u32::from(c)) {
return Some(hex_advance(h));
}
@@ -1512,6 +1569,52 @@ mod tests {
assert!(texels.iter().any(|t| t[0] == 255));
}
/// A bitmap provider's `file` can name any texture. When the atlas has that
/// texture (HUD packs keep their glyph sheets outside `textures/font`), the
/// glyphs are read from it instead of being copied into the small run-time
/// region one by one; the region then cannot fill up and turn later glyphs
/// into the missing one, whose advance shifts the rest of a HUD line.
#[test]
fn a_bitmap_sheet_outside_font_is_read_from_the_atlas() {
let sheet = png(16, 8, |x, _| x < 2 || (8..14).contains(&x));
let (_, _, pixels) = crate::hud::decode_bytes(&sheet).unwrap();
let rect = Rect {
u0: 0.0,
v0: 0.0,
u1: 1.0,
v1: 1.0,
w: 16,
h: 8,
};
for (ns, file, key) in [
("minecraft", "minecraft:hud/sheet.png", "hud/sheet"),
(
"hudengine",
"hudengine:font/sheet.png",
"hudengine:font/sheet",
),
] {
let def = format!(
r#"{{"providers":[{{"type":"bitmap","file":"{file}","ascent":7,"chars":["ab"]}}]}}"#
);
// The image is only in the atlas: the pack's copy is not on disk.
let root = root(
ns,
&[("assets/minecraft/font/default.json", def.as_bytes())],
);
let sprites = HashMap::from([(key.to_string(), rect)]);
let font = Font::load(&root, &sprites, &pixels, (16, 8), (0, 8), white());
let _ = std::fs::remove_dir_all(&root);
assert_eq!(font.glyph('a').advance, 3.0, "{ns}");
assert_eq!(font.glyph('b').advance, 7.0, "{ns}");
assert!(font.glyph('a').rect.is_some(), "{ns}");
assert!(
font.take_patches().is_empty(),
"{ns}: the atlas sheet is not copied into the run-time region"
);
}
}
#[test]
fn styles_pick_their_font_and_unknown_fonts_draw_missing_glyphs() {
let sheet = png(16, 8, |x, _| x < 2 || (8..14).contains(&x));
File diff suppressed because it is too large. Load diff
@@ -1040,7 +1040,14 @@ mod tests {
cache: None,
})
};
let mut flat = flat::Flat::new(&device, &terrain_layout, 4 * GRAIN, 0, pipeline);
let pipeline = &pipeline;
let solid = |layout: &wgpu::PipelineLayout, label: &str, _: &str, _: &str| {
super::super::SolidPipeline {
plain: pipeline(layout, label, "", "", None, None, true),
hdr: None,
}
};
let mut flat = flat::Flat::new(&device, &terrain_layout, 4 * GRAIN, 0, pipeline, solid);
let stride = std::mem::size_of::<crate::mesher::TerrainVertex>() as u64;
let mut translucent = Arena::translucent(&device, stride, 4 * GRAIN);
translucent.page_limit = 4 * GRAIN;
@@ -199,6 +199,8 @@ pub struct FrameInputs {
pub skybox: bool,
pub end_sky: bool,
pub end_sky_sprite: Option<[f32; 4]>,
#[serde(default)]
pub boss_world_fog: bool,
pub environment_fog: Option<(f32, f32)>,
pub water_fog: [f32; 3],
pub fog: Option<FogData>,
@@ -414,6 +416,7 @@ impl Renderer {
skybox: frame.skybox,
end_sky: frame.end_sky,
end_sky_sprite: frame.end_sky_sprite,
boss_world_fog: frame.boss_world_fog,
environment_fog: frame.environment_fog,
water_fog: frame.water_fog,
fog: frame.fog,
@@ -510,6 +513,7 @@ impl Scene {
skybox: f.skybox,
end_sky: f.end_sky,
end_sky_sprite: f.end_sky_sprite,
boss_world_fog: f.boss_world_fog,
environment_fog: f.environment_fog,
water_fog: f.water_fog,
fog: f.fog,
@@ -530,6 +534,7 @@ impl Scene {
visible_static_block_entity_sections: &[],
static_block_entity_stats: Default::default(),
entity_motion: &[],
entity_motion_posed: &[],
entity_spans: &[],
skin: None,
glowing: &[],
+1 -1
View File
@@ -220,7 +220,7 @@ impl Cull {
/// write storage.
pub fn supported(adapter: &wgpu::Adapter, device: &wgpu::Device) -> Option<DepthTest> {
let depth_test = match adapter.get_info().backend {
wgpu::Backend::Vulkan
wgpu::Backend::Vulkan | wgpu::Backend::Gl
if device
.features()
.contains(wgpu::Features::SHADER_EARLY_DEPTH_TEST) =>
@@ -0,0 +1,416 @@
//! OptiFine custom sky layers on the GPU: one texture per layer, kept with the renderer (its pack),
//! a static skybox mesh of the 3x2 grid's six faces, and one pipeline per blend mode. A frame
//! uploads one small uniform per drawn layer ([`crate::custom_sky::Draw`]) and draws 36 vertices
//! each, after the vanilla sky's disc and sunrise and before the sun, moon and stars.
use glam::Mat4;
use horizon_optifine::sky::{Blend, Face};
use wgpu::util::DeviceExt;
use crate::custom_sky::{CustomSky as Pack, Draw};
/// The blend modes in the shader's `mode` order (custom_sky.wgsl).
const MODES: [Blend; 9] = [
Blend::Add,
Blend::Subtract,
Blend::Multiply,
Blend::Dodge,
Blend::Burn,
Blend::Screen,
Blend::Replace,
Blend::Overlay,
Blend::Alpha,
];
/// Layers drawn in a frame at most; a pack's later ones wait for a frame with fewer.
const MAX_DRAWS: usize = 32;
/// Dynamic uniform offsets are 256-byte aligned on every backend.
const STRIDE: u64 = 256;
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
struct Uniform {
clip_from_sky: [[f32; 4]; 4],
brightness: f32,
mode: u32,
pad: [u32; 2],
}
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
struct Vertex {
position: [f32; 3],
uv: [f32; 2],
cell: [f32; 4],
}
pub(super) struct CustomSky {
pipelines: Vec<wgpu::RenderPipeline>,
layout: wgpu::BindGroupLayout,
sampler: wgpu::Sampler,
uniform: wgpu::Buffer,
vertices: wgpu::Buffer,
/// One bind group per layer texture of the pack.
groups: Vec<wgpu::BindGroup>,
/// This frame's layers: bind group and pipeline indices.
frame: Vec<(u16, u8)>,
}
/// The fragment's output for each mode is in custom_sky.wgsl; these factors finish it.
fn blend(mode: Blend) -> wgpu::BlendComponent {
use wgpu::BlendFactor as F;
let (src_factor, dst_factor, operation) = match mode {
Blend::Add => (F::One, F::One, wgpu::BlendOperation::Add),
Blend::Subtract => (F::One, F::One, wgpu::BlendOperation::ReverseSubtract),
Blend::Multiply => (F::Zero, F::Src, wgpu::BlendOperation::Add),
// dst + dst * src: lightens.
Blend::Dodge => (F::Dst, F::One, wgpu::BlendOperation::Add),
// dst * (1 - src): darkens.
Blend::Burn => (F::Zero, F::OneMinusSrc, wgpu::BlendOperation::Add),
// src + dst * (1 - src) = 1 - (1 - src)(1 - dst).
Blend::Screen => (F::One, F::OneMinusSrc, wgpu::BlendOperation::Add),
Blend::Replace => (F::One, F::Zero, wgpu::BlendOperation::Add),
// dst * src + src * dst.
Blend::Overlay => (F::Dst, F::Src, wgpu::BlendOperation::Add),
Blend::Alpha => (F::SrcAlpha, F::OneMinusSrcAlpha, wgpu::BlendOperation::Add),
};
wgpu::BlendComponent {
src_factor,
dst_factor,
operation,
}
}
/// The skybox's 36 vertices: two triangles per face, each corner with its cell's UV.
fn mesh() -> Vec<Vertex> {
let mut out = Vec::with_capacity(36);
for face in Face::ALL {
let cell = face.cell();
let [u0, v0, u1, v1] = cell;
let uv = [[u0, v0], [u1, v0], [u1, v1], [u0, v1]];
let corners = face.corners();
for i in [0, 1, 2, 0, 2, 3] {
out.push(Vertex {
position: corners[i],
uv: uv[i],
cell,
});
}
}
out
}
impl CustomSky {
pub(super) fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> Self {
let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Custom sky bindings"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: true,
min_binding_size: wgpu::BufferSize::new(
std::mem::size_of::<Uniform>() as u64
),
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
],
});
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Custom sky shader"),
source: wgpu::ShaderSource::Wgsl(include_str!("custom_sky.wgsl").into()),
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Custom sky layout"),
bind_group_layouts: &[Some(&layout)],
..Default::default()
});
let attributes = wgpu::vertex_attr_array![0 => Float32x3, 1 => Float32x2, 2 => Float32x4];
let pipelines = MODES
.iter()
.map(|&mode| {
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("Custom sky layer"),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_custom_sky"),
compilation_options: Default::default(),
buffers: &[Some(wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<Vertex>() as u64,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &attributes,
})],
},
primitive: wgpu::PrimitiveState {
cull_mode: None,
..Default::default()
},
depth_stencil: Some(wgpu::DepthStencilState {
format: wgpu::TextureFormat::Depth32Float,
depth_write_enabled: Some(false),
depth_compare: Some(wgpu::CompareFunction::Always),
stencil: Default::default(),
bias: Default::default(),
}),
multisample: Default::default(),
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs_custom_sky"),
compilation_options: Default::default(),
targets: &[Some(wgpu::ColorTargetState {
format,
// The frame's alpha is HDR output's mark (hdr.wgsl): the sky
// under the layers keeps its own.
blend: Some(wgpu::BlendState {
color: blend(mode),
alpha: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::Zero,
dst_factor: wgpu::BlendFactor::One,
operation: wgpu::BlendOperation::Add,
},
}),
write_mask: wgpu::ColorWrites::ALL,
})],
}),
multiview_mask: None,
cache: None,
})
})
.collect();
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("Custom sky"),
min_filter: wgpu::FilterMode::Linear,
mag_filter: wgpu::FilterMode::Linear,
..Default::default()
});
let uniform = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Custom sky layers"),
size: STRIDE * MAX_DRAWS as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let vertices = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Custom sky box"),
contents: bytemuck::cast_slice(&mesh()),
usage: wgpu::BufferUsages::VERTEX,
});
Self {
pipelines,
layout,
sampler,
uniform,
vertices,
groups: Vec::new(),
frame: Vec::new(),
}
}
/// The pack's layer textures, replacing any before.
pub(super) fn set_pack(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, pack: &Pack) {
let limit = device.limits().max_texture_dimension_2d;
self.groups = pack
.images
.iter()
.map(|image| {
// Too large for this GPU: a 1x1 transparent texel, so the layer draws nothing.
let (width, height, rgba) = if image.width <= limit && image.height <= limit {
(image.width, image.height, &image.rgba[..])
} else {
log::warn!(
"custom sky texture of {}x{} too large",
image.width,
image.height
);
(1, 1, &[0u8; 4][..])
};
let texture = device.create_texture_with_data(
queue,
&wgpu::TextureDescriptor {
label: Some("Custom sky layer"),
size: wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8Unorm,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
},
wgpu::util::TextureDataOrder::LayerMajor,
rgba,
);
let view = texture.create_view(&Default::default());
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Custom sky layer"),
layout: &self.layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
buffer: &self.uniform,
offset: 0,
size: wgpu::BufferSize::new(std::mem::size_of::<Uniform>() as u64),
}),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::TextureView(&view),
},
wgpu::BindGroupEntry {
binding: 2,
resource: wgpu::BindingResource::Sampler(&self.sampler),
},
],
})
})
.collect();
}
/// Upload this frame's layers; `clip` is clip space from camera-relative world.
pub(super) fn prepare(
&mut self,
uploads: &mut super::upload::Uploads,
encoder: &mut wgpu::CommandEncoder,
clip: Mat4,
draws: &[Draw],
) {
self.frame.clear();
for draw in draws.iter().take(MAX_DRAWS) {
if usize::from(draw.image) >= self.groups.len() {
continue;
}
let mode = MODES.iter().position(|m| *m == draw.blend).unwrap_or(0);
let uniform = Uniform {
clip_from_sky: (clip * draw.rotation).to_cols_array_2d(),
brightness: draw.brightness.clamp(0.0, 1.0),
mode: mode as u32,
pad: [0; 2],
};
let offset = STRIDE * self.frame.len() as u64;
uploads.buffer(encoder, &self.uniform, offset, bytemuck::bytes_of(&uniform));
self.frame.push((draw.image, mode as u8));
}
}
pub(super) fn is_drawing(&self) -> bool {
!self.frame.is_empty()
}
/// The layers [`CustomSky::prepare`] uploaded. Leaves the pass's pipeline and bind group 0
/// changed.
pub(super) fn draw(&self, pass: &mut wgpu::RenderPass<'_>) {
pass.set_vertex_buffer(0, self.vertices.slice(..));
for (i, &(image, mode)) in self.frame.iter().enumerate() {
pass.set_pipeline(&self.pipelines[usize::from(mode)]);
pass.set_bind_group(
0,
&self.groups[usize::from(image)],
&[(STRIDE * i as u64) as u32],
);
pass.draw(0..36, 0..1);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
/// What a blend leaves a sky texel of `dst` with when the shader outputs `src`
/// (custom_sky.wgsl) for a layer texel `t` at brightness `b`, per channel.
fn mix(mode: Blend, dst: f32, t: f32, a: f32, b: f32) -> f32 {
let k = a * b;
let src = match mode {
Blend::Multiply => 1.0 + (t - 1.0) * k,
Blend::Overlay => 0.5 + (t - 0.5) * k,
Blend::Replace | Blend::Alpha => t,
_ => t * k,
};
let c = blend(mode);
let f = |factor: wgpu::BlendFactor| match factor {
wgpu::BlendFactor::One => 1.0,
wgpu::BlendFactor::Zero => 0.0,
wgpu::BlendFactor::Src => src,
wgpu::BlendFactor::OneMinusSrc => 1.0 - src,
wgpu::BlendFactor::Dst => dst,
wgpu::BlendFactor::SrcAlpha => k,
wgpu::BlendFactor::OneMinusSrcAlpha => 1.0 - k,
other => panic!("{other:?}"),
};
let (s, d) = (src * f(c.src_factor), dst * f(c.dst_factor));
match c.operation {
wgpu::BlendOperation::Add => s + d,
wgpu::BlendOperation::ReverseSubtract => d - s,
other => panic!("{other:?}"),
}
}
#[test]
fn every_mode_gives_its_documented_result() {
let close = |a: f32, b: f32| assert!((a - b).abs() < 1e-6, "{a} != {b}");
// Full brightness and alpha, a sky texel of 0.4 under a layer texel of 0.8.
let (d, t) = (0.4, 0.8);
close(mix(Blend::Add, d, t, 1.0, 1.0), 1.2);
close(mix(Blend::Subtract, d, t, 1.0, 1.0), -0.4);
close(mix(Blend::Multiply, d, t, 1.0, 1.0), 0.32);
close(mix(Blend::Screen, d, t, 1.0, 1.0), 1.0 - 0.6 * 0.2);
close(mix(Blend::Replace, d, t, 1.0, 1.0), 0.8);
close(mix(Blend::Overlay, d, t, 1.0, 1.0), 0.64);
close(mix(Blend::Overlay, d, 0.2, 1.0, 1.0), 0.16);
close(mix(Blend::Alpha, d, t, 0.25, 1.0), 0.4 * 0.75 + 0.8 * 0.25);
assert!(mix(Blend::Dodge, d, t, 1.0, 1.0) > d);
assert!(mix(Blend::Burn, d, t, 1.0, 1.0) < d);
// Faded out, every mode but replace (drawn only when bright) leaves the sky alone.
for mode in MODES {
if mode != Blend::Replace {
close(mix(mode, d, t, 1.0, 0.0), d);
}
}
// Half faded: add adds half.
close(mix(Blend::Add, d, t, 1.0, 0.5), 0.8);
}
#[test]
fn the_shader_validates() {
let module = naga::front::wgsl::parse_str(include_str!("custom_sky.wgsl")).unwrap();
naga::valid::Validator::new(
naga::valid::ValidationFlags::all(),
naga::valid::Capabilities::all(),
)
.validate(&module)
.unwrap();
}
#[test]
fn the_box_covers_every_face_with_its_cell() {
let mesh = mesh();
assert_eq!(mesh.len(), 36);
for v in &mesh {
let [u0, v0, u1, v1] = v.cell;
assert!((u0..=u1).contains(&v.uv[0]) && (v0..=v1).contains(&v.uv[1]));
assert_eq!(v.position.iter().filter(|c| c.abs() == 1.0).count(), 3);
}
}
}
@@ -0,0 +1,61 @@
// OptiFine custom sky layers (gfx/custom_sky.rs): one skybox per draw, its six faces cut from a
// 3x2 texture, mixed with the sky under it by the pipeline's blend. The fragment's output is
// chosen so that blend gives the layer's documented mode at its brightness, and a layer at
// brightness 0 leaves the sky as it was.
struct Layer {
// Projection * view (camera-relative) * the layer's rotation.
clip_from_sky: mat4x4<f32>,
brightness: f32,
// horizon_optifine::sky::Blend, in gfx/custom_sky.rs's MODES order.
mode: u32,
}
@group(0) @binding(0) var<uniform> layer: Layer;
@group(0) @binding(1) var sky_texture: texture_2d<f32>;
@group(0) @binding(2) var sky_sampler: sampler;
struct Out {
@builtin(position) clip: vec4<f32>,
@location(0) uv: vec2<f32>,
// The face's cell (u0, v0, u1, v1): sampling stays half a texel inside it.
@location(1) @interpolate(flat) cell: vec4<f32>,
}
@vertex fn vs_custom_sky(
@location(0) position: vec3<f32>,
@location(1) uv: vec2<f32>,
@location(2) cell: vec4<f32>,
) -> Out {
var out: Out;
out.clip = layer.clip_from_sky * vec4(position * 100.0, 1.0);
out.uv = uv;
out.cell = cell;
return out;
}
const ADD: u32 = 0u;
const SUBTRACT: u32 = 1u;
const MULTIPLY: u32 = 2u;
const DODGE: u32 = 3u;
const BURN: u32 = 4u;
const SCREEN: u32 = 5u;
const REPLACE: u32 = 6u;
const OVERLAY: u32 = 7u;
const ALPHA: u32 = 8u;
@fragment fn fs_custom_sky(in: Out) -> @location(0) vec4<f32> {
let half = 0.5 / vec2<f32>(textureDimensions(sky_texture));
let uv = clamp(in.uv, in.cell.xy + half, in.cell.zw - half);
let t = textureSample(sky_texture, sky_sampler, uv);
let k = t.a * layer.brightness;
switch layer.mode {
// dst * src: white leaves the sky alone.
case MULTIPLY: { return vec4(mix(vec3(1.0), t.rgb, k), 1.0); }
// 2 * dst * src: grey leaves it alone.
case OVERLAY: { return vec4(mix(vec3(0.5), t.rgb, k), 1.0); }
case REPLACE: { return vec4(t.rgb, 1.0); }
// mix(dst, src, alpha).
case ALPHA: { return vec4(t.rgb, k); }
// add, subtract, dodge, burn, screen: the layer premultiplied, black leaves it alone.
default: { return vec4(t.rgb * k, 1.0); }
}
}
@@ -14,8 +14,24 @@ use super::Vertex;
/// material at the end: the render types vanilla gives an `OitPipelineSet`
/// (translucent, emissive, eyes and breeze wind entity models). Off, the tag is
/// ignored and they stay in the first one, in their order.
/// Two native world-canvas text materials follow it, NORMAL then POLYGON_OFFSET.
const MATERIALS: usize = 12;
/// Sorted still-fluid geometry follows, then the two native world-canvas text materials,
/// NORMAL and POLYGON_OFFSET. Material storage order is independent of their draw stages.
const MATERIALS: usize = 16;
pub const TERRAIN_MODEL_SOLID_OVERLAY: u32 = 42;
pub const TERRAIN_MODEL_CUTOUT_OVERLAY: u32 = 43;
pub const TERRAIN_MODEL_CUTOUT_MIPPED_OVERLAY: u32 = 44;
/// Native terrain-array UV/payloads must never enter a shared-atlas shader-pack pass.
pub fn is_native_terrain(layer: u32) -> bool {
matches!(
layer & 0xffff,
FLUID_OVERLAY
| TERRAIN_MODEL_SOLID_OVERLAY
| TERRAIN_MODEL_CUTOUT_OVERLAY
| TERRAIN_MODEL_CUTOUT_MIPPED_OVERLAY
)
}
/// Sorted depth-writing still fluid, sampling terrain sprite arrays rather than the shared atlas.
pub const FLUID_OVERLAY: u32 = 41;
/// A bit of `Vertex::layer`, above the overlay (low 16 bits) and the HDR tier
/// (bits 16 to 23): the quad's render type has an `OitPipelineSet`
/// (`RenderTypes.entityTranslucent`, `entityTranslucentEmissive`, `eyes`,
@@ -31,7 +47,9 @@ pub const DISSOLVE_OVERLAY: u32 = 38;
/// (`OIT_ENTITY`): improved transparency draws them in its stages.
pub fn mark_oit(quads: &mut [Vertex]) {
for vertex in quads {
vertex.layer |= OIT_ENTITY;
if !is_native_terrain(vertex.layer) {
vertex.layer |= OIT_ENTITY;
}
}
}
/// The overlay of depth-only quads that hide water (`RenderTypes.waterMask`).
@@ -60,12 +78,21 @@ pub const WORLD_TEXT_POLYGON_OFFSET_OVERLAY: u32 = 40;
/// The material of a quad by its overlay (`MATERIALS` order): the overlay is the layer's low 16
/// bits, as entity.wgsl reads it; HDR output's tier rides in the high ones.
fn material(layer: u32, oit: bool) -> usize {
match layer & 0xffff {
TERRAIN_MODEL_SOLID_OVERLAY => return 13,
TERRAIN_MODEL_CUTOUT_OVERLAY => return 14,
TERRAIN_MODEL_CUTOUT_MIPPED_OVERLAY => return 15,
_ => {}
}
if layer & 0xffff == FLUID_OVERLAY {
return 10;
}
if oit && layer & OIT_ENTITY != 0 {
return 9;
}
match layer & 0xffff {
WORLD_TEXT_OVERLAY => 10,
WORLD_TEXT_POLYGON_OFFSET_OVERLAY => 11,
WORLD_TEXT_OVERLAY => 11,
WORLD_TEXT_POLYGON_OFFSET_OVERLAY => 12,
TEXT_POLYGON_OFFSET_OVERLAY => 1,
11 => 2,
12 => 3,
@@ -148,10 +175,10 @@ impl EntityBatch {
}
}
}
// RenderType.text sorts alpha-blended quads from far to near. Keep
// both world-canvas layers after existing materials (including eyes
// moved out of additive); front text uses polygon offset.
for runs in &self.runs[10..] {
// Fluid and RenderType.text sort their own alpha-blended quads far to
// near. Fluid precedes both world-canvas layers, after existing materials
// (including eyes moved out of additive).
for runs in &self.runs[10..13] {
let start = self.combined.len();
for &(first, count) in runs {
self.combined
@@ -169,6 +196,13 @@ impl EntityBatch {
distance(b).total_cmp(&distance(a))
});
}
// Native solid/cutout models are depth writing and keep their submission order.
for runs in &self.runs[13..] {
for &(first, count) in runs {
self.combined
.extend_from_slice(&entities[first * 4..(first + count) * 4]);
}
}
if self.moved_additive == 0 {
self.combined.extend_from_slice(additive);
} else {
@@ -189,7 +223,57 @@ mod tests {
use super::*;
use std::{hint::black_box, time::Instant};
#[test]
fn world_canvas_sorts_after_oit_eyes_before_frame_tails() {
fn native_models_keep_opaque_order_and_do_not_move_existing_materials_or_tails() {
let quad = |layer, x| {
[Vertex {
layer,
pos: [x, 0.0, 0.0],
..geometry(1, false)[0]
}; 4]
};
let entities: Vec<_> = [
(FLUID_OVERLAY, 10.0),
(WORLD_TEXT_OVERLAY, 11.0),
(TERRAIN_MODEL_SOLID_OVERLAY | 279 << 16, 3.0),
(TERRAIN_MODEL_SOLID_OVERLAY, 1.0),
(TERRAIN_MODEL_CUTOUT_OVERLAY, 4.0),
(TERRAIN_MODEL_CUTOUT_MIPPED_OVERLAY, 5.0),
(OIT_ENTITY, 6.0),
(0, 7.0),
]
.into_iter()
.flat_map(|(layer, x)| quad(layer, x))
.collect();
let mut batch = EntityBatch::default();
let counts = batch.update(
&entities,
&quad(0, 8.0),
&quad(0, 9.0),
&quad(0, 12.0),
true,
);
assert_eq!(counts, [1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 2, 1, 1]);
let xs: Vec<_> = batch.combined.iter().step_by(4).map(|v| v.pos[0]).collect();
assert_eq!(
xs,
[7.0, 6.0, 10.0, 11.0, 3.0, 1.0, 4.0, 5.0, 8.0, 9.0, 12.0]
);
assert_eq!(
batch.combined[16].layer,
TERRAIN_MODEL_SOLID_OVERLAY | 279 << 16
);
}
#[test]
fn tagging_oit_does_not_corrupt_a_native_models_sprite_index() {
let mut vertices = [geometry(1, false)[0]; 2];
vertices[0].layer = TERRAIN_MODEL_CUTOUT_OVERLAY | 17 << 16;
vertices[1].layer = 3;
mark_oit(&mut vertices);
assert_eq!(vertices[0].layer, TERRAIN_MODEL_CUTOUT_OVERLAY | 17 << 16);
assert_eq!(vertices[1].layer, 3 | OIT_ENTITY);
}
#[test]
fn fluid_and_world_canvas_sort_after_oit_eyes_before_frame_tails() {
let quad = |layer, x| {
[Vertex {
layer,
@@ -204,6 +288,8 @@ mod tests {
(WORLD_TEXT_OVERLAY, 4.0),
(WORLD_TEXT_OVERLAY, 5.0),
(WORLD_TEXT_POLYGON_OFFSET_OVERLAY, 6.0),
(FLUID_OVERLAY, 11.0),
(FLUID_OVERLAY | OIT_ENTITY, 12.0),
]
.into_iter()
.flat_map(|(layer, x)| quad(layer, x))
@@ -215,12 +301,14 @@ mod tests {
let mut batch = EntityBatch::default();
assert_eq!(
batch.update(&entities, &additive, &quad(0, 9.0), &quad(0, 10.0), true),
[1, 1, 0, 0, 0, 0, 0, 0, 0, 2, 2, 1]
[1, 1, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 1, 0, 0, 0]
);
let positions: Vec<_> = batch.combined.iter().step_by(4).map(|v| v.pos[0]).collect();
assert_eq!(
positions,
[1.0, 2.0, 3.0, 7.0, 5.0, 4.0, 6.0, 8.0, 9.0, 10.0]
[
1.0, 2.0, 3.0, 7.0, 12.0, 11.0, 5.0, 4.0, 6.0, 8.0, 9.0, 10.0
]
);
}
@@ -320,12 +408,12 @@ mod tests {
let mut batch = EntityBatch::default();
// Off: the tag changes nothing, the quads keep their place.
let counts = batch.update(&entities, &[], &[], &[], false);
assert_eq!(counts, [4, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0]);
assert_eq!(counts, [4, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
let xs: Vec<f32> = batch.combined.iter().step_by(4).map(|v| v.pos[0]).collect();
assert_eq!(xs, [0.0, 1.0, 2.0, 4.0, 3.0]);
// On: the tagged quads come last, in order.
let counts = batch.update(&entities, &[], &[], &[], true);
assert_eq!(counts, [2, 0, 0, 0, 0, 0, 1, 0, 0, 2, 0, 0]);
assert_eq!(counts, [2, 0, 0, 0, 0, 0, 1, 0, 0, 2, 0, 0, 0, 0, 0, 0]);
let xs: Vec<f32> = batch.combined.iter().step_by(4).map(|v| v.pos[0]).collect();
assert_eq!(xs, [0.0, 2.0, 3.0, 1.0, 4.0]);
}
+10 -16
View File
@@ -17,6 +17,7 @@
//! copied after the test, which tells which ones it saw: each run of a page
//! reserves room for all of its draws, the copy packs the seen ones at its
//! start, and the run is drawn indirectly with the count the copy reached.
use super::SolidPipeline;
use super::arena::GRAIN;
/// One section draw for `flatten.wgsl`: index count, first sorted index (or
@@ -86,12 +87,12 @@ pub struct Call {
pub struct Flat {
/// The solid draws in one pass (`fs_solid`), where the GPU doesn't hide pixels
/// by tiles (`split_solid` in `Renderer::draw_frame`: elsewhere than Metal).
pub solid: wgpu::RenderPipeline,
pub solid: SolidPipeline,
/// The solid draws in two passes, each over its own part of the draws (terrain.wgsl's
/// `opaque_quad`): the quads with no cutout first, through a shader that never discards,
/// then the rest.
pub solid_opaque: wgpu::RenderPipeline,
pub solid_cutout: wgpu::RenderPipeline,
pub solid_opaque: SolidPipeline,
pub solid_cutout: SolidPipeline,
/// The solid draws as the water mirror's camera draws them (`fs_solid_mirror`).
pub solid_mirror: wgpu::RenderPipeline,
pub translucent: wgpu::RenderPipeline,
@@ -357,7 +358,8 @@ fn plan(
impl Flat {
/// `pipeline(layout, label, vertex entry, fragment entry, blend, cull, pulled)` builds a
/// terrain pipeline as the per-section ones, without the origin buffer, and without
/// vertex buffers when `pulled` (its vertex shader reads the page, `ARENA_BINDING`).
/// vertex buffers when `pulled` (its vertex shader reads the page, `ARENA_BINDING`);
/// `solid(layout, label, vertex entry, fragment entry)` a pulled solid one.
pub fn new(
device: &wgpu::Device,
terrain_layout: &wgpu::BindGroupLayout,
@@ -372,6 +374,7 @@ impl Flat {
Option<wgpu::Face>,
bool,
) -> wgpu::RenderPipeline,
solid: impl Fn(&wgpu::PipelineLayout, &str, &str, &str) -> SolidPipeline,
) -> Self {
let vertex = wgpu::ShaderStages::VERTEX;
let compute = wgpu::ShaderStages::COMPUTE;
@@ -432,32 +435,23 @@ impl Flat {
Self {
// The solid draws read either layout of the solid quads (`mesher::packed`) from
// the page; translucent ones keep their vertex buffer.
solid: pipeline(
solid: solid(
&layout,
"Terrain solid (flat)",
"vs_section_flat",
"fs_solid",
None,
back,
true,
),
solid_opaque: pipeline(
solid_opaque: solid(
&layout,
"Terrain solid opaque (flat)",
"vs_section_flat_opaque",
"fs_opaque",
None,
back,
true,
),
solid_cutout: pipeline(
solid_cutout: solid(
&layout,
"Terrain solid cutout (flat)",
"vs_section_flat_cutout",
"fs_solid",
None,
back,
true,
),
// The mirror flips the winding: its solid terrain culls the front.
solid_mirror: pipeline(
@@ -0,0 +1,69 @@
//! Sorted still-fluid material: ordinary back culling, alpha blend and color/depth writes.
pub(super) const SAMPLER: wgpu::SamplerDescriptor<'static> = wgpu::SamplerDescriptor {
label: Some("Still fluid (nearest texels, linear mipmaps)"),
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
address_mode_w: wgpu::AddressMode::ClampToEdge,
mag_filter: wgpu::FilterMode::Nearest,
min_filter: wgpu::FilterMode::Nearest,
mipmap_filter: wgpu::MipmapFilterMode::Linear,
lod_min_clamp: 0.0,
lod_max_clamp: 32.0,
compare: None,
anisotropy_clamp: 1,
border_color: None,
};
pub(super) fn pipeline(
device: &wgpu::Device,
layout: Option<&wgpu::PipelineLayout>,
shader: &wgpu::ShaderModule,
format: wgpu::TextureFormat,
constants: &[(&str, f64)],
) -> wgpu::RenderPipeline {
let attributes = wgpu::vertex_attr_array![0=>Float32x3,1=>Float32x2,2=>Uint32,3=>Unorm8x4,4=>Uint8x4,5=>Snorm8x4,6=>Float32x2];
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("Still fluid"),
layout,
vertex: wgpu::VertexState {
module: shader,
entry_point: Some("vs_native_fluid"),
compilation_options: wgpu::PipelineCompilationOptions {
constants,
..Default::default()
},
buffers: &[Some(wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<super::Vertex>() as u64,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &attributes,
})],
},
primitive: wgpu::PrimitiveState {
cull_mode: Some(wgpu::Face::Back),
..Default::default()
},
depth_stencil: Some(wgpu::DepthStencilState {
format: wgpu::TextureFormat::Depth32Float,
depth_write_enabled: Some(true),
depth_compare: Some(wgpu::CompareFunction::GreaterEqual),
stencil: Default::default(),
bias: Default::default(),
}),
multisample: Default::default(),
fragment: Some(wgpu::FragmentState {
module: shader,
entry_point: Some("fs_native_fluid"),
compilation_options: wgpu::PipelineCompilationOptions {
constants,
..Default::default()
},
targets: &[Some(wgpu::ColorTargetState {
format,
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::ALL,
})],
}),
multiview_mask: None,
cache: None,
})
}
+632 -99
View File
@@ -72,8 +72,29 @@ fn named(name: Option<&str>) -> Option<Output> {
struct Targets {
size: [u32; 2],
frame: wgpu::Texture,
/// The world as `keep_world` kept it; on the layered frame, the frame as the world and the
/// hand marked it, for `resolve`.
world: wgpu::Texture,
coverage: Option<wgpu::Texture>,
/// The layered frame's HUD target: what the HUD draws on.
hud: Option<wgpu::Texture>,
group: wgpu::BindGroup,
/// The layered frame's passes' bindings: `resolve`, `keep_world` and `composite`.
passes: Option<[wgpu::BindGroup; 3]>,
}
/// The float frame with HUD coverage (`hud_coverage::supported`, docs/design/hdr-linear-light.md
/// "The HUD and menus"): the world's marks become light once, after the world and the hand
/// (`resolve`); the camera overlays draw on that light; the HUD draws on a target of its own,
/// the world clipped to white (`keep_world`); and the last pass composites the HUD over the
/// world's light by its coverage (`fs_hdr_layered`), as the menu blur does before it blurs
/// (`composite`).
struct Layered {
resolve: (wgpu::BindGroupLayout, wgpu::RenderPipeline),
keep: (wgpu::BindGroupLayout, wgpu::RenderPipeline),
composite: (wgpu::BindGroupLayout, wgpu::RenderPipeline),
/// What `resolve` and `composite` read, written before they run (`begin`).
params: wgpu::Buffer,
}
/// How one tier of light sources (`mesher::hdr_tier`) shines: `share` of the headroom for
@@ -155,6 +176,7 @@ pub struct Hdr {
targets: Option<Targets>,
/// `HORIZON_HDR_DEBUG=1`: grey what isn't expanded, tint what is magenta.
debug: bool,
layered: Option<Layered>,
/// Paper white and peak brightness, in nits.
pub paper_white: f32,
pub peak: f32,
@@ -181,28 +203,67 @@ impl Hdr {
queue: &wgpu::Queue,
format: wgpu::TextureFormat,
output: Output,
coverage: bool,
) -> Self {
let debug = std::env::var("HORIZON_HDR_DEBUG").ok();
Self::with_debug(device, queue, format, output, coverage, debug.as_deref())
}
fn with_debug(
device: &wgpu::Device,
queue: &wgpu::Queue,
format: wgpu::TextureFormat,
output: Output,
coverage: bool,
debug: Option<&str>,
) -> Self {
let coverage_debug = coverage && debug == Some("coverage");
// The layered frame's last pass, else today's.
let entry_point = if coverage { "fs_hdr_layered" } else { "fs_hdr" };
let mut source = include_str!("../hdr.wgsl").to_owned();
if coverage_debug {
// The debug view wraps the last pass as it is, so the picture under the tint is
// exactly the one without it.
let entry = format!(
"@fragment fn {entry_point}(@builtin(position) pos: vec4<f32>) -> @location(0) vec4<f32>"
);
assert!(
source.contains(&entry),
"hdr.wgsl's {entry_point} changed its signature"
);
source = source.replace(&entry, "fn picture_hdr(pos: vec4<f32>) -> vec4<f32>");
source.push_str(&entry);
source.push_str(
r#" {
let picture = picture_hdr(pos);
let c = textureLoad(coverage, vec2<i32>(pos.xy), 0).r;
// Magenta at paper white, in the output's own encoding.
let paper = vec3(params.paper, 0.0, params.paper);
let magenta = select(paper, pq(BT709_TO_BT2020 * paper * 80.0), params.pq > 0.5);
return vec4(mix(picture.rgb, magenta, c), picture.a);
}
"#,
);
}
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Horizon HDR"),
source: wgpu::ShaderSource::Wgsl(include_str!("../hdr.wgsl").into()),
source: wgpu::ShaderSource::Wgsl(source.into()),
});
let texture = |binding| wgpu::BindGroupLayoutEntry {
binding,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: false },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
};
let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Horizon HDR bindings"),
entries: &[
texture(0),
texture(1),
texture(3),
wgpu::BindGroupLayoutEntry {
// hdr.wgsl's bindings: the textures at `textures`, and its `Params` at 2 if `params`.
let layout = |label, textures: &[u32], params: bool| {
let texture = |binding| wgpu::BindGroupLayoutEntry {
binding,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: false },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
};
let mut entries: Vec<_> = textures.iter().map(|&b| texture(b)).collect();
if params {
entries.push(wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
@@ -211,17 +272,22 @@ impl Hdr {
min_binding_size: None,
},
count: None,
},
],
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Horizon HDR"),
bind_group_layouts: &[Some(&layout)],
..Default::default()
});
let pipeline_for = |format| {
});
}
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some(label),
entries: &entries,
})
};
// One full-screen pass of hdr.wgsl's `entry` into `format`, reading `layout`.
let pass = |label, layout: &wgpu::BindGroupLayout, entry, format| {
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some(label),
bind_group_layouts: &[Some(layout)],
..Default::default()
});
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("Horizon HDR"),
label: Some(label),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
@@ -234,7 +300,7 @@ impl Hdr {
multisample: Default::default(),
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs_hdr"),
entry_point: Some(entry),
compilation_options: Default::default(),
targets: &[Some(wgpu::ColorTargetState {
format,
@@ -246,6 +312,30 @@ impl Hdr {
cache: None,
})
};
let layout_bindings: &[u32] = if coverage { &[0, 3, 4, 5] } else { &[0, 1, 3] };
let display = layout("Horizon HDR bindings", layout_bindings, true);
let pipeline_for = |format| pass("Horizon HDR", &display, entry_point, format);
let params = || {
device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Horizon HDR parameters"),
size: 64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
})
};
let layered = coverage.then(|| {
let with = |label, textures: &[u32], params, entry| {
let layout = layout(label, textures, params);
let pipeline = pass(label, &layout, entry, format);
(layout, pipeline)
};
Layered {
resolve: with("Horizon HDR marks to light", &[1], true, "fs_resolve"),
keep: with("Horizon HDR HUD backdrop", &[0], false, "fs_keep"),
composite: with("Horizon HDR composite", &[0, 4, 5], true, "fs_composite"),
params: params(),
}
});
let alpha_pass = |label, blend| {
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some(label),
@@ -288,27 +378,20 @@ impl Hdr {
alpha: scale_alpha,
}),
);
let params = || {
device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Horizon HDR parameters"),
size: 64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
})
};
Self {
logo: logo_mask(device, queue),
layout,
pipeline: pipeline_for(output.format()),
capture_pipeline: pipeline_for(Output::Pq.format()),
layout: display,
clear_alpha,
hand_mark,
params: params(),
capture_pipeline: pipeline_for(Output::Pq.format()),
capture_params: params(),
output,
format,
targets: None,
debug: std::env::var_os("HORIZON_HDR_DEBUG").is_some_and(|v| v == "1"),
debug: debug == Some("1"),
layered,
paper_white: 200.0,
peak: 1000.0,
black: 0.0,
@@ -324,7 +407,7 @@ impl Hdr {
/// The frame every pass draws into this frame, sized like the window.
pub fn frame(&mut self, device: &wgpu::Device, size: [u32; 2]) -> wgpu::Texture {
if self.targets.as_ref().is_none_or(|t| t.size != size) {
let texture = |label, usage| {
let texture = |label, format, usage| {
device.create_texture(&wgpu::TextureDescriptor {
label: Some(label),
size: wgpu::Extent3d {
@@ -335,52 +418,185 @@ impl Hdr {
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: self.format,
format,
usage: usage | wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
})
};
let frame = texture(
"Horizon HDR frame",
self.format,
wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
);
let world = texture(
"Horizon HDR world",
self.format,
wgpu::TextureUsages::COPY_DST | wgpu::TextureUsages::RENDER_ATTACHMENT,
);
let view = |t: &wgpu::Texture| t.create_view(&Default::default());
let group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Horizon HDR bindings"),
layout: &self.layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&view(&frame)),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::TextureView(&view(&world)),
},
wgpu::BindGroupEntry {
binding: 2,
resource: self.params.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 3,
resource: wgpu::BindingResource::TextureView(&self.logo),
},
],
let layered = self.layered.is_some();
let coverage = layered.then(|| {
texture(
"HUD coverage",
super::hud_coverage::FORMAT,
wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
)
});
// In the frame's format, so the HUD's pipelines draw on it as on the frame with HDR
// off; the last pass clips it at white.
let hud = layered.then(|| {
texture(
"HUD",
self.format,
wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
)
});
let group = self.group(
device,
&frame,
&world,
coverage.as_ref(),
hud.as_ref(),
&self.params,
);
let passes = self
.layered
.as_ref()
.zip(coverage.as_ref().zip(hud.as_ref()))
.map(|(l, (coverage, hud))| {
let view = |t: &wgpu::Texture| t.create_view(&Default::default());
let (frame, world, coverage, hud) =
(view(&frame), view(&world), view(coverage), view(hud));
let entry = |binding, view| wgpu::BindGroupEntry {
binding,
resource: wgpu::BindingResource::TextureView(view),
};
let params = wgpu::BindGroupEntry {
binding: 2,
resource: l.params.as_entire_binding(),
};
let group = |layout, entries: &[wgpu::BindGroupEntry]| {
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Horizon HDR layered bindings"),
layout,
entries,
})
};
[
group(&l.resolve.0, &[entry(1, &world), params.clone()]),
group(&l.keep.0, &[entry(0, &frame)]),
group(
&l.composite.0,
&[
entry(0, &frame),
params,
entry(4, &coverage),
entry(5, &hud),
],
),
]
});
self.targets = Some(Targets {
size,
frame,
world,
coverage,
hud,
group,
passes,
});
}
self.targets.as_ref().unwrap().frame.clone()
}
/// The float frame with HUD coverage (`Layered`).
pub fn layered(&self) -> bool {
self.layered.is_some()
}
/// The layered frame's HUD target, which the HUD and menus draw on instead of the frame
/// once `keep_world` made it, and which an ordinary screenshot shows.
pub fn hud(&self) -> Option<&wgpu::Texture> {
self.targets.as_ref()?.hud.as_ref()
}
/// On the layered frame, before `resolve` and `composite`: what they read, for a frame that
/// drew a world (`world`) or not.
pub fn begin(&self, queue: &wgpu::Queue, world: bool) {
if let Some(l) = &self.layered {
queue.write_buffer(
&l.params,
0,
bytemuck::cast_slice(&self.params(world, false)),
);
}
}
/// On the layered frame, after the world and the hand drew and marked it: the marks
/// become light in the frame (`fs_resolve`), read from a copy of it in the world target.
pub fn resolve(
&self,
encoder: &mut wgpu::CommandEncoder,
timing: Option<&mut super::pack_timing::PassTiming>,
) {
let (Some(l), Some(t)) = (&self.layered, &self.targets) else {
return;
};
let Some([group, ..]) = &t.passes else {
return;
};
encoder.copy_texture_to_texture(
t.frame.as_image_copy(),
t.world.as_image_copy(),
t.frame.size(),
);
let view = t.frame.create_view(&Default::default());
Self::draw(
encoder,
&l.resolve.1,
group,
&view,
"HDR marks to light",
timing,
);
}
/// On the layered frame: the picture so far (`fs_composite`), as the frame stores light,
/// into `target`, for the menu blur to blur it and make it the world (`keep_world` after).
pub fn composite(
&self,
encoder: &mut wgpu::CommandEncoder,
target: &wgpu::TextureView,
timing: Option<&mut super::pack_timing::PassTiming>,
) {
if let (
Some(l),
Some(Targets {
passes: Some([_, _, group]),
..
}),
) = (&self.layered, &self.targets)
{
Self::draw(
encoder,
&l.composite.1,
group,
target,
"HDR composite",
timing,
);
}
}
pub fn coverage_view(&self) -> Option<wgpu::TextureView> {
Some(
self.targets
.as_ref()?
.coverage
.as_ref()?
.create_view(&Default::default()),
)
}
/// The peak the HDR metadata gives: while calibration's peak step runs, PQ's own
/// 10 000 nits, so a compositor that clips at the declared peak (KWin) lets the test
/// patch through as bright as the display goes and the logo under it shows.
@@ -394,6 +610,9 @@ impl Hdr {
/// Keeps the world as drawn so far, before the hand and HUD cover it, then zeroes the
/// frame's alpha: the world's marks live on in the copy, and whatever draws from here
/// on raises the alpha where it covers the world.
///
/// On the layered frame the frame keeps the world, the hand and the camera overlays, as
/// light, and this makes the HUD's backdrop from it: clipped to white, alpha 0 (`fs_keep`).
pub fn keep_world(
&self,
encoder: &mut wgpu::CommandEncoder,
@@ -402,6 +621,11 @@ impl Hdr {
let Some(t) = &self.targets else {
return;
};
if let (Some(l), Some(hud), Some([_, group, _])) = (&self.layered, &t.hud, &t.passes) {
let view = hud.create_view(&Default::default());
Self::draw(encoder, &l.keep.1, group, &view, "HDR HUD backdrop", timing);
return;
}
encoder.copy_texture_to_texture(
t.frame.as_image_copy(),
t.world.as_image_copy(),
@@ -429,13 +653,14 @@ impl Hdr {
/// After the hand: its pixels' alpha goes down to 1/255, below anything the HUD and
/// menus leave, so HDR output tells the hand (the world's brightness, never lifted)
/// from the UI (UI Brightness).
/// from the UI (UI Brightness). The layered frame keeps the hand before `keep_world`,
/// so it needs no mark there.
pub fn mark_hand(
&self,
encoder: &mut wgpu::CommandEncoder,
timing: Option<&mut super::pack_timing::PassTiming>,
) {
let Some(t) = &self.targets else {
let Some(t) = self.targets.as_ref().filter(|_| self.layered.is_none()) else {
return;
};
let view = t.frame.create_view(&Default::default());
@@ -466,9 +691,10 @@ impl Hdr {
}
/// The world as `keep_world` kept it: where the hand's marks go (its alpha, which HDR output
/// reads for it too), and what tints the world rather than covering it.
/// reads for it too), and what tints the world rather than covering it. None on the
/// layered frame, where both draw on the frame before `keep_world`.
pub fn hand_light_view(&self) -> Option<wgpu::TextureView> {
let t = self.targets.as_ref()?;
let t = self.targets.as_ref().filter(|_| self.layered.is_none())?;
Some(t.world.create_view(&Default::default()))
}
@@ -494,7 +720,15 @@ impl Hdr {
0,
bytemuck::cast_slice(&self.params(world, self.output == Output::Pq)),
);
Self::draw(encoder, &self.pipeline, &t.group, target, timing);
let view = target.create_view(&Default::default());
Self::draw(
encoder,
&self.pipeline,
&t.group,
&view,
"HDR output",
timing,
);
}
/// The frame `present` writes, as HDR10 (BT.2020, PQ, 10 bits a channel) whatever
@@ -517,38 +751,66 @@ impl Hdr {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
let view = |t: &wgpu::Texture| t.create_view(&Default::default());
let group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Horizon HDR screenshot bindings"),
layout: &self.layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&view(&t.frame)),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::TextureView(&view(&t.world)),
},
wgpu::BindGroupEntry {
binding: 2,
resource: self.capture_params.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 3,
resource: wgpu::BindingResource::TextureView(&self.logo),
},
],
});
let group = self.group(
device,
&t.frame,
&t.world,
t.coverage.as_ref(),
t.hud.as_ref(),
&self.capture_params,
);
queue.write_buffer(
&self.capture_params,
0,
bytemuck::cast_slice(&self.params(world, true)),
);
Self::draw(encoder, &self.capture_pipeline, &group, &texture, None);
let view = texture.create_view(&Default::default());
Self::draw(
encoder,
&self.capture_pipeline,
&group,
&view,
"HDR output",
None,
);
Some(texture)
}
fn group(
&self,
device: &wgpu::Device,
frame: &wgpu::Texture,
world: &wgpu::Texture,
coverage: Option<&wgpu::Texture>,
hud: Option<&wgpu::Texture>,
params: &wgpu::Buffer,
) -> wgpu::BindGroup {
let view = |t: &wgpu::Texture| t.create_view(&Default::default());
let (frame, world) = (view(frame), view(world));
let layered = coverage.zip(hud).map(|(c, h)| (view(c), view(h)));
let texture = |binding, view| wgpu::BindGroupEntry {
binding,
resource: wgpu::BindingResource::TextureView(view),
};
let mut entries = vec![
texture(0, &frame),
wgpu::BindGroupEntry {
binding: 2,
resource: params.as_entire_binding(),
},
texture(3, &self.logo),
];
match &layered {
Some((coverage, hud)) => entries.extend([texture(4, coverage), texture(5, hud)]),
None => entries.push(texture(1, &world)),
}
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Horizon HDR bindings"),
layout: &self.layout,
entries: &entries,
})
}
/// hdr.wgsl's `Params`.
fn params(&self, world: bool, pq: bool) -> [f32; 16] {
let paper = self.paper_white.max(1.0);
@@ -577,16 +839,17 @@ impl Hdr {
params
}
/// One full-screen pass of `pipeline` into `view`.
fn draw(
encoder: &mut wgpu::CommandEncoder,
pipeline: &wgpu::RenderPipeline,
group: &wgpu::BindGroup,
target: &wgpu::Texture,
view: &wgpu::TextureView,
label: &'static str,
timing: Option<&mut super::pack_timing::PassTiming>,
) {
let view = target.create_view(&Default::default());
let attachments = [Some(wgpu::RenderPassColorAttachment {
view: &view,
view,
resolve_target: None,
depth_slice: None,
ops: wgpu::Operations {
@@ -595,8 +858,8 @@ impl Hdr {
},
})];
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("Horizon HDR"),
timestamp_writes: timing.and_then(|t| t.writes("HDR output")),
label: Some(label),
timestamp_writes: timing.and_then(|t| t.writes(label)),
color_attachments: &attachments,
..Default::default()
});
@@ -655,9 +918,279 @@ fn scene_gain(peak: f32, paper_white: f32) -> f32 {
#[cfg(test)]
mod tests {
use super::{Output, named, pick};
use super::{Hdr, Output, named, pick};
use wgpu::{SurfaceColorSpaces as S, TextureFormat as F};
#[test]
fn coverage_target_resizes_resets_and_shows_in_the_debug_view() {
let instance = crate::gfx::test_gpu::instance();
let Ok(adapter) = pollster::block_on(instance.request_adapter(&Default::default())) else {
return;
};
let Ok((device, queue)) = pollster::block_on(adapter.request_device(&Default::default()))
else {
return;
};
for enabled in [false, true] {
for debug in [None, Some("coverage")] {
let mut hdr =
Hdr::with_debug(&device, &queue, F::Rgba16Float, Output::Pq, enabled, debug);
hdr.frame(&device, [2, 1]);
assert_eq!(hdr.coverage_view().is_some(), enabled);
hdr.frame(&device, [4, 2]);
let t = hdr.targets.as_ref().unwrap();
if let Some(coverage) = &t.coverage {
assert_eq!(coverage.size(), t.frame.size());
}
let mut encoder = device.create_command_encoder(&Default::default());
let frame_view = t.frame.create_view(&Default::default());
let mut color = super::super::hud_coverage::attachment(&frame_view);
color.ops.load = wgpu::LoadOp::Clear(wgpu::Color::BLACK);
encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
color_attachments: &[Some(color)],
..Default::default()
});
hdr.keep_world(&mut encoder, None);
if let Some(view) = hdr.coverage_view() {
super::super::hud_coverage::clear(&mut encoder, &view);
let mut color = super::super::hud_coverage::attachment(&view);
color.ops.load = wgpu::LoadOp::Clear(wgpu::Color::WHITE);
encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
color_attachments: &[Some(color)],
..Default::default()
});
}
let capture = hdr.capture(&device, &queue, &mut encoder, false).unwrap();
let buffer = super::super::copy_frame(&device, &mut encoder, &capture, [4, 2]);
queue.submit([encoder.finish()]);
buffer.map_async(wgpu::MapMode::Read, .., |r| r.unwrap());
device.poll(wgpu::PollType::wait_indefinitely()).unwrap();
let bytes = buffer.get_mapped_range(..).unwrap();
let pixel = u32::from_le_bytes(bytes[..4].try_into().unwrap());
let [r, g, b] = [0, 10, 20].map(|shift| (pixel >> shift) & 0x3ff);
if enabled && debug.is_some() {
// Fully covered: BT.709 magenta at paper white (200 nits here) in BT.2020
// PQ, not PQ's 10 000 nits (1023, 0, 1023).
let near = |got: u32, want: u32| got.abs_diff(want) <= 4;
assert!(near(r, 550) && near(g, 346) && near(b, 583), "{r} {g} {b}");
} else {
assert_eq!(pixel, 0xc0000000);
}
if let Some(view) = hdr.coverage_view() {
let mut encoder = device.create_command_encoder(&Default::default());
super::super::hud_coverage::clear(&mut encoder, &view);
let coverage = hdr.targets.as_ref().unwrap().coverage.as_ref().unwrap();
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: None,
size: 512,
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
mapped_at_creation: false,
});
encoder.copy_texture_to_buffer(
coverage.as_image_copy(),
wgpu::TexelCopyBufferInfo {
buffer: &buffer,
layout: wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(256),
rows_per_image: None,
},
},
coverage.size(),
);
queue.submit([encoder.finish()]);
buffer.map_async(wgpu::MapMode::Read, .., |r| r.unwrap());
device.poll(wgpu::PollType::wait_indefinitely()).unwrap();
assert_eq!(&buffer.get_mapped_range(..).unwrap()[..4], &[0; 4]);
}
}
}
}
fn gpu() -> Option<(wgpu::Device, wgpu::Queue)> {
let instance = crate::gfx::test_gpu::instance();
let adapter = pollster::block_on(instance.request_adapter(&Default::default())).ok()?;
pollster::block_on(adapter.request_device(&Default::default())).ok()
}
/// Clears all of `texture` to `color`.
fn fill(encoder: &mut wgpu::CommandEncoder, texture: &wgpu::Texture, color: [f64; 4]) {
let view = texture.create_view(&Default::default());
let mut attachment = super::super::hud_coverage::attachment(&view);
let [r, g, b, a] = color;
attachment.ops.load = wgpu::LoadOp::Clear(wgpu::Color { r, g, b, a });
encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
color_attachments: &[Some(attachment)],
..Default::default()
});
}
/// The first texel of `texture` once `encoder` ran: four half floats, or 10-bit PQ codes
/// for HDR10.
fn texel(
device: &wgpu::Device,
queue: &wgpu::Queue,
mut encoder: wgpu::CommandEncoder,
texture: &wgpu::Texture,
) -> [f32; 4] {
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: None,
size: 256,
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
mapped_at_creation: false,
});
encoder.copy_texture_to_buffer(
texture.as_image_copy(),
wgpu::TexelCopyBufferInfo {
buffer: &buffer,
layout: wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(256),
rows_per_image: None,
},
},
wgpu::Extent3d {
width: 1,
height: 1,
depth_or_array_layers: 1,
},
);
queue.submit([encoder.finish()]);
buffer.map_async(wgpu::MapMode::Read, .., |r| r.unwrap());
device.poll(wgpu::PollType::wait_indefinitely()).unwrap();
let bytes = buffer.get_mapped_range(..).unwrap();
if texture.format() == Output::Pq.format() {
let pixel = u32::from_le_bytes(bytes[..4].try_into().unwrap());
return [0, 10, 20, 30].map(|shift| ((pixel >> shift) & 0x3ff) as f32);
}
std::array::from_fn(|i| half::f16::from_le_bytes([bytes[2 * i], bytes[2 * i + 1]]).to_f32())
}
#[test]
fn the_layered_frame_shows_the_world_as_the_last_pass_lifted_it() {
let Some((device, queue)) = gpu() else {
return;
};
// What the last pass shows of a world pixel nothing covers, from today's frame
// (`layered` false) or the layered one, whose marks turn into light before it.
let shown = |layered: bool, world: [f64; 4]| {
let mut hdr =
Hdr::with_debug(&device, &queue, F::Rgba16Float, Output::Pq, layered, None);
let frame = hdr.frame(&device, [1, 1]);
let mut encoder = device.create_command_encoder(&Default::default());
fill(&mut encoder, &frame, world);
if layered {
hdr.begin(&queue, true);
hdr.resolve(&mut encoder, None);
hdr.keep_world(&mut encoder, None);
super::super::hud_coverage::clear(&mut encoder, &hdr.coverage_view().unwrap());
} else {
hdr.keep_world(&mut encoder, None);
}
let capture = hdr.capture(&device, &queue, &mut encoder, true).unwrap();
texel(&device, &queue, encoder, &capture)
};
// Unmarked, below white and above it; HDR lighting; light sources of each tier.
for world in [
[0.5, 0.4, 0.3, 1.0],
[1.5, 1.2, 0.6, 1.0],
[0.8, 0.7, 0.6, 0.88],
[0.9, 0.8, 0.4, 0.05],
[0.9, 0.8, 0.4, 0.2],
[0.9, 0.8, 0.4, 0.4],
[0.9, 0.8, 0.4, 0.6],
] {
let (today, layered) = (shown(false, world), shown(true, world));
// Half floats round the light apart by a step at most.
assert!(
today.iter().zip(layered).all(|(a, b)| (a - b).abs() <= 1.0),
"{world:?}: {today:?} {layered:?}"
);
if world[3] < 1.0 {
assert!(layered[0] > shown(true, [world[0], world[1], world[2], 1.0])[0]);
}
}
}
#[test]
fn the_hud_lays_over_the_worlds_light_by_its_coverage() {
let Some((device, queue)) = gpu() else {
return;
};
let mut hdr = Hdr::with_debug(&device, &queue, F::Rgba16Float, Output::Pq, true, None);
let target = device.create_texture(&wgpu::TextureDescriptor {
label: None,
size: wgpu::Extent3d {
width: 1,
height: 1,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: F::Rgba16Float,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
// The world's light, the HUD's target over it (on the world clipped to white), its
// coverage, UI Brightness, and the composite, all in gamma as the frame stores them.
for (world, hud, coverage, ui, want) in [
// Nothing over it: the world, light above white included.
(3.0, 1.0, 0.0, 1.0, 3.0),
// Half-transparent black: half of the world shows, as the HUD blends.
(2.0, 0.5, 0.5, 1.0, 1.0),
// Covered: the HUD as drawn, at UI Brightness.
(2.0, 0.3, 1.0, 1.0, 0.3),
(2.0, 0.3, 1.0, 0.5, 0.3 * 0.5f32.powf(1.0 / 2.2)),
// Added over the world (the text highlight), which it doesn't hide.
(0.6, 0.8, 0.0, 1.0, 0.8),
// At or below white, at 100%: the HUD's picture as drawn.
(0.7, 0.45, 0.5, 1.0, 0.45),
] {
hdr.ui = ui;
let frame = hdr.frame(&device, [1, 1]);
let t = hdr.targets.as_ref().unwrap();
let mut encoder = device.create_command_encoder(&Default::default());
fill(&mut encoder, &frame, [world, world, world, 1.0]);
fill(&mut encoder, t.hud.as_ref().unwrap(), [hud, hud, hud, 0.0]);
fill(&mut encoder, t.coverage.as_ref().unwrap(), [coverage; 4]);
hdr.begin(&queue, true);
hdr.composite(&mut encoder, &target.create_view(&Default::default()), None);
let got = texel(&device, &queue, encoder, &target);
assert!(
(got[0] - want).abs() < 0.01 && got[3] == 1.0,
"{world} {hud} {coverage} {ui}: {got:?}"
);
}
}
#[test]
fn the_menu_blur_keeps_the_light_it_blurs() {
let Some((device, queue)) = gpu() else {
return;
};
let mut hdr = Hdr::with_debug(&device, &queue, F::Rgba16Float, Output::Pq, true, None);
let size = [8, 8];
let frame = hdr.frame(&device, size);
let mut encoder = device.create_command_encoder(&Default::default());
fill(&mut encoder, &frame, [4.0, 4.0, 4.0, 1.0]);
hdr.keep_world(&mut encoder, None);
super::super::hud_coverage::clear(&mut encoder, &hdr.coverage_view().unwrap());
let mut blur = crate::menu_blur::MenuBlur::new(&device, F::Rgba16Float, false);
let (_, source) = blur.source(&device, size);
hdr.begin(&queue, true);
hdr.composite(&mut encoder, &source, None);
let view = frame.create_view(&Default::default());
blur.blur(&device, &queue, &mut encoder, &view, size, 3.0, None);
hdr.keep_world(&mut encoder, None);
let hud = hdr.hud().unwrap().clone();
// The blurred world keeps its light, and the HUD's backdrop starts again from it.
let [r, ..] = texel(&device, &queue, encoder, &frame);
assert!((r - 4.0).abs() < 0.01, "{r}");
let encoder = device.create_command_encoder(&Default::default());
assert_eq!(texel(&device, &queue, encoder, &hud), [1.0, 1.0, 1.0, 0.0]);
}
#[test]
fn hdr_shaders_validate() {
let entity = crate::overlay::wgsl_constants() + include_str!("../entity.wgsl");
@@ -0,0 +1,335 @@
//! A held world (`WorldHold`, the native hook `native_world_hold`): the terrain goes on being
//! drawn as it stands while what changes under it is meshed, and all of it takes its place on
//! one frame. A section already on screen that is meshed again is uploaded to the arena as ever,
//! over as many frames as its mesh takes to come, but waits beside the mesh drawn (`Hold`);
//! letting go only swaps which of the two is drawn, with nothing left to upload. Sections not on
//! screen yet (terrain streaming in) are not held.
//!
//! Both meshes of a waiting section are in memory, so a hold costs up to what the changed land's
//! new meshes come to. It is bounded, and each bound lets go at once rather than losing anything:
//! `MAX_BYTES` of waiting meshes, `MAX_HOLD` of time, and the arena running out
//! (`Renderer::upload`). It never outlives what asked for it: the mods are asked every frame,
//! and the sections' being cleared (the world left, another dimension) drops what waits. A hold
//! that ended by itself (spent) stays ended until no mod has asked for a frame.
use super::SectionKey;
use crate::fast_hash::{FxHashMap, FxHashSet};
use std::time::{Duration, Instant};
/// The longest a hold lasts.
pub(super) const MAX_HOLD: Duration = Duration::from_secs(30);
/// The most bytes of new meshes that wait.
pub(super) const MAX_BYTES: u64 = 1 << 30;
/// The most circles a hold is kept to (`WorldHold::hold_within`).
pub const MAX_AREAS: usize = 64;
/// What the mods ask of the terrain's picture this frame (`native_world_hold`): nothing, all of
/// it held, or only the sections inside some circles. Several mods add up: one holding all of it
/// holds all of it, and circles join.
#[derive(Clone, Debug, Default, PartialEq)]
pub struct WorldHold {
all: bool,
within: Vec<[f64; 3]>,
}
impl WorldHold {
/// Hold the whole world's terrain as it is drawn, this frame.
pub fn hold(&mut self) {
self.all = true;
}
/// Hold the sections whose middle lies within `radius` blocks of (`x`, `z`), this frame. A
/// circle that isn't finite or has a negative radius is left out, as are those past
/// `MAX_AREAS`.
pub fn hold_within(&mut self, x: f64, z: f64, radius: f64) {
let good = x.is_finite() && z.is_finite() && radius.is_finite() && radius >= 0.0;
if good && self.within.len() < MAX_AREAS {
self.within.push([x, z, radius]);
}
}
/// Whether anything is held.
pub fn is_held(&self) -> bool {
self.all || !self.within.is_empty()
}
/// Whether section `key` is in what is held: all of the world, or a circle its middle lies in.
pub fn covers(&self, key: SectionKey) -> bool {
if self.all {
return true;
}
let (x, z) = (f64::from(key.0) * 16.0 + 8.0, f64::from(key.2) * 16.0 + 8.0);
self.within
.iter()
.any(|&[cx, cz, r]| (x - cx).powi(2) + (z - cz).powi(2) <= r * r)
}
}
/// What a frame's ask means for the sections waiting.
#[derive(Debug, PartialEq, Eq)]
pub(super) enum Release {
/// They go on waiting.
None,
/// Those the hold no longer covers take their place.
Outside,
/// All of them take their place.
All,
}
/// A section waiting behind the hold and the bytes its new mesh takes.
struct Waiting<S> {
section: S,
bytes: u64,
}
/// The renderer's side of a hold: whether it is on, what it covers, and the sections waiting.
pub(super) struct Hold<S> {
ask: WorldHold,
since: Option<Instant>,
spent: bool,
waiting: FxHashMap<SectionKey, Waiting<S>>,
bytes: u64,
}
impl<S> Default for Hold<S> {
fn default() -> Self {
Self {
ask: WorldHold::default(),
since: None,
spent: false,
waiting: FxHashMap::default(),
bytes: 0,
}
}
}
impl<S> Hold<S> {
/// Whether a new mesh of section `key` waits.
pub fn holds(&self, key: SectionKey) -> bool {
self.since.is_some() && !self.spent && self.ask.covers(key)
}
pub fn is_empty(&self) -> bool {
self.waiting.is_empty()
}
pub fn bytes(&self) -> u64 {
self.bytes
}
/// A frame's ask, at `now`.
pub fn ask(&mut self, ask: &WorldHold, now: Instant) -> Release {
if !ask.is_held() {
let was = self.since.is_some();
(self.since, self.spent) = (None, false);
self.ask = WorldHold::default();
return if was && !self.waiting.is_empty() {
Release::All
} else {
Release::None
};
}
if self.spent {
return Release::None;
}
let since = *self.since.get_or_insert(now);
if now.saturating_duration_since(since) >= MAX_HOLD {
log::warn!(
"Horizon: a mod held the world for {} s: let go",
MAX_HOLD.as_secs()
);
self.spend();
return Release::All;
}
if *ask == self.ask {
return Release::None;
}
// (fewer circles or smaller: some of what waits is let go; more: nothing waits that didn't)
self.ask.clone_from(ask);
if self.waiting.is_empty() {
Release::None
} else {
Release::Outside
}
}
/// Ended by itself: nothing waits from now on, until no mod has asked for a frame.
/// What waited is the caller's to place (`take_all`) or drop.
pub fn spend(&mut self) {
self.spent = self.since.is_some();
}
/// Section `key`'s new mesh waits: the one that waited before for it comes back, to free.
/// The bool is whether what waits is now too much (`MAX_BYTES`).
pub fn wait(&mut self, key: SectionKey, section: S, bytes: u64) -> (Option<S>, bool) {
let old = self.take(key);
self.bytes += bytes;
self.waiting.insert(key, Waiting { section, bytes });
(old, self.bytes > MAX_BYTES)
}
/// Section `key`'s waiting mesh, no longer waiting.
pub fn take(&mut self, key: SectionKey) -> Option<S> {
if self.waiting.is_empty() {
return None;
}
let w = self.waiting.remove(&key)?;
self.bytes -= w.bytes;
Some(w.section)
}
/// Everything waiting, no longer waiting.
pub fn take_all(&mut self) -> Vec<(SectionKey, S)> {
self.bytes = 0;
self.waiting.drain().map(|(k, w)| (k, w.section)).collect()
}
/// What waits that the hold no longer covers.
pub fn take_outside(&mut self) -> Vec<(SectionKey, S)> {
let ask = &self.ask;
let all = self.since.is_none() || self.spent;
let out: Vec<_> = self
.waiting
.extract_if(|&k, _| all || !ask.covers(k))
.collect();
self.bytes -= out.iter().map(|(_, w)| w.bytes).sum::<u64>();
out.into_iter().map(|(k, w)| (k, w.section)).collect()
}
/// What waits in these columns, which are going.
pub fn take_columns(&mut self, columns: &FxHashSet<(i32, i32)>) -> Vec<S> {
if self.waiting.is_empty() {
return Vec::new();
}
let out: Vec<_> = self
.waiting
.extract_if(|k, _| columns.contains(&(k.0, k.2)))
.collect();
self.bytes -= out.iter().map(|(_, w)| w.bytes).sum::<u64>();
out.into_iter().map(|(_, w)| w.section).collect()
}
/// The sections are all gone: what waits goes with them and the hold is spent.
pub fn clear(&mut self) {
self.waiting.clear();
self.bytes = 0;
self.spend();
}
pub fn sections(&self) -> impl Iterator<Item = &S> {
self.waiting.values().map(|w| &w.section)
}
pub fn sections_mut(&mut self) -> impl Iterator<Item = &mut S> {
self.waiting.values_mut().map(|w| &mut w.section)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn all() -> WorldHold {
let mut h = WorldHold::default();
h.hold();
h
}
fn within(circles: &[[f64; 3]]) -> WorldHold {
let mut h = WorldHold::default();
for &[x, z, r] in circles {
h.hold_within(x, z, r);
}
h
}
#[test]
fn a_hold_covers_sections_by_their_middle() {
assert!(all().covers((900, 0, -900)));
assert!(!WorldHold::default().covers((0, 0, 0)));
assert!(within(&[[8.0, 8.0, 1.0]]).covers((0, 3, 0)));
assert!(!within(&[[0.0, 0.0, 11.0]]).covers((0, 3, 0)));
assert!(within(&[[0.0, 0.0, 12.0]]).covers((0, 3, 0)));
assert!(within(&[[1000.0, 0.0, 1.0], [24.0, 8.0, 0.5]]).covers((1, 0, 0)));
assert!(within(&[[-8.0, -8.0, 0.0]]).covers((-1, 0, -1)));
}
#[test]
fn bad_circles_are_left_out_and_there_are_at_most_max_areas() {
let h = within(&[
[f64::NAN, 0.0, 5.0],
[0.0, f64::INFINITY, 5.0],
[0.0, 0.0, -1.0],
[0.0, 0.0, f64::INFINITY],
]);
assert!(!h.is_held());
let mut many = WorldHold::default();
for i in 0..MAX_AREAS + 10 {
many.hold_within(i as f64 * 100.0, 0.0, 1.0);
}
assert_eq!(many.within.len(), MAX_AREAS);
// (several mods: all of it from one holds all of it)
many.hold();
assert!(many.covers((-5000, 0, 5000)));
}
#[test]
fn a_hold_starts_and_lets_go_when_no_one_asks() {
let (mut g, t) = (Hold::<u32>::default(), Instant::now());
assert_eq!(g.ask(&WorldHold::default(), t), Release::None);
assert!(!g.holds((0, 0, 0)));
assert_eq!(g.ask(&all(), t), Release::None);
assert!(g.holds((0, 0, 0)));
assert_eq!(g.ask(&all(), t + Duration::from_secs(5)), Release::None);
// (nothing waited: nothing to place)
assert_eq!(g.ask(&WorldHold::default(), t), Release::None);
g.ask(&all(), t);
g.wait((0, 0, 0), 7, 100);
assert_eq!(g.ask(&WorldHold::default(), t), Release::All);
assert_eq!(g.take_all(), vec![((0, 0, 0), 7)]);
assert!(!g.holds((0, 0, 0)));
}
#[test]
fn a_hold_ends_by_itself_and_stays_ended_until_let_go() {
let (mut g, t) = (Hold::<u32>::default(), Instant::now());
g.ask(&all(), t);
assert_eq!(g.ask(&all(), t + MAX_HOLD), Release::All);
assert!(!g.holds((0, 0, 0)));
assert_eq!(g.ask(&all(), t + MAX_HOLD * 2), Release::None);
assert!(!g.holds((0, 0, 0)));
g.ask(&WorldHold::default(), t + MAX_HOLD * 2);
g.ask(&all(), t + MAX_HOLD * 3);
assert!(g.holds((0, 0, 0)));
}
#[test]
fn smaller_circles_let_go_what_they_leave() {
let (mut g, t) = (Hold::<u32>::default(), Instant::now());
g.ask(&within(&[[0.0, 0.0, 100.0]]), t);
assert!(g.holds((0, 0, 0)) && g.holds((5, 0, 0)) && !g.holds((7, 0, 0)));
g.wait((0, 0, 0), 1, 10);
g.wait((5, 0, 0), 2, 20);
assert_eq!(g.ask(&within(&[[0.0, 0.0, 100.0]]), t), Release::None);
assert_eq!(g.ask(&within(&[[0.0, 0.0, 40.0]]), t), Release::Outside);
assert_eq!(g.take_outside(), vec![((5, 0, 0), 2)]);
assert_eq!((g.sections().count(), g.bytes()), (1, 10));
assert!(!g.holds((5, 0, 0)));
// (to all of it again: nothing more is let go, and all of it waits again)
assert_eq!(g.ask(&all(), t), Release::Outside);
assert!(g.take_outside().is_empty());
assert!(g.holds((5, 0, 0)));
}
#[test]
fn what_waits_is_counted_and_capped() {
let (mut g, t) = (Hold::<u32>::default(), Instant::now());
g.ask(&all(), t);
assert_eq!(g.wait((0, 0, 0), 1, MAX_BYTES - 10), (None, false));
// (a newer mesh of the same section replaces it, and the old one comes back to free)
assert_eq!(g.wait((0, 0, 0), 2, MAX_BYTES - 20), (Some(1), false));
assert_eq!(g.bytes(), MAX_BYTES - 20);
assert_eq!(g.wait((1, 0, 0), 3, 20), (None, false));
assert_eq!(g.wait((2, 0, 0), 4, 1), (None, true));
assert_eq!(g.take((1, 0, 0)), Some(3));
assert_eq!(g.take((1, 0, 0)), None);
assert_eq!(g.bytes(), MAX_BYTES - 19);
}
#[test]
fn columns_going_and_the_sections_cleared_drop_what_waits() {
let (mut g, t) = (Hold::<u32>::default(), Instant::now());
g.ask(&all(), t);
g.wait((0, 0, 0), 1, 10);
g.wait((0, 5, 0), 2, 10);
g.wait((3, 0, 0), 3, 10);
let gone: FxHashSet<(i32, i32)> = [(0, 0)].into_iter().collect();
let mut dropped = g.take_columns(&gone);
dropped.sort_unstable();
assert_eq!(dropped, vec![1, 2]);
assert_eq!((g.sections().count(), g.bytes()), (1, 10));
g.clear();
assert!(g.is_empty() && g.bytes() == 0 && !g.holds((3, 0, 0)));
// (cleared while held: spent until let go; with no hold, nothing to end)
assert_eq!(g.ask(&all(), t), Release::None);
assert!(!g.holds((3, 0, 0)));
let mut none = Hold::<u32>::default();
none.clear();
none.ask(&all(), t);
assert!(none.holds((3, 0, 0)));
}
}
@@ -0,0 +1,485 @@
//! HUD coverage beside its unchanged colour and alpha, only on the float HDR lane.
pub(crate) const FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::R8Unorm;
pub(crate) fn supported(
lane: (bool, bool, bool),
hdr_offered: bool,
attachments: u32,
flags: wgpu::DownlevelFlags,
format: wgpu::TextureFormatFeatures,
) -> bool {
!lane.0
&& lane.1
&& lane.2
&& hdr_offered
&& attachments >= 2
&& flags.contains(wgpu::DownlevelFlags::INDEPENDENT_BLEND)
&& format
.allowed_usages
.contains(wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING)
&& format
.flags
.contains(wgpu::TextureFormatFeatureFlags::BLENDABLE)
}
pub(crate) fn attachment(view: &wgpu::TextureView) -> wgpu::RenderPassColorAttachment<'_> {
wgpu::RenderPassColorAttachment {
view,
resolve_target: None,
depth_slice: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
}
}
pub(crate) fn clear(encoder: &mut wgpu::CommandEncoder, view: &wgpu::TextureView) {
let mut color = attachment(view);
color.ops.load = wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT);
encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("HUD coverage reset"),
color_attachments: &[Some(color)],
..Default::default()
});
}
pub(crate) struct Pipeline {
plain: wgpu::RenderPipeline,
coverage: Option<wgpu::RenderPipeline>,
}
impl Pipeline {
/// `None` when `enabled` and the shader can't be given a coverage output (a resource
/// pack's, which the caller then doesn't use).
pub(crate) fn new(
device: &wgpu::Device,
desc: &wgpu::RenderPipelineDescriptor<'_>,
source: wgpu::ShaderSource<'_>,
enabled: bool,
) -> Option<Self> {
let coverage = if enabled {
Some(pipeline(device, desc, source)?)
} else {
None
};
Some(Self {
plain: device.create_render_pipeline(desc),
coverage,
})
}
pub(crate) fn get(&self, coverage: bool) -> &wgpu::RenderPipeline {
if coverage {
self.coverage.as_ref().expect("HUD coverage pipeline")
} else {
&self.plain
}
}
}
/// Keep the primary output and all its blend factors, including destination alpha. Wrap the
/// final shader result, so pack text's alpha, early returns and discard all apply to coverage.
/// `None` for a fragment shader without a colour output at location 0.
pub(super) fn pipeline(
device: &wgpu::Device,
desc: &wgpu::RenderPipelineDescriptor<'_>,
source: wgpu::ShaderSource<'_>,
) -> Option<wgpu::RenderPipeline> {
let mut desc = desc.clone();
let fragment = desc.fragment.as_mut()?;
let mut module = match source {
wgpu::ShaderSource::Wgsl(s) => naga::front::wgsl::parse_str(&s).ok()?,
wgpu::ShaderSource::Naga(m) => m.into_owned(),
_ => return None,
};
let primary = fragment.targets.first()?.clone()?;
let blend = primary.blend.unwrap_or(wgpu::BlendState::REPLACE).color;
let additive = matches!(
(blend.src_factor, blend.dst_factor),
(
wgpu::BlendFactor::One
| wgpu::BlendFactor::Src
| wgpu::BlendFactor::SrcAlpha
| wgpu::BlendFactor::Zero,
wgpu::BlendFactor::One
)
) && blend.operation == wgpu::BlendOperation::Add;
wrap(&mut module, fragment.entry_point?, blend)?;
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: desc.label,
source: wgpu::ShaderSource::Naga(std::borrow::Cow::Owned(module)),
});
let over = wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::One,
dst_factor: wgpu::BlendFactor::OneMinusSrc,
operation: wgpu::BlendOperation::Add,
};
let targets = [
Some(primary),
Some(wgpu::ColorTargetState {
format: FORMAT,
blend: Some(wgpu::BlendState {
color: over,
alpha: over,
}),
write_mask: if additive {
wgpu::ColorWrites::empty()
} else {
wgpu::ColorWrites::RED
},
}),
];
fragment.module = &shader;
fragment.targets = &targets;
Some(device.create_render_pipeline(&desc))
}
fn wrap(module: &mut naga::Module, name: &str, blend: wgpu::BlendComponent) -> Option<()> {
use naga::{Expression as E, Statement as S, TypeInner as T};
use wgpu::BlendFactor as B;
let span = naga::Span::UNDEFINED;
let entry = module.entry_points.iter_mut().find(|e| e.name == name)?;
// Checked before anything is taken apart: a resource pack's shader may have no colour.
let result = entry.function.result.clone()?;
if let T::Struct { members, .. } = &module.types[result.ty].inner {
members
.iter()
.find(|m| matches!(m.binding, Some(naga::Binding::Location { location: 0, .. })))?;
} else if !matches!(
result.binding,
Some(naga::Binding::Location { location: 0, .. })
) {
return None;
}
let mut original = std::mem::take(&mut entry.function);
entry.function.arguments = original.arguments.clone();
for arg in &mut original.arguments {
arg.binding = None;
}
original.result.as_mut().unwrap().binding = None;
original.name = Some("hud_color".into());
let function = module.functions.append(original, span);
let f = &mut entry.function;
let args = (0..f.arguments.len())
.map(|i| f.expressions.append(E::FunctionArgument(i as u32), span))
.collect();
let call = f.expressions.append(E::CallResult(function), span);
f.body.push(
S::Call {
function,
arguments: args,
result: Some(call),
},
span,
);
let one = f
.expressions
.append(E::Literal(naga::Literal::F32(1.0)), span);
let start = f.expressions.len();
let (mut members, mut values, color, offset) = match &module.types[result.ty].inner {
T::Struct {
members,
span: size,
} => {
// HUD pipelines have one target, so a pack's other colour outputs went nowhere;
// left in, one at location 1 would collide with the coverage.
let (members, values): (Vec<_>, Vec<_>) = members
.iter()
.enumerate()
.filter(|(_, m)| {
!matches!(m.binding, Some(naga::Binding::Location { location, .. }) if location != 0)
})
.map(|(i, m)| {
let value = f.expressions.append(
E::AccessIndex {
base: call,
index: i as u32,
},
span,
);
(m.clone(), value)
})
.unzip();
let color = members
.iter()
.position(|m| {
matches!(m.binding, Some(naga::Binding::Location { location: 0, .. }))
})
.unwrap();
let color = values[color];
(members, values, color, *size)
}
_ => (
vec![naga::StructMember {
name: Some("color".into()),
ty: result.ty,
binding: result.binding,
offset: 0,
}],
vec![call],
call,
16,
),
};
let mut component = |index| {
f.expressions
.append(E::AccessIndex { base: color, index }, span)
};
let [r, g, b, a] = [component(0), component(1), component(2), component(3)];
let mut math = |fun, arg, arg1| {
f.expressions.append(
E::Math {
fun,
arg,
arg1: Some(arg1),
arg2: None,
arg3: None,
},
span,
)
};
let low = math(naga::MathFunction::Min, r, g);
let low = math(naga::MathFunction::Min, low, b);
let high = math(naga::MathFunction::Max, r, g);
let high = math(naga::MathFunction::Max, high, b);
// A scalar cannot reproduce RGB-dependent replacement or inversion: surviving pixels
// cover fully, so the later composite cannot leak world light through them.
let c = if blend.operation != wgpu::BlendOperation::Add {
one
} else {
match (blend.src_factor, blend.dst_factor) {
(B::SrcAlpha | B::One, B::OneMinusSrcAlpha) => a,
(B::Zero, B::OneMinusSrc) => high,
// Multiplying retains src.rgb (or 1 - alpha + src.rgb) of the backdrop.
// Use the greatest channel darkening, as for the credits vignette above.
(B::Zero, B::Src)
| (B::Dst, B::Zero)
| (B::Dst, B::OneMinusSrcAlpha)
| (B::Dst, B::Src) => {
let top = if blend.dst_factor == B::OneMinusSrcAlpha {
a
} else {
one
};
let factor = if blend.dst_factor == B::Src && blend.src_factor == B::Dst {
f.expressions.append(
E::Binary {
op: naga::BinaryOperator::Add,
left: low,
right: low,
},
span,
)
} else {
low
};
f.expressions.append(
E::Binary {
op: naga::BinaryOperator::Subtract,
left: top,
right: factor,
},
span,
)
}
_ => one,
}
};
let c = f.expressions.append(
E::Math {
fun: naga::MathFunction::Saturate,
arg: c,
arg1: None,
arg2: None,
arg3: None,
},
span,
);
let scalar = module.types.insert(
naga::Type {
name: None,
inner: T::Scalar(naga::Scalar::F32),
},
span,
);
members.push(naga::StructMember {
name: Some("coverage".into()),
ty: scalar,
binding: Some(naga::Binding::Location {
location: 1,
interpolation: None,
sampling: None,
blend_src: None,
per_primitive: false,
}),
offset,
});
let ty = module.types.insert(
naga::Type {
name: Some("HudCoverage".into()),
inner: T::Struct {
members,
span: (offset + 4).next_multiple_of(16),
},
},
span,
);
values.push(c);
let value = f.expressions.append(
E::Compose {
ty,
components: values,
},
span,
);
f.result = Some(naga::FunctionResult { ty, binding: None });
f.body.push(S::Emit(f.expressions.range_from(start)), span);
f.body.push(S::Return { value: Some(value) }, span);
Some(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn coverage_requires_the_no_pack_float_hdr_lane_and_independent_r8_blending() {
let flags = wgpu::DownlevelFlags::INDEPENDENT_BLEND;
let format = wgpu::TextureFormatFeatures {
allowed_usages: wgpu::TextureUsages::RENDER_ATTACHMENT
| wgpu::TextureUsages::TEXTURE_BINDING,
flags: wgpu::TextureFormatFeatureFlags::BLENDABLE,
};
let lane = (false, true, true);
assert!(supported(lane, true, 2, flags, format));
// Switch unset (including legacy 16-bit HDR and 0.1 packs), HDR off, a 0.2
// pack's float frame, and unsupported devices never allocate coverage.
for other in [
(false, false, true),
(true, false, true),
(true, true, true),
(false, true, false),
] {
assert!(!supported(other, true, 2, flags, format));
}
assert!(!supported(lane, false, 2, flags, format));
assert!(!supported(lane, true, 1, flags, format));
assert!(!supported(
lane,
true,
2,
wgpu::DownlevelFlags::empty(),
format
));
assert!(!supported(
lane,
true,
2,
flags,
wgpu::TextureFormatFeatures {
flags: wgpu::TextureFormatFeatureFlags::empty(),
..format
}
));
assert!(!supported(
lane,
true,
2,
flags,
wgpu::TextureFormatFeatures {
allowed_usages: wgpu::TextureUsages::TEXTURE_BINDING,
..format
}
));
}
#[test]
fn disabled_coverage_builds_only_the_original_pipeline() {
let instance = crate::gfx::test_gpu::instance();
let Ok(adapter) = pollster::block_on(instance.request_adapter(&Default::default())) else {
return;
};
let Ok((device, _)) = pollster::block_on(adapter.request_device(&Default::default()))
else {
return;
};
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: None,
source: wgpu::ShaderSource::Wgsl(
"@vertex fn vs() -> @builtin(position) vec4<f32> { return vec4(0.0); }".into(),
),
});
let desc = wgpu::RenderPipelineDescriptor {
label: None,
layout: None,
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs"),
compilation_options: Default::default(),
buffers: &[],
},
primitive: Default::default(),
depth_stencil: Some(wgpu::DepthStencilState {
format: wgpu::TextureFormat::Depth32Float,
depth_write_enabled: Some(true),
depth_compare: Some(wgpu::CompareFunction::Always),
stencil: Default::default(),
bias: Default::default(),
}),
multisample: Default::default(),
fragment: None,
multiview_mask: None,
cache: None,
};
// The coverage source must not even be parsed outside the lane.
let pipeline = Pipeline::new(
&device,
&desc,
wgpu::ShaderSource::Wgsl("invalid".into()),
false,
)
.unwrap();
assert!(pipeline.coverage.is_none());
}
#[test]
fn coverage_wraps_the_debug_crosshair_without_changing_world_entry_points() {
let mut module = naga::front::wgsl::parse_str(include_str!("../terrain.wgsl")).unwrap();
wrap(
&mut module,
"fs_debug_line",
wgpu::BlendState::ALPHA_BLENDING.color,
)
.unwrap();
naga::valid::Validator::new(
naga::valid::ValidationFlags::all(),
naga::valid::Capabilities::all(),
)
.validate(&module)
.unwrap();
}
#[test]
fn coverage_replaces_a_colour_output_the_hud_has_no_target_for() {
let mut module = naga::front::wgsl::parse_str(
"struct Out { @location(0) color: vec4<f32>, @location(1) unused: vec4<f32> }
@fragment fn fs() -> Out { return Out(vec4(0.5), vec4(1.0)); }",
)
.unwrap();
wrap(&mut module, "fs", wgpu::BlendState::ALPHA_BLENDING.color).unwrap();
naga::valid::Validator::new(
naga::valid::ValidationFlags::all(),
naga::valid::Capabilities::all(),
)
.validate(&module)
.unwrap();
let result = module.entry_points[0].function.result.as_ref().unwrap();
let naga::TypeInner::Struct { members, .. } = &module.types[result.ty].inner else {
panic!("the wrapped entry returns a struct");
};
let names: Vec<_> = members.iter().map(|m| m.name.as_deref().unwrap()).collect();
assert_eq!(names, ["color", "coverage"]);
}
}
@@ -49,9 +49,23 @@ struct HorizonMaterialOut {
@location(14) @interpolate(flat) normal: vec3<f32>,
}
fn horizon_material_vertex(position: vec3<f32>, uv: vec2<f32>, layer: u32, color: vec4<f32>,
light: vec2<u32>, origin: vec3<f32>) -> HorizonMaterialOut {
let pos = horizon_material_displace(position + origin, uv, layer, color, light);
return horizon_material_out(pos, uv, layer, color, vec2<f32>(light));
light: vec2<u32>, origin: vec4<f32>) -> HorizonMaterialOut {
let pos = horizon_material_displace(position + origin.xyz, uv, layer, color, light);
var out = horizon_material_out(pos, uv, layer, color, vec2<f32>(light));
// The section's fade-in (`RenderSection.getVisibility`), as how far it is from whole, in the
// high 16 bits of `hdr_code`: no varying of its own, and 0 (whole) where nothing fades. It is
// the same for the whole section, so the flat varying loses nothing.
let instance = horizon_terrain_instance_visibility(origin.w);
out.hdr_code |= u32(round((1.0 - instance) * 65535.0)) << 16u;
return out;
}
// terrain.wgsl's fade-in (terrain.vsh's chunkVisibility): whole within 16 blocks, the section's
// own visibility from 32 out, toward the fog colour, alpha kept. The distance blend runs here on
// the interpolated distance, as vanilla's interpolated `visibility`, not once per triangle.
fn horizon_material_faded(in: HorizonMaterialOut, color: vec4<f32>) -> vec4<f32> {
let instance = 1.0 - f32(in.hdr_code >> 16u) / 65535.0;
let visibility = mix(1.0, instance, clamp((in.spherical - 16.0) / 16.0, 0.0, 1.0));
return vec4(mix(horizon_terrain_globals.fog_color.rgb, color.rgb, visibility), color.a);
}
// `light`: sky, block, 16 per level.
fn horizon_material_out(pos: vec3<f32>, uv: vec2<f32>, layer: u32, color: vec4<f32>,
@@ -94,7 +108,7 @@ fn horizon_material_out(pos: vec3<f32>, uv: vec2<f32>, layer: u32, color: vec4<f
@location(2) layer: u32, @location(3) color: vec4<f32>, @location(4) light: vec2<u32>,
@location(5) origin: vec4<f32>) -> HorizonMaterialOut {
return horizon_material_vertex(position, horizon_terrain_terrain_uv(uv, origin.w), layer,
color, light, origin.xyz);
color, light, origin);
}
@vertex fn horizon_material_vs_flat(
@builtin(vertex_index) vertex: u32,
@@ -103,7 +117,7 @@ fn horizon_material_out(pos: vec3<f32>, uv: vec2<f32>, layer: u32, color: vec4<f
) -> HorizonMaterialOut {
let origin = horizon_terrain_slot_origins[horizon_terrain_block_slots[vertex >> 8u]];
return horizon_material_vertex(position, horizon_terrain_terrain_uv(uv, origin.w), layer,
color, light, origin.xyz);
color, light, origin);
}
// The prelude's `block_normal_map` and `block_specular_map`: the resource packs' LabPBR maps
// in the pack's frame group (3), one array each, a blank 1x1 without them.
@@ -162,11 +176,13 @@ fn horizon_material_color(in: HorizonMaterialOut, translucent: bool) -> vec4<f32
horizon_material_face_normal = normal;
horizon_surface = vec4<f32>(0.0);
let tangent = horizon_material_tangent(in.base, in.uv, normal);
return material(MaterialInput(albedo, in.tint, in.light, in.lightmap, in.position, in.uv,
translucent, horizon_sprite_tags(in.layer & 0x7fffu),
let out = material(MaterialInput(vec4(horizon_line_in(albedo.rgb), albedo.a),
vec4(horizon_line_in(in.tint.rgb), in.tint.a), in.light, horizon_line_in(in.lightmap),
in.position, in.uv, translucent, horizon_sprite_tags(in.layer & 0x7fffu),
horizon_sprite_material(in.layer & 0x7fffu), in.clip.xy / frame.resolution,
in.face, fluid, in.emission, normal, tangent,
horizon_terrain_native_colored_light(in.position), in.displaced)).color;
horizon_line_in(horizon_terrain_native_colored_light(in.position)), in.displaced)).color;
return vec4(horizon_line_out(out.rgb), out.a);
}
var<private> horizon_material_face_normal: vec3<f32>;
struct HorizonMaterialSolid {
@@ -179,9 +195,10 @@ struct HorizonMaterialSolid {
// What `fs_solid` leaves in the frame's alpha for HDR output, from the sprite before the pack's
// material: the pack decides the colour, Horizon still knows which pixels are light sources.
fn horizon_material_mark(in: HorizonMaterialOut, texel: vec4<f32>) -> f32 {
if in.hdr_code > 0u {
let whole = in.hdr_code > 4u;
let tier = select(in.hdr_code, in.hdr_code - 4u, whole);
let code = in.hdr_code & 15u;
if code > 0u {
let whole = code > 4u;
let tier = select(code, code - 4u, whole);
let l = in.layer & 0x7fffu;
let t = textureLoad(horizon_terrain_emission_thresholds,
vec2(i32(l % horizon_terrain_SPRITE_SPLIT), i32(l / horizon_terrain_SPRITE_SPLIT)), 0);
@@ -200,7 +217,7 @@ fn horizon_material_mark(in: HorizonMaterialOut, texel: vec4<f32>) -> f32 {
return horizon_material_solid(in);
}
fn horizon_material_solid(in: HorizonMaterialOut) -> HorizonMaterialSolid {
let color = horizon_material_color(in, false);
let color = horizon_material_faded(in, horizon_material_color(in, false));
let texel = horizon_terrain_sprite(in.uv, in.layer & 0x7fffu);
// Bit 15 of the layer marks opaque leaves (forceOpaque): no alpha cutout.
if color.a < 0.5 && (in.layer & 0x8000u) == 0u { discard; }
@@ -214,7 +231,7 @@ fn horizon_material_solid(in: HorizonMaterialOut) -> HorizonMaterialSolid {
return out;
}
@fragment fn horizon_material_fs_translucent(in: HorizonMaterialOut) -> @location(0) vec4<f32> {
let color = horizon_material_color(in, true);
let color = horizon_material_faded(in, horizon_material_color(in, true));
if color.a < 0.01 { discard; }
return horizon_material_fogged(color, in.spherical, in.cylindrical);
}
@@ -332,7 +349,7 @@ fn horizon_material_displace(pos: vec3<f32>, uv: vec2<f32>, layer: u32, color: v
let fluid = high >= 8u && (high & 7u) < 6u;
let lit = min(vec2(f32(light.y), f32(light.x)) / 240.0, vec2(1.0));
return vertex(VertexInput(pos, uv, horizon_sprite_material(layer & 0x7fffu), lit,
vec4(color.rgb, 1.0), select(0u, high & 7u, fluid), u32(fluid)));
vec4(horizon_line_in(color.rgb), 1.0), select(0u, high & 7u, fluid), u32(fluid)));
}
";
@@ -349,12 +366,13 @@ fn shader_source(
source: &str,
tags: &[(u32, u32)],
materials: &[(u32, u32)],
vertex: bool,
displaced: bool,
(vertex, displaced): (bool, bool),
api: horizon_shader_api::Api,
) -> String {
format!(
"{source}\n// Horizon's terrain (terrain.wgsl)\n{}{}{}{}{ENTRIES}{}{}",
"{source}\n// Horizon's terrain (terrain.wgsl)\n{}{}{}{}{}{ENTRIES}{}{}",
vanilla_terrain(),
super::native_post::line_helpers(api),
if vertex {
DISPLACE_PACK
} else {
@@ -378,8 +396,9 @@ fn module(
materials: &[(u32, u32)],
vertex: bool,
displaced: bool,
api: horizon_shader_api::Api,
) -> Result<naga::Module, String> {
let text = shader_source(&stage.source, tags, materials, vertex, displaced);
let text = shader_source(&stage.source, tags, materials, (vertex, displaced), api);
naga::front::wgsl::parse_str(&text)
.map_err(|e| format!("{}: {}", stage.file, e.emit_to_string(&text)))
}
@@ -403,6 +422,7 @@ impl NativeMaterial {
shape.sprite_materials,
vertex,
displaced.is_some(),
frame.api,
)?;
let mut constants = present_constants(&module, constants);
let sprite_names = [
@@ -562,7 +582,7 @@ impl NativeMaterial {
#[cfg(test)]
mod tests {
use super::*;
use horizon_shader_api::{MANIFEST, Pack, StageKind, check};
use horizon_shader_api::{Api, MANIFEST, Pack, StageKind, check};
fn stage(material: &str) -> CheckedStage {
let manifest = "schema = 1\nid = \"t\"\nname = \"T\"\nversion = \"1\"\napi = \"0.1\"\n\
@@ -577,6 +597,25 @@ mod tests {
report.stages.remove(0)
}
/// The fade-in packed above the HDR code: a section a quarter visible (`section_instance`
/// with step 1: mantissa `1 - visibility` = 0.75) keeps its visibility through the code, and
/// a fragment 24 blocks away, half-way into the 16-block blend, is 1 - (1 - 0.25) / 2 = 0.625
/// visible; the code's low nibble survives.
#[test]
fn the_fade_in_rides_the_hdr_code() {
let w = f32::from_bits(1.0f32.to_bits() | (0.75 * 8_388_608.0) as u32);
// `horizon_material_vertex`.
let instance = 1.0 - (w.to_bits() & 0x7fffff) as f32 / 8_388_608.0;
let code = 7u32 | (((1.0 - instance) * 65535.0).round() as u32) << 16;
// `horizon_material_faded` and `horizon_material_mark`.
let unpacked = 1.0 - (code >> 16) as f32 / 65535.0;
assert!((unpacked - 0.25).abs() < 1.0 / 65535.0);
let blend = ((24.0f32 - 16.0) / 16.0).clamp(0.0, 1.0);
let visibility = 1.0 + (unpacked - 1.0) * blend;
assert!((visibility - 0.625).abs() < 1.0 / 65535.0);
assert_eq!(code & 15, 7);
}
#[test]
fn the_terrain_takes_a_prefix_and_validates_with_a_pack() {
let terrain = vanilla_terrain();
@@ -609,7 +648,7 @@ mod tests {
let tags = [(7, 1), (3, 1), (9, 2)];
let materials = [(5, 1000)];
assert!(layer_table("t", &tags).contains("case 3u, 7u: { return 1u; }"));
let module = module(&stage, &tags, &materials, false, false).unwrap();
let module = module(&stage, &tags, &materials, false, false, Api::V0_1).unwrap();
let info = naga::valid::Validator::new(
naga::valid::ValidationFlags::all(),
naga::valid::Capabilities::all(),
@@ -622,8 +661,8 @@ mod tests {
&stage.source,
&tags,
&materials,
false,
false
(false, false),
Api::V0_1
))
)
});
@@ -647,6 +686,33 @@ mod tests {
);
}
/// The terrain shader of a 0.2 pack with a vertex stage, whose tint the renderer decodes.
#[test]
fn the_terrain_validates_with_a_0_2_pack() {
let manifest = "schema = 1\nid = \"t\"\nname = \"T\"\nversion = \"1\"\napi = \"0.2\"\n\
[material]\nfile = \"material.wgsl\"\n[vertex]\nfile = \"vertex.wgsl\"\n";
let material = "fn material(input: MaterialInput) -> Surface {\n \
return Surface(input.albedo * input.tint * vec4(input.colored_light, 1.0));\n}\n";
let vertex = "fn vertex(input: VertexInput) -> vec3<f32> {\n \
return input.position + input.tint.rgb;\n}\n";
let files = [
(MANIFEST, manifest),
("material.wgsl", material),
("vertex.wgsl", vertex),
]
.map(|(n, t)| (n.to_owned(), t.as_bytes().to_vec()))
.into();
let report = check(&Pack::from_files(files).unwrap()).unwrap();
let stage = report.stages.iter().find(|s| s.kind == StageKind::Material);
let module = module(stage.unwrap(), &[], &[], true, false, Api::V0_2).unwrap();
naga::valid::Validator::new(
naga::valid::ValidationFlags::all(),
naga::valid::Capabilities::all(),
)
.validate(&module)
.unwrap();
}
#[test]
fn the_displaced_grid_validates_with_a_pack() {
let manifest = "schema = 1\nid = \"t\"\nname = \"T\"\nversion = \"1\"\napi = \"0.1\"\n\
@@ -661,14 +727,14 @@ mod tests {
let mut report = check(&Pack::from_files(files).unwrap()).unwrap();
let stage = report.stages.remove(0);
let materials = [(5, 1000)];
let module = module(&stage, &[], &materials, false, true).unwrap();
let module = module(&stage, &[], &materials, false, true, Api::V0_1).unwrap();
naga::valid::Validator::new(
naga::valid::ValidationFlags::all(),
naga::valid::Capabilities::all(),
)
.validate(&module)
.unwrap_or_else(|e| {
let text = shader_source(&stage.source, &[], &materials, false, true);
let text = shader_source(&stage.source, &[], &materials, (false, true), Api::V0_1);
panic!("{}", e.emit_to_string(&text))
});
for entry in [
@@ -681,4 +747,116 @@ mod tests {
);
}
}
/// An identity material (vanilla's own lighting, `albedo * tint * lightmap`) under 0.2,
/// its colours decoded on the way in and encoded on the way out as the entries do, draws
/// what 0.1's does (vanilla's) within an 8-bit step.
#[test]
fn an_identity_0_2_material_is_vanilla_s_within_an_8_bit_step() {
let instance = crate::gfx::test_gpu::instance();
let Ok(adapter) = pollster::block_on(instance.request_adapter(&Default::default())) else {
return;
};
let Ok((device, queue)) = pollster::block_on(adapter.request_device(&Default::default()))
else {
return;
};
// The entries' call of `material`, with a 16×16 grid of colours.
let entry = "@vertex fn vs(@builtin(vertex_index) i: u32) -> @builtin(position) vec4<f32> {
return vec4<f32>(vec2<f32>(f32((i << 1u) & 2u), f32(i & 2u)) * 2.0 - 1.0, 0.0, 1.0);
}
@fragment fn fs(@builtin(position) p: vec4<f32>) -> @location(0) vec4<f32> {
let c = floor(p.xy) / 15.0;
let albedo = vec4(c.x, c.y, 1.0 - c.x, 1.0);
let tint = vec4(0.9, 1.0 - 0.5 * c.y, 0.7, 1.0);
let lightmap = vec3(c.y, 0.8, 1.0 - c.y);
let out = material(MaterialInput(vec4(horizon_line_in(albedo.rgb), albedo.a),
vec4(horizon_line_in(tint.rgb), tint.a), vec2(1.0), horizon_line_in(lightmap),
vec3(0.0), vec2(0.0), false, 0u, 0u, vec2(0.0), 0u, 0u, 0.0, vec3(0.0, 1.0, 0.0),
vec4(1.0, 0.0, 0.0, 1.0), horizon_line_in(vec3(0.0)), 0.0)).color;
return vec4(horizon_line_out(out.rgb), out.a);
}
";
let vanilla = "fn material(input: MaterialInput) -> Surface {\n \
return Surface(input.albedo * input.tint * vec4(input.lightmap, 1.0));\n}\n";
let format = wgpu::TextureFormat::Rgba8Unorm;
let draw = |api: Api| {
let manifest = format!(
"schema = 1\nid = \"t\"\nname = \"T\"\nversion = \"1\"\napi = \"{api}\"\n\
[material]\nfile = \"material.wgsl\"\n"
);
let files = [(MANIFEST, manifest.as_str()), ("material.wgsl", vanilla)]
.map(|(n, t)| (n.to_owned(), t.as_bytes().to_vec()))
.into();
let report = check(&Pack::from_files(files).unwrap()).unwrap();
let source = report.stages[0].source.clone()
+ horizon_shader_api::stages::PBR_STUB
+ super::super::native_post::line_helpers(api)
+ entry;
let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: None,
source: wgpu::ShaderSource::Wgsl(source.into()),
});
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: None,
layout: None,
vertex: wgpu::VertexState {
module: &module,
entry_point: Some("vs"),
compilation_options: Default::default(),
buffers: &[],
},
primitive: Default::default(),
depth_stencil: None,
multisample: Default::default(),
fragment: Some(wgpu::FragmentState {
module: &module,
entry_point: Some("fs"),
compilation_options: Default::default(),
targets: &[Some(format.into())],
}),
multiview_mask: None,
cache: None,
});
let size = wgpu::Extent3d {
width: 16,
height: 16,
depth_or_array_layers: 1,
};
let target = device.create_texture(&wgpu::TextureDescriptor {
label: None,
size,
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
let view = target.create_view(&Default::default());
let mut encoder = device.create_command_encoder(&Default::default());
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &view,
resolve_target: None,
depth_slice: None,
ops: Default::default(),
})],
..Default::default()
});
pass.set_pipeline(&pipeline);
pass.draw(0..3, 0..1);
drop(pass);
let screenshot = crate::gfx::copy_frame(&device, &mut encoder, &target, [16, 16]);
queue.submit([encoder.finish()]);
screenshot
.slice(..)
.map_async(wgpu::MapMode::Read, |r| r.unwrap());
device.poll(wgpu::PollType::wait_indefinitely()).unwrap();
screenshot.slice(..).get_mapped_range().unwrap().to_vec()
};
let (old, new) = (draw(Api::V0_1), draw(Api::V0_2));
let worst = old.iter().zip(&new).map(|(a, b)| a.abs_diff(*b)).max();
assert!(worst.is_some_and(|w| w <= 1), "{worst:?}");
}
}
@@ -90,6 +90,27 @@ impl NativePack {
report: Report,
constants: &[(String, f64)],
lights: super::block_light::LightTable,
) -> Result<(Self, Vec<String>), String> {
// A backend may refuse a layout or pipeline the pack asks for (a stage's bindings
// past its sampled-texture limit, say, as OpenGL's 16 was before #10095): the pack
// is refused with that error instead of a validation panic in the renderer.
let scope = device.push_error_scope(wgpu::ErrorFilter::Validation);
let built = Self::build(device, queue, terrain, report, constants, lights);
let refused = pollster::block_on(scope.pop());
match (built, refused) {
(Ok(built), None) => Ok(built),
(Ok(_), Some(e)) => Err(e.to_string()),
(Err(e), _) => Err(e),
}
}
fn build(
device: &wgpu::Device,
queue: &wgpu::Queue,
terrain: &TerrainShape<'_>,
report: Report,
constants: &[(String, f64)],
lights: super::block_light::LightTable,
) -> Result<(Self, Vec<String>), String> {
let (globals, format) = (terrain.globals, terrain.format);
let textures = super::native_post::pack_textures(device, queue, &report);
@@ -101,7 +122,15 @@ impl NativePack {
let settings = chosen
.as_ref()
.map(|settings| super::native_shadow::fitted(device, settings));
let mut frame = NativeFrame::new(device, settings.as_ref(), textures, lights, terrain.pbr);
let api = report.manifest.api;
let mut frame = NativeFrame::new(
device,
settings.as_ref(),
textures,
lights,
terrain.pbr,
api,
);
// `[displacement]`, unless its option is off; nothing to displace without heights.
let displacement = report
.manifest
@@ -222,8 +251,13 @@ impl NativePack {
)
})
.transpose()?;
let post = (!post.is_empty())
.then(|| NativePost::new(device, post, &report.manifest, constants, format, &frame))
// A 0.2 pack always has one: its decode and tonemap run without a chain too.
let tonemap = others.iter().find(|s| s.kind == StageKind::Tonemap);
let post = (!post.is_empty() || api == horizon_shader_api::Api::V0_2)
.then(|| {
let manifest = &report.manifest;
NativePost::new(device, post, manifest, constants, format, &frame, tonemap)
})
.transpose()?;
let mut labels: Vec<String> = sky.iter().map(|s| s.label.clone()).collect();
labels.extend(
@@ -265,9 +299,90 @@ impl NativePack {
#[cfg(test)]
mod tests {
use super::chosen_shadow;
use super::{NativePack, TerrainShape, chosen_shadow};
use horizon_shader_api::manifest::{OptionKind, PackOption, Shadow};
/// A device with OpenGL's minimum 16 sampled textures per stage refuses a pack whose
/// sky adds the terrain's 10 to the frame's 9: that is the pack's error to report, not
/// a validation panic (#10095).
#[test]
fn a_pack_the_backend_refuses_is_an_error_not_a_panic() {
use horizon_shader_api::{MANIFEST, Pack, check};
let instance = crate::gfx::test_gpu::instance();
let Ok(adapter) = pollster::block_on(instance.request_adapter(&Default::default())) else {
return;
};
let mut limits = adapter.limits();
limits.max_sampled_textures_per_shader_stage = 16;
let Ok((device, queue)) =
pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
required_limits: limits,
..Default::default()
}))
else {
return;
};
// The terrain's group 0: its uniform and the 10 sampled textures (_terrain.wgsl_'s
// 8 sprite arrays, the lightmap and the emission thresholds).
let entry = |binding, ty| wgpu::BindGroupLayoutEntry {
binding,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty,
count: None,
};
let mut entries = vec![entry(
0,
wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
)];
entries.extend((1..=10).map(|binding| {
entry(
binding,
wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
)
}));
let globals = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: None,
entries: &entries,
});
let empty = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: None,
entries: &[],
});
let terrain = TerrainShape {
globals: &globals,
flat: None,
sprites: [1.0, 2048.0],
format: wgpu::TextureFormat::Rgba8Unorm,
sprite_tags: &[],
sprite_materials: &[],
entities: &empty,
weather_groups: [&empty, &empty],
pbr: None,
};
let manifest = "schema = 1\nid = \"t\"\nname = \"T\"\nversion = \"1\"\napi = \"0.1\"\n\
[sky]\nfile = \"sky.wgsl\"\n";
let files = [
(MANIFEST, manifest),
(
"sky.wgsl",
"fn sky(input: SkyInput) -> vec4<f32> { return input.color; }\n",
),
]
.map(|(n, t)| (n.to_owned(), t.as_bytes().to_vec()))
.into();
let report = check(&Pack::from_files(files).unwrap()).unwrap();
let built = NativePack::new(&device, &queue, &terrain, report, &[], Default::default());
assert!(built.is_err(), "the pack should be refused");
}
fn option(id: &str, kind: OptionKind) -> PackOption {
PackOption {
id: id.into(),
File diff suppressed because it is too large. Load diff
@@ -6,7 +6,7 @@
//! draw lists (`ShadowView`) serve both; the map's texels are distorted toward the camera as the
//! prelude's `shadow_position` undoes.
use horizon_shader_api::{Frame as FrameInputs, manifest::Shadow as Settings, stages::GROUP};
use horizon_shader_api::{Api, Frame as FrameInputs, manifest::Shadow as Settings, stages::GROUP};
use horizon_shaders::Shadow;
use super::native_material::TerrainShape;
@@ -75,7 +75,7 @@ fn horizon_shadow_cutout(in: HorizonShadowOut) {
@fragment fn horizon_shadow_fs_translucent(in: HorizonShadowOut) -> @location(0) vec4<f32> {
let color = horizon_terrain_sprite(in.uv, in.layer & 0x7fffu);
if color.a < 0.01 { discard; }
return vec4(color.rgb * in.tint, select(min(color.a, 0.99), 1.0, in.fluid != 0u));
return vec4(horizon_line_in(color.rgb * in.tint), select(min(color.a, 0.99), 1.0, in.fluid != 0u));
}
";
@@ -152,8 +152,13 @@ pub struct ShadowGrid<'a> {
pub normals: &'a wgpu::TextureView,
}
/// `shadow_color`'s format.
const SHADOW_COLOR: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
/// `shadow_color`'s format: under 0.2 it holds linear light, which 8 bits would band.
fn shadow_color(api: Api) -> wgpu::TextureFormat {
match api {
Api::V0_1 => wgpu::TextureFormat::Rgba8Unorm,
Api::V0_2 => super::native_post::INTERMEDIATE,
}
}
/// The shader pack's shadow camera for `settings`: orthographic over `distance` each side,
/// staying put while the camera walks a `SNAP`-block cell. The map is distorted about its
@@ -218,7 +223,7 @@ pub struct ShadowMap {
}
impl ShadowMap {
pub fn new(device: &wgpu::Device, settings: Option<&Settings>) -> Self {
pub fn new(device: &wgpu::Device, settings: Option<&Settings>, api: Api) -> Self {
let size = settings.map_or(1, |s| s.resolution);
let texture = |label, format, usage| {
device.create_texture(&wgpu::TextureDescriptor {
@@ -250,7 +255,7 @@ impl ShadowMap {
);
let color = texture(
"Native shader pack shadow colour",
SHADOW_COLOR,
shadow_color(api),
wgpu::TextureUsages::empty(),
);
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
@@ -474,6 +479,7 @@ fn shader_source(
vertex: Option<&str>,
materials: &[(u32, u32)],
grid: Option<&str>,
api: Api,
) -> String {
let table = super::native_material::layer_table("horizon_sprite_material", materials);
let (head, displace) = match (grid, vertex) {
@@ -504,11 +510,12 @@ fn shader_source(
),
};
format!(
"{head}// Horizon's terrain (terrain.wgsl)\n{}{solid}{displace}{ENTRIES}",
"{head}// Horizon's terrain (terrain.wgsl)\n{}{solid}{}{displace}{ENTRIES}",
prefixed(
section(TERRAIN, "struct Globals {", "// Sky (SkyRenderer)"),
PREFIX
)
),
super::native_post::line_helpers(api)
)
}
@@ -527,6 +534,7 @@ impl NativeShadow {
vertex.map(|s| s.source.as_str()),
shape.sprite_materials,
grid.as_ref().map(|g| g.material.source.as_str()),
frame.api,
);
let module = naga::front::wgsl::parse_str(&text)
.map_err(|e| format!("shadow pass: {}", e.emit_to_string(&text)))?;
@@ -613,7 +621,7 @@ impl NativeShadow {
attributes: &instance_attributes,
};
let color_target = [Some(wgpu::ColorTargetState {
format: SHADOW_COLOR,
format: shadow_color(frame.api),
blend: None,
write_mask: wgpu::ColorWrites::ALL,
})];
@@ -731,8 +739,9 @@ mod tests {
#[test]
fn the_shadow_shader_validates() {
for materials in [&[][..], &[(3, 7), (4, 8), (9, 7)][..]] {
let text = shader_source(&shadow_solid(materials, &[7]), None, materials, None);
let lines = [Api::V0_1, Api::V0_2];
for (materials, api) in [&[][..], &[(3, 7), (4, 8), (9, 7)][..]].iter().zip(lines) {
let text = shader_source(&shadow_solid(materials, &[7]), None, materials, None, api);
let module = naga::front::wgsl::parse_str(&text).unwrap();
naga::valid::Validator::new(
naga::valid::ValidationFlags::all(),
@@ -767,6 +776,7 @@ mod tests {
None,
&materials,
Some(&stage.source),
Api::V0_1,
);
let module = naga::front::wgsl::parse_str(&text)
.unwrap_or_else(|e| panic!("{}", e.emit_to_string(&text)));
@@ -17,7 +17,8 @@ const PREFIX: &str = "horizon_sky_";
const PACK_ENTRY: &str = "
@fragment fn horizon_sky_fs_pack(in: horizon_sky_SkyOut) -> @location(0) vec4<f32> {
let d = horizon_sky_view_ray(in.ndc);
return vec4(sky(SkyInput(d, vec4(horizon_sky_sky_along_ray(d), 1.0))).rgb, 1.0);
let color = vec4(horizon_line_in(horizon_sky_sky_along_ray(d)), 1.0);
return vec4(horizon_line_out(sky(SkyInput(d, color)).rgb), 1.0);
}
";
@@ -38,10 +39,11 @@ fn vanilla_sky() -> String {
}
/// The sky shader for the pack's sky stage `source`.
fn shader_source(source: &str) -> String {
fn shader_source(source: &str, api: horizon_shader_api::Api) -> String {
format!(
"{source}\n// Horizon's sky (terrain.wgsl)\n{}{PACK_ENTRY}",
vanilla_sky()
"{source}\n// Horizon's sky (terrain.wgsl)\n{}{}{PACK_ENTRY}",
vanilla_sky(),
super::native_post::line_helpers(api)
)
}
@@ -99,7 +101,7 @@ fn build(
frame: &NativeFrame,
stage: &CheckedStage,
) -> Result<(naga::Module, wgpu::PipelineLayout), String> {
let text = shader_source(&stage.source);
let text = shader_source(&stage.source, frame.api);
let module = naga::front::wgsl::parse_str(&text)
.map_err(|e| format!("{}: {}", stage.file, e.emit_to_string(&text)))?;
let mut groups: [Option<&wgpu::BindGroupLayout>; GROUP as usize + 1] = Default::default();
@@ -195,7 +197,7 @@ mod tests {
// A pack may use the renderer's names for its own things.
let own = "override gain: f32;\nconst PI: f32 = 3.0;\nfn view_ray() -> f32 { return PI; }\n\
fn sky(input: SkyInput) -> vec4<f32> { return input.color * gain * view_ray(); }\n";
let text = shader_source(&stage(own).source);
let text = shader_source(&stage(own).source, horizon_shader_api::Api::V0_1);
let module = naga::front::wgsl::parse_str(&text).unwrap_or_else(|e| {
panic!("{}", e.emit_to_string(&text));
});
@@ -252,7 +254,14 @@ mod tests {
resource: globals_buffer.as_entire_binding(),
}],
});
let frame = NativeFrame::new(&device, None, Vec::new(), Default::default(), None);
let frame = NativeFrame::new(
&device,
None,
Vec::new(),
Default::default(),
None,
horizon_shader_api::Api::V0_1,
);
queue.write_buffer(
&frame.buffer,
0,
@@ -30,6 +30,7 @@ struct Stitched {
/// Each layer's sprite at the first level: its top left corner and its side.
rects: Vec<[u32; 3]>,
padding: u32,
material: bool,
pipeline: wgpu::RenderPipeline,
/// One per sprite array.
groups: Vec<wgpu::BindGroup>,
@@ -66,10 +67,19 @@ fn vs(@builtin(vertex_index) v: u32, @location(0) rect: vec4<i32>, @location(1)
out.source = source.xy;
return out;
}
fn sprite_texel(in: Out) -> vec2<i32> {
return clamp(vec2<i32>(floor(in.position.xy)) - in.rect.xy, vec2(0), vec2(in.rect.z - 1));
}
@fragment
fn fs(in: Out) -> @location(0) vec4<f32> {
let texel = clamp(vec2<i32>(floor(in.position.xy)) - in.rect.xy, vec2(0), vec2(in.rect.z - 1));
return textureLoad(sprites, texel, i32(in.source.x), i32(in.source.y));
return textureLoad(sprites, sprite_texel(in), i32(in.source.x), i32(in.source.y));
}
@fragment
fn fs_material(in: Out) -> @location(0) vec4<f32> {
let texel = sprite_texel(in);
let edge = i32(textureDimensions(sprites, 0).x);
let sampled = min((texel * 2 + 1) * edge / (2 * in.rect.z), vec2(edge - 1));
return textureLoad(sprites, sampled, i32(in.source.x), i32(in.source.y));
}
"#;
@@ -210,14 +220,72 @@ impl PackAtlas {
stitch_mip_level(&sides, mip_option).min(array_levels.saturating_sub(deepest + 1));
let max = device.limits().max_texture_dimension_2d;
let layout = stitch::stitch(&sides, mip, anisotropy_bit, max)?;
let rects = layout
.holders
.iter()
.zip(&sides)
.map(|(&[x, y], &side)| [x + layout.padding, y + layout.padding, side])
.collect();
Some(Self::with_rects(
device,
encoder,
arrays,
split,
sprite,
[layout.width.max(1), layout.height.max(1)],
rects,
layout.padding,
mip + 1,
false,
))
}
/// Companion maps use the colour atlas's coordinates, not a second packing.
pub fn material_for(
&self,
device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder,
arrays: &[wgpu::Texture],
layers: u32,
) -> Option<Self> {
let Some(stitched) = &self.stitched else {
return Self::material(device, encoder, arrays, self.split, layers);
};
Some(Self::with_rects(
device,
encoder,
arrays,
self.split,
arrays[0].width(),
self.size(),
stitched.rects.clone(),
stitched.padding,
1,
true,
))
}
#[allow(clippy::too_many_arguments)]
fn with_rects(
device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder,
arrays: &[wgpu::Texture],
split: u32,
sprite: u32,
size: [u32; 2],
rects: Vec<[u32; 3]>,
padding: u32,
levels: u32,
material: bool,
) -> Self {
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("Shader pack block atlas"),
size: wgpu::Extent3d {
width: layout.width.max(1),
height: layout.height.max(1),
width: size[0],
height: size[1],
depth_or_array_layers: 1,
},
mip_level_count: mip + 1,
mip_level_count: levels,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8Unorm,
@@ -268,7 +336,7 @@ impl PackAtlas {
multisample: Default::default(),
fragment: Some(wgpu::FragmentState {
module: &module,
entry_point: Some("fs"),
entry_point: Some(if material { "fs_material" } else { "fs" }),
compilation_options: Default::default(),
targets: &[Some(wgpu::TextureFormat::Rgba8Unorm.into())],
}),
@@ -293,12 +361,6 @@ impl PackAtlas {
})
})
.collect();
let rects = layout
.holders
.iter()
.zip(&sides)
.map(|(&[x, y], &side)| [x + layout.padding, y + layout.padding, side])
.collect::<Vec<_>>();
let layers = rects.len() as u32;
let atlas = Self {
texture,
@@ -309,13 +371,14 @@ impl PackAtlas {
separate_ao: false,
stitched: Some(Stitched {
rects,
padding: layout.padding,
padding,
material,
pipeline,
groups,
}),
};
atlas.copy(device, encoder, arrays, 0..layers);
Some(atlas)
atlas
}
/// Whether the sprites keep their own sizes ([`PackAtlas::stitched`]).
@@ -425,7 +488,11 @@ impl PackAtlas {
rect: [x, y, side, stitched.padding].map(|v| (v >> level) as i32),
source: [
layer % self.split,
(self.sprite / side).ilog2() + level,
if stitched.material {
0
} else {
(self.sprite / side).ilog2() + level
},
size[0],
size[1],
],
@@ -563,7 +630,7 @@ impl PackAtlas {
"layout(set = 1, binding = 0) readonly buffer HzBlockSlots { uint hz_block_slots[]; };\n\
layout(set = 1, binding = 1) readonly buffer HzSlotOrigins { vec4 hz_slot_origins[]; };\n",
);
// Of each sprite layer, `mc_Entity` (id, then 1 for fluids or -1) and
// Of each sprite layer, `mc_Entity` (id, then 1 for fluids or 0) and
// `at_midBlock.w` (its block's light), the game's table; and the arena page's
// vertices (`TerrainVertex`, six words each).
declarations.push_str(
@@ -610,13 +677,17 @@ impl PackAtlas {
// A vertex's place in its section goes down to the 32768th of a block below it: read back
// with a probe pack of ours, Iris 1.11.7's terrain `gl_Vertex` has that for every position
// we read (water tops at 8/9 - 0.001, plants' offsets), on all three axes (#8464). A fit,
// not Iris's code. Whole and half blocks, and every 1/16, are on that grid already.
// not Iris's code. Whole and half blocks, and every 1/16, are on that grid already. A place
// up to 1/32 of a 32768th (one float step below 16) under a grid line is on it: plants whose
// random offset is 9/15 of a block read exactly on a half block in Iris, and a 32768th
// under it in Horizon (our guess, not tested: Horizon's float sum lands just under the
// line) (#8464). Also a fit.
let body = format!(
" vec4 hz_origin = {origin};\n \
uint hz_layer = hz_v_layer & 32767u;\n \
{cell}\
hz_scale = vec2({:?}, {:?}) * {side};\n \
hz_Vertex = vec4(floor(hz_v_pos * 32768.0) / 32768.0 + hz_origin.xyz, 1.0);\n \
hz_Vertex = vec4(floor(hz_v_pos * 32768.0 + 0.03125) / 32768.0 + hz_origin.xyz, 1.0);\n \
hz_uv_scale = hz_origin.w > 0.0 ? uintBitsToFloat(floatBitsToUint(hz_origin.w) & 0xff800000u) : fract(abs(hz_origin.w));\n \
hz_mid = vec2(0.5);\n \
hz_MultiTexCoord0 = vec4((hz_cell + clamp(vec2(hz_v_uv) * hz_uv_scale, 0.0, 1.0)) * hz_scale, 0.0, 1.0);\n \
@@ -637,7 +708,7 @@ impl PackAtlas {
let entity = if flat {
"vec4(float(hz_block_ids[3u * hz_layer]), float(hz_block_ids[3u * hz_layer + 1u]), 0.0, 0.0)"
} else {
"vec4(-1.0, -1.0, 0.0, 0.0)"
"vec4(-1.0, 0.0, 0.0, 0.0)"
};
// The block a face belongs to: half a block behind its centre (the quad's diagonal);
// w, the light it gives (Iris's `BLOCK_EMISSION_ATTRIBUTE`, 0 to 15).
@@ -657,6 +728,13 @@ impl PackAtlas {
),
("mc_Entity", entity),
("at_midBlock", mid_block),
// Horizon's own: the face's HDR tier (`mesher::emitting`'s code in the colour's low
// alpha nibble): 0 for none, 1 to 4 for low to very bright, plus 4 when the whole
// face shines (`mesher::WHOLE`).
(
"horizonTier",
"float(15u - (uint(round(hz_v_color.a * 255.0)) & 15u))",
),
(
"mc_midTexCoord",
"vec4((hz_cell + hz_mid) * hz_scale, 0.0, 1.0)",
@@ -890,6 +968,144 @@ mod tests {
assert_eq!([at(9, 5), at(8, 4), at(10, 6)], [[100, 0, 0, 255]; 3]);
}
fn material_fixture(animated: bool) {
let instance = crate::gfx::test_gpu::instance();
// No adapter (a CI runner without a GPU): skip, as the module's other readback tests do.
let Ok(adapter) = pollster::block_on(instance.request_adapter(&Default::default())) else {
return;
};
let (device, queue) =
pollster::block_on(adapter.request_device(&Default::default())).unwrap();
let colour_array = upscaled_array(&device, &queue, &[2, 4, 1]);
let mut encoder = device.create_command_encoder(&Default::default());
let colour = PackAtlas::stitched(
&device,
&mut encoder,
&[colour_array],
3,
4,
&[2, 4, 1],
0,
0,
)
.unwrap();
for kind in 0..2 {
// Different source resolution, with encoded channel boundaries and defaults.
let markers = if kind == 0 {
[
[128, 126, 255, 19],
crate::sprites::FLAT_NORMAL,
[130, 125, 254, 21],
]
} else {
[
[17, 240, 65, 255],
[18, 0, 66, 0],
crate::sprites::NO_SPECULAR,
]
};
let pixels: Vec<_> = markers.iter().flat_map(|m| m.repeat(4)).collect();
let array = wgpu::util::DeviceExt::create_texture_with_data(
&device,
&queue,
&wgpu::TextureDescriptor {
label: None,
size: wgpu::Extent3d {
width: 2,
height: 2,
depth_or_array_layers: 3,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8Unorm,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
},
wgpu::util::TextureDataOrder::LayerMajor,
&pixels,
);
let arrays = [array];
let material = colour
.material_for(&device, &mut encoder, &arrays, 3)
.unwrap();
assert_eq!(material.size(), [16, 8]);
assert_eq!(material.rects_table(), colour.rects_table());
assert_eq!(material.texture.mip_level_count(), 1);
queue.submit([encoder.finish()]);
encoder = device.create_command_encoder(&Default::default());
let changed = [243, 1, 229, 0];
if animated {
// Alter every source layer, but copy only the animated layer.
queue.write_texture(
arrays[0].as_image_copy(),
&changed.repeat(12),
wgpu::TexelCopyBufferLayout {
bytes_per_row: Some(8),
rows_per_image: Some(2),
offset: 0,
},
arrays[0].size(),
);
material.copy(&device, &mut encoder, &arrays, [0]);
}
let read = device.create_buffer(&wgpu::BufferDescriptor {
label: None,
size: 256 * 8,
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
mapped_at_creation: false,
});
encoder.copy_texture_to_buffer(
material.texture.as_image_copy(),
wgpu::TexelCopyBufferInfo {
buffer: &read,
layout: wgpu::TexelCopyBufferLayout {
bytes_per_row: Some(256),
..Default::default()
},
},
material.texture.size(),
);
queue.submit([encoder.finish()]);
encoder = device.create_command_encoder(&Default::default());
read.slice(..).map_async(wgpu::MapMode::Read, |_| {});
device.poll(wgpu::PollType::wait_indefinitely()).unwrap();
let data = read.slice(..).get_mapped_range().unwrap();
// Independent, hand-worked colour rectangles, including all padding pixels.
for (layer, [x, y, side]) in [[9usize, 1, 2], [1, 1, 4], [9, 5, 1]]
.into_iter()
.enumerate()
{
let expected = if animated && layer == 0 {
changed
} else {
markers[layer]
};
for py in y - 1..=y + side {
for px in x - 1..=x + side {
let at = py * 256 + px * 4;
assert_eq!(
&data[at..at + 4],
&expected,
"kind={kind}, layer={layer}, ({px},{py})"
);
}
}
}
}
queue.submit([encoder.finish()]);
}
#[test]
fn stitched_material_maps_share_colour_rectangles() {
material_fixture(false);
}
#[test]
fn animated_stitched_material_updates_only_its_rectangle() {
material_fixture(true);
}
#[test]
fn terrain_inputs_respects_separate_ao() {
let instance = crate::gfx::test_gpu::instance();
@@ -157,6 +157,7 @@ fn terrain_fade_reaches_pack_pixels_through_real_instances() {
flat,
geometry,
origin,
0x80ffffff,
);
let fade = if requested { expected } else { 1.0 };
let expected = [
@@ -182,6 +183,96 @@ fn terrain_fade_reaches_pack_pixels_through_real_instances() {
assert_eq!(count, 98);
}
/// `horizonTier`: the HDR tier the mesher marks a light source's faces with (`mesher::emitting`),
/// read by a pack's terrain program, flat or instanced.
#[test]
fn terrain_tier_reaches_pack_pixels() {
let instance = crate::gfx::test_gpu::instance();
let adapter = match pollster::block_on(instance.request_adapter(&Default::default())) {
Ok(adapter) => adapter,
Err(error) => {
eprintln!("SKIP terrain tier GPU control: no adapter: {error}");
return;
}
};
let (device, queue) = pollster::block_on(adapter.request_device(&Default::default())).unwrap();
let array = device.create_texture(&wgpu::TextureDescriptor {
label: None,
size: wgpu::Extent3d {
width: 2,
height: 2,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8Unorm,
usage: wgpu::TextureUsages::COPY_SRC | wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
});
let mut encoder = device.create_command_encoder(&Default::default());
let atlas = PackAtlas::new(&device, &mut encoder, &[array], 1, 1, 2).unwrap();
queue.submit([encoder.finish()]);
let folder = ProbePack(
std::env::temp_dir().join(format!("horizon-terrain-tier-{}", std::process::id())),
);
std::fs::create_dir_all(folder.0.join("shaders")).unwrap();
std::fs::write(
folder.0.join("shaders/gbuffers_terrain.vsh"),
"#version 330\nin float horizonTier;\nout vec4 observation;\n\
void main() {\nobservation = vec4(horizonTier, 0.0, 0.0, 0.0);\n\
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);\n}",
)
.unwrap();
std::fs::write(
folder.0.join("shaders/gbuffers_terrain.fsh"),
"#version 330\nin vec4 observation;\nlayout(location = 0) out uvec4 pixel;\n\
void main() { pixel = floatBitsToUint(observation); }",
)
.unwrap();
let pack = horizon_shaders::Pack::open(&folder.0).unwrap();
let mut macros = horizon_shaders::Macros::standard();
let (properties, _) = horizon_shaders::read_properties(&pack, &mut macros);
use crate::mesher::{BRIGHT, LOW, VERY_BRIGHT, WHOLE, emitting};
// No light, low, very bright, and bright all over (4 more).
let cases = [
(0, 0.0),
(LOW, 1.0),
(VERY_BRIGHT, 4.0),
(BRIGHT | WHOLE, 7.0),
];
for flat in [false, true] {
let post = horizon_shaders::load_post(
&pack,
"",
&properties,
&macros,
&horizon_shaders::GameInputs {
terrain: Some(atlas.terrain_inputs(flat)),
..Default::default()
},
);
let program = post
.terrain
.as_ref()
.unwrap_or_else(|| panic!("{:?}", post.notes));
for (tier, expected) in cases {
let origin = section_instance([0.0; 3], 0, 1.0);
let pixel = sample(
&device,
&queue,
&atlas,
program.program.as_ref().unwrap(),
flat,
false,
origin,
emitting(0xffff_ffff, tier),
);
assert_eq!(pixel[0], expected, "tier {tier} flat={flat}");
}
}
}
fn sample(
device: &wgpu::Device,
queue: &wgpu::Queue,
@@ -190,6 +281,7 @@ fn sample(
flat: bool,
geometry: bool,
origin: [f32; 4],
color: u32,
) -> [f32; 4] {
let buffer = |bytes: &[u8], usage| {
device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
@@ -209,7 +301,7 @@ fn sample(
let slots = buffer(bytemuck::bytes_of(&0u32), wgpu::BufferUsages::STORAGE);
let vertices: Vec<_> = [[0., 0., 0.], [1., 0., 0.], [1., 1., 0.], [0., 1., 0.]]
.into_iter()
.map(|pos| TerrainVertex::new(pos, [0.25, 0.75], 0, 0x80ffffff, 0))
.map(|pos| TerrainVertex::new(pos, [0.25, 0.75], 0, color, 0))
.collect();
let data = buffer(
bytemuck::cast_slice(&vertices),
@@ -174,7 +174,8 @@ fn flat_draws_map_uvs_onto_a_stitched_sprite() {
/// Read back with a probe pack of ours, Iris 1.11.7's terrain `gl_Vertex` (camera on the block
/// grid) puts a water top, 8/9 - 0.001 of a block up, at 29094/32768, and plants offset by 13/15
/// and 0.1 of a block at 28398/32768 and 3276/32768 (#8464): each 32768th below the position.
/// Whole blocks and sixteenths stay where they are.
/// Whole blocks and sixteenths stay where they are, and so does a place float rounding put a hair
/// below a grid line: Iris reads plants offset by 9/15 exactly on a half block.
#[test]
fn terrain_vertices_go_down_to_the_32768th_below_them() {
let instance = crate::gfx::test_gpu::instance();
@@ -204,6 +205,15 @@ fn terrain_vertices_go_down_to_the_32768th_below_them() {
([0.1, water, 13.0 / 15.0], [3276.0, 29094.0, 28398.0]),
([13.0 / 15.0, 0.1, water], [28398.0, 3276.0, 29094.0]),
([1.0, 0.5, 15.9375], [32768.0, 16384.0, 522240.0]),
// One float step below 1, 0.5 and 9.5.
(
[
1.0 - f32::EPSILON / 2.0,
0.5 - f32::EPSILON / 4.0,
9.5 - f32::EPSILON * 8.0,
],
[32768.0, 16384.0, 311296.0],
),
];
let vertices: Vec<_> = cases
.iter()
@@ -217,6 +217,41 @@ impl CenterDepth {
})
}
/// Once the GPU has finished the frame (`screenshot-sequence`), the depth it copied, as
/// the next frame reads it: an error unless exactly that one sample was read back, never
/// an older one.
pub fn take_completed(&mut self, device: &wgpu::Device) -> Result<f32, String> {
device
.poll(wgpu::PollType::Poll)
.map_err(|e| e.to_string())?;
let state = |s: &Slot| s.state.load(Ordering::Acquire);
let mut mapped = self.slots.iter().filter(|s| state(s) == MAPPED);
let (Some(slot), None) = (mapped.next(), mapped.next()) else {
return Err("the frame's centre depth wasn't read back alone".into());
};
if self
.slots
.iter()
.any(|s| !matches!(state(s), FREE | MAPPED))
{
return Err("the frame's centre depth wasn't read back alone".into());
}
let depth = slot
.buffer
.slice(..4)
.get_mapped_range()
.map(|bytes| bytemuck::pod_read_unaligned::<f32>(&bytes[..4]))
.map_err(|e| e.to_string());
slot.buffer.unmap();
slot.state.store(FREE, Ordering::Release);
let depth = depth?;
if !depth.is_finite() {
return Err("the centre depth isn't finite".into());
}
self.latest = Some(depth.clamp(0.0, 1.0));
Ok(depth.clamp(0.0, 1.0))
}
/// The newest depth read back so far, `None` before the first arrives.
pub fn collect(&mut self, device: &wgpu::Device) -> Option<f32> {
let _ = device.poll(wgpu::PollType::Poll);
@@ -265,6 +300,76 @@ mod tests {
});
}
/// Inline and on the submit thread: each finished frame's depth, then none left to
/// take; several outstanding samples are refused.
#[test]
#[ignore = "requires a host GPU"]
fn a_finished_frame_s_depth_is_taken_alone_never_an_older_one() {
let (device, raw) = test_device().expect("host GPU required");
let depth = device.create_texture(&wgpu::TextureDescriptor {
label: None,
size: wgpu::Extent3d {
width: 3,
height: 3,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Depth32Float,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
});
let view = depth.create_view(&Default::default());
for threaded in [false, true] {
let queue = super::super::submit::Queue::new(raw.clone(), threaded);
if threaded {
assert!(queue.threaded(), "deferred submit unavailable");
}
let mut center = CenterDepth::new(&device);
assert!(center.take_completed(&device).is_err());
for value in [0.125, 0.75, 0.25] {
let mut encoder = device.create_command_encoder(&Default::default());
clear(&mut encoder, &view, value);
center.copy(&device, &mut encoder, &depth);
let after = center.submitted().expect("one copied sample");
if threaded {
let job = super::super::submit::Job {
encoder,
surface: None,
after: Some(Box::new(after)),
};
assert!(queue.submit_later(job).is_ok());
queue.wait();
} else {
queue.submit([encoder.finish()]);
after();
}
device
.poll(wgpu::PollType::Wait {
submission_index: None,
timeout: Some(std::time::Duration::from_secs(30)),
})
.unwrap();
assert_eq!(center.take_completed(&device).unwrap(), value);
assert_eq!(center.collect(&device), Some(value));
assert!(
center.take_completed(&device).is_err(),
"no stale latest fallback"
);
}
// Several outstanding copies are ambiguous, even after they all map.
let mut encoder = device.create_command_encoder(&Default::default());
for _ in 0..SLOTS {
center.copy(&device, &mut encoder, &depth);
}
queue.submit([encoder.finish()]);
center.submitted().unwrap()();
device.poll(wgpu::PollType::wait_indefinitely()).unwrap();
assert!(center.take_completed(&device).is_err());
}
}
/// Frames that go to the submit thread start their readback there: the centre depth
/// follows the frames instead of keeping the first ones' depth.
#[test]
@@ -30,15 +30,30 @@ pub fn buffers() -> [Option<wgpu::VertexBufferLayout<'static>>; 1] {
/// What `gbuffers_entities` and `gbuffers_block` read from an entity vertex: camera-relative
/// position, the shared atlas's UV, colour, light (block, sky in 16ths) and the world-oriented
/// normal. The quad's corners, read from the same vertices at set 1, give the face normal where
/// the vertex has none (block entities) and `at_tangent` along the texture's u, as for terrain.
/// the vertex has none (custom geometry) and `at_tangent` along the texture's u, as for terrain.
pub fn inputs() -> horizon_shaders::GbuffersInputs {
let declarations = "layout(location = 0) in vec3 hz_v_pos;\nlayout(location = 1) in vec2 hz_v_uv;\n\
let words = std::mem::size_of::<Vertex>() / 4;
let mut declarations = "layout(location = 0) in vec3 hz_v_pos;\nlayout(location = 1) in vec2 hz_v_uv;\n\
layout(location = 2) in uint hz_v_overlay;\nlayout(location = 3) in vec4 hz_v_color;\n\
layout(location = 4) in uvec4 hz_v_light;\nlayout(location = 5) in vec4 hz_v_normal;\n\
layout(set = 1, binding = 0) readonly buffer HzEntityVertices { uint hz_entity_vertices[]; };\n\
vec4 hz_tangent; vec2 hz_mid;\n"
.to_owned();
let words = std::mem::size_of::<Vertex>() / 4;
// `at_velocity`: the vertex's offset from last frame in view space, this frame's view
// position less last frame's. Last frame's vertices stay at binding 2, relative to last
// frame's camera, and binding 3 gives each vertex its index there (none: no velocity).
declarations.push_str(&format!(
"layout(set = 1, binding = 2) readonly buffer HzEntityPrevious {{ uint hz_entity_previous[]; }};\n\
layout(set = 1, binding = 3) readonly buffer HzEntityPreviousIndex {{ uint hz_previous_index[]; }};\n\
uint hz_vi;\n\
vec3 hz_velocity(mat4 view, mat4 previous_view) {{\n \
uint p = hz_previous_index[hz_vi];\n \
if (p == 0xffffffffu) return vec3(0.0);\n \
uint w = p * {words}u;\n \
vec3 then = uintBitsToFloat(uvec3(hz_entity_previous[w], hz_entity_previous[w + 1u], hz_entity_previous[w + 2u]));\n \
return (view * vec4(hz_v_pos, 1.0)).xyz - (previous_view * vec4(then, 1.0)).xyz;\n\
}}\n"
));
let corner = |k: usize, field: &str| {
let at = |o: usize| format!("hz_entity_vertices[hz_q + {}u]", k * words + o);
match field {
@@ -47,7 +62,8 @@ pub fn inputs() -> horizon_shaders::GbuffersInputs {
}
};
let body = format!(
" uint hz_q = (uint(gl_VertexIndex) & ~3u) * {words}u;\n \
" hz_vi = uint(gl_VertexIndex);\n \
uint hz_q = (uint(gl_VertexIndex) & ~3u) * {words}u;\n \
vec3 hz_e1 = {p1} - {p0};\n \
vec3 hz_e2 = {p2} - {p0};\n \
vec2 hz_t0 = {t0};\n \
@@ -87,6 +103,14 @@ pub fn inputs() -> horizon_shaders::GbuffersInputs {
("vaUV2", "ivec2(hz_MultiTexCoord1.xy)"),
("vaColor", "hz_Color"),
("vaNormal", "hz_Normal"),
(
"at_velocity",
"hz_velocity(gbufferModelView, gbufferPreviousModelView)",
),
// Horizon's own, as for terrain: the HDR tier of a light-giving item, particle or glowing
// text (`mesher::hdr_tier`, the overlay's high bits), 0 for none, 1 to 4 for low to very
// bright.
("horizonTier", "float((hz_v_overlay >> 16u) & 255u)"),
];
horizon_shaders::GbuffersInputs {
declarations,
@@ -229,7 +253,15 @@ impl Casters {
/// The vertices and per-draw uniforms of the runs a pack draws, rebuilt each frame.
pub(super) struct PackEntities {
vertices: wgpu::Buffer,
/// Set 1: the vertices again, for the quads' corners.
/// Last frame's `vertices` (the two swap each frame), for `at_velocity`.
previous: wgpu::Buffer,
/// For each of `vertices`, its index in `previous` (`NO_PREVIOUS`: none).
previous_index: wgpu::Buffer,
/// This frame's entities (`Frame::entity_motion`) and last frame's.
motion: Motion,
previous_motion: Motion,
index_scratch: Vec<u32>,
/// Set 1: the vertices again, for the quads' corners; last frame's and the indices into them.
pub table: wgpu::BindGroupLayout,
pub table_group: wgpu::BindGroup,
slots: wgpu::Buffer,
@@ -248,23 +280,32 @@ impl PackEntities {
let stride = SLOT.max(u64::from(
device.limits().min_uniform_buffer_offset_alignment,
));
let entry = |binding| wgpu::BindGroupLayoutEntry {
binding,
visibility: wgpu::ShaderStages::VERTEX,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
};
let table = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Shader pack entity vertices"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
}],
entries: &[entry(0), entry(2), entry(3)],
});
let (vertices, table_group) = Self::vertex_buffer(device, &table, 4096);
let vertices = Self::vertex_buffer(device, 4096);
let previous = Self::vertex_buffer(device, 4096);
let previous_index = Self::index_buffer(device, 4096);
let table_group = Self::table_group(device, &table, &vertices, &previous, &previous_index);
let (slots, group) = Self::slot_buffer(device, layout, 64, stride);
Self {
vertices,
previous,
previous_index,
motion: Motion::default(),
previous_motion: Motion::default(),
index_scratch: Vec::new(),
table,
table_group,
slots,
@@ -278,28 +319,51 @@ impl PackEntities {
}
}
fn vertex_buffer(
device: &wgpu::Device,
table: &wgpu::BindGroupLayout,
vertices: u64,
) -> (wgpu::Buffer, wgpu::BindGroup) {
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
fn vertex_buffer(device: &wgpu::Device, vertices: u64) -> wgpu::Buffer {
device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Shader pack entity vertices"),
size: vertices * std::mem::size_of::<Vertex>() as u64,
usage: wgpu::BufferUsages::VERTEX
| wgpu::BufferUsages::STORAGE
| wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let group = device.create_bind_group(&wgpu::BindGroupDescriptor {
})
}
fn index_buffer(device: &wgpu::Device, vertices: u64) -> wgpu::Buffer {
device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Shader pack entity previous indices"),
size: vertices * 4,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
})
}
fn table_group(
device: &wgpu::Device,
table: &wgpu::BindGroupLayout,
vertices: &wgpu::Buffer,
previous: &wgpu::Buffer,
index: &wgpu::Buffer,
) -> wgpu::BindGroup {
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Shader pack entity vertices"),
layout: table,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: buffer.as_entire_binding(),
}],
});
(buffer, group)
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: vertices.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 2,
resource: previous.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 3,
resource: index.as_entire_binding(),
},
],
})
}
fn slot_buffer(
@@ -359,11 +423,17 @@ impl PackEntities {
shadow: Casters,
rest: &mut Vec<Vertex>,
skin: Option<&super::entity_skin::SkinFrame>,
motion: (&[(u32, u64)], &[u32]),
) {
self.draws.clear();
self.scratch.clear();
self.uniforms.clear();
rest.clear();
// Last frame's vertices stay for `at_velocity`; this frame's go to the other buffer.
std::mem::swap(&mut self.vertices, &mut self.previous);
std::mem::swap(&mut self.motion, &mut self.previous_motion);
self.motion.clear();
let mut copied = Vec::new();
let posed = skin.map_or(0, |s| s.vertices);
if let Some(skin) = skin {
for (span, range, overlay) in skin_runs(skin) {
@@ -388,8 +458,31 @@ impl PackEntities {
let first = spans.first().map_or(entities.len(), |s| s.start as usize);
extend_without_portals(rest, &entities[..first.min(entities.len())]);
let (mut other, mut portal_faces, mut beam_faces) = (Vec::new(), Vec::new(), Vec::new());
let mut without_fluids = Vec::new();
for (span, range) in runs(entities.len(), spans) {
let run_start = range.start as u32;
let whole = &entities[range];
// A native fluid carries sprite-array UVs, not shared-atlas UVs.
// Keep it in its own material even when a shader pack draws the
// model in the same block-entity run. Do not corrupt either UV space.
let whole: &[Vertex] = if whole
.chunks(4)
.any(|q| super::is_native_terrain(q[0].layer))
{
without_fluids.clear();
for quad in whole.chunks(4) {
if super::is_native_terrain(quad[0].layer) {
if !span.hidden {
rest.extend_from_slice(quad);
}
} else {
without_fluids.extend_from_slice(quad);
}
}
&without_fluids
} else {
whole
};
// A block entity's run can hold End portal faces and beams beside plain parts (an End
// gateway: its cube and its beam): each goes to its own program.
// Beacon beams come in a run of their own, at stage NONE (game.rs: no stage of 5.4
@@ -444,6 +537,10 @@ impl PackEntities {
Part::Beam => beam_segments(run, &mut self.scratch),
_ => self.scratch.extend_from_slice(run),
}
// A run copied whole: its vertices keep their order, for `at_velocity`.
if !split {
copied.push((run_start, posed + start, run.len() as u32));
}
if program == DESTROY {
project_cracks(&mut self.scratch[start as usize..], crack.0, crack.1);
}
@@ -468,6 +565,14 @@ impl PackEntities {
if self.draws.is_empty() && posed == 0 {
return;
}
let total = posed + self.scratch.len() as u32;
let posed_marks = if skin.is_some() { motion.1 } else { &[] };
self.motion
.collect(motion.0, posed_marks, entities.len() as u32, posed, &copied);
self.index_scratch.clear();
self.index_scratch.resize(total as usize, NO_PREVIOUS);
self.motion
.link(&self.previous_motion, &mut self.index_scratch);
let vertex_size = std::mem::size_of::<Vertex>() as u64;
let (need_vertices, need_slots) = (
u64::from(posed) + self.scratch.len() as u64,
@@ -476,9 +581,23 @@ impl PackEntities {
let have_vertices = self.vertices.size() / vertex_size;
let have_slots = self.slots.size() / self.stride;
if need_vertices > have_vertices {
(self.vertices, self.table_group) =
Self::vertex_buffer(device, &self.table, need_vertices.next_power_of_two());
self.vertices = Self::vertex_buffer(device, need_vertices.next_power_of_two());
}
if need_vertices * 4 > self.previous_index.size() {
self.previous_index = Self::index_buffer(device, need_vertices.next_power_of_two());
}
queue.write_buffer(
&self.previous_index,
0,
bytemuck::cast_slice(&self.index_scratch),
);
self.table_group = Self::table_group(
device,
&self.table,
&self.vertices,
&self.previous,
&self.previous_index,
);
if need_slots > have_slots {
(self.slots, self.group) =
Self::slot_buffer(device, layout, need_slots.next_power_of_two(), self.stride);
@@ -670,6 +789,87 @@ fn drawn_by_pack(span: &EntitySpan, drawn: [bool; PROGRAMS]) -> Option<usize> {
}
/// The kind of program that draws `span`'s run, when a pack may.
/// A vertex with no copy last frame: `at_velocity` reads zero.
const NO_PREVIOUS: u32 = u32::MAX;
/// Where each entity's vertices are in a frame's pack buffer, to match them with last frame's
/// (`at_velocity`): the indices of each one's GPU-posed vertices, then of its CPU ones.
#[derive(Default)]
struct Motion {
/// Entity id (`Frame::entity_motion`) → (first index in `indices`, posed count, CPU count).
entities: std::collections::HashMap<u64, (u32, u32, u32)>,
indices: Vec<u32>,
}
impl Motion {
fn clear(&mut self) {
self.entities.clear();
self.indices.clear();
}
/// Records each marked entity (`marks`: first CPU vertex and id, each run up to the next;
/// `posed_marks`: the posed count at each mark, empty without posing). A CPU vertex is in
/// the pack buffer where `copied` (first vertex in `Frame::entities`, first index in the
/// buffer, count, in order) has it, else `NO_PREVIOUS`.
fn collect(
&mut self,
marks: &[(u32, u64)],
posed_marks: &[u32],
cpu_total: u32,
posed_total: u32,
copied: &[(u32, u32, u32)],
) {
let at = |e: u32| {
let i = copied.partition_point(|c| c.0 <= e);
match i.checked_sub(1).map(|i| copied[i]) {
Some((first, index, n)) if e < first + n => index + (e - first),
_ => NO_PREVIOUS,
}
};
for (i, &(cpu, id)) in marks.iter().enumerate() {
if id == 0 {
continue;
}
let cpu_end = marks.get(i + 1).map_or(cpu_total, |m| m.0).max(cpu);
let posed = posed_marks.get(i).copied().unwrap_or(0).min(posed_total);
let posed_end = posed_marks
.get(i + 1)
.copied()
.unwrap_or(posed_total)
.clamp(posed, posed_total);
let first = self.indices.len() as u32;
self.indices.extend(posed..posed_end);
self.indices.extend((cpu..cpu_end).map(at));
self.entities
.insert(id, (first, posed_end - posed, cpu_end - cpu));
}
}
/// For each entity drawn last frame with the same posed and CPU counts, each vertex's index
/// last frame, written into `out` (indexed by this frame's buffer).
fn link(&self, previous: &Motion, out: &mut [u32]) {
for (id, &(first, posed, cpu)) in &self.entities {
let Some(&(then, p, c)) = previous.entities.get(id) else {
continue;
};
if (p, c) != (posed, cpu) {
continue;
}
let n = (posed + cpu) as usize;
let now = &self.indices[first as usize..][..n];
let before = &previous.indices[then as usize..][..n];
for (&now, &before) in now.iter().zip(before) {
if now != NO_PREVIOUS
&& before != NO_PREVIOUS
&& let Some(slot) = out.get_mut(now as usize)
{
*slot = before;
}
}
}
}
}
pub fn program_of(span: &EntitySpan) -> Option<usize> {
Some(if span.stage == render_stage("ENTITIES") {
if span.translucent {
@@ -967,6 +1167,83 @@ fn skin_runs(skin: &super::entity_skin::SkinFrame) -> Vec<(EntitySpan, Range<u32
mod gpu_span_tests {
use super::*;
use crate::gfx::entity_skin::{SkinFrame, SkinJob};
#[test]
#[ignore = "requires a Vulkan/Metal/DX12 adapter"]
fn native_fluid_stays_native_while_model_uses_pack() {
let instance = crate::gfx::test_gpu::instance();
let adapter = pollster::block_on(instance.request_adapter(&Default::default())).unwrap();
let (device, queue) =
pollster::block_on(adapter.request_device(&Default::default())).unwrap();
let layout = super::super::pack_draw::DrawUniforms::new(&device);
let mut geometry = PackEntities::new(&device, &layout.layout);
for overlay in [
super::super::FLUID_OVERLAY,
super::super::TERRAIN_MODEL_SOLID_OVERLAY,
super::super::TERRAIN_MODEL_CUTOUT_OVERLAY,
super::super::TERRAIN_MODEL_CUTOUT_MIPPED_OVERLAY,
] {
let mut mixed = vec![
Vertex {
layer: 0,
..bytemuck::Zeroable::zeroed()
};
8
];
for v in &mut mixed[4..] {
v.layer = overlay | 279 << 16;
v.uv = [0.25, 0.75];
v.glint_uv = [17.0, 0.0];
}
let mut rest = Vec::new();
for hidden in [false, true] {
geometry.prepare(
&device,
&queue,
&layout.layout,
&mixed,
&[EntitySpan {
start: 0,
id: 77,
stage: render_stage("BLOCK_ENTITIES"),
hidden,
..Default::default()
}],
[true; PROGRAMS],
([0.; 4], [1; 2]),
Casters {
blocks: true,
..Default::default()
},
&mut rest,
None,
(&[], &[]),
);
assert_eq!(
geometry.scratch.len(),
4,
"the model still uses the shader pack"
);
assert_eq!(geometry.draws.len(), 1);
assert_eq!(
(geometry.draws[0].pack, geometry.draws[0].shadow),
(!hidden, true)
);
if hidden {
assert!(
rest.is_empty(),
"hidden native fluid must not leak into the visible pass"
);
} else {
assert_eq!(
bytemuck::cast_slice::<_, u8>(&rest),
bytemuck::cast_slice::<_, u8>(&mixed[4..]),
"fluid UV/layer payload cannot be sampled from the entity atlas"
);
}
}
}
}
#[test]
fn posed_draws_preserve_subject_ids_overlays_and_hidden_shadow_casters() {
let mut skin = SkinFrame {
@@ -1079,6 +1356,7 @@ mod gpu_span_tests {
},
&mut rest,
None,
(&[], &[]),
);
assert_eq!(
(geometry.draws.iter())
@@ -1160,6 +1438,7 @@ mod gpu_span_tests {
shadows,
&mut rest,
Some(&skin),
(&[], &[]),
);
assert_eq!(rest.len(), 4, "untagged CPU prefix stays in Horizon's draw");
assert_eq!(geometry.scratch.len(), 4, "only CPU geometry is uploaded");
@@ -1203,6 +1482,7 @@ mod gpu_span_tests {
shadows,
&mut rest,
None,
(&[], &[]),
);
assert!(geometry.draws.is_empty());
assert_eq!(
@@ -1230,8 +1510,166 @@ mod gpu_span_tests {
Casters::default(),
&mut rest,
Some(&skin),
(&[], &[]),
);
assert!(geometry.draws.is_empty());
assert!(geometry.skin_target().size() >= 5000 * 44);
}
}
#[cfg(test)]
mod velocity_tests {
use super::{Motion, NO_PREVIOUS};
/// Two mobs (ids 7 and 9) and a run of unmarked geometry (id 0); `copied` places CPU
/// vertices `first..first + n` at buffer index `at`.
fn frame(order: [(u64, u32); 2], copied: &[(u32, u32, u32)]) -> Motion {
let mut marks = Vec::new();
let mut cpu = 0;
for (id, n) in order {
marks.push((cpu, id));
cpu += n;
}
marks.push((cpu, 0));
let mut m = Motion::default();
m.collect(&marks, &[], cpu + 2, 0, copied);
m
}
/// The same mob's vertices find themselves however the frame's order and the buffer's
/// layout moved between frames; geometry of no entity, and vertices not in the buffer,
/// find nothing.
#[test]
fn each_vertex_finds_its_copy_from_last_frame() {
// Last frame: mob 7 (4 vertices) then mob 9 (2), copied whole from buffer index 10.
let before = frame([(7, 4), (9, 2)], &[(0, 10, 8)]);
// This frame: mob 9 first, then mob 7, at buffer index 0; 9's last vertex not drawn.
let now = frame([(9, 2), (7, 4)], &[(0, 0, 1), (2, 1, 6)]);
let mut out = vec![NO_PREVIOUS; 8];
now.link(&before, &mut out);
// 9's first vertex (buffer 0) was 10 + 4; 7's four (buffer 1..5) were 10..14.
assert_eq!(
out,
[14, 10, 11, 12, 13, NO_PREVIOUS, NO_PREVIOUS, NO_PREVIOUS]
);
}
/// A mob whose vertex count changed (armor put on, an item picked up) gets no velocity
/// this frame rather than the wrong vertices'.
#[test]
fn a_changed_model_gets_none() {
let before = frame([(7, 4), (9, 2)], &[(0, 0, 8)]);
let now = frame([(7, 6), (9, 2)], &[(0, 0, 10)]);
let mut out = vec![NO_PREVIOUS; 10];
now.link(&before, &mut out);
assert!(out[..6].iter().all(|&i| i == NO_PREVIOUS));
assert_eq!(out[6..8], [4, 5]);
}
/// GPU-posed vertices come first in the buffer: each mob's posed part is matched by its
/// own posed count, then its CPU part.
#[test]
fn posed_and_cpu_parts_match_separately() {
let marks = [(0, 7), (2, 9), (2, 0)];
let mut before = Motion::default();
// Mob 7: 3 posed (0..3) and 2 CPU; mob 9: 1 posed (3), no CPU.
before.collect(&marks, &[0, 3, 4], 2, 4, &[(0, 4, 2)]);
let mut now = Motion::default();
// Mob 9 posed first this frame (0), then mob 7 (1..4).
now.collect(&[(0, 9), (0, 7), (2, 0)], &[0, 1, 4], 2, 4, &[(0, 4, 2)]);
let mut out = vec![NO_PREVIOUS; 6];
now.link(&before, &mut out);
assert_eq!(out, [3, 0, 1, 2, 4, 5]);
}
}
#[cfg(test)]
mod velocity_translate_tests {
/// A pack that reads `at_velocity` translates with the entity inputs (which read the
/// previous view and last frame's vertices) and with the hand's (which read zero).
#[test]
fn a_pack_reading_at_velocity_translates_for_entities_and_the_hand() {
let dir = std::env::temp_dir().join(format!("hz-at-velocity-{}", std::process::id()));
let shaders = dir.join("shaders");
std::fs::create_dir_all(&shaders).unwrap();
std::fs::write(
shaders.join("gbuffers_entities.vsh"),
"#version 120\nattribute vec3 at_velocity;\nvarying vec3 v;\n\
void main() {\n gl_Position = ftransform();\n v = at_velocity;\n}\n",
)
.unwrap();
std::fs::write(
shaders.join("gbuffers_entities.fsh"),
"#version 120\nvarying vec3 v;\nvoid main() {\n gl_FragData[0] = vec4(v, 1.0);\n}\n",
)
.unwrap();
let pack = horizon_shaders::Pack::open(&dir).unwrap();
let m = horizon_shaders::Macros::standard();
let vsh = horizon_shaders::preprocess(&pack, "gbuffers_entities.vsh", &m);
let fsh = horizon_shaders::preprocess(&pack, "gbuffers_entities.fsh", &m);
for (who, inputs) in [
("entities", super::inputs()),
("hand", super::super::pack_hand::inputs()),
] {
let p = horizon_shaders::translate_gbuffers(
&vsh,
&fsh,
horizon_shaders::Target::Buffers,
&inputs,
)
.unwrap_or_else(|e| panic!("{who}: {e:?}"));
let previous = p
.interface
.uniforms
.iter()
.any(|u| u.name == "gbufferPreviousModelView");
assert_eq!(
previous,
who == "entities",
"{who}: previous view in the block"
);
}
let _ = std::fs::remove_dir_all(&dir);
}
}
#[cfg(test)]
mod tier_translate_tests {
/// A pack's entity and hand programs may read `horizonTier`, the overlay's HDR tier.
#[test]
fn a_pack_reading_horizon_tier_translates_for_entities_and_the_hand() {
let dir = std::env::temp_dir().join(format!("hz-entity-tier-{}", std::process::id()));
let shaders = dir.join("shaders");
std::fs::create_dir_all(&shaders).unwrap();
std::fs::write(
shaders.join("gbuffers_entities.vsh"),
"#version 120\nattribute float horizonTier;\nvarying float t;\n\
void main() {\n gl_Position = ftransform();\n t = horizonTier;\n}\n",
)
.unwrap();
std::fs::write(
shaders.join("gbuffers_entities.fsh"),
"#version 120\nvarying float t;\nvoid main() {\n gl_FragData[0] = vec4(t);\n}\n",
)
.unwrap();
let pack = horizon_shaders::Pack::open(&dir).unwrap();
let m = horizon_shaders::Macros::standard();
let vsh = horizon_shaders::preprocess(&pack, "gbuffers_entities.vsh", &m);
let fsh = horizon_shaders::preprocess(&pack, "gbuffers_entities.fsh", &m);
for (who, inputs) in [
("entities", super::inputs()),
("hand", super::super::pack_hand::inputs()),
] {
let attribute = inputs.attributes.iter().find(|(n, _)| n == "horizonTier");
assert!(attribute.is_some(), "{who}");
horizon_shaders::translate_gbuffers(
&vsh,
&fsh,
horizon_shaders::Target::Buffers,
&inputs,
)
.unwrap_or_else(|e| panic!("{who}: {e:?}"));
}
let _ = std::fs::remove_dir_all(&dir);
}
}
+119 -11
View File
@@ -19,6 +19,13 @@ const SLOT: u64 = 32;
/// What `gbuffers_hand` reads: the entity inputs, and the hand's projection from set 1.
pub fn inputs() -> horizon_shaders::GbuffersInputs {
let mut inputs = super::pack_entities::inputs();
// No last frame's hand to measure from: the hand's `at_velocity` reads zero (and its set 1
// has no previous vertices).
for (name, value) in &mut inputs.attributes {
if name == "at_velocity" {
*value = "vec3(0.0)".to_owned();
}
}
// Iris 1.11.7 declares `currentRenderedItemId` in the hand's programs (observed).
inputs.item_id_declared = true;
inputs.declarations.push_str(
@@ -54,8 +61,12 @@ pub struct PackHand {
slots: wgpu::Buffer,
pub slot_group: wgpu::BindGroup,
stride: u64,
/// This frame's runs: first quad, quads, and the dynamic offset of their slot.
/// This frame's runs: first quad, quads, and the dynamic offset of their slot. The opaque
/// quads come first in the buffer, then the translucent ones (`translucent_runs`), which a
/// pack's `gbuffers_hand_water` draws after deferred.
pub runs: Vec<(u32, u32, u32)>,
pub translucent_runs: Vec<(u32, u32, u32)>,
order: Vec<Vertex>,
}
impl PackHand {
@@ -120,6 +131,8 @@ impl PackHand {
slot_group,
stride,
runs: Vec::new(),
translucent_runs: Vec::new(),
order: Vec::new(),
}
}
@@ -166,17 +179,25 @@ impl PackHand {
) {
self.quads = (hand.len() / 4) as u32;
self.runs.clear();
self.translucent_runs.clear();
if self.quads == 0 {
return;
}
let split = split(hand, items);
self.order = split.order;
let mut contents = Vec::new();
for (start, quads, id) in runs(self.quads, items) {
let slot = (self.runs.len() as u64).min(SLOTS - 1);
if slot as usize == self.runs.len() {
contents.extend_from_slice(&draw_slot(id));
for (i, &(first, quads, id, translucent)) in split.runs.iter().enumerate() {
let slot = (i as u64).min(SLOTS - 1);
if slot == i as u64 {
contents.extend_from_slice(&draw_slot(id, translucent));
contents.resize(((slot + 1) * self.stride) as usize, 0);
}
self.runs.push((start, quads, (slot * self.stride) as u32));
let run = (first, quads, (slot * self.stride) as u32);
if translucent {
self.translucent_runs.push(run);
} else {
self.runs.push(run);
}
}
queue.write_buffer(&self.slots, 0, &contents);
if self.quads > self.capacity {
@@ -185,7 +206,7 @@ impl PackHand {
Self::vertex_buffer(device, &self.layout, &self.uniform, self.capacity);
self.indices = super::pack_entities::quad_indices(device, self.capacity);
}
queue.write_buffer(&self.vertices, 0, bytemuck::cast_slice(hand));
queue.write_buffer(&self.vertices, 0, bytemuck::cast_slice(&self.order));
let matrices = [projection, projection.inverse()].map(|m| m.to_cols_array());
queue.write_buffer(&self.uniform, 0, bytemuck::cast_slice(&matrices));
}
@@ -198,11 +219,62 @@ impl PackHand {
/// The hand's runs a frame keeps slots for.
const SLOTS: u64 = 8;
/// `HzDraw` of a hand run: `renderStage` the solid hand's, `blockEntityId` -1 as outside block
/// entities, and `currentRenderedItemId`.
fn draw_slot(id: i32) -> [u8; SLOT as usize] {
/// The hand's quads reordered, opaque first and translucent after (each in their order), and
/// their runs: first quad, quads, `currentRenderedItemId`, translucent.
struct Split {
order: Vec<Vertex>,
runs: Vec<(u32, u32, i32, bool)>,
}
fn split(hand: &[Vertex], items: &[(u32, i32)]) -> Split {
let quads = (hand.len() / 4) as u32;
let mut ids = vec![0; quads as usize];
for (start, n, id) in runs(quads, items) {
ids[start as usize..(start + n) as usize].fill(id);
}
let kind = |q: usize| translucent_quad(&hand[q * 4..q * 4 + 4]);
let opaque: Vec<usize> = (0..quads as usize).filter(|&q| !kind(q)).collect();
let clear: Vec<usize> = (0..quads as usize).filter(|&q| kind(q)).collect();
let mut out = Split {
order: Vec::with_capacity(hand.len()),
runs: Vec::new(),
};
for &q in opaque.iter().chain(&clear) {
out.order.extend_from_slice(&hand[q * 4..q * 4 + 4]);
}
for (translucent, list, base) in [(false, &opaque, 0), (true, &clear, opaque.len())] {
let mut i = 0;
while i < list.len() {
let id = ids[list[i]];
let mut n = 1;
while i + n < list.len() && ids[list[i + n]] == id {
n += 1;
}
out.runs
.push(((base + i) as u32, n as u32, id, translucent));
i += n;
}
}
out
}
/// A held item's quad drawn from a translucent sheet (`item_render`: its overlay layer).
fn translucent_quad(quad: &[Vertex]) -> bool {
matches!(
quad[0].layer & 0xffff,
super::TRANSLUCENT_ITEM_OVERLAY | super::TRANSLUCENT_GLINT_ITEM_OVERLAY
)
}
/// `HzDraw` of a hand run: `renderStage` the solid or translucent hand's, `blockEntityId` -1 as
/// outside block entities, and `currentRenderedItemId`.
fn draw_slot(id: i32, translucent: bool) -> [u8; SLOT as usize] {
let mut bytes = [0u8; SLOT as usize];
let stage = horizon_shaders::render_stage("HAND_SOLID") as i32;
let stage = horizon_shaders::render_stage(if translucent {
"HAND_TRANSLUCENT"
} else {
"HAND_SOLID"
}) as i32;
bytes[0..4].copy_from_slice(&stage.to_le_bytes());
bytes[8..12].copy_from_slice(&(-1i32).to_le_bytes());
bytes[12..16].copy_from_slice(&id.to_le_bytes());
@@ -233,6 +305,42 @@ fn runs(quads: u32, items: &[(u32, i32)]) -> Vec<(u32, u32, i32)> {
mod tests {
use super::*;
/// The arm, then an item whose middle quad is translucent, then a second item: the opaque
/// quads keep their order first, the translucent one comes after them, and each run keeps
/// its item's id.
#[test]
fn translucent_quads_go_after_the_opaque_ones() {
let quad = |layer: u32, tag: f32| {
[Vertex {
layer,
pos: [tag, 0.0, 0.0],
..bytemuck::Zeroable::zeroed()
}; 4]
};
let glass = super::super::TRANSLUCENT_ITEM_OVERLAY;
let hand: Vec<Vertex> = [
quad(0, 0.),
quad(0, 1.),
quad(glass, 2.),
quad(0, 3.),
quad(0, 4.),
]
.concat();
// The arm is quad 0; item 7 quads 1..4; item 9 quad 4.
let split = split(&hand, &[(4, 7), (16, 9)]);
let order: Vec<f32> = split.order.chunks(4).map(|q| q[0].pos[0]).collect();
assert_eq!(order, [0., 1., 3., 4., 2.]);
assert_eq!(
split.runs,
[
(0, 1, 0, false),
(1, 2, 7, false),
(3, 1, 9, false),
(4, 1, 7, true)
]
);
}
/// Each item's quads draw with its id; geometry before the first item is the arm's (0).
#[test]
fn runs_split_the_hand_by_item() {
@@ -23,6 +23,13 @@ pub(super) struct PackImage {
}
impl PackImage {
pub(super) fn reset_history(&mut self) {
self.view = None;
self.copy = None;
self.before = None;
self.made_for = [0; 2];
}
pub(super) fn sample_type(&self, features: wgpu::Features) -> wgpu::TextureSampleType {
self.format
.sample_type(None, Some(features))
@@ -30,11 +37,12 @@ impl PackImage {
}
}
/// The images `image.*` declares.
pub(super) fn images(defs: Vec<CustomImage>) -> Vec<PackImage> {
/// The images `image.*` declares; `packed_float`: this device stores into `Rg11b10Ufloat`
/// (`format`).
pub(super) fn images(defs: Vec<CustomImage>, packed_float: bool) -> Vec<PackImage> {
defs.into_iter()
.map(|def| PackImage {
format: format(def.format),
format: format(def.format, packed_float),
dimension: if def.is_1d() {
wgpu::TextureViewDimension::D1
} else if def.is_3d() {
@@ -63,7 +71,10 @@ fn texture_dimension(view: wgpu::TextureViewDimension) -> wgpu::TextureDimension
}
/// The wgpu format of an image's internal format: a storage format of the same channels.
pub(super) fn format(f: Format) -> wgpu::TextureFormat {
/// `R11F_G11F_B10F` is itself where the device stores into it (`packed_float`): no alpha, so it
/// reads 1 there, as Iris 1.11.7 read it with a probe pack of ours (#10490); elsewhere an
/// `Rgba16Float` stand-in, which keeps an alpha.
pub(super) fn format(f: Format, packed_float: bool) -> wgpu::TextureFormat {
use wgpu::TextureFormat as T;
let pick = |r, rg, rgba| match f.channels {
1 => r,
@@ -78,6 +89,7 @@ pub(super) fn format(f: Format) -> wgpu::TextureFormat {
(Kind::Uint, 16) => pick(T::R16Uint, T::Rg16Uint, T::Rgba16Uint),
(Kind::Uint, _) => pick(T::R32Uint, T::Rg32Uint, T::Rgba32Uint),
(Kind::Float, 32) => pick(T::R32Float, T::Rg32Float, T::Rgba32Float),
(Kind::Float, 11) if packed_float => T::Rg11b10Ufloat,
(Kind::Float, _) => pick(T::R16Float, T::Rg16Float, T::Rgba16Float),
(Kind::Snorm, _) => pick(T::R8Snorm, T::Rg8Snorm, T::Rgba8Snorm),
(Kind::Unorm, 16) => pick(T::R16Unorm, T::Rg16Unorm, T::Rgba16Unorm),
@@ -87,8 +99,12 @@ pub(super) fn format(f: Format) -> wgpu::TextureFormat {
/// Whether this device can make `def`: its format's features, and a size in pixels within the
/// device's limits.
pub(super) fn makeable(device: &wgpu::Device, def: &CustomImage) -> Result<(), String> {
let format = format(def.format);
pub(super) fn makeable(
device: &wgpu::Device,
def: &CustomImage,
packed_float: bool,
) -> Result<(), String> {
let format = format(def.format, packed_float);
let missing = format.required_features() - device.features();
if !missing.is_empty() {
return Err(format!(
@@ -307,6 +323,11 @@ impl StandIn {
}
}
pub(super) fn reset_stand_ins(device: &wgpu::Device, images: &mut Vec<StandIn>) {
let wanted = images.iter().map(|i| (i.format, i.dimension)).collect();
*images = stand_ins(device, wanted);
}
/// One stand-in for each format and dimension of `wanted`.
pub(super) fn stand_ins(
device: &wgpu::Device,
@@ -383,6 +404,12 @@ pub(super) struct Buffers {
}
impl Buffers {
pub(super) fn reset_history(&mut self) {
self.group = None;
self.buffers.clear();
self.made_for = [0; 2];
}
/// The layout of `objects`, bound in `stages`; each buffer is at least its entry in
/// `minimums` long.
pub(super) fn new(
@@ -149,6 +149,7 @@ void main() {
post,
wgpu::TextureFormat::Rgba8Unorm,
world,
&Default::default(),
)
.expect("the pack runs");
let mut pass = post
File diff suppressed because it is too large. Load diff
@@ -99,6 +99,7 @@ fn the_atlas_blends_its_two_nearest_levels() {
weather: false,
hand: false,
},
&Default::default(),
)
.expect("the pack runs");
for p in gpu.render(&mut post, [4, 4]) {
Loaded 100 of 2002 files, more files were not shown because too many files have changed in this diff. Show more