Compare commits

..
Author SHA1 Message Date
Ludovic Marie 2a8c60b6df Merge pull request #3351 from horizon-rust/draft/remove-wasm-mods-crates
🤖 Agent: WebAssembly removal 2/4 (crates: runtime, SDK, official WebAssembly mods), on #3350, merged up to date with main c7065027b. Workspace compiles (--all-targets --locked). Merge after #3350.
2026-10-30 00:56:47 +01:00
levzzz 4693954aa1 Merge pull request #4016 from d3grando/fix-3393-eggs
Thrown eggs and snowballs turn with the camera, no flip looking straight up
2026-10-30 04:56:16 +05:00
Ludovic Marie 0208c75ef4 Merge pull request #3979 from DUzzL/fix/issue-3975-owners-doc
🤖 Agent: Ludovic's AI agent (Claude), triage. Docs only: a server.properties → server.toml page for owners, linked from docs/README. verify is green. Queued.
2026-10-30 00:51:53 +01:00
Ludovic Marie 71527fa938 Merge remote-tracking branch 'origin/main' into upd-4016
# Conflicts:
#	crates/horizon-client/src/game/creatures.rs
2026-10-30 00:42:49 +01:00
levzzz d6adbe1cb5 Merge pull request #4057 from degepie/fix/issue-3422-shield-slowdown
A raised shield keeps a sprint from starting, as in vanilla (#3422)
2026-10-30 04:37:03 +05:00
levzzz bc93f7da59 Merge pull request #4063 from arismoko/fix/command-loot-name-parity
Preserve item modifier custom name components
2026-10-30 04:36:46 +05:00
levzzz 8b843008eb Merge pull request #3776 from GIGABAIT93/fix/text-display-passengers
Text displays ride their vehicles: an NPC's name sits over its head
2026-10-30 04:36:36 +05:00
levzzz 8bd015284b Merge pull request #3605 from deloskiytbackup/fix/pumpkin-head-rendering
Render: draw carved pumpkins and mob heads on player and humanoid heads
2026-10-30 04:36:20 +05:00
levzzz b97e677cbc Merge pull request #3998 from d3grando/fix-3829
Players and heads keep their skins on busy servers
2026-10-30 04:36:04 +05:00
levzzz 117fbcd1aa Merge pull request #3922 from GIGABAIT93/feat/remote-player-scale
Other players and mannequins take their minecraft:scale
2026-10-30 04:35:35 +05:00
levzzz 5bafa93fa0 Merge pull request #4020 from DenisHumen/fix/sleep-camera
Sleeping: lie 0.125 above the bed's top, as vanilla does
2026-10-30 04:35:18 +05:00
levzzz ae4660dfdf Merge pull request #4021 from phexuss/fix/station-scrollbar-drag
Drag the stonecutter and villager trade scrollers
2026-10-30 04:35:02 +05:00
levzzz 141b1e0cf5 Merge pull request #4026 from JustKarkat/test/text-display-shadow-parity
Add shadow_color coverage to the text display render parity test
2026-10-30 04:34:45 +05:00
levzzz bf4d46df7e Merge pull request #3594 from d3grando/fix-3503
Paintings: the item's variant is placed and named in its tooltip
2026-10-30 04:34:28 +05:00
levzzz 4bf451bd2d Merge pull request #3611 from degepie/fix/issue-3027-adventure-ghost-blocks
Adventure: don't predict breaking a block the player may not break (#3027)
2026-10-30 04:33:59 +05:00
levzzz 4c8178f4fd Merge pull request #3620 from iLostName/pearl-through-end-crystal
ender pearl: one that hits an end crystal still takes its thrower there
2026-10-30 04:33:43 +05:00
levzzz f38db18183 Merge pull request #3624 from deloskiytbackup/fix/tool-break-sound
Audio: play break sound and track stats when tools break while mining
2026-10-30 04:33:26 +05:00
levzzz 919c6803c5 Merge pull request #3647 from iLostName/elder-curse-skips-cursed
elder guardian: a player already cursed isn't cursed again every minute
2026-10-30 04:33:10 +05:00
levzzz abce5d7864 Merge pull request #4044 from DenisHumen/fix/armor-stand-swing
Armor stands: placing one swings the hand, as vanilla does
2026-10-30 04:32:54 +05:00
levzzz 54ac508d90 Merge pull request #3686 from k3yh/fix/spawn-egg-entity-data
An op's spawn egg sets its entity data on a spawner
2026-10-30 04:32:37 +05:00
levzzz 2f6babf098 Merge pull request #3900 from DUzzL/fix/issue-3885-function-tag-dedupe
Fix O(n^2) function-tag resolution and tag cycles
2026-10-30 04:32:20 +05:00
levzzz df051f7089 Merge pull request #3906 from DUzzL/fix/issue-3889-explosion-drop-merge
Explosion drops merge by item, components and max stack size
2026-10-30 04:32:04 +05:00
levzzz 706fc7aa13 Merge pull request #3907 from DUzzL/fix/issue-3884-recipe-count-range
Reject recipe result counts outside 1..=99 (Fixes #3884)
2026-10-30 04:31:55 +05:00
levzzz db0a98d645 Merge pull request #4017 from DUzzL/fix/issue-4013-blocked-fireball-no-ignite
Keep a shield-blocked small fireball from igniting its target
2026-10-30 04:31:26 +05:00
levzzz b8eb63db68 Merge pull request #4054 from DUzzL/fix/issue-4038-enderman-arrow-teleport
Arrows fly on when an enderman teleports away from them
2026-10-30 04:31:10 +05:00
levzzz ce18dafd4e Merge pull request #3928 from xFedeT/fix/riptide-with-elytra
Riptide in rain works on a server's client: isInRain from its own chunks and weather
2026-10-30 04:30:26 +05:00
levzzz 971af532f9 Merge pull request #3991 from d3grando/fix-3393-darkness
Darkness dims the screen on servers and in single player
2026-10-30 04:30:10 +05:00
levzzz bdf9cb8eb0 Merge pull request #4029 from d3grando/fix-3810
No rain splashes or rain sound under a high roof
2026-10-30 04:29:42 +05:00
levzzz 1b90753fab Merge pull request #4033 from d3grando/fix-2780-wolfbeg
Wolves tilt their head at food they want
2026-10-30 04:29:26 +05:00
levzzz b890329854 Merge pull request #4035 from d3grando/fix-2893
Ender Dragon wings push the player only when their hit lands
2026-10-30 04:29:09 +05:00
levzzz e618347e38 Merge pull request #4036 from d3grando/fix-2780-babypet
Food grows a tamed baby cat or wolf instead of making it sit
2026-10-30 04:28:53 +05:00
levzzz dbf445db2d Merge pull request #4053 from d3grando/fix-3616
Your body keeps up with the camera at Levitation 255
2026-10-30 04:28:37 +05:00
levzzz bf702cfe22 Merge pull request #3853 from DUzzL/fix/armor-stand-elytra-3843
Draw a worn elytra on armour stands
2026-10-30 04:28:20 +05:00
levzzz 03805b8eeb Merge pull request #3965 from GIGABAIT93/fix/shadow-scale
Entity shadows follow the entity's scale and age
2026-10-30 04:28:03 +05:00
levzzz c081525853 Merge pull request #3959 from DenisHumen/fix/golem-pattern-orientations
Golems: a snow or iron golem builds in any orientation, as vanilla does
2026-10-30 04:27:46 +05:00
levzzz 6a6acac27c Merge pull request #4022 from d3grando/fix-3098
Withers and golems build in any orientation, like vanilla
2026-10-30 04:27:30 +05:00
levzzz c5e9265bf0 Merge pull request #3797 from Bloodulon/pr/packs-overlay
Packs: read deflate entries to their end; skip a bad entry
2026-10-30 04:27:14 +05:00
levzzz 421834b82e Merge pull request #3799 from DenisHumen/fix/bucket-eye-block
Buckets and bottles pick up the water the eyes are in
2026-10-30 04:26:58 +05:00
levzzz a98cf88458 Merge pull request #3865 from elythepreciouscat/fix/server-disconnect-saving-screen
Disconnect: do not show saving world screen on external server
2026-10-30 04:26:41 +05:00
levzzz 79aca92478 Merge pull request #3867 from bxnjxmin27/fix/3689-screenshot-while-chat
Screenshots: the key works with chat, a sign or a book open
2026-10-30 04:26:25 +05:00
levzzz 48d29b20f1 Merge pull request #3873 from DUzzL/fix/issue-3863-breed-advancement
Fix breeding advancement and statistic on the server thread
2026-10-30 04:26:09 +05:00
levzzz 0229716f22 Merge pull request #3895 from ccclosed/fix/mob-effect-attribute-modifiers
Mobs: apply Weakness, Slowness and Health Boost attribute modifiers
2026-10-30 04:25:52 +05:00
levzzz a3809490f6 Merge pull request #3898 from Bloodulon/pr/item-definitions
Client: load item model definitions from nested folders
2026-10-30 04:25:36 +05:00
levzzz 8e49a414c2 Merge pull request #3857 from DUzzL/fix/issue-3854-lever-piston
Fix lever dropping when placed on a piston's back
2026-10-30 04:24:46 +05:00
levzzz 33e56c7a56 Merge pull request #3851 from DUzzL/fix/axolotl-nbt-glowing-test-3840
Fix axolotl NBT test: query glowing effect by namespaced id
2026-10-30 04:24:18 +05:00
Ludovic MarieandClaude Opus 5.5 37a1e8859f Merge main: RemoteTextDisplay::new takes what the display shows
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 00:18:16 +01:00
Ludovic Marie 6d666c30e2 Merge remote-tracking branch 'origin/main' into upd-3776 2026-10-30 00:16:56 +01:00
Ludovic Marie 4bd1313677 Merge pull request #4049 from horizon-rust/fix/launcher-java-home-version
🤖 Agent: A Java instance's Home no longer shows Horizon's version picker or nightly updates; its chip names its own version and loader. Checked in the real launcher (scratch homes, Fabric Java instance and Horizon instance), launcher fmt/clippy/278 tests pass on this head.
2026-10-30 00:14:55 +01:00
Ludovic Marie 6eeeb454f5 Merge pull request #3805 from DenisHumen/fix/rea-vanilla-windows
🤖 Agent: Ludovic's AI agent (Claude), triage. Tooling only: `npx` is resolved by path (`npx.cmd` on Windows), with a clear error when it's missing. verify is green. Queued.
2026-10-30 00:14:52 +01:00
arismoko 8eb2ecfa76 Preserve loot name components in item commands 2026-10-29 18:10:27 -05:00
Ludovic Marie c3071c7b61 Merge pull request #3575 from d3grando/fix-3153
🤖 Agent: pr-triage-1 (Ludovic's AI agent, Claude), not Ludovic himself. Thanks for the fix: with the wither held in place the scenario now matches vanilla. On this head (f5de793b3) merged into current main, the harnesses this change can reach pass: `command_exec`'s wither spawn (4/4), wither breaks (9/9) and ender dragon (5/5) groups match vanilla. Its 2 remaining mismatches are `/data get` printing a `custom_name` as a component; they come from main and this PR doesn't touch them. fmt, clippy and the horizon-client tests pass too. Queued for merge.
2026-10-30 00:09:06 +01:00
DUzzL 80cb00a494 Arrows fly on when an enderman teleports away from them 2026-10-30 00:08:47 +01:00
Ludovic MarieandClaude Opus 5.5 fcea81151c Launcher: a Java instance's Home shows its own game, not Horizon's versions
Home drew Horizon's version picker ("No version" or a nightly) beside a Java instance's Play,
and offered Horizon's next nightly in its hero. A Java instance now shows neither: its chip
says its Minecraft version and loader ("Minecraft 1.21.1 · Fabric 0.16.9").

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 00:05:05 +01:00
d3grandoandClaude Opus 5.5 14d453048e Your body keeps up with the camera at Levitation 255
The third-person model dropped its interpolation whenever it moved more
than 10 blocks in a tick, while the camera kept lerping from the old
position. Levitation 254/255 rises about 12 blocks a tick, so the body
sat a whole tick ahead of the camera and left the frame.

The model now lerps from the player's own old position, as vanilla's
EntityRenderer.extractRenderState does from Entity.xOld/yOld/zOld, the
same one Camera.setup uses. Teleports and respawns already reset that
old position, so they still jump without a sweep.

Fixes #3616

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

armor_stand_spawn holds the checks both share.

Fixes #4024

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

Fixes #3422

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
2026-10-30 03:46:46 +05:00
d3grandoandClaude Opus 5.5 450ff69b4e Food grows a tamed baby cat or wolf instead of making it sit
The owner's sit toggle on tamed wolves and cats only stood aside for food
an adult could breed with (age 0, not in love), so fish or meat on a tamed
baby flipped it between sitting and standing and it never grew.

Wolf.mobInteract and Cat.mobInteract call TamableAnimal/Animal.mobInteract
first and toggle setOrderedToSit only when that did not consume the click;
Animal.mobInteract takes the food from an adult that canFallInLove at age 0
or from a baby that canAgeUp (AgeableMob.canAgeUp: isBaby and not
isAgeLocked), growing it by getSpeedUpSecondsWhenFeeding.

mobs::ai::takes_food is that test, shared by the sit guards and
feed_animal (which now also leaves age-locked babies alone, as canAgeUp
does).

Part of #2780.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 05:46:18 +07:00
Ludovic Marie 49aa9dd00b Merge remote-tracking branch 'origin/main' into upd-3696
# Conflicts:
#	tools/test_mob_ai_parity.py
2026-10-29 23:46:09 +01:00
d3grandoandClaude Opus 5.5 b3fdebcb92 Ender Dragon wings push the player only when their hit lands
EnderDragon.knockBack pushes the server's copy of every player in a wing's
zone each tick, but a player's own client only receives its velocity when a
hurt marks it (LivingEntity.hurtServer's markHurt, sent by
ServerEntity.sendChanges). Horizon added the push straight to the player's
velocity on every tick they stayed in the zone, perched dragon or not, so
players were flung several blocks per tick around the exit portal.

The push now goes out only with a 5-damage wing hit that lands outside the
hurt cooldown: never under a perched dragon (isSitting), and not within 2
ticks of a mob's hurt (getLastHurtByMobTimestamp() < tickCount - 2). The
vertical push is 0.2f widened, as in vanilla.

Fixes #2893

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 05:45:42 +07:00
d3grandoandClaude Opus 5.5 3ffaf67e22 Wolves tilt their head at food they want
BegGoal sets Wolf.DATA_INTERESTED_ID on the server, but the LAN host
never sent entry 20 and a client never read it, so every window
(single player joins its server thread the same way) kept the head
straight. Send the flag with its default, read it into the remote
wolf, and run the part of Wolf.tick both sides run on the client:
interestedAngle eases 0.4 of the way toward the flag each tick, which
getHeadRollAngle turns into the head's roll.

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

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 23:41:28 +01:00
Ludovic Marie bd665f28ad Merge pull request #3782 from xFedeT/fix/armor-stand-bubble-column
🤖 Agent: Ludovic's AI agent (Claude), triage. `travelInWater` (the 0.8 slowdown, then `getFluidFallingAdjustedMovement` with gravity/16) and the bubble column push for armour stands match vanilla. The per-tick water scan only covers the stand's own box. CI parity is green at this head. Queued.
2026-10-29 23:40:26 +01:00
JustKarkat 2982f6eb9a Add shadow_color coverage to the text display render parity test
Fixes #3531's render side landed in #3341; this adds the shadow cases its
branch carried for tools/test_text_display_render_parity.py so the
packed ARGB ints, the [r, g, b, a] lists and NO_SHADOW=0 are checked
against the vanilla TextDisplayRenderer oracle.
2026-10-29 17:40:25 -05:00
Ludovic Marie 499a08ba81 Merge pull request #3690 from xFedeT/fix/fish-random-swimming-parity
🤖 Agent: Ludovic's AI agent (Claude), triage. `WaterAnimal` and `AgeableWaterCreature` set the WATER malus to 0, and FishMoveControl's yaw uses `Mth.atan2` in degrees. CI parity (mob_ai with new fish cases) is green at this head. Queued.
2026-10-29 23:40:22 +01:00
Ludovic Marie 7f54b8abb4 Merge pull request #2705 from senjinthedragon/hdr-tiers
🤖 Agent: pr-triage-1: the parity run was cancelled (not failed); I reran it on this same head.
2026-10-29 23:36:18 +01:00
Ludovic Marie 573140cc5a Merge pull request #3639 from xFedeT/fix/wither-immune-to-wither-friends
🤖 Agent: Ludovic's AI agent (Claude), triage. `WitherBoss.hurtServer` ignores damage whose source entity is in `#wither_friends` (the undead), for explosions and wither skulls alike. CI parity is green at this head. Queued.
2026-10-29 23:35:00 +01:00
Ludovic Marie bdd6275570 Merge pull request #3722 from vvnikita74/fix/no-day-sleep-thunder-flag
🤖 Agent: Ludovic's AI agent (Claude), triage. Sleeping now follows the sky darkening (`skyDarken >= 4`, `isDarkOutside`) instead of a fixed time window or the thunder flag alone, so a thunderstorm at noon allows it and rain alone doesn't. CI parity is green at this head. Queued.
2026-10-29 23:34:55 +01:00
Ludovic Marie 588c0ea77a Merge pull request #3779 from leifiyoo/fix/powered-minecart-speed
🤖 Agent: Ludovic's AI agent (Claude), triage. `MinecartFurnace.push` is a Vec3, and `calculateNewPushAlong` (`projectedOn(...).normalize().scale(push.length())`) keeps its 3D length; saving drops y as `addAdditionalSaveData` does. CI parity is green at this head. Queued.
2026-10-29 23:34:52 +01:00
Ludovic Marie 3f08e59ad2 Merge pull request #3767 from leifiyoo/fix/template-block-entity
🤖 Agent: Ludovic's AI agent (Claude), triage. Matches `StructureTemplate.placeInWorld`, which sets a barrier wherever saved data goes, so the old block entity always goes and the template's data always loads. CI parity is green at this head. Queued.
2026-10-29 23:34:48 +01:00
Ludovic Marie 518ecfb511 Merge pull request #3750 from leifiyoo/fix/chunk-block-entity-replace
🤖 Agent: Ludovic's AI agent (Claude), triage. Read the whole diff: a loaded chunk's block entity replacing one at the same position keeps its ticker slot only if the new state ticks, as `LevelChunk.setBlockEntity` + `updateBlockEntityTicker` do. CI parity is green at this head. Queued.
2026-10-29 23:34:45 +01:00
levzzz 6daad36de2 Merge pull request #3931 from arismoko/content-identity-handshake
Reject mismatched content block registries
2026-10-30 03:31:18 +05:00
Ludovic Marie 91635b6852 Merge pull request #3637 from horizon-rust/feat/launcher-night-sky
🤖 Agent: Redesign R2 (the animated night sky behind every page), stacked on #3636 which merged; approved by Ludovic 2026-10-29 (8 px/s clouds accepted); launcher fmt/clippy/tests and sky.rs tests pass on this head, CI green.
2026-10-29 23:30:36 +01:00
d3grandoandClaude Opus 5.5 58cdd754cd No rain splashes or rain sound under a high roof
ClientLevel.tickWeatherEffects reads the MOTION_BLOCKING heightmap for
each splash column, but the client scanned down from 64 blocks above the
camera, so under a roof higher than that (a deep excavation) it found
the floor and made rain particles and the rain sound there. The splash
columns, the sound's roof check and the animate tick's isRainingAt now
read the chunk heightmap that the rain columns already use.

Fixes #3810

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 05:30:02 +07:00
Ludovic Marie 0e15c274da Merge pull request #3636 from horizon-rust/feat/launcher-night-specimen
🤖 Agent: Redesign R1b (the night look's specimen page, pixel-cut plates in plate.rs), stacked on #3635 which merged; approved by Ludovic 2026-10-29; launcher fmt/clippy/tests and plate.rs tests pass on this head, CI green.
2026-10-29 23:28:48 +01:00
Ludovic MarieandClaude Opus 5.5 3b6972a40a Slowness: the -0.15F modifier widened from a float, as vanilla's MobEffects
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 23:27:40 +01:00
Ludovic Marie 829c4021a3 Merge pull request #3635 from horizon-rust/feat/launcher-night-tokens
🤖 Agent: Redesign R1 (night tokens, Bricolage with Geist's Cyrillic, pixel-cut corners), approved by Ludovic on 2026-10-29; launcher fmt/clippy/278 tests and horizon-theme tests pass on this head, CI green.
2026-10-29 23:26:38 +01:00
DUzzL 7a12e4444a Keep a shield-blocked small fireball from igniting its target 2026-10-29 23:25:57 +01:00
DenisHumenandClaude Opus 5.5 c3e9ec54a9 Sleeping: lie 0.125 above the bed's top, as vanilla does
LivingEntity.setPosToBed puts a sleeper at the bed's x/z centre and at
pos.y + getSleepHeight + 0.125, getSleepHeight being the top of the bed's
shape (0.5625). Horizon left out the 0.125, so the player and sleeping
villagers lay 0.125 lower, and the first-person camera (eye 0.2 + 0.15)
ended up inside the player's own head.

Fixes #4006

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 00:25:36 +02:00
d3grandoandClaude Opus 5.5 4b09c9d7d1 Thrown eggs and snowballs turn with the camera, no flip looking straight up
The item billboard of eggs, snowballs, ender pearls, fireballs and llama
spit took its right axis from forward x Y. At pitch exactly ±90 forward is
vertical; f32 cos(90°) is slightly negative, so the axis flipped sign and
the sprite turned upside down. It also ignored third-person front view.

ThrownItemRenderer.submit rotates the item by CameraRenderState.orientation
(Camera.setRotation: rotationYXZ(π - yaw, -pitch, 0), turned round in front
view). Use the same rotation: camera_orientation's math moves to a free
camera_rotation so the billboard's axes come from it.

Part of #3393

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

Part of #3518.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Rx5M6NS763D2WVnb2HmcTH
2026-10-29 22:14:39 +00:00
d3grandoandClaude Opus 5.5 5200d36a7b Withers build in any orientation, like vanilla's BlockPattern
The wither checks (WitherSkullBlock.checkSpawn and the dispenser's
canSpawnMob) only tried upright T builds with the arms along X or Z.
Vanilla's BlockPattern.find tries all 24 forwards/up pairs from every
corner in the cube at the placed block, so flat, upside-down and
sideways withers build there.

Port BlockPattern (find, matches, translateAndRotate) to
horizon_client::block_pattern with WitherSkullBlock's full and base
patterns, and use it for checkSpawn and canSpawnMob. The wither now
takes its yaw from the match's forwards axis (main had it inverted for
upright builds), its base blocks from #minecraft:wither_summon_base_blocks,
and its summoned_entity trigger from the 50-block box, sharing the
golems' box check.

Fixes #3098.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 05:14:38 +07:00
Ludovic Marie 2204e39515 Merge pull request #2987 from kacpherek/fix/entity-physics-lan-parity
🤖 Agent: proven by pr-triage-3 (Ludovic's AI agent) on union/pt3-w2 db5591ac7: parity --changed all passed, clippy, tests, lan_e2e 11 checks on both paths; tipped/spectral arrows and text/block/item displays drawn alike by host and LAN guest, save and reload identical, the ominous item spawner seen by a LAN guest. Mojang rules, supply chain, per-tick cost read.
2026-10-29 23:13:04 +01:00
arismoko cfff58e66f Reject mismatched content block registries 2026-10-29 17:10:20 -05:00
levzzz c1255761d6 Merge pull request #3871 from arismoko/upstream/named-hook-catalog
Catalog versioned native hook points
2026-10-30 03:05:20 +05:00
levzzz a88e84f3c9 Merge pull request #3880 from arismoko/native-bundle-data
Allow data files in native mod bundles
2026-10-30 03:05:09 +05:00
Ludovic Marie 33f0fa8c94 Merge pull request #3341 from damnurmum/fix/rich-item-name-colors
🤖 Agent: proven by pr-triage-3 (Ludovic's AI agent): 6da84cf19 was proven on union/pt3-w2; this head only merges main and adapts main's two new held-item tests to the Tag custom name. Workspace check, 1508 tests and the 11 item/save harnesses pass on it.
2026-10-29 23:02:14 +01:00
d3grandoandClaude Opus 5.5 42ff9e6482 Players and heads keep their skins on busy servers
The 128 atlas skin slots went round every frame once more skins were in
use than slots: update_remote_skins touched every player-list entry's skin,
each frame's uploads pushed out the least recently shown one (even one drawn
the frame before), and that one came back the next frame by pushing out
another. Players vanished for a frame (their name tag stayed) and heads
swapped to the default skin and back.

- skins::SkinSlots holds the slots: a new skin takes a free slot or the
  least recently drawn one, never one drawn in the last frame; with every
  slot drawn it waits and its player keeps the default skin.
- Downloader::drawn records what each frame draws (level players and their
  capes, the local player, mannequins, the faces the tab list and spectator
  menu show, skull blocks, item and text heads).
- Only drawn skins ask for a slot, not every player-list entry, and a skin
  not in the atlas yet draws the default one (SkinManager.createLookup's
  getNow(DefaultPlayerSkin.get)) instead of a missing texture.

Fixes #3829

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

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

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 22:54:06 +01:00
Ludovic Marie c8012dff3a Merge pull request #3119 from NetuosoDEV/fix/enderman-carried-block-tints
🤖 Agent: pr-triage-2 (Ludovic's AI agent, Claude): proven at c32dddb1f: carried and worn blocks draw their block-state model with vanilla's per-face default tints (block_tint harness against 26.3), grass colour once per draw; parity --changed passes, clippy clean, tests pass but for main's own glowing test. Merging.
2026-10-29 22:53:15 +01:00
Ludovic Marie 04da927a44 Merge pull request #3815 from horizon-rust/perf/light-view-cache
🤖 Agent: verified by server-tick-1: proto light views built once per proto state (streaming span 10-24 → 3-4 ms/tick on 25 explore bots, RSS unchanged); new generation_saves test fails on main and passes here; pr_ready --quick green; merging once CI parity passes.
2026-10-29 22:53:11 +01:00
k3yhandClaude Opus 5.5 67c4e5eb1a Merge main into the branch; the guest swap keeps both the acting permission and known_movement
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 16:47:34 -05:00
Ludovic Marie 12930d6237 Merge pull request #3572 from v3nn7/PolishLang
🤖 Agent: pr-triage-1 merged main once more (fast-forward): your Polish checks sit beside main's new test that every key has a Russian value. horizon-client-core's lang tests, the launcher's i18n tests and rustfmt pass on this head; proven before on union/pt1-w4.
2026-10-29 22:46:15 +01:00
d3grandoandClaude Opus 5.5 945ad05579 Darkness dims the screen on servers and in single player
The darkness effect's blend (MobEffectInstance.BlendState, which the
lightmap and the fog read) was ticked only by tick_sculk_entities, which
runs in the integrated level's tick_world. A server's client (multiplayer
and the default single player, whose server runs on a thread) ticks its
effects in tick_client_effects instead, so the blend stayed at 0 and the
effect did nothing. tick_client_effects now ticks the blend after counting
the effects down, as MobEffectInstance.tickClient does.

A new darkness from ClientboundUpdateMobEffectPacket starts its fade from
nothing, or at full strength when the server sent it unblended (joining,
changing dimension), as MobEffectInstance.skipBlending does.

Part of #3393.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 04:44:02 +07:00
Ludovic MarieandClaude Opus 5.5 9bba7215f5 Merge main into #3341: main's new held-item tests set a Tag custom name
Two tests main added (entity_nbt, mobs) set custom_name to a string and compared custom_name() with Option<&str>; with this PR it is the component tag and the plain text is an owned String.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 22:43:55 +01:00
DUzzL f822855c64 docs: add an owner's page mapping server.properties to server.toml 2026-10-29 22:43:10 +01:00
Ludovic Marie 0fff625c22 Merge pull request #3441 from xKumorio/fix-arrow-known-movement
🤖 Agent: proven by pr-triage-3 (Ludovic's AI agent) on union/pt3-w2 db5591ac7: parity --changed all passed, clippy, tests, lan_e2e 11 checks on both paths, and the throws levzzz asked for (walking/jumping carry the movement, none after a tick without a move) for host and guest on both paths. Mojang rules, supply chain, per-tick cost (a Vec3 copy per guest turn) read.
2026-10-29 22:39:35 +01:00
Ludovic Marie d52705984a Merge remote-tracking branch 'origin/main' into fix/pt3-3341 2026-10-29 22:37:25 +01:00
Ludovic MarieandClaude Opus 5.5 73c8eb9b60 Merge main into PolishLang: the Polish checks beside main's every-key test
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 22:34:22 +01:00
Ludovic Marie a305d38fc5 Merge pull request #3581 from v3nn7/fix/furnace-cart-client
🤖 Agent: proven on the integration branch union/pt1-w4 by pr-triage-1. The affected vanilla harnesses, clippy -D warnings and the tests pass, and so does lan_e2e (vehicles, commands, armour, self, tab) on both single-player paths. lan_e2e's minecarts check fails on main's own code too, with or without this PR. Furnace carts now clear their lit flag for clients, and their smoke follows MinecartFurnace.tick. Thank you!
2026-10-29 22:27:41 +01:00
Ludovic Marie 14b7b252ac Merge pull request #3106 from malordin/fix/missing-assets-download
🤖 Agent: proven by pr-triage-3 (Ludovic's AI agent): launcher-only change; on this exact head launcher fmt, clippy -D warnings and 299 tests pass on the Mac mini, clean merge with main. Asset refill only through download_for_owner (owners only).
2026-10-29 22:27:28 +01:00
Ludovic Marie 393ff56184 Merge pull request #3584 from v3nn7/fix/pumpkin-player-head
🤖 Agent: proven on the integration branch union/pt1-w4 by pr-triage-1: the affected vanilla harnesses pass (the only failures are main's own or another PR's), clippy -D warnings and the tests pass, the launcher's checks pass, and lan_e2e passes vehicles, commands, armour, self and tab on both single-player paths. Thank you!
2026-10-29 22:27:21 +01:00
Ludovic Marie e515379b04 Merge pull request #3925 from horizon-rust/feat/claim-respects-kira
🤖 Agent: merging the Kira/GitHub claim alignment (#3859 step 1 follow-up): verify green, claim tests pass.
2026-10-29 22:20:19 +01:00
Ludovic Marie eb19c2723b Merge pull request #3522 from Domanffe/i18n/own-screens-remainder-2
🤖 Agent: pr-triage-2 (Ludovic's AI agent, Claude): proven at d8e912738 in union/pt2-e3 (17 PRs on main cbea22fbb): parity --changed passes but for main's own failures, lan_e2e on both paths, policy read (Mojang rules, supply chain, speed). Merging.
2026-10-29 22:18:20 +01:00
Ludovic Marie ccc323a1b7 Merge pull request #3374 from ThatCakeIsAPie/feat/upgrade-datapack-4
🤖 Agent: pr-triage-2 (Ludovic's AI agent, Claude): proven at a717d6d58 in union/pt2-e3 (17 PRs on main cbea22fbb): parity --changed passes but for main's own failures, lan_e2e on both paths, policy read (Mojang rules, supply chain, speed). Merging.
2026-10-29 22:18:15 +01:00
Ludovic Marie d242616d68 Merge pull request #3373 from ThatCakeIsAPie/feat/upgrade-datapack-3
🤖 Agent: pr-triage-2 (Ludovic's AI agent, Claude): proven at 1ffe68635 in union/pt2-e3 (17 PRs on main cbea22fbb): parity --changed passes but for main's own failures, lan_e2e on both paths, policy read (Mojang rules, supply chain, speed). Merging.
2026-10-29 22:18:11 +01:00
DenisHumenandClaude Opus 5.5 d1a62e68ec Golems: build a snow or iron golem in any orientation, as vanilla does
CarvedPumpkinBlock.trySpawnGolem uses BlockPattern.find, which tries every
corner in the box from the pumpkin and every forwards/up pair, so the snow
column and the iron T also count lying down or upside down. Horizon only
checked the upright column and the upright T with arms along X or Z.

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

Fixes #3929

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 23:18:08 +02:00
Ludovic Marie 6175960663 Merge pull request #3372 from ThatCakeIsAPie/feat/upgrade-datapack-2b
🤖 Agent: pr-triage-2 (Ludovic's AI agent, Claude): proven at 217216e26 in union/pt2-e3 (17 PRs on main cbea22fbb): parity --changed passes but for main's own failures, lan_e2e on both paths, policy read (Mojang rules, supply chain, speed). Merging.
2026-10-29 22:12:19 +01:00
Ludovic Marie 7eaa770716 Merge pull request #3370 from ThatCakeIsAPie/feat/upgrade-datapack-2a
🤖 Agent: pr-triage-2 (Ludovic's AI agent, Claude): proven at 825f8f884 in union/pt2-e3 (17 PRs on main cbea22fbb): parity --changed passes but for main's own failures, lan_e2e on both paths, policy read (Mojang rules, supply chain, speed). Merging.
2026-10-29 22:12:15 +01:00
Ludovic Marie 7d6a05e18f Merge pull request #2847 from sirrayi/fix/release-notes-contributors-heading
🤖 Agent: pr-triage-2 (Ludovic's AI agent, Claude): proven at c455bb6ec in union/pt2-e3 (17 PRs on main cbea22fbb): parity --changed passes but for main's own failures, lan_e2e on both paths, policy read (Mojang rules, supply chain, speed). Merging.
2026-10-29 22:12:11 +01:00
Ludovic Marie fab10b1922 Merge pull request #2821 from levzzz5154/fix/death-message-item-name
🤖 Agent: pr-triage-2 (Ludovic's AI agent, Claude): proven at a11e54e7b in union/pt2-e3 (17 PRs on main cbea22fbb): parity --changed passes but for main's own failures, lan_e2e on both paths, policy read (Mojang rules, supply chain, speed). Merging.
2026-10-29 22:12:06 +01:00
Ludovic Marie 8a363878ee Merge pull request #2570 from levzzz5154/fix/world-types
🤖 Agent: pr-triage-2 (Ludovic's AI agent, Claude): proven at c7aca8489 in union/pt2-e3 (17 PRs on main cbea22fbb): parity --changed passes but for main's own failures, lan_e2e on both paths, policy read (Mojang rules, supply chain, speed). Merging.
2026-10-29 22:12:02 +01:00
GIGABAIT93andClaude Opus 5.5 d7f9535406 Entity shadows follow the entity's scale and age
LivingEntityRenderer.getShadowRadius multiplies the type's radius by
minecraft:scale, and MobRenderer by the age scale (babies 0.5, or the
species' own: goat 0.55, camel 0.6...). Villagers and striders halve a
baby's shadow again, turtles take 0.83 of it; slimes, magma cubes,
sulfur cubes and pufferfish size theirs by their size or puff state.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 23:10:55 +02:00
Ludovic Marie c770074449 Merge pull request #3405 from degepie/fix/issue-3137-suspicious-stew
🤖 Agent: pr-triage-2 (Ludovic's AI agent, Claude): proven at 95c234cc1 in union/pt2-e3 (17 PRs on main cbea22fbb): parity --changed passes but for main's own failures, lan_e2e on both paths, policy read (Mojang rules, supply chain, speed). Merging.
2026-10-29 22:06:55 +01:00
DUzzL 9226f507a3 Remove dead ctov modular_compat code and unused test helper in horizon-port-pack 2026-10-29 22:05:48 +01:00
Ludovic Marie 8169314c66 Merge pull request #3369 from ThatCakeIsAPie/feat/upgrade-datapack-1
🤖 Agent: pr-triage-2 (Ludovic's AI agent, Claude): proven at 0771f59bc in union/pt2-e3 (17 PRs on main cbea22fbb): parity --changed passes but for main's own failures, lan_e2e on both paths, policy read (Mojang rules, supply chain, speed). Merging.
2026-10-29 22:04:32 +01:00
Ludovic Marie 7b581fb43c Merge pull request #3366 from ThatCakeIsAPie/perf/worldgen-surface-biomes
🤖 Agent: pr-triage-2 (Ludovic's AI agent, Claude): proven at 52cc53a4f in union/pt2-e3 (17 PRs on main cbea22fbb): parity --changed passes but for main's own failures, lan_e2e on both paths, policy read (Mojang rules, supply chain, speed). Merging.
2026-10-29 22:04:28 +01:00
Ludovic MarieandClaude Opus 5.5 c32dddb1fe Merge main into the branch: default tints through the tint closure
Main's #3401 drew blocks through tinted_block_model_vertices with a per-face
tint closure; block_model_vertices now passes the default tints there
(grass colour once per call). The enderman's carried block uses the default
tints, so block_model_vertices_tinted has no caller left.

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

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

Part of #3859.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 21:58:34 +01:00
Ludovic Marie 74904e32c1 Merge pull request #3903 from horizon-rust/feat/overlap-note
🤖 Agent: merging the overlap note (#3859 step 2): verify green, tests pass, false positives measured on the 207 open PRs (21 pairs: 16 related, 4 doubtful, 1 unrelated).
2026-10-29 21:55:40 +01:00
Ludovic MarieandClaude Opus 5.5 7c8e0d8e9b Roadmap: Now at most 8 items, each with an owner and a tracking issue
roadmap.toml items get `owner` (a GitHub login) and `issue` (the tracking
issue); `check` requires both on every Now item, at most 8 Now and 12 Next
items, and the tracking issue to be an open issue. ROADMAP.md shows them,
roadmap.json carries them (the hub ignores unknown keys).

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

Part of #3859.

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

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

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 21:46:31 +01:00
DUzzL 2403ca40a2 Reject data-pack recipe result counts outside 1..=99
ItemStackTemplate.CODEC bounds the count with intRange(1, 99). Crafting,
cooking and stonecutting results were cast with 'as u32', so 0, 2^31 and
wrapped 2^32 reached item stacks. Such recipes are now not loaded.
2026-10-29 21:40:55 +01:00
DUzzL e7b19995ea Explosion drops merge by item, components and max stack size
ServerExplosion.addOrAppendStack uses ItemEntity.areMergable: same item and
components, and the combined count within the item's max stack size.
2026-10-29 21:39:27 +01:00
Ludovic Marie f47c14d93f Merge pull request #3872 from horizon-rust/feat/kind-labels
🤖 Agent: merging the kind and area labels (#3859 step 3): verify green, workflow-safety and labelling tests pass; labels created and the open pull requests labelled once.
2026-10-29 21:38:13 +01:00
DUzzL a5f4e32bfc Dedupe function-tag expansion with hash sets and end tag cycles
Fixes #3885
2026-10-29 21:37:00 +01:00
Ludovic MarieandClaude Opus 5.5 ca0888663e Overlap note: one comment naming the open pull requests doing the same work
On open and push, overlap-note.yml (pull_request_target, main's script only)
lists, in one comment edited on every push, the other open pull requests that
close the same issue or change the same functions. Functions come from git's
own rust/python hunk headers, which name indented methods (GitHub's stop at
`impl Game<'_>`). The heads are fetched as objects and diffed, never checked
out; hooks, fsmonitor, external diff and textconv are off.

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

Part of #3859.

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

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

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

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

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

Fixes #3887.

Co-Authored-By: DeepSeek Harness (deepseek-v4.1-flash) <[email protected]>
2026-10-29 23:33:35 +03:00
Ludovic Marie 90aded96a3 Merge pull request #3874 from horizon-rust/fix/display-log-linux-only
🤖 Agent: macOS clippy fix (log_display is only called on Linux); clippy clean on macOS.
2026-10-29 21:24:43 +01:00
matydenandClaude Opus 5.5 7b33479e15 Port publish: write the issue body as UTF-8
issue() wrote the body file in the system's code page, while gh reads
it as UTF-8, so on Windows a body with "→" raised UnicodeEncodeError
(cp1251 here) or would reach GitHub garbled. #3587 described this fix,
but the merged pull request kept only its roadmap files.
test_apply_lists_the_new_issue_numbers_in_the_tracking_issue now passes
on Windows without PYTHONUTF8.

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

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

Remember who fed each animal into love (Animal.loveCause) and credit that
player, the parent's feeder first and else the partner's, as vanilla does.
2026-10-29 21:11:07 +01:00
Aria Figueredo d3e0774ca3 Catalog versioned native hook points 2026-10-29 15:09:42 -05:00
bxnjxmin27andDeepSeek V4.1 Flash c3fdf7192d Screenshots: the key works with chat, a sign or a book open
F2 was handled in `Game::key_pressed`, which a screen with a text box never
reaches: chat, a sign, a book, the command block and the search boxes take the
key first, so F2 did nothing there (#3689).

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

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

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

Part of #3859.

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

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

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

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

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

Part of #3859.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 20:54:06 +01:00
Ludovic Marie 668419b92a Merge pull request #3526 from kacpherek/tools/port-ledger-harness
🤖 Agent: verified by pr-triage-3. Dev tooling only (tools/port_ledger.py harness coverage + gaps --kind untested, docs, port_map paths moved to the split crates: every new path exists on main). Merges cleanly with main; tests/test_port* 30 pass; skip-release.
2026-10-29 20:52:30 +01:00
d3grandoandClaude Opus 5.5 f5de793b38 Wither breaks parity: hold the wither in place until it breaks
The scenario depended on the wither staying put for the 20 ticks before
destroyBlocksTick fires. The wither's WaterAvoidingRandomFlyingGoal can
set off on a stroll in that time (its random is unseeded in vanilla),
which shifts the 3x3x4 box it breaks, so the stone at 7 210 7 was broken
or left depending on the run, on either side.

Teleport it back to 8.5 210 8.5 every tick until then: customServerAiStep
runs before travel, so the break always sees that position.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 02:44:22 +07:00
DUzzL bfda2bf165 Only a piston base stands in for a retracting moving_piston 2026-10-29 20:38:51 +01:00
DUzzL 5782df1c03 Lever on a piston's back no longer drops when the piston retracts
A retracting piston is a moving_piston block for two ticks. The support
facts are exported without a block entity, so the block read as empty and
every face as unsupported, and the lever attached to the piston's back
popped off. Read the extended base for a retracting source piston, which
is what vanilla's moving-piston block entity shape contains.
2026-10-29 20:36:08 +01:00
Ludovic MarieandClaude Opus 5.5 d3c6f0d937 Merge main into #2987 (lan_entities.rs: lan_display_data beside main's lan_entity_uuids)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 20:34:23 +01:00
Ludovic Marie baa379d717 Merge pull request #3578 from d3grando/fix-3432
🤖 Agent: proven on union/pt1-w3 by pr-triage-1: every vanilla harness this branch reaches passes (mob_ai and the equine ones included), clippy -D warnings and the horizon-client tests pass (the one failing test is main's own, fixed by #3851), and the last commit only reorders an import for rustfmt. Thank you!
2026-10-29 20:31:31 +01:00
d3grandoandClaude Opus 5.5 52a380cd2f Merge origin/main into fix-3503
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 02:30:27 +07:00
d3grandoandClaude Opus 5.5 150412d353 benchmarks: rerun the Windows RD 8 and RD 32 games on an idle machine
ludovic111 pointed out that the first pass's RD 8 and RD 32 games had builds
running alongside, which docs/benchmark.md keeps out of published results.
Each game now started only with no compiler or other game running and the
CPU under 15 % for 30 s, and a game during which one started (or other
processes used over 15 % for 10 s) was run again. RD 16 was already idle.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 02:30:03 +07:00
Ludovic MarieandClaude Opus 5.5 5fb9092073 Merge main into #3341: inline objects (#3327) beside the component shadow colour
The glyph loop takes both: an inline object draws and skips the shadow passes; other glyphs use the component's shadow_color. Both sides' tests kept.

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

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

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

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 20:13:11 +01:00
Ludovic MarieandClaude Opus 5.5 84c3af5122 Merge main into this branch: the pumpkin beside main's cape and glowing changes
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 20:00:31 +01:00
GIGABAIT93andClaude Opus 5.5 b6b9a5a307 Text displays riding the local player sit over its head too
/ride a text_display mount @s makes the window's own player the
vehicle, which isn't in mobs: the display stayed where it spawned. The
local player seats it at the top of its box in its pose and scale, its
passenger attachment (Avatar.POSES), as for other players.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 20:48:57 +02:00
Ludovic Marie 7ebf7d26cb Merge pull request #3350 from horizon-rust/draft/remove-wasm-mods-game
🤖 Agent: WebAssembly removal 1/4 (game), merged up to date with main c7065027b. The official mods are native in the registry (Zoom, FPS Counter, Keystrokes, Minimap, Recipes, Coordinates HUD 2.0.0, verified in the published index), and the authors of the WebAssembly mods with no native version were told (list in the body). Each layer compiles; pr_ready --quick on the tip: only main's own macOS clippy (log_display) and axolotl test fail. Parity: 340/340 locally on the previous heads; CI parity on these. Merge before #3351, #3353, #3371.
2026-10-29 19:48:54 +01:00
Ludovic MarieandClaude Opus 5.5 d8e912738b Merge main into the branch: the HDR graphics page shares the video title key
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 19:46:02 +01:00
Ludovic Marie cbea22fbb6 Merge pull request #3831 from horizon-rust/perf/streaming-results-no-wait
🤖 Agent: merging. The frame no longer waits on a descheduled worker's mpsc send: 8–24 ms stalls in Streaming::update gone in the RD 32 ocean flight (0–1 per run vs 5–14), 109/109 affected harnesses pass, CI verify green.
2026-10-29 19:43:45 +01:00
Ludovic MarieandClaude Opus 5.5 c7aca8489f Merge main into the branch: keep the world icon beside the preset check
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 19:42:46 +01:00
Ludovic Marie 319876a186 Merge pull request #2795 from s1f102500012/launcher-simplified-chinese
🤖 Agent: correction from pr-triage-1: my first push of the main merge missed one line (`Language::ALL` still said six), so it didn't build; 46ac49ad0 adds it and merges main once more. On this exact head the launcher's fmt, clippy -D warnings and all its tests pass, including the check that every language has every string. Sorry for the noise.
2026-10-29 19:38:07 +01:00
levzzz 9093960719 Merge pull request #3355 from Sticky-Bug/feat/audio-output-device
options: choose the audio output device in Music & Sounds (#1035)
2026-10-29 23:37:50 +05:00
Ludovic MarieandClaude Opus 5.5 68e57e86c5 Streaming: the frame takes finished jobs without waiting for a worker
The game thread drained the workers' results with std's mpsc try_recv,
which spins and then yields until a sender it finds half way through a
send has written its message (std's mpmc list: start_recv, wait_write).
A worker descheduled at that point on a busy machine held the frame for
a scheduler quantum: 8 to 24 ms waits with no CPU time in
Streaming::update, measured with HORIZON_SPIKES during the RD 32 ocean
flight. The results now go through a mutex-guarded queue the frame
takes with try_lock: while a worker holds it, the frame leaves the
results for the next one. The blocking waits (save, wait, drop) keep
their timeouts on a condvar.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 19:28:52 +01:00
d3grandoandClaude Opus 5.5 67a960b045 Merge origin/main into bench-windows-results
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 01:25:47 +07:00
Ludovic MarieandClaude Opus 5.5 36ca9df57b Merge the game layer (main c7065027b) into the crates layer
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 19:22:04 +01:00
Ludovic MarieandClaude Opus 5.5 ab0309581c Merge main (c7065027b) into the WebAssembly removal (game)
Conflict: the help text; main's --in-frame deprecation kept, --dev-mod still
takes content mods only.

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

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 19:20:16 +01:00
Ludovic MarieandClaude Opus 5.5 1ea5ec5964 Merge main into hdr-tiers: the tiers paragraph beside HDR's own screen and Auto
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 19:19:29 +01:00
d3grandoandClaude Opus 5.5 8168d96082 Merge origin/main into fix-3503
Conflicts: the creative screen's tooltip takes main's tab names
(draw_stack_tooltip_with) and this branch's TooltipFlag.isCreative for a
grid slot; horizon-protocol's item.rs keeps main's instrument holder and
this branch's painting_variant holder. The new wire test's Dynamic gets
main's dialog_tags by default.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 01:12:36 +07:00
Ludovic MarieandClaude Opus 5.5 46ac49ad05 Launcher: Language::ALL holds seven languages, formatted
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 19:02:12 +01:00
Ludovic Marie d23ab5ae50 Merge remote-tracking branch 'origin/main' into pt1-2795 2026-10-29 19:02:03 +01:00
Ludovic MarieandClaude Opus 5.5 9ad4c9fb8b Merge main into launcher-simplified-chinese: keep Chinese beside main's Ukrainian
Language::ALL holds seven, and every list of codes names zh, es and uk.

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

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

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 22:10:21 +05:00
levzzz 3e31a55950 Merge pull request #3644 from iLostName/invisible-player-third-person
invisibility: the player's own body is hidden in third person and the inventory, its armour still drawn
2026-10-29 22:10:15 +05:00
levzzz 4e910746ce Merge pull request #3625 from GIGABAIT93/feat/ukrainian
Launcher: Ukrainian
2026-10-29 22:10:04 +05:00
levzzz 06254caea3 Merge pull request #3751 from leifiyoo/fix/creative-drop-items
Creative display drops reach the server on a server connection (#3523)
2026-10-29 22:07:59 +05:00
levzzz 7b78be8d4e Merge pull request #3738 from GIGABAIT93/fix/server-pack-overlay-cache
Server resource packs: keep the overlay when the pack is only touched
2026-10-29 22:07:53 +05:00
levzzz 73602536bb Merge pull request #3717 from 1038april/feat/own-name-tag
Extras: show your own name tag in third person
2026-10-29 22:07:47 +05:00
levzzz d601fc2e54 Merge pull request #3701 from Domanffe/fix/launcher-nightly-label
Launcher: a Nightly row in Versions reads as its commit
2026-10-29 22:05:21 +05:00
levzzz 9ca1b032ce Merge pull request #3673 from d3grando/fix-3001
Type AltGr characters into chat and search boxes on Windows
2026-10-29 22:05:16 +05:00
levzzz 5b8898d018 Merge pull request #3660 from xFedeT/fix/mod-settings-at-title
Mods: Options > Mods > Settings opens the mod's page at the title screen
2026-10-29 22:05:10 +05:00
levzzz 28a4e5443f Merge pull request #3652 from d3grando/fix-3211
The mouse wheel selects a bundle's item in the creative inventory
2026-10-29 22:05:04 +05:00
levzzz dc05ba0cc7 Merge pull request #3641 from iLostName/pack-mcmeta-folder-entry
resource packs: a zip that stores pack.mcmeta as pack.mcmeta/ loads, as ZipFile.getEntry finds it
2026-10-29 22:04:58 +05:00
Ludovic MarieandClaude Opus 5.5 2c8b7ee172 Merge main into the branch: keep both lang test blocks
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 18:04:37 +01:00
levzzz 74cf4f8c53 Merge pull request #3646 from d3grando/fix-3525
Shader packs: a program the shader compiler panics on no longer crashes the game
2026-10-29 22:04:15 +05:00
levzzz 335d11e35e Merge pull request #3640 from d3grando/fix-3552
Graphics settings: the scrollbar can be held and dragged
2026-10-29 22:02:16 +05:00
levzzz cc7bcbf4e5 Merge pull request #3731 from Joansitoh/refactor/launcher-fields-share-line-edit
Launcher: every text field uses the shared LineEdit
2026-10-29 22:01:18 +05:00
levzzz 182b9a1473 Merge pull request #3775 from martipops/split/05-viewports
Render: the splitscreen layout model (where each player's view goes)
2026-10-29 22:01:03 +05:00
levzzzandClaude Opus 5.5 cb3f4387db Merge main into the invisibility branch: render_layers beside main's cape, and no cape while invisible
Main added CapeLayer (mobs::render::cape) at the spot this branch adds render_layers;
both stay. CapeLayer draws only when !state.isInvisible, so the player's own cape is
left out under Invisibility too, in the world and the inventory.

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 18:40:25 +02:00
levzzzandClaude Opus 5.5 0b63d040ea Merge main into hdr-ui: hdrUiBrightness beside main's taa and upscaling
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 21:38:13 +05:00
levzzzandClaude Opus 5.5 b19d8659ff Merge main into hdr-black: settings now also save taa and upscaling
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 21:37:24 +05:00
levzzzandClaude Opus 5.5 f9b002f804 Merge horizon-rust/main into fix/spawner-mob-display
Resolves the conflict in game/remote.rs tick_remote: keeps both the
remote spawner tick and main's client_tick_beacons.

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

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

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 17:01:41 +01:00
levzzz 42e7d94082 Merge pull request #3479 from ThatCakeIsAPie/feat/datapack-worldgen-4
Harness: chunks generated with a world's data packs against vanilla
2026-10-29 20:59:01 +05:00
levzzz 5f7341487f Merge pull request #3478 from ThatCakeIsAPie/feat/datapack-worldgen-3
Data packs: a world's packs change its world generation
2026-10-29 20:58:44 +05:00
levzzz a5fba2ddac Merge pull request #3477 from ThatCakeIsAPie/feat/datapack-worldgen-2
Worldgen: the generator loads a world's data packs over the bundles
2026-10-29 20:58:25 +05:00
levzzz f93b7cd0fa Merge pull request #3476 from ThatCakeIsAPie/feat/datapack-worldgen-1
Worldgen: data packs' entries over the prepared bundles
2026-10-29 20:58:07 +05:00
Federico TaglianiandClaude Sonnet 5.5 8b5145eaa5 Armor stands: bubble columns push them, and water slows and floats them
An armor stand used the air travel in water too and ignored bubble columns,
so one standing in a soul sand column never rose. LivingEntity.travel now has
its water branch (slow down 0.8, gravity / 16 through getFluidFallingAdjusted
Movement), and the blocks the stand is inside (a bubble column) push it
between the move and the friction, as Entity.move's checkInsideBlocks does.

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

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

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

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

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

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

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

Implemented by OpenAI Codex on behalf of NetuosoDEV.

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

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

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

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

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

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

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

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

Refs #3708

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

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

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 16:23:23 +01:00
Ludovic Marie 8bb23aa1f7 Merge remote-tracking branch 'origin/main' into pt3/2927 2026-10-29 16:21:14 +01:00
levzzz a491181961 Merge pull request #3253 from hectoreccles/perf/opt-01-storage
perf: storage primitives (dirty sections, packed index, palette scan, Fx maps)
2026-10-29 20:18:07 +05:00
levzzz 4f69f45167 Merge pull request #3450 from d3grando/fix-3319
End the dragon fight when /kill removes the dragon
2026-10-29 20:16:49 +05:00
levzzz 2f9d889dcc Merge pull request #3367 from ThatCakeIsAPie/perf/worldgen-decoration-reads
Decoration: inlined block reads, column scans, per-chunk heightmap reads
2026-10-29 20:16:24 +05:00
levzzz a459a00584 Merge pull request #3365 from ThatCakeIsAPie/perf/worldgen-biome-stage
Biome stage: build free neighbour biome chunks first, cheaper id lookups
2026-10-29 20:16:13 +05:00
levzzz ede6d6dfbd Merge pull request #3364 from ThatCakeIsAPie/perf/worldgen-structure-climate
Structure checks: one climate volume per column, and each worker keeps its samples
2026-10-29 20:16:02 +05:00
levzzz 2cf38dd095 Merge pull request #3368 from ThatCakeIsAPie/perf/far-tiles-biome-memo
Far tiles: resolve each biome quart once per tile, and a far-tile bench
2026-10-29 20:15:34 +05:00
Ludovic Marie 89f4b34c1f Merge remote-tracking branch 'origin/main' into feat/launcher-java-join
# Conflicts:
#	launcher/src/main.rs
2026-10-29 16:15:13 +01:00
Ludovic MarieandClaude Opus 5.5 929e75181b Launcher Multiplayer: Horizon Official on top of the list, once
The Multiplayer section puts "Horizon Official" (horizon.bunchy7s.dev) first in the instance's
servers.dat, as the game does: once (horizon_official_server.txt beside the list, the same file the
game writes), never again after the player removes it, never when the list already has its address
(any case, with or without :25565), and never into a list that can't be read.

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

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

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: OpenAI Codex <[email protected]>
2026-10-29 16:27:13 +02:00
levzzz b869bdcacd Merge pull request #3591 from d3grando/fix-3185
Wolves: any target makes a wolf angry, so an alerted pack looks angry
2026-10-29 19:25:29 +05:00
Claude ba6575314c Draw the empty main hand on the main arm, and hide it while invisible
renderArmWithItem draws an empty main hand with renderPlayerArm on the
player's main arm (the left one when the main hand is set to left) and
only when the player isn't invisible. hand_geometry always drew the right
arm and drew it while invisible. The map arms (#3444) already follow
both rules.

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

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

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

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

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

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 16:22:06 +02:00
levzzz cef8fa7e6d Merge pull request #3240 from some-du6e/fix/spear-jab-animation
Spears: the jab animates in first and third person
2026-10-29 19:17:10 +05:00
Joansitoh 1582cc638f Merge origin/main into refactor/launcher-fields-share-line-edit
The Post-exit field main added is a LineEdit too.
2026-10-29 15:15:34 +01:00
umcekoandClaude Opus 5.5 5c66b27c9a Content mods: items patch their template's data components
An item file may carry `components`, a 26.3 data component patch over its
template's: `minecraft:<type>` sets one, `!minecraft:<type>` with `{}`
removes it. A modded sword gets its own damage and durability, a tool its
mining speed, without a template per stat. Types no vanilla item has are
refused, so a typo fails the load. Issue #3305, item 3.

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

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

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

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 18:58:55 +05:00
Ludovic Marie 45ecf7bef0 Merge pull request #3718 from horizon-rust/feat/launcher-java-mod-toggle
🤖 Agent: launcher matrix 40883773220 green on all platforms at this head; queued.
2026-10-29 14:57:38 +01:00
Ludovic Marie 181040539d Merge pull request #3713 from horizon-rust/feat/launcher-post-exit
🤖 Agent: launcher matrix 40884459459 green on all platforms at this head; queued.
2026-10-29 14:57:33 +01:00
Joansitoh 17864b867d Merge remote-tracking branch 'origin/main' into refactor/launcher-fields-share-line-edit 2026-10-29 14:56:12 +01:00
Joansitoh 09146bfecf Launcher: rustfmt 2026-10-29 14:56:09 +01:00
Ludovic MarieandClaude Opus 5.5 f2337d0a73 Launcher: a server's Join starts a Java instance into it
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 14:52:27 +01:00
levzzzandClaude Opus 5.5 e852db5e60 A wolf angry at the player stays angry across a reload
On the server thread a reloaded wolf ticks before the player joins, and
the anger ended because no player counted as no valid target. Vanilla's
updatePersistentAnger ends it only once it finds the player and the player
is in creative or spectator, or the difficulty is peaceful.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 18:50:37 +05:00
levzzz f781769177 Merge remote-tracking branch 'hr/main' into pr-3591 2026-10-29 18:49:06 +05:00
vvnikita74 c5a734eace fix: sleep only when it is dark outside, not whenever the thunder flag is set (#3709)
Beds checked the level's raw thundering flag, whose timer also runs in
clear weather, so players could sleep through a sunny day. The Overworld's
bed_rule sleeps WHEN_DARK, as vanilla's Level.isDarkOutside: skyDarken >= 4,
from the time of day and the rain and thunder levels.
2026-10-29 16:47:08 +03:00
Joansitoh 3e83f8a48a Merge origin/main into refactor/launcher-fields-share-line-edit
The proxy field and the new Java-instance, launch, switch and multiplayer
fields use LineEdit too.
2026-10-29 14:46:50 +01:00
levzzz 57d4801bff Merge pull request #3583 from d3grando/fix-3430
Mobs: undead with NoAI burn in daylight
2026-10-29 18:44:13 +05:00
Ludovic Marie 0050a65600 Merge remote-tracking branch 'origin/main' into feat/launcher-post-exit
# Conflicts:
#	launcher/src/cli.rs
#	launcher/src/main.rs
2026-10-29 14:42:02 +01:00
Ludovic MarieandClaude Opus 5.5 0914c90455 Launcher: the post-exit test's Windows command keeps its last word
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 14:38:52 +01:00
Ludovic MarieandClaude Opus 5.5 2dd4903b5e Launcher: turn single mods on and off in Java instances
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 14:37:50 +01:00
levzzz a3b4d135aa Merge pull request #2706 from RoyalPixelsDE/perf/surface-skip-same-writes
Worldgen: the surface step skips writes that change nothing
2026-10-29 18:35:56 +05:00
martipopsandCodex f9a97f264f Add a compositor for offscreen frames on one GPU device
Expose renderer frames and surface handoff, with a standalone two-view GPU smoke example. Part 1 of the splitscreen series; normal game rendering is unchanged.

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

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

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_018ksHJwFp11RfYk6S9xnyCf
2026-10-29 23:26:36 +10:00
Ludovic MarieandClaude Opus 5.5 edd340a621 Launcher: a command after the game exits, and the instance's variables
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 14:22:49 +01:00
Ludovic MarieandClaude Opus 5.5 201ea8b393 Merge main into the branch
Main's hud_disconnected() now resets the boss overlay; the item
activation reset stays after it.

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

Fix #3700.

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 13:56:51 +01:00
levzzz 4103382062 Merge pull request #2995 from ASOwnerYT/fix/2869-horse-max-health
Horses, donkeys, mules and llamas sync their rolled max health
2026-10-29 17:56:44 +05:00
levzzz 7f9cc4084d Merge pull request #2345 from kacpherek/fix/burn-in-daylight-list
Follow the burn in daylight tag for mobs that burn in the sun
2026-10-29 17:56:40 +05:00
Ludovic Marie c784e572d7 Merge remote-tracking branch 'origin/main' into pt3/3253 2026-10-29 13:56:14 +01:00
levzzzandClaude Opus 5.5 479d10e794 Tutorial hints are recorded by the window only: the server thread no longer saves options.txt from its stale copy of the settings when a guest picks up a log or breaks one.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 17:56:06 +05:00
levzzz 89f949155c Merge pull request #3589 from d3grando/fix-3427
Commands: coordinate suggestions skip entities and offer the hit point
2026-10-29 17:55:49 +05:00
levzzz 129ad4fdf8 Merge pull request #3573 from Vadyanik/fix/xkb-caps-swapescape
Follow the XKB keymap for Escape and Caps Lock
2026-10-29 17:55:43 +05:00
levzzz ea3c3532b6 Merge pull request #3570 from d3grando/fix-3426
Creative tooltips list the tabs that hold the item, as vanilla does
2026-10-29 17:55:36 +05:00
levzzz 46a5e18630 Merge pull request #3569 from d3grando/fix-3431
Villager gossip: hits and murders count against the player who did them
2026-10-29 17:55:29 +05:00
levzzz 703ba156da Merge pull request #3565 from d3grando/fix-2862
Copper golem eyes glow on the body model, not the empty eyes layer
2026-10-29 17:55:22 +05:00
levzzz dd40744480 Merge pull request #3564 from d3grando/fix-3158
Key binds: a moved key no longer keeps its default action
2026-10-29 17:55:16 +05:00
levzzz cca5298864 Merge pull request #3560 from d3grando/fix-3429
Count a zombie villager's cure down in its tick, with or without AI
2026-10-29 17:55:09 +05:00
Ludovic MarieandClaude Opus 5.5 98c0883c8f Merge main into this branch; suspicious blocks are not containers
remote.rs: main's spectator check (a spectator's use predicts nothing) and
this branch's shelf prediction are both kept, the spectator first.

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

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 13:55:05 +01:00
levzzz 44c88b6c7b Merge pull request #3559 from d3grando/fix-3209
Attack indicator shows at a spear's extra range
2026-10-29 17:55:02 +05:00
levzzz 6a9ff4a38f Merge pull request #3558 from d3grando/fix-3136
Draw the arms under a map held in first person
2026-10-29 17:54:56 +05:00
levzzz 4c33a0fdfe Merge pull request #3557 from d3grando/fix-3289
Invisible mobs cast no shadow
2026-10-29 17:54:49 +05:00
levzzz 2e01c336a3 Merge pull request #3554 from d3grando/fix-3293
Draw glowing sign text in front of its outline
2026-10-29 17:54:42 +05:00
levzzz d26740a103 Merge pull request #3553 from d3grando/fix-3381
Projectiles hit a seated enderman
2026-10-29 17:54:36 +05:00
levzzzandClaude Opus 5.5 fdcefc0d90 /list uuids in single player shows the player's saved profile id, as the other commands do, instead of the offline id of the name.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 17:54:35 +05:00
levzzz 1c87226ac9 Merge pull request #3511 from Domanffe/fix/windows-overlay-hardlink-render
Windows: link the overlay's mirrored files instead of copying them
2026-10-29 17:54:28 +05:00
levzzz 9c86860e40 Merge pull request #3498 from JustKarkat/fix/mob-drop-scatter
Mob-origin item drops get the ItemEntity constructor's random motion
2026-10-29 17:54:19 +05:00
levzzz ca3d2d32af Merge pull request #3467 from AHHhHHHHH-H/pr/far-sampling-threads
Far terrain: sample on up to 6 threads when the machine has 12 or more, behind the game
2026-10-29 17:54:10 +05:00
levzzz 1b0923d8cf Merge pull request #3464 from d3grando/native-settings-refresh-on-open
Native mod settings: ask for the labels again when the page opens
2026-10-29 17:54:04 +05:00
levzzz 68f50bbd41 Merge pull request #3463 from d3grando/native-hooks-f3-corners
native_corner_shown: F3 takes the top right and its charts' corners
2026-10-29 17:53:55 +05:00
levzzz 47bce76078 Merge pull request #3456 from d3grando/fix-3312
Tooltips: potion effect and armour trim lines in the selected language
2026-10-29 17:53:48 +05:00
levzzz 125e540126 Merge pull request #3449 from d3grando/fix-3302
Keep the player model's legs still while flying straight up
2026-10-29 17:53:41 +05:00
levzzz 1578a97c79 Merge pull request #3444 from d3grando/fix-3408
Fog option off clears the Nether's fog too
2026-10-29 17:53:34 +05:00
levzzz dbd5fb7f62 Merge pull request #3438 from Joansitoh/fix/windows-overlay-junctions
Windows: link untouched directories into the pack overlay with junctions
2026-10-29 17:53:27 +05:00
levzzz 2a8dc29c69 Merge pull request #3437 from d3grando/fix-resize-absurd-size
Window: a size above the GPU's largest texture no longer crashes
2026-10-29 17:53:20 +05:00
levzzz 81a0e7e9a5 Merge pull request #3428 from d3grando/win-parity-sighup
parity: catch SIGHUP only where it exists, so parity.py starts on Windows
2026-10-29 17:53:13 +05:00
levzzz ce980a90de Merge pull request #3416 from d3grando/fix-3084-cyrillic-bold
Name tags: draw text as Font.prepareText does (bold, tall glyphs)
2026-10-29 17:53:07 +05:00
levzzz dd32d3dc38 Merge pull request #3399 from d3grando/fix-mod-screen-list-clip
Mod screens: a list's rows are cut at its frame
2026-10-29 17:53:00 +05:00
levzzz 29f427dabe Merge pull request #3363 from ThatCakeIsAPie/fix/launcher-windows-unused-import
launcher: drop an unused import in the Windows make_link
2026-10-29 17:52:54 +05:00
levzzz b156515edd Merge pull request #3330 from josegil1909/fix/launcher-clipboard-linux
Launcher: sync clipboard on Linux Wayland and X11
2026-10-29 17:52:42 +05:00
levzzz 0fdf1aeee1 Merge pull request #3328 from echo-ilovech3ss/fix/creative-piston-head-drop
Piston head: destroy fitting base without drops in creative mode
2026-10-29 17:52:35 +05:00
levzzz ee2fb39605 Merge pull request #3241 from SolomonNumb1/fix/modifier-tooltip
tooltip fix
2026-10-29 17:52:27 +05:00
levzzz 559e36ee04 Merge pull request #3193 from SlaVKsVolks/contribute/horizon-windows-parity-20261028
Parity: select affected harnesses from Windows dependency records
2026-10-29 17:52:20 +05:00
levzzz 7037510e65 Merge pull request #3167 from sleepe229/fix/3143-sunset-sky-cost
Build the sunset fan once a frame instead of in every sky pixel
2026-10-29 17:52:13 +05:00
levzzz 417a1cebc1 Merge pull request #3161 from jaszkus/fix/block-outline-open-screens
Keep the block outline while a screen is open
2026-10-29 17:52:06 +05:00
levzzz 9edc7eb80a Merge pull request #3130 from d3grando/mod-sprites-nine-slice
Mod API: a sprite shape draws as vanilla's blitSprite, nine-sliced by its gui.scaling
2026-10-29 17:52:00 +05:00
Ludovic MarieandClaude Opus 5.5 5667e9ebd1 Merge main into the branch
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 13:51:56 +01:00
levzzz d9628314ab Merge pull request #3113 from kacpherek/fix/ender-eye-debug-hitbox
Show a thrown eye of ender's hitbox with F3+B
2026-10-29 17:51:53 +05:00
levzzz 4984a1329a Merge pull request #3103 from Domanffe/fix/creative-hotbar-save-message
Creative hotbar: say 'Saved' only when it was saved
2026-10-29 17:51:47 +05:00
levzzz d22b63b9ca Merge pull request #3101 from Domanffe/fix/scoreboard-write-failure
Scoreboard: report a failed write instead of dropping it
2026-10-29 17:51:41 +05:00
levzzz dc5e8c45f7 Merge pull request #3099 from Domanffe/fix/launcher-servers-atomic-list
Launcher: write the server list beside it and rename
2026-10-29 17:51:34 +05:00
levzzz 2782a9057a Merge pull request #3076 from Domanffe/fix/port-publish-posix-path
Port publish: a body names the diff store with Posix separators
2026-10-29 17:51:27 +05:00
levzzz 66620f22a3 Merge pull request #2996 from feeldev12/feat/book-click-events
fix(client): book text runs and copies its click commands and shows its hover text
2026-10-29 17:51:20 +05:00
levzzz 18070bb4ce Merge pull request #2814 from sirrayi/fix/snbt-depth-and-words
SNBT: bound nesting depth, keep nan/inf words as strings
2026-10-29 17:51:14 +05:00
levzzz b575402076 Merge pull request #2753 from iLostName/world-access-failure-toast
title: a world open in another instance shows the access failure toast instead of crashing
2026-10-29 17:51:07 +05:00
Ludovic MarieandClaude Opus 5.5 0c06082dfa Merge main into this branch
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 13:51:05 +01:00
Federico TaglianiandClaude Sonnet 5.5 e0fecd8b8e Fish: swim targets pass the water malus check, and FishMoveControl turns as vanilla
Cod, salmon, tropical fish, pufferfish, tadpoles, squid and dolphins keep
PathType.WATER at 0 (WaterAnimal and AgeableWaterCreature constructors).
Horizon left it at 8, so DefaultRandomPos rejected every water position
and RandomSwimmingGoal retried ten times, ending on a spot in the air.
FishMoveControl also computes its yaw like MoveControl does.

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

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

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

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 07:42:40 -05:00
Ludovic Marie 56c1100644 Merge pull request #3678 from horizon-rust/feat/launcher-java-mods-page
🤖 Agent: Mods page for a selected Java instance (its version and loader; no Horizon port check). Horizon instances unchanged. Verify green; matrix 40873026396 green on Windows, macOS and Linux.
2026-10-29 13:41:31 +01:00
Ludovic Marie a881d284a7 Merge pull request #3433 from xKumorio/fix-kelp-sheet-mips
🤖 Agent: verified by pr-triage-3. Now only the kelp mipmap fix in gfx/animate.rs (the save.rs part is on main via #3088, port_publish.py is in #3587, as the author offered). union/pt3-2 285ecb6e8 (main 2b294bbe3 + #2417 #2951 #2981 #3000 #3417 #3433 #2544 #2586): fmt, clippy -D warnings (horizon-client, -client-render, -client-core, -world, -items, all targets), their tests pass.
2026-10-29 13:41:27 +01:00
levzzz 9da77d9d4b Merge remote-tracking branch 'hr/main' into pr-2795
# Conflicts:
#	launcher/src/i18n/tests.rs
2026-10-29 17:41:21 +05:00
repositorykreml1n 01791aba51 Add files via upload 2026-10-29 15:41:10 +03:00
levzzz b5b762b840 Merge remote-tracking branch 'hr/main' into pr-2964
# Conflicts:
#	crates/horizon-client/src/game/remote.rs
2026-10-29 17:41:04 +05:00
levzzz 08c0fc1286 Merge remote-tracking branch 'hr/main' into pr-2951 2026-10-29 17:40:55 +05:00
levzzz e81e23e64e Merge remote-tracking branch 'hr/main' into pr-3339 2026-10-29 17:40:54 +05:00
Federico TaglianiandClaude Sonnet 5.5 0a770a0e14 Merge main into fix/mod-structure-suggestions
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-29 13:40:17 +01:00
Ludovic Marie 2cd11c926f Merge branch 'draft/remove-wasm-mods-game' into draft/remove-wasm-mods-crates 2026-10-29 13:38:59 +01:00
Ludovic Marie a7f1f70508 Merge remote-tracking branch 'origin/main' into draft/remove-wasm-mods-game 2026-10-29 13:38:59 +01:00
Ludovic Marie 5005a7ad9f Merge remote-tracking branch 'origin/main' into feat/launcher-import-apps
# Conflicts:
#	launcher/src/cli.rs
#	launcher/src/other_launchers.rs
2026-10-29 13:34:39 +01:00
Aria FigueredoandGPT-6.1 Terra 4808d801fe Allocate mod item IDs in deterministic registry order
Co-Authored-By: GPT-6.1 Terra <[email protected]>
2026-10-29 07:31:30 -05:00
damnurmumandCodex 0d0411fc7a Merge latest main into rich item names and shadows
Pick up launcher and dragon changes that landed while the earlier merge was being tested.

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

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

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

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

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

* Keep the steering comment

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

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

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

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

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

---------

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-29 12:58:28 +01:00
Ludovic Marie 5e391eb673 Merge pull request #3500 from spidercraft/fix/firework-gaps
🤖 Agent: proven on the integration branch union/pt1-wave1 (22 PRs on main 13d7d9995): tools/pr_ready.py all green (clippy -D warnings, 1686 tests, the 140 affected vanilla harnesses pass), lan_e2e on both single-player paths (22 checks, the only failure, boats, fails on main too), agent_control_e2e consume and pause-save. Thank you!
2026-10-29 12:54:07 +01:00
Ludovic Marie c10553eebf Merge pull request #3439 from spidercraft/taa
🤖 Agent: proven on the integration branch union/pt1-wave1 (22 PRs on main 13d7d9995): tools/pr_ready.py all green (clippy -D warnings, 1686 tests, the 140 affected vanilla harnesses pass), lan_e2e on both single-player paths (22 checks, the only failure, boats, fails on main too), agent_control_e2e consume and pause-save. Thank you!
2026-10-29 12:54:03 +01:00
Ludovic Marie 215d8e779a Merge pull request #3411 from spidercraft/cape-rendering
🤖 Agent: proven on the integration branch union/pt1-wave1 (22 PRs on main 13d7d9995): tools/pr_ready.py all green (clippy -D warnings, 1686 tests, the 140 affected vanilla harnesses pass), lan_e2e on both single-player paths (22 checks, the only failure, boats, fails on main too), agent_control_e2e consume and pause-save. Thank you!
2026-10-29 12:53:59 +01:00
Ludovic Marie 58a4fd8263 Merge pull request #3571 from d3grando/fix-3501
🤖 Agent: proven on the integration branch union/pt1-wave1 (22 PRs on main 13d7d9995): tools/pr_ready.py all green (clippy -D warnings, 1686 tests, the 140 affected vanilla harnesses pass), lan_e2e on both single-player paths (22 checks, the only failure, boats, fails on main too), agent_control_e2e consume and pause-save. Thank you!
2026-10-29 12:53:55 +01:00
Ludovic MarieandClaude Opus 5.5 3c63c5913e run-parity: a push cancels the old head's run, other labels cancel nothing; cancel-on-close takes every head (#3651)
- run-parity.yml also starts on synchronize, only to cancel the queued or running run of the
  head that was replaced (its job runs on the run-parity label alone, as before).
- Labels other than run-parity get a concurrency group of their own: in the shared one, adding
  needs-proof cancelled a parity run queued 9 s before (run 40766180150).
- cancel-on-close cancels the closed pull request's runs on every head (its branch in its own
  repository), not only the last: #3104's run on an older head stayed queued after its merge.

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-29 12:52:10 +01:00
Ludovic Marie 7f870d7447 Merge pull request #3563 from d3grando/fix-3322
🤖 Agent: proven on the integration branch union/pt1-wave1 (22 PRs on main 13d7d9995): tools/pr_ready.py all green (clippy -D warnings, 1686 tests, the 140 affected vanilla harnesses pass), lan_e2e on both single-player paths (22 checks, the only failure, boats, fails on main too), agent_control_e2e consume and pause-save. Thank you!
2026-10-29 12:51:30 +01:00
Ludovic Marie f49659c78b Merge pull request #3586 from d3grando/fix-3246
🤖 Agent: proven on the integration branch union/pt1-wave1 (22 PRs on main 13d7d9995): tools/pr_ready.py all green (clippy -D warnings, 1686 tests, the 140 affected vanilla harnesses pass), lan_e2e on both single-player paths (22 checks, the only failure, boats, fails on main too), agent_control_e2e consume and pause-save. Thank you!
2026-10-29 12:51:26 +01:00
Ludovic Marie 62ff5241a4 Merge pull request #3566 from d3grando/fix-3034
🤖 Agent: proven on the integration branch union/pt1-wave1 (22 PRs on main 13d7d9995): tools/pr_ready.py all green (clippy -D warnings, 1686 tests, the 140 affected vanilla harnesses pass), lan_e2e on both single-player paths (22 checks, the only failure, boats, fails on main too), agent_control_e2e consume and pause-save. Thank you!
2026-10-29 12:51:22 +01:00
Ludovic Marie 4d460c7374 Merge pull request #3577 from d3grando/fix-3505
🤖 Agent: proven on the integration branch union/pt1-wave1 (22 PRs on main 13d7d9995): tools/pr_ready.py all green (clippy -D warnings, 1686 tests, the 140 affected vanilla harnesses pass), lan_e2e on both single-player paths (22 checks, the only failure, boats, fails on main too), agent_control_e2e consume and pause-save. Thank you!
2026-10-29 12:51:19 +01:00
Ludovic Marie d99f7d6d7b Merge pull request #3579 from d3grando/fix-3147
🤖 Agent: proven on the integration branch union/pt1-wave1 (22 PRs on main 13d7d9995): tools/pr_ready.py all green (clippy -D warnings, 1686 tests, the 140 affected vanilla harnesses pass), lan_e2e on both single-player paths (22 checks, the only failure, boats, fails on main too), agent_control_e2e consume and pause-save. Thank you!
2026-10-29 12:51:15 +01:00
Ludovic Marie 49c6c47f98 Merge pull request #3562 from d3grando/fix-3474
🤖 Agent: proven on the integration branch union/pt1-wave1 (22 PRs on main 13d7d9995): tools/pr_ready.py all green (clippy -D warnings, 1686 tests, the 140 affected vanilla harnesses pass), lan_e2e on both single-player paths (22 checks, the only failure, boats, fails on main too), agent_control_e2e consume and pause-save. Thank you!
2026-10-29 12:48:31 +01:00
Ludovic Marie b029efa3d3 Merge pull request #3454 from d3grando/fix-3308
🤖 Agent: proven on the integration branch union/pt1-wave1 (22 PRs on main 13d7d9995): tools/pr_ready.py all green (clippy -D warnings, 1686 tests, the 140 affected vanilla harnesses pass), lan_e2e on both single-player paths (22 checks, the only failure, boats, fails on main too), agent_control_e2e consume and pause-save. One small follow-up for later, not blocking: Player.updatePlayerPose keeps the desired pose for a passenger even where it doesn't fit (isPassenger), which update_player_pose doesn't check yet. Thank you!
2026-10-29 12:48:27 +01:00
Ludovic Marie 37b4ee3abd Merge pull request #3585 from d3grando/fix-3280
🤖 Agent: proven on the integration branch union/pt1-wave1 (22 PRs on main 13d7d9995): tools/pr_ready.py all green (clippy -D warnings, 1686 tests, the 140 affected vanilla harnesses pass), lan_e2e on both single-player paths (22 checks, the only failure, boats, fails on main too), agent_control_e2e consume and pause-save. Thank you!
2026-10-29 12:48:22 +01:00
Ludovic Marie b07f2775a2 Merge pull request #3555 from d3grando/fix-3424
🤖 Agent: proven on the integration branch union/pt1-wave1 (22 PRs on main 13d7d9995): tools/pr_ready.py all green (clippy -D warnings, 1686 tests, the 140 affected vanilla harnesses pass), lan_e2e on both single-player paths (22 checks, the only failure, boats, fails on main too), agent_control_e2e consume and pause-save. Thank you!
2026-10-29 12:48:17 +01:00
Ludovic Marie e831cb0333 Merge pull request #3504 from ASOwnerYT/fix/3425-potion-duration-padding
🤖 Agent: read in full (pr-triage-1), checked against vanilla 26.3 StringUtil.formatTickDuration (Mth.floor(ticks / tickrate), %02d on every field, hh:mm:ss from an hour up): the tooltip and the inventory's effect panel now share that one port. Client text only.
2026-10-29 12:48:13 +01:00
d3grandoandClaude Opus 5.5 63b8018505 Type AltGr characters into chat and search boxes on Windows
Windows reports AltGr as Ctrl plus Alt, and a layout's third-level
characters (ABNT2 `/` on Ctrl+Alt+Q, German `@ { [`, Polish letters)
arrive as text while a Control key is down. The chat box, the command
block's command, the world options' fields and the recipe and mod-panel
search boxes dropped any typed text while Control was held, so none of
these characters could be typed.

Vanilla's KeyboardHandler.textInput hands every code point to charTyped
whatever modifiers are held; EditBox.charTyped only checks
StringUtil.isAllowedChatCharacter. Text now reaches those fields unless
the edit modifier is held without Alt (where AltGr is Ctrl+Alt), so
Ctrl and Cmd shortcuts still type nothing.

Fixes #3001

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 18:46:23 +07:00
Federico TaglianiandClaude Sonnet 5.5 9c6325c1f8 Mods: Options > Mods > Settings opens the mod's page at the title screen
open_mod_screen returned early while the title was up, so the Settings
button at the title did nothing, and the mod screen was neither drawn nor
given clicks or keys there. Draw it like Options (before the title), route
its clicks and keys, and blur the panorama behind it. A mod's own screen
(ui.open-screen) still waits for a world.

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

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

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 12:27:23 +01:00
d3grandoandClaude Opus 5.5 675298eac2 Screenshots on OpenGL draw the frame where it can be read back
OpenGL swapchain images offer RENDER_ATTACHMENT only, so the surface is
configured without COPY_SRC, but the screenshot path copied the swapchain
image into the readback buffer anyway: wgpu's validation aborted the game
("Usage flags RENDER_ATTACHMENT ... do not contain required usage flags
COPY_SRC") on every screenshot under the GL backend.

A frame that is read back on such a surface is now drawn into a texture
that can be copied from, read back from there, and then drawn onto the
swapchain image (wgpu::util::TextureBlitter), so the window still shows
it. Other frames, and surfaces with COPY_SRC, draw straight to the
swapchain as before. The check in write_screenshot that refused every
readback on these surfaces goes with it.

Fixes #3510

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 18:23:20 +07:00
d3grandoandClaude Opus 5.5 8b8ab7fd5d The mouse wheel selects a bundle's item in the creative inventory
The wheel over a bundle only reached bundle_scroll on the survival
inventory and container screens. On the creative screen it always
scrolled the item grid, and the hover tick that clears the selection
(onStopHovering) didn't run there either.

CreativeModeInventoryScreen.mouseScrolled calls
AbstractContainerScreen.mouseScrolled first, which offers the wheel to
the hovered slot's ItemSlotMouseActions (BundleMouseActions), and only
scrolls the grid when none took it. takes_bundle_wheel names the
screens that do this (inventory and container menus, and creative), and
both the wheel dispatch and tick_bundle_hover use it.

Fixes #3211

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 18:23:06 +07:00
d3grandoandClaude Opus 5.5 2f3f8d7a7f Draw lightning bolts: sample inside the white sprite
The bolt quads (RenderTypes.lightning has no texture) sampled the atlas at
the white sprite's corner (u0, v0). That point is on the sprite's border,
where the atlas's nearest sampler picks the neighbouring transparent texels
for most fragments, so a bolt came out as a few scattered pixels in
singleplayer and on servers alike. Sample the middle of the sprite, as the
clouds do.

Fixes #3171

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

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

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
2026-10-29 16:15:54 +05:00
SolomonNumb1 0dcc9a15fd smithing cleanup 2026-10-29 16:14:17 +05:00
d3grandoandClaude Opus 5.5 17e0143745 Shader packs: a program the shader compiler panics on no longer crashes the game
Photon v1.4's gbuffers programs make naga's SPIR-V writer panic
("Expression [89] is not cached!") while wgpu-hal's Vulkan backend makes
the render pipeline in PackPost::new. Nothing caught it, so the game
crashed. The pack stays picked in horizon-graphics.txt, so every later
world load crashed again until the file was edited by hand (#3525).

- crash::contain runs a closure under catch_unwind and returns its
  panic as an error. The panic hook logs such a panic with its stack
  and makes no crash report.
- pack_post::checked, which builds every pack program's pipeline under
  a validation error scope, contains a panic too. The pass is skipped
  with a note, as a validation error already skips it. The rest of the
  pack runs, and the device keeps working.
- Any other panic while loading a pack (translation, PackPost::new)
  turns the pack off. Nothing runs in its place, it is unpicked and
  saved so the next start does not load it again, and the chat says so
  in red, as a refused Horizon pack already does.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 18:11:14 +07:00
Federico TaglianiandClaude Sonnet 5.5 9828599067 Wither: take no damage from wither friends (skulls, blasts)
WitherBoss.hurtServer rejects damage caused by a non-player mob in
#wither_friends (the undead, withers included). Horizon only filtered
the wither's own targeting, so skulls and explosions from one wither hurt
its neighbours, which then retaliated through HurtByTarget.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-29 12:10:07 +01:00
d3grandoandClaude Opus 5.5 8469dcc953 Graphics settings: the scrollbar can be held and dragged
On the Simple and Advanced graphics pages, the scrollbar was drawn but
GraphicsUi::click never checked for it. A press on it did nothing, and
a drag only moved a held slider, so the list could only be scrolled
with the wheel (#3552).

GraphicsUi now keeps its scroll in ui::Scroll, the AbstractScrollArea
port the other lists use:
- a press on the scrollbar column holds it, silently (updateScrolling);
- dragging moves the list with the pointer, and going above or below
  the list jumps to an end (mouseDragged);
- releasing lets go (onRelease).
The column is computed once, for both drawing and input.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 18:09:08 +07:00
Ludovic Marie 8a290d810a Merge pull request #2533 from kacpherek/fix/server-mob-animation-sync-2
🤖 Agent: PR triage (Ludovic's AI agent, Claude): main merged in (ff, no conflict). The PR's diff is the one proven in cg19's union A2 at e5ca012ed (server-thread clients animate ravager, hoglin, iron golem, creaking, vex, allay…; entity event and data ids checked against 26.3 then). union/pt3-1 edfb0e31d (main 2b294bbe3 + 11 PRs): clippy -D warnings clean, tests 1342/0. CI parity on this head.
2026-10-29 12:07:58 +01:00
Ludovic MarieandClaude Opus 5.5 dffd3973bc Merge main into perf/far-tiles-biome-memo
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 12:07:12 +01:00
Ludovic MarieandClaude Opus 5.5 84185641bc Merge main into perf/worldgen-surface-biomes
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 12:07:11 +01:00
Ludovic MarieandClaude Opus 5.5 e60455d12c Merge main into perf/worldgen-biome-stage
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 12:07:11 +01:00
Ludovic MarieandClaude Opus 5.5 043f9c7b7b Merge main into perf/worldgen-structure-climate
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 12:07:11 +01:00
d3grandoandClaude Opus 5.5 9f28fe8903 startup: report why resource loading failed instead of "loader stopped"
The loader's result was taken by App::about_to_wait to end loading, but the
event loop polls once more after exit(): the loader had finished, its channel
was closed, and poll_menu_resources replaced the real error with "resource
loader stopped before finishing". Every failed load (missing or stale prepared
files) ended with that message.

- Loading keeps its first error and stops polling once Done has arrived.
- A loader that really panicked reports its panic message.
- Before any window or loader, a missing horizon-resources.json names the
  folder it was looked for in and how to prepare it (the launcher, or
  horizon-prepare --prepare).

Fixes #3597

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 18:07:05 +07:00
Ludovic Marie 43e7700791 Merge pull request #3447 from d3grando/fix-3396
🤖 Agent: read in full (pr-triage-1). Item names use Util.makeDescriptionId's key in the item's own namespace (item.<ns>.<path with / as .>, then block.), so mod items translate; vanilla items unchanged. Approved by rdbtCVS.
2026-10-29 12:00:14 +01:00
Ludovic MarieandClaude Opus 5.5 c0b3b5b3e9 Launcher: the night sky behind every page
sky.rs draws the night look's backdrop: the sky in texel bands, stars that
twinkle in steps, three layers of hand-drawn clouds drifting a texel at a
time, a sun cut in slices and two skylines. Sprites are baked once per
appearance and scale into tiles that don't depend on the window's size; a
frame is asked for only when something on screen changes, never under
reduced motion (macOS, or HORIZON_LAUNCHER_MOTION=off), and not 10 s after
the window loses focus. It replaces the stone texture and Home's landscape.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 11:58:28 +01:00
iLostName 88a001b7fa resource packs: a zip that stores pack.mcmeta as pack.mcmeta/ loads, as ZipFile.getEntry finds it
Java's ZipFile.getEntry falls back to the folder entry `name/` when no file has the name, and vanilla reads its bytes like a file's; Horizon left folder entries out, so Loka's server pack had no metadata and the reload failed. Fixes #3413.
2026-10-29 11:50:45 +01:00
Ludovic Marie e932a2da61 Merge remote-tracking branch 'origin/main' into upd/2924
# Conflicts:
#	crates/horizon-client-render/src/options_ui.rs
2026-10-29 11:47:35 +01:00
Ludovic Marie edd11b6103 Merge remote-tracking branch 'origin/main' into upd/3401 2026-10-29 11:47:06 +01:00
Ludovic Marie 73ae9f6cf1 Merge branch 'upd/3478' into upd/3479 2026-10-29 11:46:40 +01:00
Ludovic Marie 69fc342a1a Merge remote-tracking branch 'origin/main' into upd/3478
# Conflicts:
#	crates/horizon-client/src/game/world_loading.rs
#	crates/horizon-client/src/vanillagen.rs
#	crates/horizon-world/src/worldgen/generator.rs
2026-10-29 11:46:37 +01:00
Ludovic Marie ad7904946e Merge remote-tracking branch 'origin/main' into upd/3477
# Conflicts:
#	crates/horizon-world/src/worldgen/generator.rs
2026-10-29 11:45:59 +01:00
Ludovic Marie 4baa571566 Merge pull request #2819 from dreadpiratejoelius/fix/remote-player-teams
🤖 Agent: gate cg29: parity failure at 5f80ddffc was the remote_fireball wind-charge load flake. Gate fix 48ab8b093: team-push puts both players back (it left the guest's next spot inside a hill, pvp failed after it on the server thread). Team by profile name matches Player.getScoreboardName / createEntityFromPacket. union/cg29-1 (main b19b4e92d + #3104 #2799 #2819 #2812): fmt, clippy -p horizon-client/-client-render/-client-core --all-targets -D warnings, tests 1334/0, fast build; lan_e2e team-push pvp frames minecarts boats deflect cushions commands tab on the server thread and --in-frame all pass (team-push -> pvp -> tab both paths with the fix). CI parity on this head.
2026-10-29 11:43:56 +01:00
iLostName 2677a397e5 invisibility: the player's own body is hidden in third person and the inventory, its armour still drawn
The model check looked up `minecraft:invisibility`, but the player's effects are kept without the namespace, so the body always showed. As LivingEntityRenderer.submit does, the body is now skipped unless the player's team sees friendly invisibles, while armour, elytra and held items still draw. Fixes #3615.
2026-10-29 11:42:52 +01:00
Ludovic MarieandClaude Opus 5.5 a7e279cfed Launcher: Java instances' mods follow their loader, and copies keep theirs
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 11:36:30 +01:00
Ludovic Marie 7c3364d30d Merge pull request #3627 from horizon-rust/ci/nightly-own-runner
🤖 Agent: the nightly's untrusted macOS jobs move to ludovics-mac-mini-nightly (registered, online, same trust as horizon-game, never the release runner). test_nightly updated; verify green.
2026-10-29 11:33:22 +01:00
Ludovic MarieandClaude Opus 5.5 2fe5fa0939 Launcher: new Microsoft accounts get a token that joins online-mode servers
The Minecraft token now comes from launcher/login, with login_with_xbox as the fallback.

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 12:15:38 +02:00
Ludovic Marie 1816cf0b52 Merge pull request #3612 from horizon-rust/feat/launcher-import-launchers
🤖 Agent: imports from Prism/MultiMC/official launcher, read-only on their folders (copy into a new Java instance), formats from public docs. Verify green; matrix 40853481205 green on Windows, macOS and Linux.
2026-10-29 11:15:16 +01:00
Ludovic Marie 258c5946b5 Merge pull request #3603 from horizon-rust/feat/launcher-java-modrinth
🤖 Agent: Modrinth for Java instances, filtered by version and loader; downloads go through download::save (safe names, checksums). Verify green; matrix 40853843158 green on Windows, macOS and Linux.
2026-10-29 11:15:11 +01:00
deloskiyt c6fbd42aee Audio: play break sound and track stats when tools break while mining (#3524)
Fixes missing item break sound effect and broken statistic tracking:
- When a tool breaks from mining a block (mine_block_with_held), route
  durability wear through damage_held rather than clearing the slot silently.
- Route weapon wear in mace.rs and brush wear on armadillos in creatures.rs
  through damage_held.
- Play entity.item.break sound and re-apply equipment when worn armor
  breaks from Thorns in enchant_effects.rs.
2026-10-29 11:13:20 +01:00
Ludovic Marie 452224a28a Merge pull request #3617 from horizon-rust/docs/native-mods-on-after-install
🤖 Agent: docs only (native-mods.md); pr_ready --quick green (408 Python tests); skip-release.
2026-10-29 11:09:54 +01:00
Ludovic Marie ca1b200c79 Merge remote-tracking branch 'origin/main' into upd/3391
# Conflicts:
#	crates/horizon-world/src/worldgen/generator.rs
2026-10-29 11:09:33 +01:00
Ludovic Marie b603db6176 Merge remote-tracking branch 'origin/main' into upd/3390
# Conflicts:
#	crates/horizon-world/src/worldgen/generator.rs
2026-10-29 11:09:18 +01:00
Ludovic Marie 779b773530 Merge remote-tracking branch 'origin/main' into upd/3367
# Conflicts:
#	crates/horizon-world/src/worldgen/generator_decoration.rs
2026-10-29 11:08:25 +01:00
Mik00000 7fb1f77870 Address review: drop overlapping spectral arrow paths and remove double arms 2026-10-29 12:06:20 +02:00
GIGABAIT93andClaude Opus 5.5 8a9db2dc7b Fonts: Pixelify Sans gets the Cyrillic І
Ukrainian headings, buttons and navigation need the capital І (U+0406),
which upstream doesn't map, so it came from the fallback font, smooth
and taller than the pixel letters around it. As #2844 did for О, it
takes each weight's own Latin I glyph, the same shape. Only cmap changes
(and head's checksum adjustment); every other table, the timestamps
included, is byte-identical to the files it replaces.

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

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

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

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

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

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

Fixes #3027.

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

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 11:23:53 +02:00
levzzzandOpenAI Codex 69689b3fdd Cover named equipment replacement through save and reload
Co-Authored-By: OpenAI Codex <[email protected]>
2026-10-29 14:23:48 +05:00
Ludovic Marie 99b52c2617 Merge remote-tracking branch 'origin/main' into feat/launcher-java-play
# Conflicts:
#	launcher/lang/fr.json
#	launcher/lang/ru.json
#	launcher/src/main.rs
2026-10-29 10:23:05 +01:00
Ludovic Marie 77bfff8eb5 Merge pull request #3595 from horizon-rust/docs/roadmap-triage-10-29
🤖 Agent: docs only (roadmap.toml + generated ROADMAP.md/roadmap.json + README). roadmap.py build --check and check --offline pass; verify green; parity correctly skipped.
2026-10-29 10:22:55 +01:00
Ludovic Marie 8ea9a2c849 Merge remote-tracking branch 'origin/main' into fix/launcher-native-mods-auto-on
# Conflicts:
#	launcher/lang/fr.json
#	launcher/lang/ru.json
2026-10-29 10:20:52 +01:00
Ludovic MarieandClaude Opus 5.5 cc15e8ebb3 Rename the hitbox view's eye_positions to eye_motions
Main's c5fe5d4bc added EndState::eye_positions (id and position, for
status); the hitbox view needs the previous and current positions.

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

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

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

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

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

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

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

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

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

Fixes #3040.
Fixes #3042.

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

Fixes #3008.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
2026-10-29 14:02:41 +05:00
levzzzandOpenAI Codex ace27c11a9 Keep named held stacks current and use full death-message display names
Co-Authored-By: OpenAI Codex <[email protected]>
2026-10-29 14:01:23 +05:00
levzzzandOpenAI Codex 8742e1768e Merge current main before isolating fluid checks
Co-Authored-By: OpenAI Codex <[email protected]>
2026-10-29 14:01:20 +05:00
levzzzandOpenAI Codex c91ca07115 Merge current main and use its preset-aware terrain interface
Co-Authored-By: OpenAI Codex <[email protected]>
2026-10-29 13:58:50 +05:00
levzzzandOpenAI Codex c001f56d0f Merge current main before death-message review fixes
Co-Authored-By: OpenAI Codex <[email protected]>
2026-10-29 13:58:45 +05:00
d3grandoandClaude Opus 5.5 e086b2cfdd Wolves: any target makes a wolf angry, so an alerted pack looks angry
Wolf.aiStep ends with NeutralMob.updatePersistentAnger(level, true): a
wolf with a target is angry at it, and the 20-39 s timer
(PERSISTENT_ANGER_TIME) starts over every tick it holds one. Horizon only
set the anger in hurtServer when the player hit the wolf, so the pack
mates HurtByTargetGoal alerted attacked without the angry texture, and the
random draws of an angry wolf came in the wrong order. isAngryAt(player)
now reads the anger's target, and stopBeingAngry ends it when the time
runs out, the target mob dies, or the player is in creative, spectator
or peaceful. Saving a wolf keeps that anger target instead of replacing
it with the current target.

The mob_ai harness prints a wolf's anger and gets a wolf_hurt scenario.

Fixes #3185

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 15:58:42 +07:00
levzzzandOpenAI Codex b36a63fb39 Merge current main and retain vanilla features save status
Co-Authored-By: OpenAI Codex <[email protected]>
2026-10-29 13:58:03 +05:00
v3nn7andCodex 65043ba092 Sync furnace minecart fuel exhaustion
Co-Authored-By: Codex <[email protected]>
2026-10-29 09:56:12 +01:00
GIGABAIT93andClaude Opus 5.5 0f4880bacd Python tools and tests: the same results on Windows
port_publish names the version store with forward slashes, so an issue
reads the same whoever publishes it (test_port_publish expected
~/code/... and Windows wrote ~\code\...), and writes the issue body as
UTF-8 for gh whatever the code page. test_roadmap reads ROADMAP.md as
UTF-8. All 408 Python tests now pass on Windows, with or without
PYTHONUTF8.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 10:55:09 +02:00
Ludovic Marie d203ca04be Merge pull request #2894 from hectoreccles/lod-chorus-vegetation
🤖 Agent: requeued by contrib-gate-28 after merging main (B9 render crate split; stopped on a conflict or a known flake). fmt, clippy -D warnings and tests pass on the new head; proven earlier by cg26/cg27.
2026-10-29 09:54:49 +01:00
d3grandoandClaude Opus 5.5 daa5415724 Paintings: the item's variant is placed and named in its tooltip
A painting item with a `painting/variant` component (every one but the
first in the creative tab) placed a random painting and showed only
"Painting". HangingEntityItem.useOn runs Painting.create's random pick,
then EntityType.createDefaultStackConfig applies the stack's components
(Painting.applyImplicitComponents sets the variant) and the painting must
still survive. HangingEntityItem.appendHoverText adds the variant's title,
author and size, or "Random variant" in a creative tab without one.

The component also crosses the wire now (a painting_variant holder,
component 109): a creative client's pick reaches the server, and the
server's stacks keep their variant on the client.

Fixes #3503

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 15:54:33 +07:00
v3nn7andCodex f2edc255d8 Render carved pumpkins on player heads
Co-Authored-By: Codex <[email protected]>
2026-10-29 09:54:31 +01:00
v3nn7 f6144df193 Show powered furnace minecarts on remote clients 2026-10-29 09:53:17 +01:00
Username59138andClaude Opus 5.5 3451e4e29a Command blocks: keep the command in the edit screen, suggest as vanilla
Command blocks barely worked from the default single player (the window
is a client of its server thread), a LAN guest or a dedicated server:

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

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 11:53:14 +03:00
d3grandoandClaude Opus 5.5 4bd3210f78 Commands: coordinate suggestions skip entities and offer the hit point
ClientSuggestionProvider.getRelevantCoordinates and getAbsoluteCoordinates
read Minecraft.hitResult only when it is a block hit. Horizon took the
block behind a mob under the crosshair, and Vec3/Vec2 arguments (summon,
tp, worldborder center...) only ever offered ~. Now an entity before the
block leaves the ~ defaults, and Vec3/Vec2 offer the exact hit point with
two decimals (prettyPrint: "%.2f", Locale.ROOT).

Fixes #3427

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 15:52:30 +07:00
levzzzandOpenAI Codex bd59c0bab2 Pick up validated standard macros for body hooks
Co-Authored-By: OpenAI Codex <[email protected]>
2026-10-29 13:47:44 +05:00
levzzzandOpenAI Codex d3cb2f5c29 Pick up HEAD macro preservation in the launcher mixins pin
Co-Authored-By: OpenAI Codex <[email protected]>
2026-10-29 13:47:44 +05:00
d3grandoandClaude Opus 5.5 a30b9525d1 Horses rear when hurt
AbstractHorse.hurtServer had no port: after a hurt that went through,
vanilla draws random.nextInt(3) and on 0 calls standIfPossible, which
rears the horse for 20 ticks unless canPerformRearing is false (llamas,
camels). Mob::damage_with now ends with that roll for every equine, and
make_mad shares the new stand_if_possible.

The stand counter runs down in AbstractHorse.tick, with or without AI,
but Horizon ran it down only in the AI step, so a NoAI horse that reared
from a hit stayed reared for good. A mob with no AI now runs it down too.

The mob_ai harness gets a llama_hurt scenario (the draw without the
rear), which matched for 10 ticks before and all 201 after; the probe
names MountPanic and LlamaHurtBy as vanilla does.

Fixes #3432

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 15:45:42 +07:00
v3nn7andCodex e851af06d0 Add Polish translations
Co-Authored-By: Codex <[email protected]>
2026-10-29 09:43:11 +01:00
vadyanikandClaude Sonnet 5.5 6255c02096 Follow the XKB keymap for Escape and Caps Lock
Key events carry the scancode's US-layout code, which ignores options like
caps:swapescape. Take Escape and Caps Lock from the resolved keysym instead.

Fixes #3133

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

Fixes #3028.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
2026-10-29 13:39:03 +05:00
d3grandoandClaude Opus 5.5 ced6323bb2 Single player: save when the game pauses
IntegratedServer.tickServer saves everything on the tick the game
becomes paused and logs "Saving and pausing game...". Horizon's server
thread only stopped ticking, so a paused world was saved no sooner than
the next autosave or quit. The server thread now saves (as the autosave
does) once each time the game pauses.

agent_control_e2e gets pause-save: opening the pause menu writes
level.dat.

Fixes #3280

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 15:36:00 +07:00
d3grandoandClaude Opus 5.5 698e7fd662 Input: release held keys when leaving a server
Keys held when the player left a server stayed in Game::keys: once the
title or disconnect screen is open their releases go to that screen, so
W and Space held through a LAN host closing the world walked and jumped
on in the next world. Vanilla's Gui.setScreen calls KeyMapping.releaseAll
whenever a screen opens; leave_server now releases the held keys and the
attack and use buttons before it shows the title or disconnect screen.

lan_e2e gets keys_released: a guest holding W and Space leaves and
rejoins, and must stand still with no keys held.

Fixes #3246

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 15:35:58 +07:00
d3grandoandClaude Opus 5.5 cf0b5a8e63 Mobs: undead with NoAI burn in daylight
NoAI only skips Mob.serverAiStep; Mob.aiStep still calls burnUndead, so
a NoAI zombie at noon catches fire like any other. Horizon skipped the
whole of ai_step for NoAI mobs, and the daylight burn with it. The burn
is now its own burn_undead, run with or without AI.

The mob_ai harness gets zombie_noai_day_burn (a NoAI zombie in the sun).

Fixes #3430

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 15:35:53 +07:00
SenjinandClaude Opus 5.5 ef2ec4c0dc CI: run the checks again (horizon-server's lists test got no answer from Mojang's services)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 09:33:06 +01:00
d3grandoandClaude Opus 5.5 7808916266 Ominous bottles list their Bad Omen level and duration in the tooltip
OminousBottleAmplifier.addToTooltip lists MobEffectInstance(BAD_OMEN,
120000, amplifier) as PotionContents.addPotionTooltip does: blue (NEUTRAL),
"Bad Omen IV (1:40:00)". Nothing drew it, and the amplifier never reached
the client either: the server's network_stack left component 65 out and the
protocol read it as raw bytes. It now goes over the wire as a VarInt
(OminousBottleAmplifier.STREAM_CODEC) and item_sync decodes it.

Fixes #3501

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 15:32:08 +07:00
d3grandoandClaude Opus 5.5 53beacfd13 Let the off hand go when a bucket, bottle or boat has no target
On a server client (single player included) remote_use_item predicted
SUCCESS for every bucket, glass bottle and boat, which ended
Minecraft.startUseItem before the off hand's turn: a Riptide trident
there never charged. BucketItem, BottleItem and BoatItem.use PASS or
FAIL with nothing to fill, empty or place, and startUseItem stops only
on SUCCESS. The client now checks the target first (a waterlogged block
counts as a water source, as SimpleWaterloggedBlock is a BucketPickup);
place_boat is split into boat_to_place (no side effects) so both share
BoatItem's checks. A powder snow bucket (SolidBucketItem, only useOn)
always passes its use.

Fixes #3147

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 15:31:50 +07:00
d3grandoandClaude Opus 5.5 11d3f08c12 Wither: broken blocks give way to their fluid, so water stays water
A wither's block breaking (WitherBoss.customServerAiStep) calls
Level.destroyBlock, which sets the cell to fluidState.createLegacyBlock()
with flag 3. Horizon's mob BreakBlock event wrote air instead, without
neighbour updates: a wither in water carved an air pocket, a waterlogged
block lost its water, and the pool did not flow back in. The event now
sets the fluid's block through setBlock (skipped when that is already
the cell), and the wither's break sound (level event 1022) only counts
cells that changed, as destroyBlock's return value does.

Checked by a new "wither breaks" group in the command execution harness.

Fixes #3153

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 15:28:03 +07:00
d3grandoandClaude Opus 5.5 6a59002283 Creative tooltips list the tabs that hold the item, as vanilla does
CreativeModeInventoryScreen.getTooltipFromContainerItem keeps the plain
tooltip only for a category tab's own grid slots. Everywhere else in the
creative screen (the search tab, the hotbar row under a category tab,
the inventory tab) it puts, after the name, the display name of every
tab whose CreativeModeTab.contains(stack) holds, in blue, and on the
search tab's grid during a `#` search the stack's visible tags
(updateVisibleTags), in dark purple. Horizon drew the plain tooltip
everywhere.

Creative::tabs_containing matches a tab's stacks by item and components;
the search tab's own stacks that no tab shows (enchanted books below
their top level) belong to the tab that shows their item.
Creative::search_tag_lines follows updateVisibleTags' matching, and
draw_stack_tooltip_with inserts the lines from line 1.

Fixes #3426

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 15:27:35 +07:00
d3grandoandClaude Opus 5.5 ab9ceb6a6d Wither: stay still while charging, and spare itself from its spawn blast
While a wither charges (Invul > 0), WitherBoss.customServerAiStep returns
before setAlternativeTarget(0, ...), so aiStep has no target to fly to.
Horizon stored the target's position first, so the charging wither flew
toward whatever it targeted. The position is now taken after the charge.

The blast that ends the charge is level.explode(this, ...): the wither is
the source and ServerExplosion's getEntities(source, box) leaves it out.
The wither now names itself as the explosion source (as a creeper does),
and the blast's mob loop skips a source named that way, so it is neither
hurt nor pushed. A source only guessed from the mobs near the centre (a
sulfur cube, already off the list) may be a bystander and is still hurt.

The command_exec corpus gets a "wither spawn" group: a wither summoned
with Invul:20 next to a pig, its position mid-charge and its health after
the blast.

Fixes #3322

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 15:25:46 +07:00
d3grandoandClaude Opus 5.5 46d466f7c3 An eye of ender stays in use while the button is held, slowing the player
EnderEyeItem.use calls startUsingItem before it looks for a stronghold
(unless an End portal frame is aimed at), and getUseDuration is 0, so the
use never completes: the player is slowed until the button lets go, with
or without a stronghold, and a held button throws one eye, not one every
four ticks. The server and in-frame use now start it, and a server's
client starts it too and keeps it like a bow's until released.

Fixes #3505

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 15:24:52 +07:00
d3grandoandClaude Opus 5.5 113cafd2d1 Copper golem eyes glow on the body model, not the empty eyes layer
CopperGolemRenderer's LivingEntityEmissiveLayer (RenderTypes.eyes) draws
the eye texture on a CopperGolemModel baked from ModelLayers.COPPER_GOLEM,
the body's layer. Horizon drew it on copper_golem#eyes, whose prepared model
has no polygons, so the eyes showed the body texture's black sockets.

Fixes #2862

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 15:24:31 +07:00
d3grandoandClaude Opus 5.5 ab640c7646 Respawn anchor: glowstone charges it on a server instead of being placed
On a server (and the default server-thread single player) the client runs
the block's useItemOn on a copy of its level to decide whether the click
ends there. horizon-world's use_item_on had no RespawnAnchorBlock case, so
glowstone on an anchor fell through to useWithoutItem (PASS on an empty
anchor) and the client predicted a glowstone block that the server then
took back.

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

Fixes #3034

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

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

Fixes #3158

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

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

Fixes #3431

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

Fixes #3136

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

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

Fixes #3209

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

Fixes #3424

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

Fixes #3243

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

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

Fixes #3173

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

Fixes #3429

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

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

Fixes #3474

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

Fixes #3289

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

Fixes #3293

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

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

Fixes #3381

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

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

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

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

* Document the port ledger

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

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

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

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

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

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

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

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

* Document class homes, structural matches and the verdict map

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

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

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

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

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

* Map client rendering, particles, sounds and resources

* map-check: accept full Class#method keys

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

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

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

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

* Follow the furnace code to furnace.rs on main; answer Fox.getBreedOffspring
2026-10-29 08:15:06 +01:00
Ludovic MarieandClaude Opus 5.5 0c9a49390e Crates: remove the WebAssembly mod runtime, its SDK and the official WebAssembly mods
The game no longer runs WebAssembly code mods (the previous commit), so the
workspace drops what made and ran them: horizon-mods' runtime, commands and
test harness (wasmtime, wit-component, wit-parser), the SDK and its WIT
contract and examples, the sandbox's guest crates, and mods/official (the
official mods are native now, one native/<name>/ folder each in horizon-rust/horizon-mods).

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Fixes #3408

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Fixes #3425.

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

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

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

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

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

Fixes #3144.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Fixes #2827.

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

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

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

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

Fixes #3308

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

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

Fixes #3357

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

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

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

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

Fixes #3319

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

Fixes #3302

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

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

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

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

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

Fixes #3396

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

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

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

Fixes #3407

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

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

Fixes #3414

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 04:18:46 +05:00
Ludovic Marie 791d8b8950 Merge pull request #3434 from horizon-rust/feat/native-hooks-v2-tables
🤖 Agent: hook points v2, part 1/2 (recipe tables and helpers for native mods). pr_ready green on the stack's head 1dba99f: clippy, 1286 client tests, parity 33/33 selected; the native Recipes mod builds on it and its proof passes. Merging it queues part 2, #3435.
2026-10-29 00:16:10 +01:00
Joansitoh 90014f63d2 Windows: link untouched directories into the pack overlay with junctions, not a file-by-file mirror
Refs #3385, #3234
2026-10-29 00:15:45 +01:00
ThatCakeIsAPieandClaude Opus 5.5 a849545f63 Harness: chunks generated with a world's data packs against vanilla
test_datapack_worldgen_parity.py runs the whole-chunk oracle and
chunk_dump with HORIZON_DATAPACKS: the oracle selects every pack of the
folder after vanilla through vanilla's pack repository, chunk_dump reads
them through worldgen::overrides into GeneratorData::load_with.
tools/fixtures/datapacks holds Horizon's own pack (MIT), overriding a
noise, a density function, the overworld noise settings, a placed
feature and a biome tag, and adding a noise and two density functions.
Each site must match vanilla with the pack and differ from Horizon
without it. HORIZON_PARITY_DATAPACKS compares another folder of packs.

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

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

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

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

Nothing calls it yet.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Fixes #1703. Fixes #3073.

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 23:08:34 +01:00
Ludovic Marie 9f1a998403 Merge pull request #2833 from levzzz5154/fix/empty-compressed-packet
🤖 Agent: needs-proof check by Ludovic's AI agent (Claude), not by Ludovic himself. Passed on a union of 19 contributor PRs: clippy -D warnings, the touched crates' tests (1391 passed), 80/80 parity harnesses, and lan_e2e with the host on its server thread and in-frame. zero-length frame rejected as Varint21FrameDecoder does. Queued for merge at 65bca4ce9.
2026-10-28 23:07:10 +01:00
Ludovic Marie 527cd7225c Merge pull request #2816 from sirrayi/fix/malformed-network-input
🤖 Agent: needs-proof check by Ludovic's AI agent (Claude), not by Ludovic himself. Passed on a union of 19 contributor PRs: clippy -D warnings, the touched crates' tests (1391 passed), 80/80 parity harnesses, and lan_e2e with the host on its server thread and in-frame. Queued for merge at 2812cf201.
2026-10-28 23:07:06 +01:00
Ludovic Marie f64fdeaa75 Merge pull request #2813 from sirrayi/fix/guest-block-reach
🤖 Agent: needs-proof check by Ludovic's AI agent (Claude), not by Ludovic himself. Passed on a union of 19 contributor PRs: clippy -D warnings, the touched crates' tests (1391 passed), 80/80 parity harnesses, and lan_e2e with the host on its server thread and in-frame. reach checks match ServerGamePacketListenerImpl.handleUseItemOn/handleBlockBreakAction; digging/mining/instant_digging lan_e2e PASS. Queued for merge at 3be828d01.
2026-10-28 23:07:01 +01:00
Ludovic Marie a0c79c8b26 Merge pull request #2805 from ASOwnerYT/fix/2775-jukebox-offhand-eject
🤖 Agent: needs-proof check by Ludovic's AI agent (Claude), not by Ludovic himself. Passed on a union of 19 contributor PRs: clippy -D warnings, the touched crates' tests (1391 passed), 80/80 parity harnesses, and lan_e2e with the host on its server thread and in-frame. jukebox use read against JukeboxBlock.useItemOn/useWithoutItem. Queued for merge at b38fe43b0.
2026-10-28 23:06:57 +01:00
Ludovic Marie 0284d3bb16 Merge pull request #2952 from kacpherek/fix/lan-block-entity-data
🤖 Agent: needs-proof check by Ludovic's AI agent (Claude), not by Ludovic himself. Passed on a union of 19 contributor PRs: clippy -D warnings, the touched crates' tests (1391 passed), 80/80 parity harnesses, and lan_e2e with the host on its server thread and in-frame. screenshot: the single-player window (server thread) now shows banner patterns and the vault's display item (main: plain banner, empty vault). Follow-up: the trial spawner's mob still does not show on the server-thread window (it does in-frame), not a regression. Queued for merge at 1deb3bd36.
2026-10-28 23:06:54 +01:00
degepie 57a6f4e9aa Merge remote-tracking branch 'origin/main' into fix/goat-horn-instrument-sync
# Conflicts:
#	crates/horizon-client/src/item_sync.rs
#	crates/horizon-protocol/src/item.rs
2026-10-29 03:06:31 +05:00
degepie ac7e491f3b Merge remote-tracking branch 'origin/main' into fix/issue-3137-suspicious-stew
# Conflicts:
#	crates/horizon-protocol/src/item.rs
2026-10-29 03:04:30 +05:00
Ludovic Marie 72b4d8ecbb Merge pull request #3395 from horizon-rust/feat/launcher-java-instances
🤖 Agent: launcher-java-1 verified this head: PR checks green (verify, launcher) and the launcher build matrix green on Windows, macOS and Linux (dry run https://github.com/horizon-rust/horizon/actions/runs/40775337700); docs/launcher-commands.md is regenerated.
2026-10-28 23:02:35 +01:00
Ludovic Marie 1ed31e54eb Merge pull request #3062 from kacpherek/fix/shield-arm-and-banner
🤖 Agent: needs-proof check by Ludovic's AI agent (Claude), not by Ludovic himself. Passed on a union of 19 contributor PRs: clippy -D warnings, the touched crates' tests (1391 passed), 80/80 parity harnesses, and lan_e2e with the host on its server thread and in-frame. screenshot: patterned shields in the hotbar and offhand on the single-player window (main: plain). Queued for merge at 99008baad.
2026-10-28 23:00:42 +01:00
Ludovic Marie b3aaa04129 Merge pull request #3055 from kacpherek/fix/horse-feeding-particles-on-server
🤖 Agent: needs-proof check by Ludovic's AI agent (Claude), not by Ludovic himself. Passed on a union of 19 contributor PRs: clippy -D warnings, the touched crates' tests (1391 passed), 80/80 parity harnesses, and lan_e2e with the host on its server thread and in-frame. horse/llama/camel foods match handleEating (no isAgeLocked check: the client has no synced flag). Queued for merge at 9d32bebad.
2026-10-28 23:00:37 +01:00
Ludovic Marie bb38695695 Merge pull request #3052 from kacpherek/fix/hand-drops-after-use-on-server
🤖 Agent: needs-proof check by Ludovic's AI agent (Claude), not by Ludovic himself. Passed on a union of 19 contributor PRs: clippy -D warnings, the touched crates' tests (1391 passed), 80/80 parity harnesses, and lan_e2e with the host on its server thread and in-frame. Queued for merge at e24394ca3.
2026-10-28 23:00:33 +01:00
Ludovic Marie 6bda112225 Merge pull request #3050 from kacpherek/fix/disconnect-no-wait
🤖 Agent: needs-proof check by Ludovic's AI agent (Claude), not by Ludovic himself. Passed on a union of 19 contributor PRs: clippy -D warnings, the touched crates' tests (1391 passed), 80/80 parity harnesses, and lan_e2e with the host on its server thread and in-frame. Queued for merge at a5ee1e588.
2026-10-28 23:00:29 +01:00
Ludovic MarieandClaude Opus 5.5 d3559e2a5f Proceed past the safety notice in the F5 server list test
main's f5_reloads_the_server_list (4c4fcedf4) came after this branch: its first Multiplayer click now opens SafetyScreen.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 23:00:28 +01:00
Ludovic MarieandClaude Opus 5.5 c90893c29c Merge main into fix/multiplayer-safety-notice
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 23:00:18 +01:00
Ludovic MarieandClaude Opus 5.5 dcd733c7c1 Launcher: play a Java instance from the command line
`horizon-launcher play --instance <java instance>` starts its Minecraft:
Java Edition. Past the owner gate itself (no developer option), the
version is downloaded from Mojang the first time, the Java it needs is
the instance's own, one found here or Mojang's (downloaded), and the
game starts in the instance's folder as the account that owns it. With
no answer from the services, an owner plays with its last token.

Options only Horizon has (--version, --world, --creative, --join,
--server) are refused for Java instances; --arg passes through. The
window's Play still refuses them and names this command.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 22:59:59 +01:00
Ludovic Marie 77fa9fb89f Merge pull request #3388 from horizon-rust/fix/native-mods-mixins-cargo
🤖 Agent: verify green; launcher-only, one function. pr_ready green (launcher tests 228/228). Parity --changed: nothing selected (lan_integration is nightly-only, and the launcher is not in the LAN suite). Proof end to end with no system Rust: the launcher's toolchain built zoom on macOS (546 s) and Linux (729 s, launcher from this branch's dry-run 40767814775), and zoom's offscreen proof passed on both launcher-built games. Before the fix both stopped at E_METADATA.
2026-10-28 22:58:12 +01:00
Ludovic Marie 153d18a102 Merge pull request #3346 from horizon-rust/feat/native-mod-settings
🤖 Agent: verify green. pr_ready on the Mac mini (vanilla data): fmt, clippy, horizon-client 1264 tests, Python 401, parity 75 affected, 74 passed, none failed. Earlier offscreen proofs: the four native mods listed ON with working settings pages. New on/off commit tested by mod_list::tests::toggles_installed_and_native_mods_but_not_dev_ones; horizon-mods#22's mods build on this head with native_on (check all ok).
2026-10-28 22:55:57 +01:00
spiderandClaude Opus 5.5 e2b2b2ccd0 Players wear their capes: CapeLayer, the cloak's swing, and the elytra in the cape's texture
A player's cape texture was looked up and downloaded, then thrown away: nothing kept the image
and nothing drew it. Now:
- Cape downloads go into the skin slots as `capes/<sha1>` (SkinManager's cape cache), a 64x32
  texture in the top half of a 64x64 slot; an evicted one uploads again from memory or the disk
  cache. A larger cape is scaled to 64x32, the older 22x17 fits as it is.
- `PlayerSkin.cape` reaches the player models (other players and the local one), which load
  `player#cape` (PlayerCapeModel).
- CapeLayer draws it: no cape under an elytra's wings, moved out over a chestplate, hidden with
  the Cape skin-customisation option (the local player). PlayerCapeModel.setupAnim turns the cape
  by `ModelPart.rotateBy` with AvatarRenderer.extractCapeState's flap and leans, from
  ClientAvatarState's cloak (moveCloak: a quarter of the way each tick, snapping from 10 blocks),
  its bob and walked distance; other players tick theirs as RemotePlayer does.
- WingsLayer.getPlayerElytraTexture: a player with a cape wears it on the elytra.
- `HORIZON_OFFLINE_CAPE`, which the launcher sets for an offline name, is the local player's
  cape when its profile has none.

Other players' Cape option (Avatar.DATA_PLAYER_MODE_CUSTOMISATION) is left to #2971, which
tracks it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 22:55:56 +01:00
Ludovic MarieandClaude Opus 5.5 b85a9e8350 Merge main into fix/server-core-parity
main added the pack prompt and server dialogs to the screens that stay open when the window loses focus;
they move into pause_if_inactive with the rest of that check.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 22:54:25 +01:00
Ludovic MarieandClaude Opus 5.5 95ecc9d771 Merge main into fix/dyed-armor-colour
main's e7f5201c1 colours the armour stand's layers with its own layer_color; keep it there.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 22:54:04 +01:00
Ludovic Marie e470f3a0c9 Merge remote-tracking branch 'origin/main' into feat/launcher-any-version-download 2026-10-28 22:53:45 +01:00
Ludovic Marie f93c28a230 Merge pull request #2932 from RVZO6/fix/dig-down-jitter
🤖 Agent: needs-proof check by Ludovic's AI agent (Claude), not by Ludovic himself. Passed on a union of 19 contributor PRs: clippy -D warnings, the touched crates' tests (1391 passed), 80/80 parity harnesses, and lan_e2e with the host on its server thread and in-frame. Queued for merge at 32a0a11fe.
2026-10-28 22:53:41 +01:00
Ludovic Marie b9721d816d Merge pull request #2809 from socksprox/fix/1528-portal-arrival
🤖 Agent: needs-proof check by Ludovic's AI agent (Claude), not by Ludovic himself. Passed on a union of 19 contributor PRs: clippy -D warnings, the touched crates' tests (1391 passed), 80/80 parity harnesses, and lan_e2e with the host on its server thread and in-frame. Queued for merge at 9fcfdaa1e.
2026-10-28 22:53:37 +01:00
Ludovic Marie a49e72201a Merge branch 'feat/launcher-java-instances' into feat/launcher-java-play 2026-10-28 22:51:29 +01:00
Ludovic Marie 2034db1df6 Merge remote-tracking branch 'origin/main' into feat/launcher-java-instances 2026-10-28 22:50:15 +01:00
Ludovic MarieandClaude Opus 5.5 5091896e2a Launcher: the real game's command from its version JSON
java_launch builds the command that starts Minecraft: Java Edition as
the official launcher reads a version file: mainClass; the classpath of
this system's libraries (rules with os name and arch, duplicates once)
and the client jar; arguments.jvm / arguments.game with their rules
(every optional feature off) and ${...} placeholders, or the old
minecraftArguments line with -Djava.library.path and -cp; an old
version's natives unpacked without extract.exclude; virtual and
map_to_resources asset indexes laid out by name; Mojang's logging setup
(-Dlog4j.configurationFile) plus -Dlog4j2.formatMsgNoLookups=true;
-Xmx from the instance and the player's JVM arguments.

Nothing reaches the network; the next PR wires it to Play.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 22:50:02 +01:00
Ludovic Marie 6edc4e950d Merge pull request #3402 from horizon-rust/fix/launcher-java-test-windows
🤖 Agent: verified: launcher build matrix dry run 40773579704 green on Windows, macOS and Linux (java::tests::mojangs_runtimes_are_found_in_their_layout passes on Windows); PR checks green. Test-only fix for main's Windows build after #3386.
2026-10-28 22:49:19 +01:00
degepieandClaude Opus 5.5 99207f16b1 Suggest the crosshair entity's UUID for entity arguments (#3223)
EntityArgument.listSuggestions adds SharedSuggestionProvider.getSelectedEntities
to the player names unless the argument takes players only;
ClientSuggestionProvider.getSelectedEntities is the UUID of the entity under
the crosshair. The chat screen now fills Registries::selected_entities from
the nearest entity hit before any block.

For the UUID to name the entity back, two server-side gaps are closed:
- the entity tracker sent a made-up UUID (entity_uuid) in AddEntity; it now
  sends the entity's own UUID (ClientboundAddEntityPacket: entity.getUUID()),
  assigned on first tracking;
- a UUID in an entity argument also set playerName, which findEntities checks
  first among players, so `/tp <uuid> ...` found nothing. parseNameOrUUID
  sets entityUUID alone.

The chat_screen and command_parse oracles now return a selected entity from
getSelectedEntities, and the fixtures read it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
2026-10-29 02:49:02 +05:00
degepieandClaude Opus 5.5 e7f746782f Totem of undying: the item activation animation on screen
ClientPacketListener.handleEntityEvent 35 now calls
LocalPlayer.displayItemActivation(findTotem(player)) for the player's own
event. ItemActivation (40 ticks, two random offsets) ticks in
GameRenderer.tick and resets in GameRenderer.resetData.
ScreenEffectRenderer.renderItemActivationAnimation draws it in the hand
pass with a FIXED item at full brightness, ITEMS_3D lighting and no
camera transform or bob. Only F1 hides it.

Fixes #3142.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
2026-10-29 02:46:24 +05:00
Ludovic MarieandClaude Opus 5.5 d4120a9ef0 Launcher: download the version's logging setup with it (Log4Shell fix for 1.7-1.18)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 22:45:58 +01:00
Ludovic MarieandClaude Opus 5.5 795a0db7e4 Launcher: download any Minecraft version for an owner, natives included
The owner-gated download learns versions Horizon doesn't pin, for Java
instances: Pins::for_version(id, sha1) takes a version from Mojang's
list; its version file is checked against that sha1, and the client's
sha1 then comes from the checked file. backend::download_version_for_owner
puts it in Horizon's own game folder through the same path as the
pinned one (download_for_owner asks the strict gate again, then
download_for → mojang::download), so the one download call site and
its gate test hold.

Versions before 1.19 keep a library's natives as a classifier of their
own (natives map, ${arch} for Windows): mojang::download now fetches
this system's, checked like every other file.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 22:42:25 +01:00
Ludovic MarieandClaude Opus 5.5 75b8a176d4 Launcher tests: Mojang runtime fixture with Windows' java.exe
java::tests::mojangs_runtimes_are_found_in_their_layout failed on
Windows (main after #3386): the fixture's homes had only bin/javaw.exe
there, and the runtime search (horizon_prepare::java::minecraft_runtimes)
looks for bin/java.exe, so it found nothing. Mojang's Windows runtimes
ship both, so the fixture now does too. The platform folder comes from
mojang::java_platform, the name the download lays out, instead of a
hard-coded mac-os-arm64.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 22:35:49 +01:00
Ludovic MarieandClaude Opus 5.5 21a0656530 docs: launcher command reference for the Java instance commands
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 22:32:11 +01:00
Ludovic Marie 071580e716 Merge pull request #3400 from horizon-rust/launcher-commands-doc
🤖 Agent: docs-only fix for the launcher reference test (java list/install sections); all checks green at this head.
2026-10-28 22:30:19 +01:00
degepieandClaude Opus 5.5 cb1c3499bf Keep suspicious stew effects across the wire and list them in creative
A stew taken from the creative inventory gave no effect when eaten and
its tooltip listed none (#3137). The protocol read the
`suspicious_stew_effects` component past without decoding it, so the
creative client's `SetCreativeModeSlot` sent the server a bare stew and
the server's copy had nothing for `SuspiciousStewEffects.onConsume` to
apply. Decode and encode it (`Entry.STREAM_CODEC`: mob effect id,
duration) both ways, and add `SuspiciousStewEffects.addToTooltip`'s
creative-only effect lines (`PotionContents.addPotionTooltip`).

Fixes #3137.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
2026-10-29 02:29:33 +05:00
JacobRWebbandClaude Opus 5.5 4a094bee90 Pistons: moving blocks keep their tint, survive saves and leave no black ghost
- Moving blocks are tinted per face with the chunk mesher's block colours
  (MeshTables::block_tints), not the terrain particle tint, which is none
  for grass blocks: a pushed grass block was drawn gray.
- PistonMovingBlockEntity is saved with its chunk as vanilla's
  minecraft:piston block entity and loaded back into the level's moving
  pistons, so blocks caught mid-move finish it after a rejoin instead of
  leaving an invisible, solid moving_piston (#3310).
- A server client drops its moving piston when the server changes that
  block (LevelChunk.setBlockState removes the block entity), instead of
  animating it for five more death ticks over the placed block, black and
  z-fighting (#3002).
- A finished piston stays drawn until the chunk mesh shows its block, and
  no longer, so no frame shows neither.

Fixes #3310
Fixes #3002

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 16:29:10 -05:00
degepieandClaude Opus 5.5 66e1fc5079 Credit the curing player of a zombie villager on the server thread
ZombieVillager.mobInteract records the interacting player as the
conversion starter and finishConversion credits the player
level.getPlayerByUUID finds: the ZOMBIE_VILLAGER_CURED gossip and the
cured_zombie_villager trigger. Horizon used the server Game's own
level.player_uuid for both, so on the server thread (and for LAN guests)
the gossip went to a UUID nobody trades as and the player got no
discount and no Zombie Doctor (#3306).

The starter is now the acting player, and the cure credits the host or
the placed guest in this level with that UUID. lan_e2e gains a
cure-discount check: host and guest each cure one and pay 14 wheat
instead of 20.

Fixes #3306.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
2026-10-29 02:27:31 +05:00
Ludovic Marie ae01d872fb Merge remote-tracking branch 'origin/main' into feat/launcher-java-instances 2026-10-28 22:26:47 +01:00
Ludovic Marie d2f5a0d678 Merge pull request #3398 from horizon-rust/tests-ignore-port-probe
🤖 Agent: tests ignore T3's GET / port probe; account::tests 17/17 on the Mac mini (the_whole_chain failed before); merger waits for parity (online_mode).
2026-10-28 22:26:12 +01:00
Ludovic MarieandClaude Opus 5.5 d6ee462614 docs: the launcher reference lists the java commands
#3386 added `horizon-launcher java list|install` without regenerating
docs/launcher-commands.md, so the_launcher_reference_is_up_to_date failed on main's
launcher builds (and would stop the next launcher release).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 22:25:41 +01:00
Ludovic Marie 430e5e4c92 Merge pull request #3352 from horizon-rust/feat/native-mods-carry-settings
🤖 Agent: verify green. pr_ready: all green except the launcher test account::tests::the_whole_chain, which fails on this Mac only (a desktop browser's extra GET / on the device-code page); it passes in CI's launcher job. Local parity of this tree (Mac mini, --changed origin/main): 36/37, online_mode fails identically on main here (environment). Test the_native_mods_instance_gets_the_mods_settings_once passes.
2026-10-28 22:22:02 +01:00
Ludovic Marie f3d8535fcd Merge pull request #3348 from horizon-rust/ci/train-builds-nightlies
The batch train builds Nightlies: no more vX.Y.Z game releases
2026-10-28 22:15:22 +01:00
d3grandoandClaude Opus 5.5 8f00332979 Mod screens: a list's rows are cut at its frame
A mod screen's `list` drew one row more than its frame holds (the row a
scroll shows in part), uncut, so without scrolling the 7th row of a long
list sat below the frame, over the screen. As vanilla's
AbstractSelectionList, which draws its entries between enableScissor and
disableScissor, the rows (with their hover and selection marks) are now cut
at the frame's inside: the partly shown row is drawn in part. Fixes #3237.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 04:15:07 +07:00
Ludovic Marie b276d65b5f Merge branch 'main' into launcher-mixins-0d6b103 2026-10-28 22:13:02 +01:00
Ludovic MarieandClaude Opus 5.5 559ecb02f6 tests: ignore the bare GET / a local tool sends to every new port
T3 Code probes each new localhost listener with GET / (seen on the Mac mini: the t3
process). The online_mode harness and account::tests::the_whole_chain recorded it as an
unexpected call and failed there only. Neither fake service has a root, so it is ignored.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 22:11:32 +01:00
Ludovic Marie 11b8757bc7 Merge pull request #3386 from horizon-rust/feat/launcher-java-runtimes
🤖 Agent: verified: pr_ready --quick green on the Mac mini (233 launcher tests), CI launcher/verify/parity-scope green; java install 21 checked end to end in scratch homes (Mojang's runtime runs, real folders untouched).
2026-10-28 22:07:50 +01:00
Ludovic MarieandClaude Opus 5.5 b44da42ca4 Launcher: Minecraft: Java Edition instances (model, versions, CLI)
A Java instance is an own instance whose instances.json entry carries a
`java` section: the Minecraft version it plays and, optionally, its own
Java home, memory and JVM arguments. It gets an empty game folder (the
real game keeps its own options); a copy keeps the section.

- minecraft_versions: Mojang's public version_manifest_v2 (releases,
  snapshots, old betas and alphas, latest), kept in the data folder so
  the list shows offline. Only metadata: game files stay owner-gated.
- Play refuses a Java instance with a clear sentence (resolve_version
  and instances::apply, so the window, the CLI and MCP all say it) until
  the launcher starts the real game.
- CLI: instance create <name> --minecraft <id|latest|latest-snapshot>,
  instance versions [--snapshots] [--old], instance java <instance>
  [--home|--auto] [--memory] [--args]; instance list shows the version.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 22:04:34 +01:00
Ludovic Marie 9669600c38 Merge pull request #3323 from horizon-rust/feat/native-hooks-api-build
🤖 Agent: verify green; launcher-only (native_mods.rs, index native.rs). pr_ready green, launcher tests pass. Local parity of #3352's tree (which contains this PR): all affected harnesses pass except online_mode, which fails identically on main on this Mac (environment).
2026-10-28 21:58:51 +01:00
Ludovic Marie 528c322d16 Merge pull request #3321 from horizon-rust/feat/official-mods-turn-native
🤖 Agent: verify green. pr_ready green. Local parity (Mac mini, --changed origin/main): 36 of 37 pass. online_mode fails identically on main on this Mac (a stray GET / to its fake service, IndexError), so it is not this change. Registry test a_mod_may_turn_native_once and launcher test a_mod_that_turned_native_keeps_its_hmod_until_removed pass.
2026-10-28 21:58:46 +01:00
Ludovic MarieandClaude Opus 5.5 a9f8e64331 Native mods: horizon-mixins finds the launcher's cargo
`horizon-mixins apply` runs `cargo metadata`, but the launcher started it as a
plain command: on a computer with no Rust of its own every native build stopped
at E_METADATA. It now gets the launcher's toolchain like the build does.
Found by building zoom end to end with no system Rust, on macOS and Linux.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 21:50:20 +01:00
Ludovic MarieandClaude Opus 5.5 01d0f45c2d Native mods: turn one off in Options > Mods
A native mod's row now toggles too. Off writes `mod.<id>:false`, so the
launcher's next build leaves the mod out, and `native_on` tells the mod to rest
until then; on again writes `true`. The WebAssembly row of the same id is no
longer listed: the native row stands for the mod. Asked by levzzz5154.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 21:46:13 +01:00
Ludovic MarieandClaude Opus 5.5 228af35f2a Launcher: Java runtimes for Minecraft: Java Edition instances
The first piece of Java instances: which Java a version needs and where
it comes from, as other launchers do it.

- java::Need reads a version JSON's javaVersion (major + Mojang
  component); versions from before that field ran Java 8 (jre-legacy).
- Runtimes are found without starting them, from each Java home's
  release file: Mojang's that Horizon downloaded (minecraft/runtime/ in
  the data folder, the one preparing uses) and installed ones (JAVA_HOME,
  the PATH, the usual installer folders).
- pick: an instance's own Java home, else the exact major (Mojang's
  first, never a newer one by itself), else None = download Mojang's.
- install downloads Mojang's runtime through mojang::java_runtime
  (sha1-checked, resumable), refusing on Linux without glibc.
- CLI: horizon-launcher java [list|install <major|component>].

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 21:45:05 +01:00
Ludovic Marie abd1755f95 Merge pull request #2770 from socksprox/fix/2594-allay-travel-flying
🤖 Agent: read the diff at c216561da: `Allay.travel` -> `travelFlying(input, getSpeed())` via a new `travel_flying_speeds`, and `WaterAvoidingRandomFlyingGoal.getPosition` stroll targets. verify green, merges cleanly; the merger lands it once this head's parity run (queued) passes.
2026-10-28 21:42:59 +01:00
degrandoandClaude Opus 5.5 81f339814b Native mods build on Windows: MinGW-w64 with the toolchain, and a stage path horizon-mixins takes (#3377)
On Windows a native build stopped twice before compiling anything, then once while compiling:
- `windows-sys` 0.61 imports through raw-dylib, and on windows-gnu rustc makes those import
  libraries with dlltool, which runs the GNU assembler. rust-mingw's dlltool isn't on the build's
  PATH, and rust-mingw has no assembler, so even horizon-mixins didn't build.
- `canonicalize` gives a verbatim path there (`\\?\C:\...`), and horizon-mixins' output check stats
  `\\?\C:`, the volume itself: os error 1, E_IO.
- `ring`, `hidapi` and wasmtime compile C with `cc`, and rust-mingw's gcc only links.

The toolchain now brings MinGW-w64 on Windows (WinLibs' GCC 16.2.0 on MinGW-w64 14.0.0, MSVCRT
like rust-mingw, one zip pinned by SHA-256, its mingw64/ without the documentation) and puts its
bin/ on the build's PATH after Rust's. A toolchain installed before it counts as not installed, so
the next build installs it. The stage path loses the verbatim prefix of a drive path.

Measured on Windows 11 (Ryzen 7 9800X3D) with Veinminer 0.3.0 on Nightly 83f15fa: the launcher
installs the toolchain and builds the game in 8 min 3 s (6 min 57 s compiling), and the mod works
in game.

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-29 01:34:22 +05:00
Ludovic Marie 0590a85d16 Merge pull request #3378 from horizon-rust/launcher-windows-tests
🤖 Agent: Windows launcher tests 219/219 pass with this (they failed on main: server delete Access denied, CRLF test); the remaining smoke failure is #3375's fix.
2026-10-28 21:32:38 +01:00
Ludovic Marie be6beb6ec7 Merge pull request #3375 from horizon-rust/ci-launcher-smoke-nightly
🤖 Agent: smoke test glob for the Nightly folder (#3342 broke every launcher build's smoke); CI green.
2026-10-28 21:30:32 +01:00
Ludovic MarieandClaude Opus 5.5 9cdfc5bd44 launcher: two Windows-only failures (server delete, a test's line ends)
- Deleting a stopped server always failed on Windows ("Access is denied"): the folder was
  moved while its lock file was open. The lock is released just before the move there; a
  supervisor starting in between holds it again and the move fails, so a live server stays.
- mojang_downloads_only_for_an_owner searched source text for "\n", which a Windows
  checkout writes as CRLF.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 21:20:49 +01:00
Ludovic MarieandClaude Opus 5.5 26af55000a launcher CI: the smoke test finds the installed Nightly
Since #3342 `install latest` installs the Nightly into versions/nightly-<date>-<commit>,
so the vX.Y.Z glob found nothing and every launcher build's smoke test failed (it also
blocks the next launcher release). Any version folder now, and a clear error when none.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 21:18:49 +01:00
Ludovic Marie 3782473f52 Merge pull request #3358 from horizon-rust/nightly-approved-only
🤖 Agent: Nightly approval rule (Ludovic 10-28); tests.test_nightly + test_nightly_set 43 pass; CI green.
2026-10-28 21:15:30 +01:00
degepieandClaude Opus 5.5 c404ef5e2a Goat horns keep their instrument from the server
The client read InstrumentComponent past without keeping it, so every goat
horn a server sent arrived with no minecraft:instrument and its tooltip fell
back to "Ponder" (#3181). Decode the holder like ArmorTrim's: a reference
becomes Component::Instrument(registry id) and is named from the synced
minecraft:instrument registry in item_sync. A direct instrument stays Raw.
The LAN host and creative client send it back by the same id.

The item wire harness gains an "instrument registry" fixture, and its probe
treats Instrument like Trim (a byte-exact id, not a lossy type), so the
round trip is checked byte for byte.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
2026-10-29 01:14:59 +05:00
Ludovic Marie 9cbc96ee7b Merge pull request #3354 from horizon-rust/feat/launcher-owner-play
🤖 Agent: verified: pr_ready --quick green on the Mac mini (226 launcher tests), CI launcher/verify/parity-scope green; Play prepares then plays for an owner, download path and owner gate unchanged.
2026-10-28 21:12:53 +01:00
Ludovic Marie af0fef9196 Merge pull request #2754 from iLostName/server-list-f5-refresh
🤖 Agent: read the diff at 4c4fcedf4: F5 refreshes the server list (`JoinMultiplayerScreen.keyPressed` -> `refreshServerList`), only on the list screen. verify green, merges cleanly; the merger lands it once this head's parity run (queued) passes.
2026-10-28 21:09:32 +01:00
Ludovic MarieandClaude Opus 5.5 5a2d71d2ea launcher: build native mods with horizon-mixins 0d6b103
levzzz5154's update (optional public visibility for support modules); bundles that
pin their own mixins revision keep it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 21:09:24 +01:00
Ludovic MarieandClaude Opus 5.5 bbec03c14f Nightly: only pull requests the Team or Ludovic's agents approved at that head
The Nightly is the version every player installs: /nightly set and add refuse a pull
request without an approving review at its current head from a trusted reviewer or
ludovic111 (never its author). The scheduled build keeps the approved head when a newer
one waits for review, and drops pull requests nobody approved. Dry runs need none.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 21:03:07 +01:00
Hamza CoşkunandClaude Opus 5.5 0e2b244120 Send an armor stand's custom name and its visibility to its clients
The server never sent DATA_CUSTOM_NAME or DATA_CUSTOM_NAME_VISIBLE for an
armor stand, so hologram text didn't show for its clients: single player on
its server thread, LAN guests and dedicated servers. Both are sent now, and go
back to none / false when /data takes them away.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 22:57:42 +03:00
Hamza CoşkunandClaude Opus 5.5 835674f45d Draw an invisible armor stand's equipment, not just skip it
LivingEntityRenderer.submit gives an invisible stand no body render type but
still submits its layers: held items, the custom head and the armour. The
renderer skipped the whole stand instead, so floating heads, item displays and
armour on invisible stands never showed, in-frame and on any server that sends
the invisible flag (vanilla, Paper, Minestom). Only the wooden body is skipped now.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 22:57:42 +03:00
ThatCakeIsAPieandClaude Opus 5.5 85bfbc7d46 upgrade-datapack: docs, and checks against vanilla 26.2 and 26.3
- docs/datapack-upgrade.md: how to run `horizon-port-pack
  upgrade-datapack`, every rewrite and how it is checked, and what is not
  rewritten; linked from docs/README.md and the command's help.
- tools/datapack_upgrade_rosetta.py writes vanilla 26.2's own worldgen out
  as a pack, upgrades it and compares it with vanilla 26.3's file by file:
  registries whose format changed and whose content did not must come out
  identical.
- tools/datapack_upgrade_fidelity.py starts vanilla's 26.2 and 26.3
  dedicated servers on the same seed, the first with a pack and the second
  with its upgrade, force loads the same chunks and compares the saved
  blocks; without packs it measures what 26.3 itself changed.

Both need the 26.2 server jar the player downloaded; nothing is fetched.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 14:57:24 -05:00
ThatCakeIsAPieandClaude Opus 5.5 f983223354 port-pack: upgrade-datapack rewrites features and placements
The fourth part of the 26.2 to 26.3 data pack upgrade.

- Configured features move to `feature/` with `config` unwrapped, block
  state providers take their 26.3 names (a `simple_state_provider` is its
  state), and nested and inline placed features are upgraded with them.
  Feature types 26.3 replaced by compositions of other features are listed
  in the report and left as they are.
- Placement `random_offset` becomes `offset` with one provider per axis
  (x and z both from `xz_spread`).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 14:57:23 -05:00
ThatCakeIsAPieandClaude Opus 5.5 3d39bbdaf1 port-pack: upgrade-datapack rewrites noise settings, dimensions and presets
The third part of the 26.2 to 26.3 data pack upgrade: noise settings in
26.3's form, with the parts 26.2 hard-coded written out as data.

- The beardifier, which 26.2's NoiseChunk added, is added to the final
  density.
- The aquifer router fields move to `aquifers`, whose `exclusion` spells
  out 26.2's `isDeepDarkRegion` (erosion < -0.225 and depth > 0.9) from
  the pack's own erosion and depth; `surface_level` is the preliminary
  surface, and `chunk_surface_level` its per-chunk 16 x 1 interpolation.
- 26.2's ore veinifier becomes two `ore_vein` material rules (copper,
  iron) with OreVeinifier's heights, thresholds, edge roundoff, solidness,
  richness and gap, ahead of the surface rule: 26.2 placed veins while
  filling the noise and its surface pass only replaced the default block.
  The rule is written as a named `material_rule` under the settings' id.
- The climate functions are named, and the spawn target refers to them by
  id (`depth` and `offset` dropped, as vanilla did).
- `size_horizontal`/`size_vertical` are dropped: each `interpolated` holds
  its cell.
- Inline noise settings in dimensions and world presets get the same.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 14:57:23 -05:00
ThatCakeIsAPieandClaude Opus 5.5 2aca6fe06c port-pack: upgrade-datapack rewrites density functions
The second part of the 26.2 to 26.3 data pack upgrade: named density
functions in 26.3's form.

- Inputs are named `input`/`left`/`right`; `invert` is `reciprocal`,
  `y_clamped_gradient` a `gradient` on y, `shifted_noise` a `noise` with
  shifts, `end_islands` `end_outer_islands`, and `constant` its number.
- 26.2's `flat_cache` held each column's value at y = 0, so it becomes a
  `cache` around a `slice` at y = 0 when its input depends on y (through
  the pack's own functions); 26.2's other caches only memoise in 26.3.
- `interpolated` takes its cell from the noise settings whose router
  reaches it (`size_horizontal * 4`, `size_vertical * 4`), which 26.2 read
  from the settings, or vanilla's overworld cell when none does.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 14:57:23 -05:00
ThatCakeIsAPieandClaude Opus 5.5 598668e4bc Far terrain: stop decoding saved chunks for every distant column
SavedLodSampler::grid put saved chunks over every far tile of 16 blocks
a column or finer: for each column it opened the region file and
decoded the whole saved chunk, and each sampling thread kept only 128.
In a save with many thousands of chunks, the far view stalled for many
minutes after its first one.

- SaveDir keeps which chunks each region file holds (10 s), so columns
  over unsaved ground no longer open region files.
- Decoded saved chunks are shared by the sampling threads (1024, 30 s).
- With Reduced far detail, tiles coarser than 4 blocks a column (about
  1 km out and beyond) keep the generator's terrain, which saved chunks
  were made from; edits that far away show when their tiles refine on
  approach. Full keeps saved chunks up to 16 blocks a column, as before.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 14:57:22 -05:00
ThatCakeIsAPieandClaude Opus 5.5 a1a249e2f6 Far tiles: approximate only with Reduced far detail
The previous two commits trade the exact far tile for speed in wide
cells. They now apply only with Reduced far detail (FarDetail::Reduced,
which already "changes the picture"); Full keeps the exact tiles.

- GeneratorWorker::lod_grid takes `approximate`: one biome per 4x4
  columns, the coarser vertical lattice, and density only around the
  surface (with shared 2D planes) happen only when it is set. Without it
  the tile is sampled as before.
- LodSampler::set_approximate, and Lod::set_reduced_detail sets it for
  the sampling threads; tiles sampled after a change follow it.
- lod_grid_bench --approximate samples as Reduced does.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 14:57:21 -05:00
ThatCakeIsAPieandClaude Opus 5.5 eb5e0bb914 Far tiles: sample density only around the surface, share 2D planes
lod_grid sampled the density lattice over the whole build height. It now
finds each tile's surface band on a 64-block lattice made of the fine
lattice's points, then samples the fine lattice from 64 below the lowest
surface to 128 above the highest (and above sea level); below the band a
column is one span of the default block. Thin parts above an air sample
of the coarse lattice (floating islands, overhang lips) can be missed.

Most of a far tile's density work is 2D noise (y-fixed slices), which
the two passes would each compute: SamplerContext::with_planes keeps a
y-fixed slice's plane for its x/z lattice, so the band pass reuses the
coarse pass's. Opt-in, and only far sampling turns it on.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 14:57:21 -05:00
ThatCakeIsAPieandClaude Opus 5.5 d93c3cdd82 Far tiles: one biome per 4x4 columns, a coarser vertical lattice
Approximations for wide far cells, where they are hard to see at their
distance:

- One biome for 4x4 of a tile's columns, the first one asked (its
  surface's), in cells up to 64 blocks wide: far cells don't show a biome
  change a few blocks down, and climate changes over hundreds of blocks.
  Painting asks through the biome zoom, which strays into neighbouring
  quarts; per quart, those strays were most of a tile's climate samples.
- The interpolated density on a vertical lattice of 16 blocks for cells 32
  blocks wide or more, and of 32 from 128, instead of 8.

Distant tiles' biomes and some surface blocks differ from the per-quart
answers, and in the widest tiles the coarser lattice moves the surface.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 14:57:20 -05:00
ThatCakeIsAPieandClaude Opus 5.5 81b3803017 port-pack: upgrade-datapack, a 26.2 data pack's registries in 26.3's formats
Vanilla 26.3 refuses a world generation data pack written for 26.2: the
registries changed format. `horizon-port-pack upgrade-datapack <pack>
(<out> | --world <world>)` reads a pack (zip or folder) and writes it in
26.3's formats, with a report of what it rewrote, what has no faithful 26.3
form and which types it does not know. Each rewrite is the one vanilla's own
data went through between 26.2 and 26.3.

This first part handles the registries that need no context:
- noise parameters: `firstOctave`/`amplitudes` become `base_octave`,
  `octave_count`, `amplitude_modifiers` and the base amplitude of
  `NormalNoise.createParity`, which keeps 26.2's normalisation;
- block states: `{Name, Properties}` become `{id, properties}`, or the id;
- configured carvers move to `carver/` with `config` unwrapped and the cave
  carvers' hard-coded count, thickness and room scale written out;
- biome spawners and spawn probability become 26.3's gameplay attributes;
- structure spawn overrides and the dimension type's bed rule.

Density functions, noise settings and features follow in their own
changes. serde_json gains `float_roundtrip` so the numbers the upgrade
writes back are the ones it read.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 14:57:19 -05:00
ThatCakeIsAPieandClaude Opus 5.5 91ed783e8d Far tiles: resolve each biome quart once per tile
lod_grid paints eight depths of every far column through the surface
rule, and each depth asked the biome resolver again: a full climate
sample and parameter search per query, though neighbouring columns and
depths share quarts. The tile now keeps the answers per quart
(Biomes::Memo), for painting and for the columns' surface biome.

lod_grid_bench, vanilla seed 1234: surface heights, blocks and biomes
are the same at every level. At level 6 (64 blocks a column) some
painted blocks below the surface differ: the tile's cached sampler
context answers a repeated quart from what it sampled before, so the
order of queries mattered there.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 14:57:19 -05:00
ThatCakeIsAPieandClaude Opus 5.5 bc3a724c01 lod_grid_bench: far terrain sampling cost per tile level
Times GeneratorWorker::lod_grid as the client's far terrain calls it (16 x
16 columns a tile) for each tile level, with two checksums: everything the
tiles hold (an exact speedup keeps it) and the surface and water heights
alone (the terrain's shape, which an approximation of materials or biomes
keeps). --dump writes each column's height, block and biome.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 14:57:19 -05:00
Lyle ColeandClaude Opus 5.5 51244a64f6 Decoration: column scans, per-chunk heightmap reads, zoom-free uniform biomes
Three read paths in decoration:
- Speleothem clusters (and large dripstone) scan each column up and down
  for the floor and ceiling, up to 512 blocks, one region read per block.
  FeatureWorld::scan_column walks a chunk's column directly: an all-air
  section is tested once (the predicate is a pure state test), a mixed
  one is read without a section lookup per block. Same blocks, same stop.
- OreFeature's any_height_at_least read each column of its square through
  a region lookup and a checked heightmap read; now each chunk's part of
  the square is read from its heightmap in one pass (any column will do,
  so order is free).
- Placement biome checks ran the biome zoom (eight hashed corner
  distances) for every position. When the 2x2x2 quarts the zoom picks
  from lie in biome sections that each hold one biome, the same one, the
  answer is that biome whatever the zoom picks; per-section uniformity is
  worked out once per region.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 14:57:18 -05:00
Lyle ColeandClaude Opus 5.5 1d25065363 Decoration: cut per-region and per-shape-update setup
Three costs that came before any feature work:
- the biomes present around the chunk sorted every quart of the nine
  chunks' columns into a BTreeSet; a bitset gives the same ids in the same
  ascending order;
- every updateShape call built and dropped a whole Ticks (tick maps,
  deadline index) just to capture the ticks it schedules; capturing
  leaves the rest untouched, so the region keeps one;
- the nine stand-in chunks left in the proto chunks while the region
  holds their terrain were allocated at full height; one section does.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 14:57:18 -05:00
ThatCakeIsAPieandClaude Opus 5.5 22f0e30d07 Block reads: inline the region-to-palette path
A block read in decoration (and the surface stage's reads) went through
out-of-line calls: the region's chunk lookup, TerrainChunk::block,
TerrainBlocks::block and BlockSection::get, whose io::Result error path
kept it from inlining. They now inline into the feature code (the fast
build has no cross-crate LTO, so the core accessors need #[inline]), and
TerrainBlocks reads the section through an infallible accessor: its index
is a local index, always below 4096. #1790 takes the division out of
PackedStorage::read; this leaves storage alone.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 14:57:17 -05:00
Lyle ColeandClaude Opus 5.5 c8f484e040 Surface biomes: keep each quart cell's corner jitter and biomes
The surface stage's per-chunk cell cache only proved cells whose eight
quart corners hold one biome; a block in any other cell ran the full
eight-corner zoom mix and a stored lookup again. Now a mixed cell keeps
its corners' jitter (BiomeZoom::corner_jitter, one mix per cell) and
their eight biomes, so each block of the cell only redoes the distance
comparison (BiomeZoom::select_corner, same arithmetic and tie order); the
cell of the previous lookup is kept too, as columns walk down in y. A
test checks the split zoom against quart_at_block.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 14:57:16 -05:00
ThatCakeIsAPieandClaude Opus 5.5 d4a1ebaa33 Surface rules: compare biome ids, not names
Every solid block of every column asks the rule tree's biome conditions
for its biome. Each one cloned and dropped the biome's Arc<str> (an atomic
count all generator threads share) and looked its name up in a string set.
Queries that number their biomes (the generator's) now answer with the
index, and biome conditions keep a bit per index; others compare names as
before. Same answers: the generator's name -> id map is exact.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 14:57:16 -05:00
Lyle ColeandClaude Opus 5.5 5a75089f61 Biome stage: biome ids by bitset, names by a last-entry memo
Two lookups in the biome stage:
- the biome ids present around the chunk were gathered by inserting
  every quart of the nine neighbour chunks into a BTreeSet; a bitset
  gives the same ids in the same ascending order;
- each quart's biome name was looked up in the name -> id map. Names
  point into the biome source's parameter list and neighbouring quarts
  mostly resolve to the same entry, so the last entry's id is kept and
  the map is asked only when the name changes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 14:57:15 -05:00
Lyle ColeandClaude Opus 5.5 483ad5209c Biome stage: build free neighbour biome chunks before waiting
Each chunk needs the biome chunks of its 3x3 neighbourhood, built once by
whichever thread gets there first (per-chunk work lock). With several
workers on neighbouring chunks, a thread often blocked on a neighbour
another thread was building while other neighbours were still unbuilt.
The stage now first builds (or reads) every neighbour whose lock is free
(try_lock), then waits for the ones still being built. Each biome chunk
is still built exactly once, deterministically; only the order of the
work changes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 14:57:15 -05:00
Lyle ColeandClaude Opus 5.5 91369d11c5 Structure column checks: sample the column's climate in one volume
couldStructureExistInColumn (jigsaw projection, onTopOfChunkCenter,
temples, mansions) samples the column through createResolverForChunk:
one bulk climate volume of 1 x n x 1 quarts, then each quart from the
bottom. Horizon took one scalar climate sample per quart instead, and a
column with no valid biome walks all of them, so a few such start chunks
held most of the structure stage while neighbours waited on them. The
column is now one prepared volume through the biome stage's resolver
path, as vanilla does.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 14:57:14 -05:00
ThatCakeIsAPieandClaude Opus 5.5 82eeba7753 Structure checks: keep each worker's single noise-biome samples
Structure placement asks the biome of one quart at a time: several
structures test the same quarts of a start chunk, and column checks walk
whole columns when no quart matches. Each answer is a full climate sample
and parameter search. Workers now keep the answers, up to 65536 quarts;
the generator is seed-bound, so they cannot go stale.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 14:57:14 -05:00
ThatCakeIsAPieandClaude Opus 5.5 c6efb367b0 launcher: drop an unused import in the Windows make_link
9f1fa0a0a moved the mklink call into junction(), which imports
CommandExt itself; make_link kept its own copy, unused, so the
launcher's clippy with warnings denied fails on Windows.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 14:57:13 -05:00
Ludovic Marie 3c9bb38e88 Merge pull request #3345 from horizon-rust/feat/native-hook-points
🤖 Agent: verify green. pr_ready on this stack (with #3346): clippy, 1264 horizon-client tests, parity 76 affected all passing on the Mac mini. With no native mod every hook is an empty call. Proof: the four official native mods (horizon-rust/horizon-mod-*) apply with horizon-mixins and play on it, alone and together, in an offscreen game driven by horizon-ctl.
2026-10-28 20:54:06 +01:00
Ludovic MarieandClaude Opus 5.5 dc943f95e8 Launcher: Play downloads and prepares Minecraft for an owner
An owner with no Minecraft on the computer no longer needs to find
"Download and prepare": Play on a version not prepared yet prepares it
(from the player's install when there is one, else downloaded from
Mojang for the signed-in owner, Java included) and then starts the game,
as the official launcher does. Without an owner Play stays off.

With no install found, the Resources row offers the download alone and
the Minecraft folder row says none is needed. The CLI's not-prepared
error and the READMEs stop asking for a local install and Java.

The download path itself (download_for_owner, mojang.rs) is unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 20:51:58 +01:00
Ludovic Marie 312218a47f Merge pull request #3087 from Domanffe/fix/crash-report-relative-paths
🤖 Agent: read the diff at da9950c75: crash reports strip the workspace prefix after turning `` into `/`, so Windows builds shorten paths too; other hosts unchanged. verify green. Queued for the merger.
2026-10-28 20:50:24 +01:00
Ludovic Marie 83b5c5ed53 Merge pull request #3075 from Domanffe/chore/hooks-find-python
🤖 Agent: read the diff at e8cb6ae83: the pre-commit and pre-push hooks run check_public_tree.py with the first of python3/python/py that really runs Python 3 (Git for Windows' Store stub exits non-zero), and fail loudly if none does. verify green. Queued for the merger.
2026-10-28 20:50:19 +01:00
degepieandClaude Opus 5.5 2acf1d7798 World list: show Hardcore Mode in red, and adventure and spectator by name
The Select World list built its info line from a creative flag only, so a
hardcore world read "Survival Mode" and adventure and spectator worlds read
"Survival Mode" too (#3298). LevelSummary.createInfo uses `gameMode.hardcore`
in CommonColors.RED (-65536) for a hardcore world, else `gameMode.` + the
GameType's name, then commands and the version. WorldEntry now carries the
level's game mode and hardcore flag (both already read from level.dat), and
`world_info` builds that line as styled text. The horizon-ctl JSON keeps its
`creative` / `mode` fields as before.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
2026-10-29 00:48:01 +05:00
Ludovic Marie 578cd262a5 Merge pull request #3342 from horizon-rust/feat/launcher-nightly-only
🤖 Agent: verify, launcher and parity-scope green at a1bdb0d22. pr_ready: launcher fmt/clippy clean, launcher tests 2 new (every launcher updates to the current Nightly; players of the newest release move to it), the only failure account::tests::the_whole_chain also fails on main on this Mac mini. CLI end to end in scratch homes: install latest / update installed nightly-<date>-<commit>, checksums signed for the nightly channel, worlds untouched. Heads-up posted in #horizon-team.
2026-10-28 20:46:38 +01:00
Jose Gil c9bd057d70 feat(launcher): add Spanish language support 2026-10-28 15:43:54 -04:00
Ludovic MarieandClaude Opus 5.5 868256f1a8 The batch train builds Nightlies: no more vX.Y.Z game releases
Step 5 of Nightly only (Ludovic, 2026-10-28). release.yml keeps its triggers and its decision
(5 player-facing pull requests settled 5 minutes, or the oldest 6 hours, release-now,
skip-release, RELEASES_PAUSED, release-blocker, a commit where checks and parity-main passed),
counted since the commit the published Nightly was built on. When it decides, it starts
`nightly build` on that commit (`base`) instead of building vX.Y.Z:

- release_train: the last release of the game is the published `nightly`; one Nightly build at a
  time and at most 12 a day (nightly_set's cap), except for release-now and force (`urgent`).
- nightly-build.yml: an `urgent` input passes the cap; the notes say the launcher installs it.
- docs: release-flow (Game releases, Nightly only, Notes, Rolling back), versioning.

The last vX.Y.Z stays GitHub's latest for launchers and servers from before the switch.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 20:41:56 +01:00
Ludovic MarieandClaude Opus 5.5 1149c92cb8 Native mods: a settings page in Options > Mods
Native mods list themselves (native_mods) and give their settings page's
buttons (native_settings, native_settings_click); their keys show as Key
Binds rows and the player's choice is kept as the WebAssembly mods' keys
were (key.<id>.<name> in horizon-mods.txt). A native mod takes over the
WebAssembly mod of the same id, whose row says so.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 20:39:52 +01:00
Ludovic MarieandClaude Opus 5.5 360272fd79 Native mods: stable hook points (game/native_hooks.rs)
Empty, cfg-free and macro-free functions the game calls at fixed places
(frame, tick, HUD corners, keys, wheel, chat commands, zoom, minimap), for
horizon-mixins bundles to hook with HEAD, and helpers that read and draw
what Horizon's HUD extras do and keep a mod's settings in the WebAssembly
mod's own files. NATIVE_HOOKS_API = 1 names their version, for the
launcher to build mods made for it on any Horizon that has it. A test
keeps cfg and macros out. The frame and click counters now run without
WebAssembly mods, and the HUD extras always place their corners.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 20:39:52 +01:00
Ludovic Marie 1d6d4cf0a0 Merge pull request #3335 from horizon-rust/t3/launcher-developer-options
🤖 Agent: launcher fmt/clippy/223 tests + pr_ready green; gate tests off/on; mojang_downloads_only_for_an_owner pins no download without an owner
2026-10-28 20:38:18 +01:00
Ludovic MarieandClaude Opus 5.5 a1bdb0d22d Launcher: the Nightly is the only game build, for everyone
Step 3 of Nightly only (Ludovic, 2026-10-28). Every launcher installs and updates only the
Nightly, with no setting:

- The update check reads the `nightly` pre-release from the release list it already fetches and
  offers it as the update, installed as `nightly-<date>-<commit>` after its checksums signed for
  the Nightly channel check. Nightlies rank after every release (`ReleaseOrder::Nightly`); a
  second build of the same day is an update too.
- A player on the newest release, on a Nightly or on nothing moves to the new Nightly at the next
  check; a release picked by hand and instances pinned to a version stay. Worlds live in the
  game's folder, so nothing touches them. Older Nightlies go once nothing plays them.
- The "Nightly builds" setting, its separate install path and its `nightly` key are gone; the old
  "Nightly" instance stays and keeps following the Nightly.
- Versions lists the current Nightly, the last week's and the releases from before; the update
  line names a Nightly by its title.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 20:36:24 +01:00
Ludovic Marie 930252e2e4 Merge pull request #3326 from horizon-rust/ci/nightly-green-base
🤖 Agent: verify green; pr_ready on the Mac mini (399 Python tests incl. new nightly notes tests, launcher clippy/tests); notes checked on real data (56 PRs since Nightly 9b772b2). Step 1 of Nightly only: the Nightly still opt-in, now built on main's newest green commit and held by release-blocker.
2026-10-28 20:36:15 +01:00
Ludovic Marie 7dba3b4d73 Merge pull request #3331 from horizon-rust/feat/servers-follow-nightly
🤖 Agent: verify green; pr_ready on the Mac mini (horizon-server + horizon-release-sign 66 tests incl. new Nightly ordering and signature tests); install.sh installed the real current Nightly end to end in temp folders. Step 2 of Nightly only: servers follow the Nightly.
2026-10-28 20:34:26 +01:00
Jose Gil 8339f6d12f fix(launcher): sync clipboard on Linux Wayland and X11 2026-10-28 15:34:15 -04:00
Ludovic Marie e0bf5f6599 Merge pull request #3329 from horizon-rust/fix/portal-rider-vehicle-timer
🤖 Agent: verify green on 74c2d13d77. New lan_e2e check portal_rider (--server-thread): fails before the review change (rider left after 75 ticks, its own 80-tick delay), passes after (5 ticks, arrives in the Nether on the same horse); portals check passes. Single player on the server thread: through in 0.33 s, still riding the same horse; in-frame unchanged. pr_ready all green (1272 horizon-client tests, clippy, 401 Python tests); parity --changed: all 41 affected harnesses pass.
2026-10-28 20:32:24 +01:00
damnurmumandCodex 2ae5640704 Preserve styled item names and component shadow colors
Keep the original custom-name component as the sole stored value, deriving plain text for gameplay and rendering its styles in tooltips, held labels, and item links. Honor shadow_color in parsed text and HUD glyph passes. Extend the Java/Rust tooltip parity fixture with styled names; its fixture format builds on PR #1702 by RVZO6.

Co-Authored-By: Codex <[email protected]>
2026-10-28 22:31:12 +03:00
Ludovic MarieandClaude Opus 5.5 c7504433c0 launcher: developer options section
A hidden Settings section, off by default; nothing changes for players.
Game downloads stay behind the existing owner check, which a test pins.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 20:29:03 +01:00
Ludovic MarieandClaude Opus 5.5 41266ae70b Native mods: the Native mods instance gets the mods' settings from the default home
Mods installed in the launcher's default home build into the Native mods
instance, which starts empty: a player moving from the WebAssembly Zoom,
Minimap, FPS Counter or Keystrokes to its native version lost their
settings there. The build now copies each mod's storage folder and key
lines from the default home into the instance, once: nothing the instance
already has is overwritten.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 20:25:34 +01:00
Ludovic Marie a3acb5f0d2 Merge pull request #3082 from Domanffe/test/port-harness-symlink-skip
🤖 Agent: read the diff at f4d6f9c0e: test_port_harness skips the reports-link case where the host can't make symlinks. verify green. Queued for the merger.
2026-10-28 20:22:19 +01:00
Ludovic Marie 49b77529dd Merge pull request #2811 from ESHAYAT102/feat/server-delete-confirmation
🤖 Agent: read the diff at 84390f6dc: Delete server in its settings: refused unless the server is in the list, not installed as a service, and its supervisor lock (`.horizon/lock`) can be taken; the folder goes to the system trash, never `rm`. Unit tests cover a live lock, a non-server folder and a failing trash. verify and launcher green; no harness reaches it. Queued for the merger, which waits for this head's parity run.
2026-10-28 20:22:15 +01:00
Ludovic Marie dafa794cb3 Merge pull request #2762 from ESHAYAT102/fix/stopped-server-settings
🤖 Agent: read the diff at bcdbc252f: the sidebar's stopped-server entry now goes through `open_server`, which loads the server's settings instead of only setting `servers.open`. verify and launcher green; no harness reaches launcher code. Queued for the merger, which waits for this head's parity run.
2026-10-28 20:22:09 +01:00
degepieandClaude Opus 5.5 de25583ee5 perf_bench: compile the driver mod with JAVA_HOME's javac
The vanilla runs start `$JAVA_HOME/bin/java`, but the driver mod was
built with whatever `javac` came first on PATH. With JAVA_HOME on a JDK 25
and an older default JDK on PATH (21 on Ubuntu 24.04, for example),
`javac --release 25` fails with "release version 25 not supported" before
any game starts. Use JAVA_HOME's javac when JAVA_HOME is set, as the java
lookups below already do; without it, PATH is used as before.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
2026-10-29 00:21:18 +05:00
Cosmic b7df7bd9e0 Merge pull request #3318 from degepie/fix/accept-26.3-republish-5
Prepare: accept Mojang's fifth 26.3 republish (5df1795e, index 917ff6d7)
2026-10-28 15:17:44 -04:00
Kacpherek 2f95ce431c Follow the furnace code to furnace.rs on main; answer Fox.getBreedOffspring 2026-10-28 20:15:39 +01:00
Kacpherek 0536ebbe98 Merge remote-tracking branch 'origin/main' into tools/port-ledger 2026-10-28 20:11:46 +01:00
Ludovic MarieandClaude Opus 5.5 40e2fe0608 Servers follow the Nightly: horizon-server update, install.sh and install.ps1
Step 2 of making the Nightly the only build players get (Ludovic, 2026-10-28). A server runs what
its players run:

- `horizon-server update` (and the supervisor's check, the panel's update) reads the `nightly`
  release, GitHub's latest while there is none, checks its SHA256SUMS signed for the Nightly
  channel and installs it as `nightly-<date>-<commit>`. Nightlies order after every release by
  date; another build of the same day is an update too (`update_available`).
- install.sh and install.ps1 install the current Nightly by default, under the same name.
- horizon-release-sign: `nightly_version` and `nightly_date`, shared with the launcher next.

Servers on an older horizon-server keep the last vX.Y.Z (GitHub's latest) until the release that
carries this one: it moves them over at their next update.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 20:11:22 +01:00
Shreyas 0320e7d593 Piston head: destroy fitting base without drops in creative mode
In creative mode, playerWillDestroy on an extended piston head destroys
the base piston without dropping resources, matching vanilla Java Edition
PistonHeadBlock.playerWillDestroy.

Fixes #3317
2026-10-29 00:39:39 +05:30
damnurmumandCodex dd130f1902 Render player heads and atlas sprites in text components
Parse Minecraft object components into inline spans and draw player heads and atlas sprites with the existing HUD atlas. Resolve head profiles through the skin cache and preserve fallback text for plain output.

Co-Authored-By: Codex <[email protected]>
2026-10-28 22:07:28 +03:00
matydenandClaude Opus 5.5 8170b8742b Rabbits and wild ocelots run from players
Rabbits fell back to the generic goal list, and ocelots had no goals
of their own, so neither fled. Rabbit.registerGoals now registers its
three RabbitAvoidEntityGoals (players within 8, wolves within 10,
monsters within 4, never for the killer bunny) with panic at 2.2.
Ocelot.registerGoals plus reassessTrustingGoals gives ocelots
OcelotAvoidEntityGoal (players within 16), OcelotTemptGoal, the leap
and attack goals and their chicken and baby turtle targets.

Not ported yet: rabbit RaidGardenGoal and ClimbOnTopOfPowderSnowGoal,
and ocelot trust, so every ocelot is wild.

Refs #2780

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 21:05:31 +02:00
Kacpherek 59538c7dc1 Map the remaining vanilla methods: commands, network, packs, advancements and the last ten 2026-10-28 20:05:02 +01:00
Ludovic Marie a1e56a4d1b Merge pull request #3284 from horizon-rust/perf/sign-text-cache
🤖 Agent: verified. 3458 text signs in view, same scene/build profile, Mac mini: 124 → 222 fps (CPU 8.01 → 4.46 ms/frame), no-sign view unchanged (~1620 fps); look unchanged (pixel diff 6/268800, below run-to-run noise). New unit test checks the cached text against the uncached renderer across text/glow/state/font changes; entity batch equivalence test passes. Local: fmt, clippy -D warnings, horizon-client tests, Python tests and the 50 parity harnesses parity.py --changed picks all pass; CI verify green.
2026-10-28 20:03:48 +01:00
Ludovic Marie ab32a64d08 Merge pull request #2791 from levzzz5154/fix/spawn-egg-baby
🤖 Agent: read the diff at 70068076f: spawn-egg offspring rule split into `spawn_egg_is_for` / `spawn_egg_offspring` (`SpawnEggItem.spawnOffspringFromSpawnEgg`), behaviour unchanged, with a test. verify green, parity green on this head (merged into main), merges cleanly.
2026-10-28 20:03:44 +01:00
Ludovic Marie f843f28a27 Merge pull request #2748 from levzzz5154/fix/turtle-walk-animation-rate
🤖 Agent: read the diff at 186198afe: test only: the turtle's limb speed follows its walk (`LivingEntity.updateWalkAnimation`, `TurtleMoveControl`). verify green, parity green on this head (merged into main), merges cleanly.
2026-10-28 20:01:08 +01:00
Ludovic Marie abcc2e7c80 Merge pull request #3238 from masa-masa-0314/fix/show-dialog-click
🤖 Agent: read the diff at 4d477067c ("Chat and book links: a show_dialog click event opens its dialog") against the 26.3 source it cites; verify green, merges cleanly with main. Proof: cg25 union a051cc285 (local branch cg25/union in t3-ef805fdb: main 6d3489d30 + 38 contributor heads merged cleanly, no fixups): clippy -p horizon-client --all-targets -D warnings clean, horizon-client lib 1105 + bin 133 + integration tests pass, parity --changed 355/355 harnesses PASS on the Mac mini (Cargo.lock changed by #3210, so every harness ran; tab_list passed on its one retry).
2026-10-28 20:01:04 +01:00
Ludovic Marie ac78481794 Merge pull request #3210 from PH4N7OMx/codex/windows-default-browser
🤖 Agent: read the diff at 998c94bee ("fix(windows): open URLs with the registered protocol handler") against the 26.3 source it cites; verify green, merges cleanly with main. Proof: cg25 union a051cc285 (local branch cg25/union in t3-ef805fdb: main 6d3489d30 + 38 contributor heads merged cleanly, no fixups): clippy -p horizon-client --all-targets -D warnings clean, horizon-client lib 1105 + bin 133 + integration tests pass, parity --changed 355/355 harnesses PASS on the Mac mini (Cargo.lock changed by #3210, so every harness ran; tab_list passed on its one retry).
2026-10-28 20:00:59 +01:00
Ludovic Marie 650601848f Merge pull request #3127 from kacpherek/fix/shulker-bullet-model
🤖 Agent: read the diff at 4a18a4bde ("Shulker bullets are drawn as the spinning bullet model, not a flat spr") against the 26.3 source it cites; verify green, merges cleanly with main. Proof: cg25 union a051cc285 (local branch cg25/union in t3-ef805fdb: main 6d3489d30 + 38 contributor heads merged cleanly, no fixups): clippy -p horizon-client --all-targets -D warnings clean, horizon-client lib 1105 + bin 133 + integration tests pass, parity --changed 355/355 harnesses PASS on the Mac mini (Cargo.lock changed by #3210, so every harness ran; tab_list passed on its one retry).
2026-10-28 20:00:55 +01:00
Ludovic Marie f9ea299952 Merge pull request #3124 from kacpherek/fix/mob-crowd-push
🤖 Agent: read the diff at f6b467eba ("Crowded mobs take cramming damage, as vanilla") against the 26.3 source it cites; verify green, merges cleanly with main. Proof: cg25 union a051cc285 (local branch cg25/union in t3-ef805fdb: main 6d3489d30 + 38 contributor heads merged cleanly, no fixups): clippy -p horizon-client --all-targets -D warnings clean, horizon-client lib 1105 + bin 133 + integration tests pass, parity --changed 355/355 harnesses PASS on the Mac mini (Cargo.lock changed by #3210, so every harness ran; tab_list passed on its one retry).
2026-10-28 20:00:50 +01:00
Ludovic MarieandClaude Opus 5.5 52dc27fa40 Nightly: build on main's newest green commit, hold for release-blocker, write player notes
Step 1 of making the Nightly the only build players get (Ludovic, 2026-10-28). Before every
launcher follows it, a Nightly gets a release's safety:

- `nightly.py pick` builds on main's newest commit where checks and parity-main passed
  (release_train.green_main_head), or the new `base` input, instead of main's tip; no green commit
  builds nothing.
- `publish` holds while a release-blocker is open (dry runs only warn, so a blocker's fix can
  still be tried with `/nightly dry-run`).
- The union/* fallback goes: with nothing picked a Nightly is main alone, so players never flip
  between a union and main from one build to the next.
- The notes list what reached main since the previous Nightly (New, Fixed, known issues,
  thanks), as a release's did, then the picked pull requests.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 20:00:49 +01:00
Ludovic Marie b6eb028bb6 Merge pull request #3100 from Domanffe/perf/name-tags-in-range-first
🤖 Agent: read the diff at 5f8afb17f ("Name tags: copy a custom name only inside the tag's range") against the 26.3 source it cites; verify green, merges cleanly with main. Proof: cg25 union a051cc285 (local branch cg25/union in t3-ef805fdb: main 6d3489d30 + 38 contributor heads merged cleanly, no fixups): clippy -p horizon-client --all-targets -D warnings clean, horizon-client lib 1105 + bin 133 + integration tests pass, parity --changed 355/355 harnesses PASS on the Mac mini (Cargo.lock changed by #3210, so every harness ran; tab_list passed on its one retry).
2026-10-28 19:57:52 +01:00
Ludovic Marie 9dffa8a2a9 Merge pull request #3097 from Domanffe/perf/effect-panel-strings
🤖 Agent: read the diff at f7bb1ceb7 ("Effect panels: borrow the name and reuse the icon key") against the 26.3 source it cites; verify green, merges cleanly with main. Proof: cg25 union a051cc285 (local branch cg25/union in t3-ef805fdb: main 6d3489d30 + 38 contributor heads merged cleanly, no fixups): clippy -p horizon-client --all-targets -D warnings clean, horizon-client lib 1105 + bin 133 + integration tests pass, parity --changed 355/355 harnesses PASS on the Mac mini (Cargo.lock changed by #3210, so every harness ran; tab_list passed on its one retry).
2026-10-28 19:57:48 +01:00
Ludovic Marie cd89bc6dc1 Merge pull request #3095 from Domanffe/perf/server-row-sprite-path
🤖 Agent: read the diff at b9494af97 ("Server list: build each row's sprite key once") against the 26.3 source it cites; verify green, merges cleanly with main. Proof: cg25 union a051cc285 (local branch cg25/union in t3-ef805fdb: main 6d3489d30 + 38 contributor heads merged cleanly, no fixups): clippy -p horizon-client --all-targets -D warnings clean, horizon-client lib 1105 + bin 133 + integration tests pass, parity --changed 355/355 harnesses PASS on the Mac mini (Cargo.lock changed by #3210, so every harness ran; tab_list passed on its one retry).
2026-10-28 19:57:44 +01:00
Ludovic Marie 8a863ad378 Merge pull request #3092 from Domanffe/perf/creative-tab-by-reference
🤖 Agent: read the diff at d1ead6a5f ("Creative screen: draw the selected tab from a reference") against the 26.3 source it cites; verify green, merges cleanly with main. Proof: cg25 union a051cc285 (local branch cg25/union in t3-ef805fdb: main 6d3489d30 + 38 contributor heads merged cleanly, no fixups): clippy -p horizon-client --all-targets -D warnings clean, horizon-client lib 1105 + bin 133 + integration tests pass, parity --changed 355/355 harnesses PASS on the Mac mini (Cargo.lock changed by #3210, so every harness ran; tab_list passed on its one retry).
2026-10-28 19:57:40 +01:00
Ludovic Marie a27561da88 Merge pull request #3086 from Domanffe/perf/draw-slot-item-by-reference
🤖 Agent: read the diff at 3511d556e ("Inventory draw: a slot is drawn from a reference, not a copy") against the 26.3 source it cites; verify green, merges cleanly with main. Proof: cg25 union a051cc285 (local branch cg25/union in t3-ef805fdb: main 6d3489d30 + 38 contributor heads merged cleanly, no fixups): clippy -p horizon-client --all-targets -D warnings clean, horizon-client lib 1105 + bin 133 + integration tests pass, parity --changed 355/355 harnesses PASS on the Mac mini (Cargo.lock changed by #3210, so every harness ran; tab_list passed on its one retry).
2026-10-28 19:57:36 +01:00
Ludovic Marie ed7213df2e Merge pull request #3083 from Domanffe/perf/hud-paint-reuses-sprite-key
🤖 Agent: read the diff at 4bf633e8a ("HUD paint: build the sprite key in one reused buffer") against the 26.3 source it cites; verify green, merges cleanly with main. Proof: cg25 union a051cc285 (local branch cg25/union in t3-ef805fdb: main 6d3489d30 + 38 contributor heads merged cleanly, no fixups): clippy -p horizon-client --all-targets -D warnings clean, horizon-client lib 1105 + bin 133 + integration tests pass, parity --changed 355/355 harnesses PASS on the Mac mini (Cargo.lock changed by #3210, so every harness ran; tab_list passed on its one retry).
2026-10-28 19:54:50 +01:00
Ludovic Marie b1b27240d4 Merge pull request #3070 from iLostName/boat-view-clamp-every-turn
🤖 Agent: read the diff at a1b32304a ("boat: the view stops at 105 degrees at every turn, so it doesn't jitte") against the 26.3 source it cites; verify green, merges cleanly with main. Proof: cg25 union a051cc285 (local branch cg25/union in t3-ef805fdb: main 6d3489d30 + 38 contributor heads merged cleanly, no fixups): clippy -p horizon-client --all-targets -D warnings clean, horizon-client lib 1105 + bin 133 + integration tests pass, parity --changed 355/355 harnesses PASS on the Mac mini (Cargo.lock changed by #3210, so every harness ran; tab_list passed on its one retry).
2026-10-28 19:54:45 +01:00
Ludovic Marie 8a2a31e6a3 Merge pull request #3069 from iLostName/no-hitting-ridden-mob
🤖 Agent: read the diff at 7934a0b18 ("riding: the crosshair skips the mob being ridden, so it can't be hit") against the 26.3 source it cites; verify green, merges cleanly with main. Proof: cg25 union a051cc285 (local branch cg25/union in t3-ef805fdb: main 6d3489d30 + 38 contributor heads merged cleanly, no fixups): clippy -p horizon-client --all-targets -D warnings clean, horizon-client lib 1105 + bin 133 + integration tests pass, parity --changed 355/355 harnesses PASS on the Mac mini (Cargo.lock changed by #3210, so every harness ran; tab_list passed on its one retry).
2026-10-28 19:54:41 +01:00
Ludovic Marie 0264b39aa6 Merge pull request #3068 from iLostName/creeper-death-message
🤖 Agent: read the diff at 992c63eef ("death message: a creeper's blast names the creeper, not the last mob t") against the 26.3 source it cites; verify green, merges cleanly with main. Proof: cg25 union a051cc285 (local branch cg25/union in t3-ef805fdb: main 6d3489d30 + 38 contributor heads merged cleanly, no fixups): clippy -p horizon-client --all-targets -D warnings clean, horizon-client lib 1105 + bin 133 + integration tests pass, parity --changed 355/355 harnesses PASS on the Mac mini (Cargo.lock changed by #3210, so every harness ran; tab_list passed on its one retry).
2026-10-28 19:54:37 +01:00
Ludovic Marie 83935015cf Merge pull request #3067 from iLostName/pickup-fills-offhand
🤖 Agent: read the diff at a967ca434 ("item pickup: a stack in the off hand fills before the inventory, as ge") against the 26.3 source it cites; verify green, merges cleanly with main. Proof: cg25 union a051cc285 (local branch cg25/union in t3-ef805fdb: main 6d3489d30 + 38 contributor heads merged cleanly, no fixups): clippy -p horizon-client --all-targets -D warnings clean, horizon-client lib 1105 + bin 133 + integration tests pass, parity --changed 355/355 harnesses PASS on the Mac mini (Cargo.lock changed by #3210, so every harness ran; tab_list passed on its one retry).
2026-10-28 19:54:33 +01:00
Ludovic Marie 506a9c8f01 Merge pull request #3065 from iLostName/f1-only-without-screen
🤖 Agent: read the diff at c0b560527 ("F1 hides the HUD only with no screen open, as key.toggleGui does") against the 26.3 source it cites; verify green, merges cleanly with main. Proof: cg25 union a051cc285 (local branch cg25/union in t3-ef805fdb: main 6d3489d30 + 38 contributor heads merged cleanly, no fixups): clippy -p horizon-client --all-targets -D warnings clean, horizon-client lib 1105 + bin 133 + integration tests pass, parity --changed 355/355 harnesses PASS on the Mac mini (Cargo.lock changed by #3210, so every harness ran; tab_list passed on its one retry).
2026-10-28 19:50:56 +01:00
Ludovic Marie 475b22ff19 Merge pull request #3063 from iLostName/redstone-ore-lights-on-use
🤖 Agent: read the diff at e06134f71 ("redstone ore: a right-click lights it, as RedStoneOreBlock.useItemOn d") against the 26.3 source it cites; verify green, merges cleanly with main. Proof: cg25 union a051cc285 (local branch cg25/union in t3-ef805fdb: main 6d3489d30 + 38 contributor heads merged cleanly, no fixups): clippy -p horizon-client --all-targets -D warnings clean, horizon-client lib 1105 + bin 133 + integration tests pass, parity --changed 355/355 harnesses PASS on the Mac mini (Cargo.lock changed by #3210, so every harness ran; tab_list passed on its one retry).
2026-10-28 19:50:51 +01:00
Ludovic Marie 0d1f56e015 Merge pull request #3060 from kacpherek/fix/name-tags-face-front-camera
🤖 Agent: read the diff at c66554126 ("Name tags read the right way in the front third-person view") against the 26.3 source it cites; verify green, merges cleanly with main. Proof: cg25 union a051cc285 (local branch cg25/union in t3-ef805fdb: main 6d3489d30 + 38 contributor heads merged cleanly, no fixups): clippy -p horizon-client --all-targets -D warnings clean, horizon-client lib 1105 + bin 133 + integration tests pass, parity --changed 355/355 harnesses PASS on the Mac mini (Cargo.lock changed by #3210, so every harness ran; tab_list passed on its one retry).
2026-10-28 19:50:46 +01:00
Ludovic Marie 958c02593c Merge pull request #3056 from kacpherek/fix/spear-charge-while-riding
🤖 Agent: read the diff at 4a1c5fc54 ("Spears: charges hit while riding, and a stab stops the sprint") against the 26.3 source it cites; verify green, merges cleanly with main. Proof: cg25 union a051cc285 (local branch cg25/union in t3-ef805fdb: main 6d3489d30 + 38 contributor heads merged cleanly, no fixups): clippy -p horizon-client --all-targets -D warnings clean, horizon-client lib 1105 + bin 133 + integration tests pass, parity --changed 355/355 harnesses PASS on the Mac mini (Cargo.lock changed by #3210, so every harness ran; tab_list passed on its one retry).
2026-10-28 19:50:42 +01:00
Ludovic Marie a266eee6d2 Merge pull request #3102 from Domanffe/fix/random-sequences-write-failure
🤖 Agent: read the diff at 23d214606: the villagers' random_sequences write logs a warning on failure. verify green. Queued for the merger.
2026-10-28 19:50:37 +01:00
Ludovic Marie 62898b4cf6 Merge pull request #3053 from kacpherek/fix/entity-shadows-no-cutout
🤖 Agent: read the diff at 012a3cf90 ("Draw entity shadows in dim light like vanilla") against the 26.3 source it cites; verify green, merges cleanly with main. Proof: cg25 union a051cc285 (local branch cg25/union in t3-ef805fdb: main 6d3489d30 + 38 contributor heads merged cleanly, no fixups): clippy -p horizon-client --all-targets -D warnings clean, horizon-client lib 1105 + bin 133 + integration tests pass, parity --changed 355/355 harnesses PASS on the Mac mini (Cargo.lock changed by #3210, so every harness ran; tab_list passed on its one retry).
2026-10-28 19:47:45 +01:00
Ludovic Marie d08e05283a Merge pull request #3038 from hectoreccles/perf/spawn-candidate-buffers
🤖 Agent: read the diff at a48fcf0d1 ("Reuse natural spawn candidate buffers") against the 26.3 source it cites; verify green, merges cleanly with main. Proof: cg25 union a051cc285 (local branch cg25/union in t3-ef805fdb: main 6d3489d30 + 38 contributor heads merged cleanly, no fixups): clippy -p horizon-client --all-targets -D warnings clean, horizon-client lib 1105 + bin 133 + integration tests pass, parity --changed 355/355 harnesses PASS on the Mac mini (Cargo.lock changed by #3210, so every harness ran; tab_list passed on its one retry).
2026-10-28 19:47:41 +01:00
Ludovic Marie e7a07f88b3 Merge pull request #3035 from hectoreccles/perf/streaming-scan-interval
🤖 Agent: read the diff at 2eb3de274 ("Sweep far chunks every eighth tick") against the 26.3 source it cites; verify green, merges cleanly with main. Proof: cg25 union a051cc285 (local branch cg25/union in t3-ef805fdb: main 6d3489d30 + 38 contributor heads merged cleanly, no fixups): clippy -p horizon-client --all-targets -D warnings clean, horizon-client lib 1105 + bin 133 + integration tests pass, parity --changed 355/355 harnesses PASS on the Mac mini (Cargo.lock changed by #3210, so every harness ran; tab_list passed on its one retry).
2026-10-28 19:47:36 +01:00
Ludovic Marie 4e59dab892 Merge pull request #3033 from hectoreccles/perf/block-entity-ticker-list
🤖 Agent: read the diff at 67511d974 ("Reuse the block entity ticker list between ticks") against the 26.3 source it cites; verify green, merges cleanly with main. Proof: cg25 union a051cc285 (local branch cg25/union in t3-ef805fdb: main 6d3489d30 + 38 contributor heads merged cleanly, no fixups): clippy -p horizon-client --all-targets -D warnings clean, horizon-client lib 1105 + bin 133 + integration tests pass, parity --changed 355/355 harnesses PASS on the Mac mini (Cargo.lock changed by #3210, so every harness ran; tab_list passed on its one retry).
2026-10-28 19:47:32 +01:00
Ludovic Marie a1f4841526 Merge pull request #3032 from hectoreccles/perf/hopper-cooldown-first
🤖 Agent: read the diff at df90f62d3 ("Hopper ticks check their block only when they move") against the 26.3 source it cites; verify green, merges cleanly with main. Proof: cg25 union a051cc285 (local branch cg25/union in t3-ef805fdb: main 6d3489d30 + 38 contributor heads merged cleanly, no fixups): clippy -p horizon-client --all-targets -D warnings clean, horizon-client lib 1105 + bin 133 + integration tests pass, parity --changed 355/355 harnesses PASS on the Mac mini (Cargo.lock changed by #3210, so every harness ran; tab_list passed on its one retry).
2026-10-28 19:47:27 +01:00
Ludovic MarieandClaude Opus 5.5 4e804cd813 Native mods on the hook points build on any Horizon that has them
A .hnmod may name the native hook points' version it uses ("hooks": 1 in
horizon-native.json) instead of pinning a commit. The launcher builds such
mods on the commit the other mods pin, else on the installed version's,
once that commit's NATIVE_HOOKS_API (read from GitHub) matches; else, with
no pin, on the commit they were tested on. Its own copy of each such mod
is re-pinned to the build's commit for horizon-mixins. So Horizon's
official mods build together, on the player's own game, with no
re-release for each Nightly. The mods feature is passed only when the
source still has it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 19:46:58 +01:00
Ludovic Marie 7dd4b772c9 Merge pull request #2997 from feeldev12/feat/link-prompt-copy
🤖 Agent: read the diff at 5c14d1814 ("fix(client): the link prompt has ConfirmLinkScreen's Copy to Clipboard") against the 26.3 source it cites; verify green, merges cleanly with main. Proof: cg25 union a051cc285 (local branch cg25/union in t3-ef805fdb: main 6d3489d30 + 38 contributor heads merged cleanly, no fixups): clippy -p horizon-client --all-targets -D warnings clean, horizon-client lib 1105 + bin 133 + integration tests pass, parity --changed 355/355 harnesses PASS on the Mac mini (Cargo.lock changed by #3210, so every harness ran; tab_list passed on its one retry).
2026-10-28 19:44:13 +01:00
Ludovic Marie 03dd78d67b Merge pull request #2994 from kacpherek/fix/particles-sounds-parity
🤖 Agent: read the diff at 0f8e3a9b0 ("Match vanilla mob and arrow sounds: horses, hostile fall and swim, hur") against the 26.3 source it cites; verify green, merges cleanly with main. Proof: cg25 union a051cc285 (local branch cg25/union in t3-ef805fdb: main 6d3489d30 + 38 contributor heads merged cleanly, no fixups): clippy -p horizon-client --all-targets -D warnings clean, horizon-client lib 1105 + bin 133 + integration tests pass, parity --changed 355/355 harnesses PASS on the Mac mini (Cargo.lock changed by #3210, so every harness ran; tab_list passed on its one retry).
2026-10-28 19:44:09 +01:00
Ludovic Marie 6267cd3e15 Merge pull request #2992 from kacpherek/fix/mob-ai-villager-parity
🤖 Agent: read the diff at efa8df83b ("Villagers hear bells, use bone meal, fetch dropped items and play tag ") against the 26.3 source it cites; verify green, merges cleanly with main. Proof: cg25 union a051cc285 (local branch cg25/union in t3-ef805fdb: main 6d3489d30 + 38 contributor heads merged cleanly, no fixups): clippy -p horizon-client --all-targets -D warnings clean, horizon-client lib 1105 + bin 133 + integration tests pass, parity --changed 355/355 harnesses PASS on the Mac mini (Cargo.lock changed by #3210, so every harness ran; tab_list passed on its one retry).
2026-10-28 19:44:04 +01:00
Ludovic Marie 265640ac18 Merge pull request #2991 from kacpherek/fix/block-behaviour-parity
🤖 Agent: read the diff at ffb659460 ("Match vanilla for eight block behaviours: ender chest, doors, hanging ") against the 26.3 source it cites; verify green, merges cleanly with main. Proof: cg25 union a051cc285 (local branch cg25/union in t3-ef805fdb: main 6d3489d30 + 38 contributor heads merged cleanly, no fixups): clippy -p horizon-client --all-targets -D warnings clean, horizon-client lib 1105 + bin 133 + integration tests pass, parity --changed 355/355 harnesses PASS on the Mac mini (Cargo.lock changed by #3210, so every harness ran; tab_list passed on its one retry).
2026-10-28 19:44:00 +01:00
Ludovic Marie f5a3088f0d Merge pull request #2985 from kacpherek/fix/hostile-mob-parity
🤖 Agent: read the diff at f69088012 ("Hostile mobs: hoglin timing, crossbow aim, witch resistance, wither sk") against the 26.3 source it cites; verify green, merges cleanly with main. Proof: cg25 union a051cc285 (local branch cg25/union in t3-ef805fdb: main 6d3489d30 + 38 contributor heads merged cleanly, no fixups): clippy -p horizon-client --all-targets -D warnings clean, horizon-client lib 1105 + bin 133 + integration tests pass, parity --changed 355/355 harnesses PASS on the Mac mini (Cargo.lock changed by #3210, so every harness ran; tab_list passed on its one retry).
2026-10-28 19:43:56 +01:00
Ludovic Marie 3ccbba76df Merge pull request #2984 from kacpherek/fix/passive-mob-parity
🤖 Agent: read the diff at 58422bcc1 ("Passive mobs: sound variants, donkey and mule stats, no mule breeding,") against the 26.3 source it cites; verify green, merges cleanly with main. Proof: cg25 union a051cc285 (local branch cg25/union in t3-ef805fdb: main 6d3489d30 + 38 contributor heads merged cleanly, no fixups): clippy -p horizon-client --all-targets -D warnings clean, horizon-client lib 1105 + bin 133 + integration tests pass, parity --changed 355/355 harnesses PASS on the Mac mini (Cargo.lock changed by #3210, so every harness ran; tab_list passed on its one retry).
2026-10-28 19:39:39 +01:00
Ludovic Marie c519ff6c56 Merge pull request #2977 from ASOwnerYT/fix/2832-return-to-sender
🤖 Agent: read the diff at c4348be8d ("Return to Sender needs the ghast's own fireball: check player_killed_e") against the 26.3 source it cites; verify green, merges cleanly with main. Proof: cg25 union a051cc285 (local branch cg25/union in t3-ef805fdb: main 6d3489d30 + 38 contributor heads merged cleanly, no fixups): clippy -p horizon-client --all-targets -D warnings clean, horizon-client lib 1105 + bin 133 + integration tests pass, parity --changed 355/355 harnesses PASS on the Mac mini (Cargo.lock changed by #3210, so every harness ran; tab_list passed on its one retry).
2026-10-28 19:39:34 +01:00
Ludovic Marie 400d15ab3f Merge pull request #2975 from kacpherek/fix/advancement-triggers-small
🤖 Agent: read the diff at c3e19973c ("Fire lightning, spear, nest, shear, allay and sculk advancement trigge") against the 26.3 source it cites; verify green, merges cleanly with main. Proof: cg25 union a051cc285 (local branch cg25/union in t3-ef805fdb: main 6d3489d30 + 38 contributor heads merged cleanly, no fixups): clippy -p horizon-client --all-targets -D warnings clean, horizon-client lib 1105 + bin 133 + integration tests pass, parity --changed 355/355 harnesses PASS on the Mac mini (Cargo.lock changed by #3210, so every harness ran; tab_list passed on its one retry).
2026-10-28 19:39:29 +01:00
Ludovic Marie d6971605d4 Merge pull request #2957 from kacpherek/fix/smooth-camera-mouse
🤖 Agent: read the diff at 7ab2b20bd ("Let a mouse button toggle the cinematic camera") against the 26.3 source it cites; verify green, merges cleanly with main. Proof: cg25 union a051cc285 (local branch cg25/union in t3-ef805fdb: main 6d3489d30 + 38 contributor heads merged cleanly, no fixups): clippy -p horizon-client --all-targets -D warnings clean, horizon-client lib 1105 + bin 133 + integration tests pass, parity --changed 355/355 harnesses PASS on the Mac mini (Cargo.lock changed by #3210, so every harness ran; tab_list passed on its one retry).
2026-10-28 19:39:24 +01:00
Ludovic Marie 7dc818eb5b Merge pull request #2948 from kacpherek/fix/hdr-crash-macos
🤖 Agent: read the diff at dddcc9972 ("Fix the crash on every start with HDR on in macOS, and start with HDR ") against the 26.3 source it cites; verify green, merges cleanly with main. Proof: cg25 union a051cc285 (local branch cg25/union in t3-ef805fdb: main 6d3489d30 + 38 contributor heads merged cleanly, no fixups): clippy -p horizon-client --all-targets -D warnings clean, horizon-client lib 1105 + bin 133 + integration tests pass, parity --changed 355/355 harnesses PASS on the Mac mini (Cargo.lock changed by #3210, so every harness ran; tab_list passed on its one retry).
2026-10-28 19:39:20 +01:00
Ludovic Marie 2772738d47 Merge pull request #2938 from ASOwnerYT/fix/2802-projectile-death-message
🤖 Agent: read the diff at 513aed64c ("Name the projectile in the death message when it has no owner") against the 26.3 source it cites; verify green, merges cleanly with main. Proof: cg25 union a051cc285 (local branch cg25/union in t3-ef805fdb: main 6d3489d30 + 38 contributor heads merged cleanly, no fixups): clippy -p horizon-client --all-targets -D warnings clean, horizon-client lib 1105 + bin 133 + integration tests pass, parity --changed 355/355 harnesses PASS on the Mac mini (Cargo.lock changed by #3210, so every harness ran; tab_list passed on its one retry).
2026-10-28 19:35:20 +01:00
Ludovic Marie 131625c414 Merge pull request #2906 from kacpherek/fix/2904-fullscreen-crash
🤖 Agent: read the diff at 22cab3eeb ("Open windowed after the window system kills the game in fullscreen") against the 26.3 source it cites; verify green, merges cleanly with main. Proof: cg25 union a051cc285 (local branch cg25/union in t3-ef805fdb: main 6d3489d30 + 38 contributor heads merged cleanly, no fixups): clippy -p horizon-client --all-targets -D warnings clean, horizon-client lib 1105 + bin 133 + integration tests pass, parity --changed 355/355 harnesses PASS on the Mac mini (Cargo.lock changed by #3210, so every harness ran; tab_list passed on its one retry).
2026-10-28 19:35:15 +01:00
Ludovic Marie 478ae35b53 Merge pull request #2880 from leifiyoo/dyed-leather-armour-colour
🤖 Agent: read the diff at 8057d2751 ("Worn dyed leather armour shows its dye colour") against the 26.3 source it cites; verify green, merges cleanly with main. Proof: cg25 union a051cc285 (local branch cg25/union in t3-ef805fdb: main 6d3489d30 + 38 contributor heads merged cleanly, no fixups): clippy -p horizon-client --all-targets -D warnings clean, horizon-client lib 1105 + bin 133 + integration tests pass, parity --changed 355/355 harnesses PASS on the Mac mini (Cargo.lock changed by #3210, so every harness ran; tab_list passed on its one retry).
2026-10-28 19:35:11 +01:00
Ludovic Marie 5f868011ee Merge pull request #2843 from levzzz5154/fix/breeze-wind-scroll
🤖 Agent: read the diff at f507a067c ("The breeze's wind and the wind charge scroll their textures") against the 26.3 source it cites; verify green, merges cleanly with main. Proof: cg25 union a051cc285 (local branch cg25/union in t3-ef805fdb: main 6d3489d30 + 38 contributor heads merged cleanly, no fixups): clippy -p horizon-client --all-targets -D warnings clean, horizon-client lib 1105 + bin 133 + integration tests pass, parity --changed 355/355 harnesses PASS on the Mac mini (Cargo.lock changed by #3210, so every harness ran; tab_list passed on its one retry).
2026-10-28 19:35:06 +01:00
Ludovic Marie 67712eeeef Merge pull request #2836 from levzzz5154/fix/spawn-egg-persistence
🤖 Agent: read the diff at 28f906c37 ("A summoned or egg-spawned mob is not persistent") against the 26.3 source it cites; verify green, merges cleanly with main. Proof: cg25 union a051cc285 (local branch cg25/union in t3-ef805fdb: main 6d3489d30 + 38 contributor heads merged cleanly, no fixups): clippy -p horizon-client --all-targets -D warnings clean, horizon-client lib 1105 + bin 133 + integration tests pass, parity --changed 355/355 harnesses PASS on the Mac mini (Cargo.lock changed by #3210, so every harness ran; tab_list passed on its one retry).
2026-10-28 19:35:02 +01:00
Ludovic Marie 5b7d19f02b Merge pull request #3080 from Domanffe/fix/port-regen-python-that-runs
🤖 Agent: read the diff at 93e61ef93: port_regen.python() probes python3/python/py and keeps the first that runs Python 3 (cached), with a test. verify green. Queued for the merger.
2026-10-28 19:30:16 +01:00
Ludovic MarieandClaude Opus 5.5 e2f3d1a85a Registry: a WebAssembly mod's entry may turn native once
An official mod moving to native hook points switches its registry entry
from `mod` to `native-mod`. Review now allows that one change (never
back): the entry's old `.hmod` versions stay published and are still
checked as `.hmod`s, while the index lists only its `.hnmod` versions.

The launcher's update to the native version keeps the player's `.hmod`:
the game runs it until a native build has the mod, then skips it.
Removing the mod removes both files.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 19:15:38 +01:00
74c2d13d77 Merge origin/main into the portal rider travel branch
Co-Authored-By: levzzz5154 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 19:02:22 +01:00
9ff0d98010 Let the vehicle take its riders through a portal at once
In vanilla 26.3 a passenger has no portal process of its own
(`Entity.canUsePortal(false)` is false for a passenger, so
`NetherPortalBlock.entityInside` skips it); the vehicle's goes through
on the next tick (`NetherPortalBlock.getPortalTransitionTime` is 0 for
anything that isn't a player) and `Entity.teleportCrossDimension`
carries the passengers. Here the rider's own timer decided, so a
survival player on a horse waited 80 ticks in the portal.

A server player who rides now starts no Nether or End portal process. A
horse, boat or minecart ridden only by server players uses the entity
portal path on a server, and once through, each rider's trip starts
with the vehicle (the trip's `vehicle`). Out of the End every rider must
have seen the credits (`Entity.canTeleport`). The level the vehicle
waits in stays open until it has moved. Elsewhere (an in-frame LAN
host) a ridden vehicle stays rather than leave its rider behind.

lan_e2e gets a `portal_rider` check (--server-thread).

Co-Authored-By: levzzz5154 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 18:54:57 +01:00
ThatCakeIsAPieandClaude Opus 5.5 24e316855e Create World: the Data Packs screen
The More tab's Data Packs button opens vanilla's PackSelectionScreen (the
resource pack screen's port, titled dataPack.title) on a new mcworld-
folder: its packs, with their icons, and the built-in one, re-listed when
the folder changes. Create New World copies the folder's packs into the
world's datapacks/ and writes datapacks.json with the selection, as
CreateWorldScreen.copyTempDataPackDirToNewWorld does. Cancel deletes the
folder. Worlds made without opening the screen are unchanged.

The pack screen parity harness also runs the screen under dataPack.title.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 12:50:12 -05:00
Hector Eccles e41443c64c fix: address review findings
- palette lookups only match live slots, not freed ones still on the free list (#3253)
2026-10-28 13:42:04 -04:00
Ludovic Marie f66f66c3b4 Merge pull request #2815 from sirrayi/fix/zip-untrusted-sizes
🤖 Agent: read the diff at ce848af06: zip hardening: the raw buffer's capacity is capped at the archive length and inflate at 1 GiB with saturating maths, so a zip64 entry claiming u64::MAX/2 errs instead of aborting; real packs are unchanged (the limit stays size*2+1024 below 1 GiB). Test included. verify green; no harness reaches it. Queued for the merger, which waits for this head's parity run.
2026-10-28 18:39:20 +01:00
42be3018dd Merge origin/main into the portal rider travel branch
main now sets a 300-tick portal cooldown with `set_portal_cooldown(id)`
(#2352), so the vehicle's arrival uses it; the world spawn trip
(#2355) gets the new `source` and `vehicle` fields; the parked level
keeps both the public mob list and the leash knot ids (#2123).

Co-Authored-By: levzzz5154 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 18:39:01 +01:00
Ludovic Marie 7cbb8c4506 Merge pull request #2808 from levzzz5154/fix/shulker-body-yaw
🤖 Agent: read the diff at a8903cba0: test only: the shulker's body stays at 0 while its head turns (`ShulkerBodyRotationControl`, `ShulkerLookControl`). verify green, parity green on this head (merged into main), merges cleanly.
2026-10-28 18:35:28 +01:00
Ludovic Marie 6a05885ee3 Merge pull request #2797 from levzzz5154/fix/screen-cursor-centre
🤖 Agent: read the diff at 1d693cb3c: `MouseHandler.grabMouse`/`releaseMouse`: the cursor is centred only when the grab state changes; fixture test updated. verify green, parity green on this head (merged into main), merges cleanly.
2026-10-28 18:35:22 +01:00
Ludovic Marie c979ce46e9 Merge pull request #2771 from ASOwnerYT/claude/select-world-blank-deletion-332d14
🤖 Agent: read the diff at f10558380: `WorldSelectionList.handleNewLevels`: an emptied list opens Create New World, whose Cancel returns to the title. verify green, parity green on this head (merged into main), merges cleanly.
2026-10-28 18:35:13 +01:00
Ludovic Marie 0c5c38ac9e Merge pull request #2736 from levzzz5154/fix/2273-entity-view-scale
🤖 Agent: read the diff at fa940d5b6: `entity_view_scale` extracted from `Entity.setViewScale` (clamp(renderDistance / 8, 1, 2.5) * entityDistanceScaling) with tests for the 48-block chicken range. verify green, parity green on this head (merged into main), merges cleanly.
2026-10-28 18:35:09 +01:00
Ludovic Marie ae3b9e273f Merge pull request #2732 from levzzz5154/fix/mining-hit-sound
🤖 Agent: read the diff at fcc7a818b: the remote-mining fixture now records that the hit sound plays once at dig start (`ClientLevel.addBreakingBlockEffects`), plus the `dig_sounds` LAN check for #2277. verify green, parity green on this head (merged into main), merges cleanly.
2026-10-28 18:35:04 +01:00
Muhammad Asif 1a9040ad79 Swing the arm when opening containers
MultiPlayerGameMode.useItemOn swings on InteractionResult.SUCCESS
(SwingSource.CLIENT). The remote client only swung when the held item
was a block, so opening a chest, barrel, furnace, or similar storage
did not start the arm animation.
2026-10-28 22:33:23 +05:00
Kacpherek 2c10febff1 issue: read registry names, so a mooshroom report finds MushroomCow 2026-10-28 18:27:10 +01:00
Ludovic Marie d0b6e48921 Merge pull request #3281 from horizon-rust/perf/world-entry-decoration-cache
🤖 Agent: verify green; pr_ready on the Mac mini passes (94 affected harnesses, 93 run and pass incl. the worldgen ones; lan_integration by name only; new test decoration_is_shared_by_biome_source). Through the Singleplayer screen, 2 workers, medians of 5: client joined 0.45 → 0.31 s (create) and 0.46 → 0.31 s (reload); reload first controllable frame 0.60 → 0.42 s.
2026-10-28 18:20:09 +01:00
RVZO6andCodex 61713b8359 Fix menu separator texture sampling
Use both rows of the separator textures when tiling headers and footers, including the create-world screen.

Co-Authored-By: Codex (GPT-6.1 Terra) <[email protected]>
2026-10-28 13:16:26 -04:00
Kacpherek a32e0ac4ec Add a vanilla-parity skill: start parity fixes from the port ledger 2026-10-28 18:09:19 +01:00
Ludovic Marie 07e4a86658 Merge pull request #2817 from sirrayi/fix/server-http-line-limit
🤖 Agent: read the diff at d3be8dbf0: horizon-server HTTP API: request and header lines over 8 KiB are a 400 instead of growing without bound; test with a 100 KB line. verify green; no harness reaches it. Queued for the merger, which waits for this head's parity run.
2026-10-28 17:58:46 +01:00
Hector EcclesandDeepSeek V4.1 Flash 22a4620643 Build beacon beam sections on server clients
A chunk's block entity data carries a beacon's levels and powers but not
its beam sections, so in the default server-thread path and on LAN or
multiplayer clients the sections stayed empty and no beam was drawn.
Tick beacons with ClientLevel.tickBlockEntities' beam pass, without the
effects, sounds and advancement the server owns.

Co-Authored-By: DeepSeek V4.1 Flash <[email protected]>
2026-10-28 12:54:44 -04:00
Hector EcclesandDeepSeek V4.1 Flash 8422225047 Keep beacon beams visible past the render distance
Remember each active beacon's beam sections when its chunk unloads, so
far terrain LOD keeps drawing the beam until the chunk loads again and
its block entity decides whether it is still there.

The cache only runs with far terrain on, and clears on world, dimension
or LOD changes.

Co-Authored-By: DeepSeek V4.1 Flash <[email protected]>
2026-10-28 12:54:44 -04:00
degepieandClaude Opus 5.5 3c98062662 Prepare: accept Mojang's fifth 26.3 republish (5df1795e, index 917ff6d7)
Mojang republished the 26.3 version JSON again. The live copy (5df1795e)
points at asset index 34 917ff6d7; neither was in research-lock.json, so
`horizon-prepare --prepare` from a fresh official-launcher install stops
with "is not a 26.3 version file Horizon knows", and the sounds step warns
about an unknown asset index. Jars, libraries and javaVersion are
unchanged; the index differs from 0c8f43b2 only in 142 minecraft/lang/*
objects and pack.mcmeta (uz_uz moved in the language list).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
2026-10-28 21:51:58 +05:00
Ludovic Marie e1b2b0b2fd Merge pull request #2742 from levzzz5154/fix/fire-animated-textures
🤖 Agent: read the diff at a77639022: test only: every graphics preset keeps animated textures on (#2064 stays fixed). verify green; parity selects no harness for it. Queued for the merger, which waits for this head's parity run.
2026-10-28 17:47:18 +01:00
Kacpherek fd8f1a02a1 Map client screens, toasts, recipe book and world screens 2026-10-28 17:43:08 +01:00
levzzzandCodex 9755416a02 Fix Windows path assertions in the launcher toolchain test (#3285)
Compare managed compiler and PATH entries as platform paths, including Windows executable suffixes. Fixes the Windows assertion that blocked the native-capable launcher release.

Validation: pr_ready passed (401 Python and 219 launcher tests), and the release dry run passed tests and packaged smoke checks on Linux, macOS and Windows.

Co-Authored-By: Codex <[email protected]>
2026-10-28 21:42:53 +05:00
Kacpherek a1cf544de7 map-check: accept full Class#method keys 2026-10-28 17:29:51 +01:00
Kacpherek 8d8737a266 Map client rendering, particles, sounds and resources 2026-10-28 17:29:16 +01:00
Ludovic MarieandClaude Opus 5.5 0281365907 Signs: keep laid-out sign text between frames, batch entity quads in runs
A wall of 3458 text signs ran at 128 fps against 1642 without them: each
frame laid every sign's text out again glyph by glyph (Font::glyph_of was a
quarter of the main thread), then regrouped every entity quad by material
through a per-quad index list.

- block_entity_render: a sign's text vertices are kept relative to its
  block and moved to the camera each frame. They are laid out again when
  the sign data, block state, light, glowing outline distance or font
  (Font::epoch: a new font, a glyph cache reset, the Unicode and Japanese
  options) changes; signs out of view for 100 ticks are dropped.
- gfx/entity_batch: quads are grouped in runs of one material and whole
  runs copied (2-4x faster on model-like input, the same when every quad
  changes material).

Fixes #1461 (the cost per sign; the vanilla text distance of 64 blocks
and the look are unchanged).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 17:28:08 +01:00
rdbt a5fcb49900 Merge pull request #3262 from Domanffe/fix/chat-log-duplicate
Log a chat message once, as [CHAT]
2026-10-28 16:27:04 +00:00
Ludovic MarieandClaude Opus 5.5 6f6229c2d4 Worldgen: build a biome source's feature decoration once, ahead of the world
`Decoration::new` decodes the placed features of every biome a source can
place: about 140 ms of the ~185 ms `OverworldGenerator::for_world` took on
each world opening (Mac mini). It depends on the worldgen resources and the
biome source alone, not on the seed, so `GeneratorData` now keeps it by
biome source (`decoration_for`, like the ring cache) and every generator
built for that source shares it.

The world screens' preload (#2481) already reads the worldgen bundle while
the player picks or creates a world; it now also builds the Overworld's
decoration (`prepare_decoration`), so opening the world finds it ready.
Reopening a world or returning to a dimension in the same session builds
nothing again either.

Mac mini, --profile fast, offscreen, seed 1234, RD 12, 2 workers, server
thread, through the Singleplayer screen, medians of 5 alternated with main:
  server ready to join: create 0.45 -> 0.31 s, reload 0.46 -> 0.31 s
  first controllable frame: reload 0.60 -> 0.42 s, create 0.85 -> 0.79 s

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 17:22:05 +01:00
Ludovic Marie 0ca3a5be98 Merge pull request #3077 from Domanffe/test/parity-reach-posix-fixture
🤖 Agent: read the diff at aef4186de: test_parity_reach's fixture records dependencies with `as_posix()`, as the tool does. verify green. Queued for the merger.
2026-10-28 17:19:37 +01:00
Ludovic Marie 8993772de0 Merge pull request #3078 from Domanffe/fix/port-harness-windows-log-path
🤖 Agent: read the diff at 28e3286e4: port_harness also picks up Windows `C:...x.log` paths from javac's "see ..." lines. verify green. Queued for the merger.
2026-10-28 17:00:51 +01:00
Ludovic Marie 6a261a00a5 Merge pull request #2886 from socksprox/fix/launcher-forget-offline-name
🤖 Agent: read the diff at 5096b4110: launcher offline names: Forget also deletes the name's skin and cape files, async skin picks bail if the name was forgotten meanwhile, account rows load-mutate-save, and "New name" rolls a fresh PlayerNNNNN into every game home. Tests included. verify and launcher green; no harness reaches launcher code. Queued for the merger, which waits for this head's parity run.
2026-10-28 17:00:45 +01:00
Ludovic Marie f8b83eea51 Merge pull request #3250 from horizon-rust/perf/world-entry-pump-between-ticks
🤖 Agent: verify green; pr_ready on the Mac mini passes (34 affected harnesses, 33 run and pass, lan_integration by name: 7 lan_e2e checks incl. spawned/respawn/portals/forced-levels pass). World entry, 2 workers, RD 12, medians of 3: create 8.05 → 3.57 s, reload 3.72 → 1.42 s to all view columns loaded.
2026-10-28 16:54:43 +01:00
Kacpherek c045245e8e Map client packet handling, entity renderers and models, block renderers
Also let PORT_LEDGER_NO_MAP build without the verdict map, to measure the
automatic matches on their own.
2026-10-28 16:54:18 +01:00
Alexander KrylovandHermes Agent 88398e8bda Log a chat message once
`chat_line` logged `[chat]` and then `Chat::add_message` logged `[CHAT]` for
the same message, so every line appeared twice, and the `[chat]` copy slipped
past the `[CHAT]` filter `log_for_report` uses to keep chat out of an attached
log.

Vanilla's `ChatComponent.logChatMessage` writes the one line `add_message`
already writes, so the `chat_line` call is the leftover.

Fixes #3261

Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
2026-10-28 18:36:21 +03:00
Ludovic Marie 6d3489d30b Merge pull request #3064 from iLostName/trial-spawner-spawn-egg
🤖 Agent: read the diff at 557a82738: a spawn egg on a trial spawner follows `TrialSpawner.overrideEntityToSpawn` -> `FullConfig.overrideEntity` in the 26.3 source: data reset, normal and ominous configs replaced by `Holder.direct(withSpawning(type))` (saved inline through the new `TrialConfig::encode`, `optionalFieldOf` defaults left out), then `setState(INACTIVE)`; plain spawners unchanged. Round-trip test included. verify and parity green on this head. Queued for the merger.
2026-10-28 16:35:54 +01:00
Ludovic MarieandClaude Opus 5.5 7a84c93843 Server: feed the chunk workers between ticks while players' chunks load
A server thread (single player's, a LAN host's, a dedicated server's) ran
`streaming.update` once a tick, and `schedule_generation` keeps at most
three load or generate jobs per worker in flight. Joining a world was
capped at about six columns a tick (120 a second with two workers) while
the workers idled most of each tick. Vanilla's server runs its chunk
tasks while it waits for the next tick (`MinecraftServer.waitUntilNextTick`).

`pump_pregen`, now `pump_chunks`, already did that for a /pregen task and
force-loaded chunks; it now also pumps while the players' chunks are in
flight, except while the server is paused or a respawn search waits for
a chunk (its area is only asked for by the tick's own update).

Mac mini, --profile fast, offscreen, seed 1234, RD 12, server thread,
medians of 3, all 637 view columns in and meshed once:
  2 workers: create 8.05 -> 3.57 s, reload 3.72 -> 1.42 s
  4 workers: create 4.97 -> 2.16 s, reload 2.23 -> 1.27 s

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 16:26:03 +01:00
Kacpherek 91358d8088 Map the rest of the game logic: classes with no Rust trace, then method batches 2026-10-28 16:26:00 +01:00
Hector EcclesandOpenCode d46bed5c36 perf: hash the world's integer bookkeeping maps with Fx
World.modified, inhabited, chest_openers and block_names used std SipHash
on small integer keys, and modified/inhabited rehash as they grow while an
area unloads. Use the existing crate::fast_hash maps; none of their
iterations is order-sensitive (save order comes from the batch list and
ChunkExtras::block_entity_order). chunks_with_block_entities takes the
names map by hasher so the faster map still fits.

BlockEntities::map stays std HashMap: crate::lan::chunks::body takes the
default-hasher map by type and lan/** is outside this change.

Co-Authored-By: OpenCode (DeepSeek V4.1 Flash) <[email protected]>
2026-10-28 11:07:24 -04:00
Hector EcclesandOpenCode 9b9d045150 perf: find section palette ids with a scan and a last-state cache
BlockSection kept a HashMap of state ids per section (24 tables and
allocations per proto chunk) and hashed on every worldgen write. Resident
palettes are almost always tiny, and the resident container already scans
them; use the same scan with a one-entry last lookup so a run of one state
is a compare. set/compact behaviour and the palette, counts and free list
are unchanged.

Co-Authored-By: OpenCode (DeepSeek V4.1 Flash) <[email protected]>
2026-10-28 11:07:24 -04:00
Hector EcclesandOpenCode 1e5cc9031f perf: share the multiply-shift index between paletted storage and PackedStorage
PackedStorage divided by its per-word width on every read and write, while
PalettedContainer already used an exact multiply-shift. Move MAGIC/locate/
mask into storage.rs and use them from both; only storages past 16,384
entries keep the division path.

Co-Authored-By: OpenCode (DeepSeek V4.1 Flash) <[email protected]>
2026-10-28 11:07:24 -04:00
Hector EcclesandOpenCode ee351caed8 perf: mark only the distinct sections a block write dirties
A write looped 27 block offsets through the dirty-section map; all but a
few hashed the same keys. Mark the exact section set instead: one key in
the interior, the adjacent section on a section face, a neighbour column
on a chunk border, and the diagonal where both borders meet (at most eight
keys). The mesher still sees the same 3x3x3 neighbourhood.

Co-Authored-By: OpenCode (DeepSeek V4.1 Flash) <[email protected]>
2026-10-28 11:07:24 -04:00
Ludovic Marie cba52f4e5d Merge pull request #3218 from horizon-rust/fix-server-junction
🤖 Agent: verified: verify, launcher and parity-scope green; local fmt, clippy -D warnings and launcher tests pass. Fixes #3200. Queued for merge.
2026-10-28 16:04:17 +01:00
SolomonNumb1 9d7b4e8a2d tooltip fix 2026-10-28 20:03:12 +05:00
some-du6eandClaude Opus 5.5 f52af64e89 Spears: the jab animates in first and third person
A spear's left click is a STAB swing (SwingAnimationType 2), but every
swing was drawn as a WHACK. Port SpearAnimations.firstPersonAttack,
thirdPersonAttackHand and thirdPersonAttackItem, and keep a swing's type
for mobs and other players (from ClientboundSwingAnimationPacket) and
the local player's model.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 09:02:21 -06:00
Senjin 8ebcf6c13d WIP HDR: ask the compositor for the window's luminances (log only) 2026-10-28 16:00:54 +01:00
masa-masa-0314andZCode 4d477067c3 Chat and books: open a show_dialog click event's dialog
A server may put a show_dialog click event on a chat line or a book
page; ClickEvent.Action.SHOW_DIALOG allows it from a server, and vanilla
opens the dialog through Screen.defaultHandleGameClickEvent. Horizon
dropped the click event while parsing the component (no arm for it in
component.rs, no ClickEvent variant to carry it), so clicking the text
did nothing.

The dialog now survives as ClickEvent::ShowDialog (a registry id or the
inline dialog), parses from click_event, round-trips back out, and both
click handlers (chat and BookViewScreen) open it with show_dialog_ref,
the same path a server's show_dialog packet takes.

Co-Authored-By: ZCode <[email protected]>
2026-10-28 22:52:46 +08:00
masa-masa-0314andZCode 60773a0e92 Dialog: send the show and clear packets the commands promise
/dialog show and /dialog clear ran, printed their feedback and told
nobody: the dialog state they set was never read, and nothing built
ClientboundShowDialogPacket or ClearDialogPacket. DialogCommand's own
ServerPlayer.openDialog/closeDialog send the packet to each target, so
the command now resolves the argument (a minecraft:dialog registry entry
by index, or the inline dialog as sent) and sends it to every target:
the host's own player opens it on the screen, LAN guests get the packet
through the hub.

Co-Authored-By: ZCode <[email protected]>
2026-10-28 22:21:00 +08:00
Ludovic MarieandClaude Opus 5.5 9f1fa0a0a2 launcher: quote server junction paths and show why mklink failed
Creating a server on Windows failed with a bare "mklink /J failed" (#3200).
servers.rs passed the paths to cmd unquoted, so a user folder with `&`, `^`
or `(` broke cmd's parsing; sources.rs already quoted them. Both now use
one sources::junction, which also puts cmd's own message in the error.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 15:15:13 +01:00
Claude 0291a5864f Look up players' teams by name for client-side pushes on servers
remote_pushes found teams through mob_teams, which keys entities by UUID
text and the local player by PLAYER_NAME. On a server, players are team
members by profile name (Player.getScoreboardName), so neither lookup hit
and every player pushed the local player, even on teams with
collisionRule never. The server applied no push and corrected the
position, so walking into players jittered (seen on Hypixel).

Use the profile name for the local player and the tab list name for other
players; other entities keep their UUID.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_018ksHJwFp11RfYk6S9xnyCf
2026-10-29 00:13:19 +10:00
Mik00000 08b9765278 Ensure mob effect IDs are namespaced so tipped arrows tick damage on living mobs 2026-10-28 16:09:27 +02:00
Ludovic Marie fe84cc07a1 Merge pull request #3208 from horizon-rust/launcher-sidebar-sponsor
🤖 Agent: verified: verify, launcher and parity-scope green; local cargo fmt, clippy -D warnings and launcher tests pass. Queued for merge.
2026-10-28 14:55:08 +01:00
Ludovic Marie d4e42cf726 Merge pull request #3195 from horizon-rust/feat/launcher-native-mods-window
🤖 Agent: proven with a real launcher GUI run in isolated dirs (Settings → Native mods row, red warning naming veinminer 0.3.0, trust → cached build reused, status line); launcher clippy and 265 tests green; #3194 merged and this now targets main with only its own 5 files. Verify green at 505de825.
2026-10-28 14:51:41 +01:00
PH4N7OMxandOpenAI Codex 998c94bee3 fix(windows): open URLs with the registered protocol handler
Co-Authored-By: OpenAI Codex <[email protected]>
2026-10-28 14:46:01 +01:00
Ludovic MarieandClaude Opus 5.5 466512c90e launcher: a Sponsor link in the sidebar footer
The GitHub Sponsors link was only on the patch notes page. Show it beside
Discord and Website in the sidebar footer (heart icon), from links.json's
sponsor entry, translated in fr and ru.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 14:44:51 +01:00
Kacpherek 969749da3d Map 13 game-logic batches and add map-check
map-check checks verdict-map files against the last build without
rebuilding, so several mappers can check their own rows at once.
2026-10-28 14:42:02 +01:00
Ludovic Marie 4db071e281 Merge pull request #3194 from horizon-rust/feat/launcher-native-mods-page
🤖 Agent: proven with a real launcher run in isolated dirs: registry install of Veinminer's .hnmod into an instance, native-build of that instance's enabled native mods, instance pinned to the build; launcher clippy and tests green after merging main (#3140 and #3189 merged). Verify green at 1f6511d0.
2026-10-28 14:40:43 +01:00
Ludovic MarieandClaude Opus 5.5 d2e7a54082 rustfmt: wrap horizon-client's re-export of the render modules
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 14:32:21 +01:00
Ludovic Marie 2649c5e506 Merge pull request #3202 from horizon-rust/ci/nightly-cap-12
Nightly: 12 builds a day instead of 6
2026-10-28 14:27:40 +01:00
Ludovic MarieandClaude Opus 5.5 505de825db Merge feat/launcher-native-mods-page (main merged in)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 14:27:32 +01:00
Ludovic MarieandClaude Opus 5.5 1f6511d0f3 Merge main into feat/launcher-native-mods-page (#3140 and #3189 merged)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 14:26:02 +01:00
Ludovic MarieandClaude Opus 5.5 1523c118d8 Nightly: 12 builds a day instead of 6
The Team tests fixes on the Nightly and hit the cap of 6 builds a day
(Ludovic, 2026-10-28: raise it to 12).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 14:21:08 +01:00
Ludovic Marie 9239089b76 Merge pull request #3140 from levzzz5154/feat/native-mod-bundles
🤖 Agent: merging levzzz5154's native-mod registry kind; proven with the real Veinminer .hnmod through registry build, the launcher's install check and a native build that ran (see comment).
2026-10-28 14:20:09 +01:00
Ludovic Marie 281ab9759c Merge pull request #3180 from horizon-rust/refactor/furnace-cooking
🤖 Agent: verify passed at 8b5a27ed3. Move only (crate split B7): the furnace's recipe logic leaves containers as the FurnaceCooking extension trait. Full pr_ready green on this head (1261 tests, parity 69 affected / 0 failed); #2959, #2960 and #3131 still merge cleanly. Ready once parity is green.
2026-10-28 14:13:59 +01:00
Ludovic MarieandClaude Opus 5.5 febad05c78 launcher: French and Russian for the Native mods row
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 14:07:55 +01:00
Ludovic MarieandClaude Opus 5.5 519edfac19 docs: native mods in the launcher window
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 14:06:21 +01:00
Ludovic Marie 6561df77f6 Merge pull request #3189 from horizon-rust/feat/launcher-native-build
🤖 Agent: merging: native-build compiles horizon-mixins bundles on their pinned Nightly with the launcher's own Rust; proven by building levzzz's Veinminer .hnmod on 83f15fa (580 s cold, 2m46s warm) and /veinminer answering in the built game.
2026-10-28 14:05:32 +01:00
Ludovic MarieandClaude Opus 5.5 cd7f177104 Launcher: a Native mods switch and Build in Settings, with the warning
Settings → Game → Native mods (experimental), off by default. On, Build names the native mods
turned on in the instance Play starts, warns in red that they run with full access to the
computer, and compiles them only from "I trust these mods: build"; the row shows the step while
it runs, the error if it fails, and the build the instance plays. Ported from native-mixins-1.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 14:04:35 +01:00
SenjinandClaude Opus 5.5 380d5ebe9f HDR: its settings get a screen of their own
Paper White and Peak Brightness sat among the display settings, where players
without an HDR display took them for settings of their own display. Display now
has only HDR Settings..., greyed where the display path offers no HDR output (the
renderer tells the settings when it opens its surface, `graphics::offer_hdr`).
It opens Graphics: HDR: the HDR switch, then its sliders, greyed while it is off.

Where HDR isn't offered, `horizon-graphics.txt`'s HDR settings are left as saved
and not used: the renderer already keeps HDR off there, and the screen no longer
opens, so a game folder copied from another computer, or a display swapped for
an SDR one, shows nothing that does nothing; an HDR display brings them back.

The screen leaves room for a calibration step and an in-game shortcut to it
later. horizon-ctl opens it as `options graphics-hdr`.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 13:58:55 +01:00
SlaVKsVolksandOpenAI Codex GPT-6.1 Terra 54360a74e3 fix(parity): normalize Windows dependency records for selection
Port the production path portability repair developed in Beyond onto original Horizon. Keep stored records and narrowed-root annotations intact; cover legacy local/shared records, changed probes and oracles, and reachability.

Co-Authored-By: OpenAI Codex GPT-6.1 Terra <[email protected]>

Co-Authored-By: OpenAI Codex GPT-6.1 Sol <[email protected]>
2026-10-28 09:58:22 -03:00
Ludovic MarieandClaude Opus 5.5 07ab7ccfc1 Merge the native build's measured numbers
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 13:54:36 +01:00
Ludovic MarieandClaude Opus 5.5 727f6c932a docs/native-mods.md: the measured build times and sizes
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 13:54:36 +01:00
Ludovic MarieandClaude Opus 5.5 cd1f58ef72 Launcher: native mods from the registry install and build like the others
- native-mod entries (levzzz5154's #3140) are listed as mods and install as
  horizon-native-mods/<id>.hnmod (the folder #3112's inventory reads), turned on in
  horizon-mods.txt, after the .hnmod is checked as the registry checks it.
- native-build with no folder builds the native mods turned on in the instance, and that
  instance then plays the build (the game's own home builds into the Native mods instance).
- Every registry project says who made it; a project not made by Horizon says it is not made
  or endorsed by the Horizon project, with the mod policy's link (playhorizon.dev/mods/policy).
  A native mod says it runs with full access to the computer and which Horizon it builds on.
- Pruning keeps every native build an instance plays.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 13:54:35 +01:00
Kacpherek 5a8b9687e5 Document class homes, structural matches and the verdict map 2026-10-28 13:53:58 +01:00
Kacpherek 0f7036f9b1 Map the first three batches: animals, inventory screens, world generation 2026-10-28 13:53:58 +01:00
Kacpherek 12c503e154 Port ledger: class homes, structural matches and the verdict map
Give every vanilla class its Rust home files, weighed from all its matches.
In those files, match a method to the fn that touches the same fields and
reaches the ports of the same callees. Read tools/port_map/*.tsv, where a
person or agent records a verdict for what nothing else finds (port, inline,
missing, skip with a reason), and list the next unanswered batch with todo.
Count field-only constructors as trivial and fold initializer lambdas into
<clinit> and <init>.
2026-10-28 13:53:58 +01:00
Ludovic Marie 291aae00e8 Merge pull request #3174 from horizon-rust/ci/nightly-source
🤖 Agent: merging: nightly publishes horizon-source.tar.gz (signed) and tags its commit nightly-source/<date>-<short>; proven by a full dry run of the workflow (run 40686585965) and tests/test_nightly.py.
2026-10-28 13:49:40 +01:00
Ludovic Marie bec8cce82d Merge pull request #3179 from horizon-rust/feat/launcher-rust-toolchain
🤖 Agent: merging: the launcher's portable Rust 1.99.1 (pinned SHA-256 components, own data dir); proven by a real isolated rust install + crate build on the Mac mini and the launcher tests.
2026-10-28 13:49:37 +01:00
Ludovic MarieandClaude Opus 5.5 0ee2425d40 Merge levzzz5154's #3140 (native-mod registry kind, .hnmod validator)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 13:44:24 +01:00
Ludovic MarieandClaude Opus 5.5 7f3bd901bc Launcher: build native mods on the Nightly they pin, with its own Rust
horizon-launcher native-build FOLDER --trust-native-code compiles mixin bundles (a folder with a
horizon-mixin.toml, or an unpacked .hnmod from the registry) into a game of their own:
- the Horizon commit the mods pin (all must agree; else the installed version's), its source from
  GitHub by commit (Nightlies keep theirs as nightly-source/ tags);
- horizon-mixins at the commit the mods were tested with, built once with the launcher's Rust;
- horizon-mixins apply, then the release's cargo build, with the portable toolchain;
- installed as native-<commit>-<key>, played by the Native mods instance.
Based on native-mixins-1's unmerged work; docs/native-mods.md describes it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 13:43:52 +01:00
Ludovic MarieandClaude Opus 5.5 d308df6167 Launcher: a portable Rust toolchain of its own, for native mods
Native mods compile the game on the player's computer (levzzz5154: many native mods per build). The
launcher now downloads the official rustc, cargo and rust-std components of Rust 1.99.1 (plus
rust-mingw on Windows, the windows-gnu host that needs no Visual Studio) from static.rust-lang.org,
each pinned by SHA-256, into <launcher data>/rust/, and runs them with its own CARGO_HOME. No rustup,
nothing installed on the system. `horizon-launcher rust status|install|remove`.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 13:32:09 +01:00
Ludovic MarieandClaude Opus 5.5 a7efe597ea refactor: the client's world data, rendering and screens become horizon-client-render
80 modules of horizon-client's lib (chunks, meshing, light, streaming, gfx, the HUD,
menus, settings, resource packs, particles, block entity data) and its 8 WGSL shaders move
to crates/horizon-client-render with git mv. Nothing in them reaches gameplay. horizon-client
re-exports each under its old path, so horizon_client::gfx and the rest are unchanged.

Beyond the moves: ParsedModels, lod's Column::reduced/push and the spawner's Next,
Spawner.next and Selector.current become pub; Properties::for_tests sits behind a
test-support feature; crash.rs strips the new crate's path; tools and docs point at the
new paths, and three harnesses that hash every client source hash the new crate too.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 13:23:12 +01:00
Ludovic MarieandClaude Opus 5.5 5ded675a7d Nightly: publish the exact source, and tag its commit for good
The nightly release names its commit, and now also attaches horizon-source.tar.gz (git archive of that
commit, made from git objects in pick and signed with the builds) and tags the commit
nightly-source/<date>-<short>. The dated release and the nightly/build-* branch are pruned after 7 days;
the tag is not, so native mods pinned to a Nightly's commit keep building.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 13:16:48 +01:00
Kacpherek 2dd963a539 Document the port ledger's new matching, gaps and commands 2026-10-28 13:16:02 +01:00
Kacpherek cd42938b88 Port ledger: bytecode calls, wider matching, drift, issue and check
Read the game jar's bytecode for exact call targets, constants and ids, and
resolve Rust calls through the receiver's type. Match more methods: same-named
fns in an impl or file named for the class (the impl type must fit the class),
and callees of known pairs linked through the call graph. Leave trivial methods
out of coverage.

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

New commands: issue (the classes, Rust code and gaps a bug report points at),
check (what a branch touches, gaps it opens and closes against its base), diff,
and a searchable offline report.html.
2026-10-28 13:16:01 +01:00
Ludovic Marie f70501aa42 Merge branch 'refactor/cut-biome-spawn-parsers' into refactor/horizon-client-render 2026-10-28 13:13:50 +01:00
Ludovic Marie d0a29a0f66 Merge branch 'refactor/furnace-cooking' into refactor/spawner-hive-traits 2026-10-28 13:13:03 +01:00
Ludovic Marie 564162a0b0 Merge pull request #2741 from horizon-rust/refactor/cut-biome-spawn-parsers
🤖 Agent: verify passed at 0f565f4e4. Move only: parse_entries/parse_costs/SpawnCost go from mobs/spawn_selection.rs to biome.rs (re-exported), cutting biome -> mobs (step A of the cycle plan). pr_ready --quick green (1315 tests). Ready to merge once parity is green.
2026-10-28 13:12:01 +01:00
Ludovic Marie 8b5a27ed3f Merge remote-tracking branch 'origin/main' into refactor/furnace-cooking 2026-10-28 13:11:00 +01:00
SenjinandClaude Opus 5.5 70e3fe006d Screenshots are named in local time, and the log says where they went
screenshot_name worked the date and time out from the Unix clock by hand, in
UTC; vanilla's Util.getFilenameFormattedDateTime is local time, as Horizon's own
log is, so a screenshot taken at 12:28 in CET was named 11.28 (and near midnight
elsewhere, the day before). The log now also has the full path of a saved
screenshot; vanilla's chat line, which names only the file, is unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 13:07:27 +01:00
SenjinandClaude Opus 5.5 18c23a6df3 HDR: UI Brightness, and the HUD and menus never lift
Pixels counted as the world when they matched it darkened evenly (the vignette),
and grey text over the white sun matches that: menu text in front of the sun was
lifted to the peak. Once the world is kept for the last pass, the frame's alpha
is now zeroed; the hand, HUD and menus raise it where they draw (the vignettes
leave it), and after the hand a blend pass scales its alpha down to 1/255. The
last pass lifts nothing covered, keeps the hand at the world's brightness (it's
the player's, not UI), and scales the rest by UI Brightness.

UI Brightness (Display, 100% by default) is the HUD and menus as a share of paper
white, so pure white labels needn't sit at the world's white. At 100% the picture
is as before.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 12:57:35 +01:00
Stepan Bogomazov cd97b34460 Build the sunset fan once a frame instead of in every sky pixel
While the sunrise/sunset glow shows (about 11500-14500 and 22500-500),
sunrise_weight rebuilt the fan's 16 triangles for every pixel of the
fullscreen sky pass, with four sin/cos per triangle. The triangles depend
only on the glow's alpha and the sun's horizon, so they are now built on
the CPU and passed in Globals as normals; the shader's Moller-Trumbore
reduces to dot products, and only triangles whose rim normals change sign
along the ray are tested in full. The weights are unchanged (a test checks
them against the old per-pixel code).

Sky pass at 1920x1080, dawn: Intel UHD 730 10.2 ms -> 1.8 ms (0.6 ms by
day), RTX 3060 0.47 ms -> 0.27 ms.

Fixes #3143
2026-10-28 14:27:17 +03:00
Mik00000 445183a7df Display mob inside spawners and trial spawners (fixes #3107) 2026-10-28 13:22:28 +02:00
Shreyas 380d71d8a8 lists: fall back to offline resolution when services do not answer in tests 2026-10-28 16:31:23 +05:30
Mik00000 85f53198ce Allow bows and crossbows to shoot spectral and tipped arrows (#3071) 2026-10-28 12:37:54 +02:00
SenjinandClaude Opus 5.5 5479b0362f HDR: a black level setting, for displays whose black isn't 0
HDR output told the display its content goes down to 0 nits, which only an OLED
shows. Black Level (Display, 0 by default) is the display's darkest black: the
last pass lifts the shadows onto it with ITU-R BT.2390's black level lift in PQ
(the peak stays, the lift fades out by the fourth power above black, so midtones
barely move and 100 nits and up don't), and the HDR metadata gives it as the
minimum luminance. At 0 the picture is exactly as before.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 11:32:28 +01:00
Shreyas 0543cf0345 Select the picked hotbar slot in singleplayer and LAN
Preserve the guest's updated hotbar selection when restoring the host
player state after an action. The previous restore sequence discarded
the guest's new selection while keeping the inventory changes.

The held-slot response now uses the selected slot produced by the pick,
so integrated-server and LAN players select the picked block or spawn
egg. Keep the host's selection independent of the guest's selection.

Vanilla reference: ServerGamePacketListenerImpl.tryPickItem,
Minecraft.pickBlockOrEntity, and ClientPacketListener.handleSetHeldSlot.

Fixes #3031
2026-10-28 16:00:22 +05:30
Hermes AgentandHermes Agent c0505424bd fix(launcher): keep native sources hidden until build integration lands
Co-Authored-By: Hermes Agent (OpenAI) <[email protected]>
2026-10-28 15:27:51 +05:00
Kacpherek ef304f30c9 Document the port ledger 2026-10-28 11:23:50 +01:00
Kacpherek e36fd24008 Add the port ledger: vanilla methods next to the Rust functions that port them
tools/port_ledger.py indexes the decompiled sources and the Rust tree into .local/port-ledger/ledger.json and answers where/what/class/coverage queries both ways, with the gaps where a port isn't wired like vanilla (seam, client, class, callee, stale).
2026-10-28 11:23:50 +01:00
Hermes AgentandHermes Agent 137045e321 feat(mods): validate source-only native mod bundles
Co-Authored-By: Hermes Agent (OpenAI) <[email protected]>
2026-10-28 15:20:36 +05:00
NetuosoDEVandOpenAI Codex 818a2ec52a Align the carried block between the enderman hands
Cancel item-model centering in the vanilla carried-block pose. Add a regression test using point coordinates captured from Minecraft 26.3 CarriedBlockLayer.submit, including a transformed entity root.

Implemented by OpenAI Codex on behalf of NetuosoDEV. NetuosoDEV tested the updated Linux binary in game and confirmed the fix works.

Co-Authored-By: OpenAI Codex <[email protected]>
2026-10-28 12:04:11 +02:00
Mik00000 13e441e235 Fix invisible item frames and custom head skins 2026-10-28 12:00:31 +02:00
Kacpherek 4a18a4bde4 Draw the shulker bullet with its model instead of a sprite
The bullet fell through to the item billboard and showed the whole spark texture. Vanilla ShulkerBulletRenderer draws the tumbling bullet model plus a 1.5x translucent copy at full block light.
2026-10-28 10:47:31 +01:00
Kacpherek f6b467eba9 Hurt crammed mobs by 6 at the max entity cramming limit
Mobs only drew the cramming random and never took damage, so a crowd was never thinned. Vanilla's LivingEntity.pushEntities hurts a mob by 6 on a quarter of the ticks while max_entity_cramming or more non-riding entities are pushable around it.
2026-10-28 10:47:13 +01:00
Kacpherek e3fda9ce67 Keep golems built by a player from attacking players
A built iron golem was never marked player created, so it targeted its builder. Vanilla sets PlayerCreated on build (saved in NBT) and IronGolem.canAttack refuses players, which every target goal honours.
2026-10-28 10:43:10 +01:00
NetuosoDEVandOpenAI Codex f852985693 Fix enderman carried block textures and item tints
Render the carried block through the inventory item model pipeline so every quad uses its proper texture and tint source. Remove the single-sprite cube renderer and let the item pipeline center the model.

Implemented by OpenAI Codex on behalf of NetuosoDEV. NetuosoDEV confirmed the fix works in game.

Co-Authored-By: OpenAI Codex <[email protected]>
2026-10-28 11:40:04 +02:00
Kacpherek 03f3fb057d Show thrown eyes of ender in the hitbox view
F3+B drew every entity list but the eyes in flight, so a thrown eye of ender had no box. EntityType.EYE_OF_ENDER is 0.25 by 0.25 and never turned.
2026-10-28 10:35:17 +01:00
SenjinandClaude Opus 5.5 8502d86e24 tools/hdr_gallery.py: an HDR test gallery of every block that gives light
Writes a datapack into a world: hdr:gallery (one or two of each family) and
hdr:gallery_full (every block with a light level, in its brightest state), each
alone in dark stone tunnels, after hdr:setup holds the world at midnight with no
weather, mobs or random ticks. To judge and tune how light sources shine in HDR
output (#1441) under one standard; the block list comes from the prepared
resources, so it follows the pinned version.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 10:19:33 +01:00
SenjinandClaude Opus 5.5 41d3890a3c Revert "HDR: bright lifts from darker pixels"
Only a soul lantern needed it, and it lifted less for its light level, not for
its texture: that is better fixed where the lift is worked out.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 10:14:15 +01:00
malordinandClaude Sonnet 5.5 abdff5c89d Launcher: tell a prepared version without the language list by pack.mcmeta
`lacks_languages` counted the files in `assets/minecraft/lang` and took one or none as
"English alone". The client jar carries two there, `en_us.json` and `deprecated.json`,
so a real prepared version never counted as lacking and the languages #1594 copies from
the asset store were never added to one prepared before it, or by a preparer that could
not read the store. The test's fixture held `en_us.json` alone, which hid it.

The list the language screen offers, `pack.mcmeta`, is only in the asset index, so its
absence is what says the languages are missing. The fixture now has `deprecated.json`.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-28 13:50:26 +05:00
AlexanderandHermes Agent 3fe4834ceb Creative hotbar: say 'Saved' only when it was saved
hotbar::save's error was dropped and the action bar showed "Saved hotbar N"
anyway. Log the failure and skip the message, as saveHotbar does by setting
it inside the try.

Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
2026-10-28 11:39:17 +03:00
AlexanderandHermes Agent 23d2146066 Villagers: report a failed random_sequences write
The merchant's random sequences (the salts that keep its offers stable) were
written with the result dropped, so a failed write rerolled every trade on
the next load with nothing in the log. Log it like the other save paths.

Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
2026-10-28 11:38:23 +03:00
hashtheboardandClaude Opus 5.5 b78ff5f55e client: aim every crosshair target along one ray (VR groundwork)
Game::aim() returns the eyes and the view direction; pick, pick_fluid
and every *_hit go through it instead of rebuilding the pair. A tracked
controller can later stand in for it. No behaviour change.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 09:38:16 +01:00
AlexanderandHermes Agent be8b86d470 Scoreboard: report a failed write instead of dropping it
save_scoreboard wrote scoreboard.dat and world_border.dat with `let _ =`, so
a full disk or a read-only world lost objectives, scores and the border with
nothing in the log. Log the failure the way command storage and boss bars
already do.

Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
2026-10-28 11:37:19 +03:00
SenjinandClaude Opus 5.5 2f13cb3ac6 HDR: bright lifts from darker pixels
The bright tier's window goes to 0.04-0.28, from a round in the HDR gallery: a soul
lantern's glass (light level 10) now lifts, and the level-15 bright blocks still look
right.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 09:34:28 +01:00
AlexanderandHermes Agent 5f8afb17fc Name tags: copy a custom name only inside the tag's range
name_tags cloned every named mob's custom name each frame and dropped the tag
afterwards when the mob was beyond 64 blocks. Test the distance first, so the
copy happens only for a mob the frame can show.

Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
2026-10-28 11:26:00 +03:00
AlexanderandHermes Agent f755d7fba0 Launcher: write the server list beside it and rename
servers.json was written in place, so an interrupted write left a truncated
list and every server vanished from the sidebar. Write servers.json.tmp and
rename it, as the launcher's other state files do.

Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
2026-10-28 11:18:41 +03:00
AlexanderandHermes Agent f7bb1ceb79 Effect panels: borrow the name and reuse the icon key
The panel list is painted every frame the inventory is open. An unclipped
name was cloned per panel and the icon key was a fresh format! per panel; the
name is borrowed and the key is built in one buffer now.

Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
2026-10-28 11:16:09 +03:00
AlexanderandHermes Agent b9494af976 Server list: build each row's sprite key once
The row made the short sprite name (format! or to_owned) and then wrapped it
in format!("gui/sprites/{sprite}") for the atlas. Build the full key in the
match arm and hand it straight to sprite_scaled.

Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
2026-10-28 11:14:14 +03:00
AlexanderandHermes Agent d1ead6a5fa Clippy: tab is already a reference
Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
2026-10-28 11:09:17 +03:00
AlexanderandHermes Agent 9d9e1a97a8 rustfmt
Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
2026-10-28 10:58:47 +03:00
AlexanderandHermes Agent 3511d556ef rustfmt
Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
2026-10-28 10:58:20 +03:00
AlexanderandHermes Agent 4ff7118db7 Creative screen: draw the selected tab from a reference
tab_headers() builds a header per tab without the item lists, and the caller
then cloned the selected one again: three Strings copied every frame the
screen is open. Borrow it instead.

Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
2026-10-28 10:55:47 +03:00
AlexanderandHermes Agent 1743bbc525 Tests: read the session lock after releasing it
LockFileEx refuses a read through another handle while the lock is held, so
reading session.lock between acquire and drop fails on Windows with error 33.
The exclusivity assertion stays; the bytes are read once the lock is released.

Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
2026-10-28 10:47:04 +03:00
AlexanderandHermes Agent da9950c75b Crash report: strip the workspace prefix on Windows too
The root came from CARGO_MANIFEST_DIR.strip_suffix("crates/horizon-client"),
which never matches a Windows manifest (backslashes), so every workspace frame
was printed as /src/... instead of crates/horizon-client/src/... Normalise the
separators of the manifest and of each frame before shortening.

Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
2026-10-28 10:45:04 +03:00
AlexanderandHermes Agent 626068da3a Inventory draw: a slot is drawn from a reference, not a copy
draw_slot_item needs neither ownership nor &mut self: it reads the carried
stack only for a drag. Taking `&Option<ItemStack>` and &self drops the copy
every caller made, including the two the creative hotbar made per slot each
frame (and one clone per hovered slot becomes a clone only when hovered).

Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
2026-10-28 10:40:11 +03:00
Kacpherek cd2fea1f02 Throw projectiles with the thrower's known movement
Pearls, snowballs, eggs, splash potions, tridents, wind charges and arrows added the server's stored velocity of the player, which for a guest can hold knockback or a fall from ticks ago, so some throws dove. Projectile.shootFromRotation adds getKnownMovement: the move the client last sent.
2026-10-28 08:38:46 +01:00
AlexanderandHermes Agent 4bf633e8a9 HUD paint: build the sprite key in one reused buffer
paint() runs for every drawn HUD call each frame and made a fresh String per
sprite to look it up in the atlas: sprite.to_string() for a mob_effect and
format!("gui/sprites/{sprite}") otherwise. One buffer outside the loop keeps
the same keys without the allocation.

Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
2026-10-28 10:33:46 +03:00
AlexanderandHermes Agent f4d6f9c0ed Tests: skip the reports-link case when the host cannot link
os.symlink needs Developer Mode or an elevated token on Windows, so the test
errored with WinError 1314 instead of exercising own_reports. Skip it there
and keep it on hosts that can link.

Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
2026-10-28 10:30:03 +03:00
AlexanderandHermes Agent 93e61ef936 Port regen: hand the tools a Python that runs
python() returned shutil.which("python3"), which on Git for Windows is the
Microsoft Store stub (prints "Python", exits 49): every tool it launched
failed before its first line. Probe python3, python and py and keep the first
that is Python 3, once per process.

Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
2026-10-28 10:27:21 +03:00
AlexanderandHermes Agent 28e3286e40 Port harness: read the log a Windows javac error names
named_logs only matched a Posix absolute path (`see /...log`), so on Windows
horizon-prepare's "see C:\...\export.log" matched nothing and its javac
errors were never shown. The pattern now takes either form.

Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
2026-10-28 10:21:06 +03:00
AlexanderandHermes Agent aef4186de4 Tests: the parity-reach fixture names files the way the tool does
narrow() and its graph key files by Posix path (`crates/a/src/used.rs`), so
a fixture built from str(relative_to) on Windows produced backslashes and all
seven tests errored in setUp. as_posix() in the fixture matches the tool.

Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
2026-10-28 10:16:41 +03:00
AlexanderandHermes Agent b08e04cd8a Port publish: a body names the diff store with Posix separators
str(Path) keeps the host's separators, so a Windows run wrote
`~\code\horizon-parity\...` into the published body while every other line
of that body is the same on every machine. as_posix() makes it stable.

Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
2026-10-28 10:16:00 +03:00
AlexanderandHermes Agent e8cb6ae83b Hooks: run the public tree check through a Python that works
Git for Windows puts a Microsoft Store stub on PATH as python3: it prints
"Python" and exits 49, so both hooks failed before running the check. The
hooks now try python3, python and py and use the first that is Python 3.

Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
2026-10-28 10:14:16 +03:00
Cosmic b6c936f82b Merge pull request #3058 from kacpherek/perf/cart-signals-only-on-detector-rails
Server lag in new forests: count minecart signals only when a detector rail reads them
2026-10-28 02:52:35 -04:00
Kacpherek 99008baad9 Hold items at the middle of a slim arm
A player with a slim skin held items at the wide arm's hand, half a pixel off the 3-pixel arm. PlayerModel.translateToHand shifts the arm by 0.5 toward the hand before the item's offset, as SkeletonModel does by 1.
2026-10-28 07:24:11 +01:00
Kacpherek e1bc2cbea3 Show banner patterns on shields sent by a server
The banner_patterns and base_color components were read past on the wire, so a stack from a server never carried them and its shield drew plain. Decode both (reference patterns; direct ones stay raw) and write them back, as vanilla's BannerPatternLayers and DyeColor stream codecs do.
2026-10-28 07:24:11 +01:00
Kacpherek 6960b65c62 Keep the cart and its box apart in detector_rail_signal 2026-10-28 07:07:43 +01:00
hashtheboardandClaude Opus 5.5 c9a8361493 render: draw from a ViewSet of per-eye views (VR groundwork)
The renderer now builds its camera from `View` (rotation, lens, aspect,
tan(fov/2)) gathered in a `ViewSet`: one view on a flat screen, and the
view culling and back-face removal use. A headset can later supply one
view per eye without touching draw_frame's consumers.

No visible change: the products keep their order (projection * pose *
rotation), and a test pins the screen view to the old formula bit for bit.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 07:05:51 +01:00
Kacpherek c66554126d Turn name tags to the camera in third-person front view
Name tags faced the player's own look, so in the front view (F5 twice) every name and hologram read mirrored. They now face the camera, turned round in that view, like Camera.rotation().
2026-10-28 06:58:59 +01:00
Kacpherek 4a1c5fc54c Stop sprinting after a spear stab with knockback
Player.causeExtraKnockback stops the attacker's sprint, as the melee path already does.
2026-10-28 06:57:48 +01:00
Kacpherek 5b13af2f75 Count a minecart's comparator signal only when a detector rail reads it
Every with_level built every chest and hopper cart's signal through item lookups. tick_scheduled calls it once per scheduled tick, so freshly generated forests (thousands of leaf ticks) next to mineshaft chest carts spent a large share of the server tick there. detector_rail_signal now counts the cart it reads.
2026-10-28 06:54:33 +01:00
iLostName 7b93b310a0 death: the player hears their own death sound on a server and in single player
LivingEntity.handleEntityEvent 3 plays it for the dying player; the server's goes to the others. Fixes #3021.
2026-10-28 06:52:29 +01:00
iLostName a1b32304a3 boat: the view stops at 105 degrees at every turn, so it doesn't jitter
Fixes #2983.
2026-10-28 06:52:29 +01:00
iLostName 7934a0b180 riding: the crosshair skips the mob being ridden, so it can't be hit
Fixes #2870.
2026-10-28 06:52:28 +01:00
iLostName 992c63eefb death message: a creeper's blast names the creeper, not the last mob that hit
The creeper is out of the mob list when its blast lands, so its name now comes with the explosion event. Fixes #2978.
2026-10-28 06:52:28 +01:00
iLostName a967ca4340 item pickup: a stack in the off hand fills before the inventory, as getSlotWithRemainingSpace does
Fixes #2865.
2026-10-28 06:52:28 +01:00
iLostName 0112a6feac item pickup: one sound per item, played by the client at the item
The server sent its own pickup sound beside TakeItemEntity, so single player heard two. Fixes #3029, fixes #2861.
2026-10-28 06:52:27 +01:00
iLostName c0b560527f F1 hides the HUD only with no screen open, as key.toggleGui does
Fixes #2942.
2026-10-28 06:52:27 +01:00
iLostName 557a827383 trial spawner: a spawn egg sets its mob, as overrideEntityToSpawn does
Fixes #3041.
2026-10-28 06:52:26 +01:00
iLostName e06134f71a redstone ore: a right-click lights it, as RedStoneOreBlock.useItemOn does
Fixes #3044.
2026-10-28 06:52:15 +01:00
SenjinandClaude Opus 5.5 f52e94cfb9 docs: glowing particles shine in HDR now; translucent blocks don't yet
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 06:41:34 +01:00
Kacpherek 330314327a Give a mounted spear charge its mount's speed
The charge read the rider's own last move, which a rider never sends, so on horseback it had no speed and hit nothing. ServerPlayer.getKnownSpeed gives a rider its vehicle's movement.
2026-10-28 06:30:14 +01:00
Kacpherek 9d32bebad3 Show the happy particle when feeding a baby horse on a server
AbstractHorse.handleEating draws it on the client's own copy of the mob; Horizon drew it only on the server's level, which a server's client never sees. Same for llamas and camels.
2026-10-28 06:26:23 +01:00
Kacpherek 345f64e47c Import the Sounds trait for the elytra sound 2026-10-28 06:23:27 +01:00
Kacpherek 8f84c2cd48 Play the elytra's flying sound while gliding
Horizon had no ElytraOnPlayerSoundInstance, so gliding and diving were silent. The loop starts when the player starts gliding, fades in after a second, and follows the player's whole speed, so a straight dive is the loudest.
2026-10-28 06:16:05 +01:00
Kacpherek 012a3cf908 Use the End's ambient light for entity shadows
DimensionType.ambientLight is 0.25 in the End; shadows used 0, so they were weaker there.
2026-10-28 06:10:38 +01:00
Kacpherek 0e03cac368 Draw faint entity shadows instead of cutting them out
Shadows used the entity fragment shader, which discards alpha under 0.1, so they vanished at block light 7 and below and faded out a few blocks away. rendertype_entity_shadow.fsh has no cutout.
2026-10-28 06:10:38 +01:00
Kacpherek e24394ca3a Drop the hand after placing or using an item on a server
The server-client use path never called LocalPlayer.itemUsed, so the held item stayed up after a placement. Vanilla drops it after a swinging useItemOn that spent the item (or in creative) and after every successful useItem.
2026-10-28 06:09:40 +01:00
Kacpherek a5ee1e588c Leave a server at once instead of waiting for the reader thread
On Windows a socket shutdown doesn't wake a read already waiting, so Disconnect froze the window until the server closed the connection. Vanilla closes the connection and moves on.
2026-10-28 05:59:37 +01:00
Kacpherek e0a48b3f66 Document the 2..128 render distance range 2026-10-28 05:56:49 +01:00
Kacpherek d35efa25ec Accept render distances up to 128 from the command line and horizon-ctl
--render-distance and horizon-ctl render-distance check against the same cap as the options.
2026-10-28 05:56:48 +01:00
Kacpherek b8b15b1682 Raise the render distance cap from 64 to 128 chunks
The options slider and options.txt now accept 2..128. 128 is where the server protocol's view distance (an i8) tops out.
2026-10-28 05:56:48 +01:00
Kacpherek 05f8ec650d Pass the safety notice in the direct connection test
Multiplayer now opens SafetyScreen first, so the test proceeds through it.
2026-10-28 05:45:51 +01:00
時穂andCodex 2702542c73 launcher: complete Chinese strings and strengthen translation checks
Fill the 17 missing Microsoft sign-in, resource setup and Nightly strings.
Scan Rust tokens for multiline translation calls and require shared catalog
keys in every language, including labels translated through widgets or constants.

Co-Authored-By: Codex <[email protected]>
2026-10-28 13:37:54 +09:00
SenjinandClaude Opus 5.5 113d694a87 HDR: very bright lifts over bright's window, a step above bright
Very bright's window (0.88 to 0.94) was tuned on the sun alone: on a block
only the hottest pixels reached it, and they blew out while the rest
stayed below bright. It now lifts over bright's window with the whole
headroom; the sun keeps its yellow corona near the horizon.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 05:22:02 +01:00
Hector EcclesandOpenCode a48fcf0d16 Reuse natural spawn candidate buffers
Every spawning pass built a new 289-per-player candidate Vec and a new
dedup set. Keep both in a scratch struct on the game and clear them between
passes, so their capacity survives. The first pass and the thunder pass use
the same buffers.

Co-Authored-By: OpenCode <[email protected]>
2026-10-27 21:55:47 -04:00
Hector EcclesandOpenCode 2eb3de2748 Sweep far chunks every eighth tick
Streaming walked every loaded column and every proto chunk on each update to
drop the ones that had fallen out of range. The sweep now runs at once when
the view moves and every eighth tick otherwise; unloads can wait 400 ms
without changing what is visible or generated.

Co-Authored-By: OpenCode <[email protected]>
2026-10-27 21:47:29 -04:00
Hector EcclesandOpenCode 67511d9749 Reuse the block entity ticker list between ticks
The list of ticking block entities was cloned into a new Vec on every tick,
even when no slot had been freed and nothing had been added. Keep it behind an
Arc and clone the Arc instead; promotions and sweeps use Arc::make_mut, so the
list is copied only when it actually changes.

Also skip the block lookup in tick_spawner: the caller only reaches it for a
BlockEntity::Spawner, and changing the block removes or replaces that entity.

Co-Authored-By: OpenCode <[email protected]>
2026-10-27 21:40:12 -04:00
Hector EcclesandOpenCode df90f62d35 Hopper ticks check their block only when they move
A hopper on its 8-tick cooldown looked up its block and compared the state
name every tick, then returned without moving. Advance the cooldown first and
only resolve the block when it is due to move. The sweep that drops hoppers
whose block entity is gone can run once a second; a hopper that ticks still
checks its own block.

Co-Authored-By: OpenCode <[email protected]>
2026-10-27 21:28:52 -04:00
asownerytandClaude Opus 5.5 f1212a0e5a Beds: occupied while someone sleeps in them, and sleeping villagers are saved
LivingEntity.startSleeping sets the bed head's OCCUPIED and stopSleeping
clears it (the foot follows through updateShape):

- Villagers send MobEvent::BedOccupied when they lie down and get up
  (brain, hurt, bed gone, kicked out, death), and keep the bed they lie
  in as sleeping_pos.
- A player's bed is marked when it lies down (try_sleep) and freed in the
  level's tick once nobody sleeps in it any more, which covers waking, the
  Leave Bed button, death, a broken bed and a guest leaving; the server's
  last save (and closing an in-frame world) frees every player's bed, as
  PlayerList.removeAll does.
- AbstractBedBlock.useWithoutItem: an occupied bed wakes the villager in it
  (kickVillagerOutOfBed) or says "This bed is occupied".
- LivingEntity's sleeping_pos is saved and loaded, so a villager asleep at
  a save is still asleep in its occupied bed when the world opens again.
- The in-frame player gets up when its bed is gone (checkBedExists), and a
  client that leaves a server while asleep is no longer stuck in bed.

lan_e2e: bed-occupied and sleep-reload.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 14:24:58 +13:00
asownerytandClaude Opus 5.5 df4877a10b lan_e2e: sleep-villager, a bed wakes its villager and the sleeper's body shows
The guest uses a bed a villager has lain down in: the villager gets up and
the guest stays up, then the next use lies down. `status` reports whether
the world view draws the player's own body (`own_body`): yes in bed, in
first person too, and no once up again in first person.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 14:01:21 +13:00
asownerytandClaude Opus 5.5 cc575e2c6a Sleeping: villagers lie on the pillow, the player's body shows, beds wake villagers
- A sleeping villager is moved toward the foot by its standing eye height
  (LivingEntityRenderer's getEyeHeight(Pose.STANDING)), not by the 0.2 of
  its sleeping box, so its head lies on the pillow instead of past the bed.
- The player's own body is drawn while sleeping, in third person and in
  first person (LevelExtractor skips the camera entity unless it sleeps),
  lying the way the bed faces.
- Using a bed a villager sleeps in wakes the villager and keeps the player
  up (AbstractBedBlock.kickVillagerOutOfBed), instead of both sharing it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 14:01:21 +13:00
rdbt 56f968e11c Merge pull request #2868 from masa-masa-0314/fix/closed-captions
Subtitles: fix closed captions never appearing
2026-10-28 00:48:18 +00:00
rdbt ae4cea18ef Merge pull request #3004 from feeldev12/feat/slim-skins
feat(client): slim skins wear the slim player model
2026-10-28 00:47:38 +00:00
rdbt 296802a618 Merge pull request #2980 from feeldev12/fix/own-skin-first-person
fix(client): the first-person arm and the player models wear the player's own skin
2026-10-28 00:47:14 +00:00
rdbt a5d3ad165a Merge pull request #2891 from socksprox/t3/clickable-book-links
Books: page click events work, run_command ones included
2026-10-28 00:46:58 +00:00
rdbt 2120ffedb4 Merge pull request #2878 from leifiyoo/riptide-spin-server-countdown
Count down Riptide's spin on a server's copy of a player
2026-10-28 00:46:44 +00:00
rdbt 04e6e21813 Merge pull request #2883 from sirrayi/deps/webpki-advisories
Update rustls-webpki and webpki-roots for RUSTSEC-2026-0382 and -0381
2026-10-28 00:46:15 +00:00
rdbt acee253e1c Merge pull request #2940 from ASOwnerYT/tools/harness-no-vscreen
Harnesses run offscreen on a Linux without vscreen
2026-10-28 00:46:02 +00:00
rdbt 555dce8bb3 Merge pull request #2944 from kacpherek/fix/armor-stand-plates-op-warning
Armor stands press pressure plates; match vanilla's op warning in item tooltips
2026-10-28 00:43:56 +00:00
rdbt 067163ded8 Merge pull request #2968 from kacpherek/fix/block-advancement-triggers
Award the target hit and honey slide advancements
2026-10-28 00:43:49 +00:00
rdbt 704cf97780 Merge pull request #2972 from kacpherek/fix/player-nausea-eyeblossom
Show Nausea on the player: spin the view and draw the swirl
2026-10-28 00:43:30 +00:00
rdbt 007d38fe49 Merge pull request #2822 from sirrayi/fix/telemetry-ipv6-buckets
Telemetry server: count IPv6 clients by /64 in the rate limits
2026-10-28 00:43:14 +00:00
rdbt 43e2ad34c7 Merge pull request #2953 from kacpherek/fix/beacon-christmas-chest-glow-frame
Render beacon beams, Christmas chests and glow frame maps like vanilla
2026-10-28 00:42:39 +00:00
rdbt a6dbc6651f Merge pull request #2837 from iLostName/pack-prompt-keeps-pointer
server packs: the download prompt acts as the screen it is
2026-10-28 00:42:18 +00:00
rdbt 4bb3de29e7 Merge pull request #2931 from elythepreciouscat/feat/vanilla-mouse-settings-26-3
Options: add Java 26.3 mouse settings
2026-10-28 00:41:15 +00:00
rdbt 786f67f77b Merge pull request #2967 from kacpherek/fix/blindness-sky-fog
Apply blindness fog and hide the blocked sky
2026-10-28 00:40:49 +00:00
rdbt 797852c62b Merge pull request #2840 from iLostName/client-skips-loot-tables
remote: a server's client doesn't load the loot tables on its first tick
2026-10-28 00:39:53 +00:00
rdbt 1f8f1055d5 Merge pull request #2852 from levzzz5154/fix/armadillo-roll-ball
A hit rolls the armadillo up into a ball (fixes #2759)
2026-10-28 00:39:36 +00:00
rdbt 0121466ed2 Merge pull request #2943 from kacpherek/fix/2928-mod-recipes-before-world-rest
Skip mod recipe tables until the world resources are loaded
2026-10-28 00:38:21 +00:00
rdbt 55b0545c11 Merge pull request #3017 from DanielGodriNeto/perf/sprite-pixels-shrink
perf(assets): drop the spare capacity of the sprite pixel buffer
2026-10-28 00:37:35 +00:00
rdbt 5e14e309bc comment not needed 2026-10-28 00:37:08 +00:00
rdbt 9e236c7914 Merge pull request #2834 from socksprox/fix/2747-eat-sounds
Play the player's own bites while eating on a remote server
2026-10-28 00:35:59 +00:00
rdbt 2d7fc7f000 Merge pull request #2958 from kacpherek/fix/extended-keybinds
Allow F13-F24 and other extended keys in key bindings
2026-10-28 00:35:39 +00:00
rdbt 644a399389 Merge pull request #2965 from kacpherek/fix/high-contrast-block-option
Enable the High Contrast Block Outline option
2026-10-28 00:34:16 +00:00
rdbt 9e31ce2f3d Merge pull request #2844 from Domanffe/fix/pixelify-cyrillic-o-p
Fonts: Pixelify Sans gets the Cyrillic О and П it was missing
2026-10-28 00:32:49 +00:00
rdbt 66d03b4d1c Merge pull request #2887 from masa-masa-0314/fix/creative-search-reset-on-open
Creative inventory: fix the search box keeping the last search after closing
2026-10-28 00:32:36 +00:00
rdbt 84d5574887 Merge pull request #2956 from gemakfy/fix/creative-f-offhand-crash
Fix creative inventory off-hand swap crash on Key F
2026-10-28 00:32:16 +00:00
Ludovic Marie 4118e026c3 Merge pull request #3014 from horizon-rust/docs/team-merge-rule
🤖 Agent: Merged by Ludovic's AI agent after verify: docs and lists only (Team merge rule, levzzz5154 trusted reviewer, licence holder), Ludovic's request 2026-10-28.
2026-10-28 00:56:21 +01:00
Ludovic MarieandClaude Opus 5.5 51b374121b Team merge rule: ordinary pull requests merge on CI, risky ones get needs-proof; levzzz5154 trusted
The Horizon Team merges ordinary pull requests once verify and parity pass; saves, network, world generation,
launcher updates, the harnesses and big changes wait for a local proof. Also names the licence holder with the
contributors.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 00:46:44 +01:00
DanielGodriNeto 9042746704 perf(assets): drop the spare capacity of the sprite pixel buffer 2026-10-27 20:39:24 -03:00
DanielGodriNeto e14d6f79b1 perf(assets): keep decoded block sprites on disk between launches 2026-10-27 20:38:51 -03:00
feeldev 752fc2170a fix(client): ConnectScreen as 26.3 draws it, with Cancel
Joining a server showed a flat dark fill with the status and the address.
ConnectScreen draws the menu background over the blurred panorama, the
status centred at height / 2 - 50, and a 200-wide Cancel at height / 4 +
132 that aborts the connection and goes back; Escape does nothing there
(shouldCloseOnEsc is false). Once logged in, the LevelLoadingScreen's
Loading terrain takes over until the player is placed.
2026-10-27 18:05:11 -05:00
feeldev 356b69aea7 fix(client): the window keeps drawing while single player's server starts
Minecraft.doWorldLoad shows the LevelLoadingScreen and keeps running the
client while its server prepares the world. Horizon's window waited on the
server thread's ready message instead, frozen (not responding on Windows)
for the whole load. The server's answer is now polled each frame from
resume_world_entry, the LevelLoadingScreen's title is drawn meanwhile,
and the window joins once the server is ready. Runs without a window and
the synchronous harness and control runs still wait for it, as before.
2026-10-27 18:02:04 -05:00
Ludovic MarieandClaude Opus 5.5 827bd7f609 Launcher: test which versions pruning deletes
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 23:32:54 +01:00
Ludovic Marie 795490271d Merge pull request #2988 from horizon-rust/refactor/be-layer-deps
🤖 Agent: verify passes on this head; full pr_ready green on the Mac mini (1340 tests, parity 89 affected / 0 failed). Move-only step B6 of the crate split (crate-split-1).
2026-10-27 23:27:53 +01:00
feeldev 9fe3ddfec3 feat(client): slim skins wear the slim player model
A skin whose metadata says model slim (PlayerSkin.Model.SLIM) now draws
with PlayerModel's slim layer (3-pixel arms), on other players, the local
player's model, mannequins and the first-person arm. Without a skin, the
profile's default skin (DefaultPlayerSkin.get) keeps its own model, so
Alex and the other slim defaults are slim again.

Fixes #2926
2026-10-27 17:26:01 -05:00
Ludovic MarieandClaude Opus 5.5 efa8df83b3 villagers: a torchflower crop is never ripe
TorchflowerCropBlock.getMaxAge is 2 while its age property stops at 1, so
CropBlock.isMaxAge never holds for it: HarvestFarmland leaves it alone and
UseBonemeal keeps it as a target until it turns into a torchflower.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 23:06:19 +01:00
Ludovic MarieandClaude Opus 5.5 f049a08e48 packs: keep the available packs in one id order, as PackRepository does
PackRepository.discoverAvailable puts the packs of every source into a
single TreeMap by id, so the folder's file/ packs sort among the built-in
ones rather than after them.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 23:02:52 +01:00
Ludovic MarieandClaude Opus 5.5 3e930fc74b Launcher reference: the language setting lists zh
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 22:55:39 +01:00
Ludovic MarieandClaude Opus 5.5 58422bcc19 Bred babies keep the classic sound set; allays walk back to within 4 blocks
- Animal.spawnChildFromBreeding creates the baby with getBreedOffspring and
  no finalizeSpawn, so its sound variant stays the default (classic); the
  spawn path here had just drawn a random one.
- StayCloseToTarget sets a walk target with closeEnough 4: once started at
  16 blocks, the allay walks until MoveToTargetSink.reachedTarget (Manhattan
  block distance 4), not only back inside 16.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 22:54:14 +01:00
feeldev fdefcfeac0 perf(client): read the loot tables while joining a server
On a server's world the loot tables were first read on the game thread by
the first block placed (StepContext's loot), a ~0.6 s freeze on a phone.
They are now read on a helper thread as the connection starts, like the
world screens' preload does for local worlds, and kept from then on.
2026-10-27 16:54:03 -05:00
Ludovic MarieandClaude Opus 5.5 04ec03745d Launcher Chinese: drop the coming-soon strings main removed
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 22:52:52 +01:00
Ludovic MarieandClaude Opus 5.5 6a6b5957eb Launcher Chinese: the nightly strings main added since
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 22:52:29 +01:00
feeldev 5c14d18141 fix(client): the link prompt has ConfirmLinkScreen's Copy to Clipboard button
ConfirmLinkScreen.addButtons: an untrusted link asks with three 100-wide
buttons, Yes, Copy to Clipboard and No; Copy puts the link on the
clipboard and answers no. confirm_screen::Choice lays out a row of any
number of buttons; Confirm keeps its two of 150 through it.
2026-10-27 16:52:15 -05:00
Hector EcclesandDeepSeek V4.1 Flash a96f66ce1e Replay vanilla's small End island placement
The hash estimator placed islands with the right density but different
roots, so an estimate could vanish when the real chunk arrived. Reproduce
the feature's decoration seed, step-local index and placement modifiers
instead, run the feature on a no-op world to keep the random stream in
sync, and use those exact origins for the far cone. The per-cell biome
sampling is no longer needed; the feature's biome filter decides.

Co-Authored-By: DeepSeek V4.1 Flash <[email protected]>
2026-10-27 17:52:07 -04:00
Hector EcclesandDeepSeek V4.1 Flash b561f83220 Draw small End islands in LOD terrain
The small_end_islands biome's only feature is end_island_decorated, so
unexplored End void rendered empty beyond the refined rings. Carry the
cell biome for void direct samples and estimate the feature: rarity and
count from its placement modifiers, in-square positions, the 55..70
height range, and the vanilla end-stone cone.

Vanilla reference: EndIslandFeature (BasicFeature::EndIsland and
minecraft:end_island_decorated), already ported in horizon-world.

Co-Authored-By: DeepSeek V4.1 Flash <[email protected]>
2026-10-27 17:52:07 -04:00
Ludovic Marie 8c18d9429f Merge remote-tracking branch 'origin/main' into cg24/f2795 2026-10-27 22:50:42 +01:00
Ludovic MarieandClaude Opus 5.5 7a62c6f4a7 Give the capture test's fixture the pack prompt
capture() now asks the server pack prompt and the dialog; the fixture
gets both, and the prompt's own case: it keeps the pointer.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 22:46:23 +01:00
Ludovic MarieandClaude Opus 5.5 150a29974d Merge main into fix/burn-in-daylight-list
lan.rs mob_meta now builds all shared flags on main: its on-fire bit uses
is_on_fire, so a fire-immune mob never reports burning. game.rs keeps main's
charged-blaze flames with is_on_fire.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 22:43:53 +01:00
Kacpherek 06fe3e8e69 Raise the bounce game event when a player bounces
A slime block or bed bounce made no vibration. Vanilla's Entity.restituteMovementAfterCollisions posts GameEvent.BOUNCE after bounceOn (Entity.java:891); the bounce motion event was dropped.
2026-10-27 22:40:48 +01:00
Kacpherek 158d99a528 Raise entity_mount and entity_dismount game events
Riding a vehicle and getting off it made no vibrations, so sculk sensors and wardens never heard it. Vanilla's startRiding and removeVehicle post entity_mount and entity_dismount at the vehicle (Entity.java:2533, 2573); derived here from the vehicle a rider had last tick.
2026-10-27 22:40:48 +01:00
Kacpherek 98da8f82b8 Enforce max_command_forks in /execute
/execute had no limit on how many sources its modifiers produce. Vanilla's BuildContexts ends the chain with command.forkLimit when the sources would reach the max_command_forks rule (silently after a forking modifier).
2026-10-27 22:40:48 +01:00
Kacpherek 465832b0e4 Honour global_sound_events for the wither, dragon and portal sounds
The wither spawn roar was a local sound at the wither, and none of the three global events checked the rule. Vanilla sends them as global level events (heard everywhere) only with global_sound_events on; without it they are plain level events the client has no sound for (ServerLevel.globalLevelEvent).
2026-10-27 22:40:48 +01:00
Kacpherek 0318c132eb Let projectiles break chorus flowers and decorated pots
Arrows, snowballs and the other impact projectiles never broke either block. Vanilla's onProjectileHit destroys them (a pot is cracked first) when projectiles_can_break_blocks is on and the shooter may break blocks (Projectile.mayBreak).
2026-10-27 22:40:48 +01:00
Kacpherek 0f8e3a9b09 Format the mob sound changes 2026-10-27 22:40:25 +01:00
Kacpherek 4ce1eb6b0a Play the crossbow hit sound for arrows shot from a crossbow
CrossbowItem.createProjectile sets the arrow's sound event to item.crossbow.hit; every arrow hit played entity.arrow.hit.
2026-10-27 22:40:16 +01:00
Kacpherek 47d249e7dd Pick hurt and death sounds by water, size, age and shell
Drowned and guardians use their water or land pair, tiny slimes and magma cubes the _small pair, a closed shulker hurt_closed and a baby turtle the _baby pair, as getHurtSound and getDeathSound do. Horizon always used the plain sound.
2026-10-27 22:40:10 +01:00
Kacpherek ea6ebae264 Fix horse standing, angry, ambient and baby horse voices
RandomStandGoal plays getAmbientStandSound (the ambient sound) for every horse, not entity.horse.angry for horses only; makeMad uses getAngrySound (zombie horse its own, skeleton horse none, baby horse baby_horse.angry); a baby horse and a skeleton horse in water pick their own ambient, hurt and death sounds.
2026-10-27 22:40:10 +01:00
Kacpherek d878f3bed2 Give donkeys, mules and baby horses their own jump and eating sounds
playJumpSound is entity.donkey.jump or entity.mule.jump (skeleton_horse.jump_water in water); Horse.getEatingSound is entity.baby_horse.eat for a baby. Horizon played the horse's for all.
2026-10-27 22:39:37 +01:00
Kacpherek ec5fc9d5e4 Play horse hoof, wood, gallop and breathing steps
AbstractHorse.playStepSound plays entity.horse.step (baby_horse.step), step_wood on wood or for the first five ridden steps, and gallops every third step with Horse.playGallopSound's breathing. Horizon played the block's step sound.
2026-10-27 22:39:37 +01:00
Kacpherek 99948fdf3f Play the hostile fall sounds when a monster takes fall damage
Monster.getFallSounds is entity.hostile.small_fall and big_fall; Horizon played the generic pair.
2026-10-27 22:39:37 +01:00
Kacpherek 73ad5f1dbb Play the hostile swim sound for swimming monsters and hoglins
Monster and Hoglin override getSwimSound with entity.hostile.swim; Horizon played the generic swim sound for them.
2026-10-27 22:39:37 +01:00
Ludovic MarieandClaude Opus 5.5 eff8ae41c6 Merge main into fix/death-advancement-messages-to-guests
Kept system_message_to next to main's overlay_message in lan/hub.rs.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 22:39:30 +01:00
Kacpherek 6e26990ad3 Run cargo fmt on the villager changes
Formatting only.
2026-10-27 22:39:05 +01:00
Ludovic MarieandClaude Opus 5.5 4d0962e611 Merge main into fix/fog-lava-and-night-vision
Kept fog_night_vision next to main's level_flat/level_min_y and main's
flat-world dark disc.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 22:38:12 +01:00
Kacpherek ffb6594600 Format the block behaviour changes 2026-10-27 22:37:59 +01:00
Ludovic MarieandClaude Opus 5.5 e0bb0881e8 item frames: glow frame map light is block coordinate 210, as vanilla's 15728850
15728850 is 0xF000D2: sky coordinate 240 and block coordinate 210, which
the packed light here stores as 240 | 210 << 8 (208 was 13 * 16).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 22:36:57 +01:00
Kacpherek 632179bda6 Format
cargo fmt.
2026-10-27 22:36:44 +01:00
Ludovic Marie 4b4950d7c4 Merge branch 'refactor/be-layer-deps' into refactor/furnace-cooking 2026-10-27 22:36:31 +01:00
Kacpherek 1944dd45e4 Fix display light borrow and format
display_light took &self while text displays were borrowed mutably; it now takes the world. Also rustfmt.
2026-10-27 22:35:25 +01:00
Ludovic MarieandClaude Opus 5.5 09043cd199 trident: only the owner picks up its trident while the owner is in the level
ThrownTrident.playerTouch hands the touch to AbstractArrow only when the
player owns the trident or the owner is gone (getOwner() == null). Without
this check any guest standing on another player's landed or returning
trident took it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 22:34:28 +01:00
Ludovic Marie e902b6fc18 Merge remote-tracking branch 'origin/main' into refactor/be-layer-deps 2026-10-27 22:34:05 +01:00
Ludovic MarieandClaude Opus 5.5 1d693cb3c3 Read mouse.rs where the input crate keeps it now
#2750 moved it to crates/horizon-client-input.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 22:33:10 +01:00
Kacpherek c5dead28a0 Draw the poppy an iron golem offers
IronGolemFlowerLayer draws a poppy at the right arm while getOfferFlowerTick() is above 0, with NO_OVERLAY; Horizon only ran the offer, never drew it.
2026-10-27 22:33:06 +01:00
Kacpherek 15099724ed Draw the item a fox holds in its mouth
FoxHeldItemLayer puts the main hand item at the head's position, rolled and turned by the render state, in the ground display context; Horizon drew nothing.
2026-10-27 22:33:06 +01:00
Ludovic MarieandClaude Opus 5.5 69bb386a6d Merge main into dyed-armor-colour
main sends a wolf armor's damage and components in the body slot; the
worn armour's stacks go beside them, and the body slot never takes the
helmet's stack.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 22:33:03 +01:00
Ludovic MarieandClaude Opus 5.5 23e3cf465c Merge main into screen-cursor-centre
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 22:33:03 +01:00
Kacpherek 0d3ca74bd0 Break turtle eggs by stepping on them
TurtleEggBlock.stepOn calls destroyEgg(100) for any step that is not careful; only the fall path (1 in 3) existed.
2026-10-27 22:33:01 +01:00
Kacpherek 52a820e708 Match vanilla's shadow radius for armadillos, elder guardians, happy ghasts and shulkers
ArmadilloRenderer 0.4, ElderGuardianRenderer 1.2, HappyGhastRenderer 2.0 and ShulkerRenderer 0.0; Horizon used the 0.5 default for all four.
2026-10-27 22:32:59 +01:00
Kacpherek f69088012b Format the hostile mob fixes 2026-10-27 22:32:05 +01:00
Kacpherek 154aaa2051 Fix clippy and format the passive mob fixes 2026-10-27 22:32:05 +01:00
Kacpherek 1ab273641e Disarm a tripwire with shears before breaking it
TripWireBlock.playerWillDestroy sets DISARMED (flags 260) and fires the shear event when the main hand holds shears; the property was never set.
2026-10-27 22:31:36 +01:00
Kacpherek 85f2a9ad87 Draw an enderman's carried block as its baked model
CarriedBlockLayer submits the block's model on the unit cube. Horizon drew a cube of one face sprite, centred half a block off, so grass lost its sides and flowers became cubes.
2026-10-27 22:31:30 +01:00
Kacpherek 11b36e3867 Read and write saved NBT without the node cap
Chunks, entities, POI and .dat files were capped at 1M nodes, so a chunk with thousands of full containers would not load. Vanilla reads them with NbtAccounter.unlimitedHeap().
2026-10-27 22:31:27 +01:00
Kacpherek 785d579f47 Stop vines spreading when the spread_vines rule is off
VineBlock.randomTick only runs when GameRules.SPREAD_VINES is on; Horizon ignored the rule.
2026-10-27 22:31:17 +01:00
Kacpherek 382cc39713 Save and restore previousPlayerGameType
ServerPlayer.storeGameTypes writes the previous game mode (the switcher's return mode) next to playerGameType; Horizon never wrote it, so a fresh save dropped it.
2026-10-27 22:30:45 +01:00
Kacpherek 16d71b0a24 Cast a giant's shadow at 3 blocks
GiantMobRenderer passes 0.5 times its scale of 6 as the shadow radius; the default 0.5 was used.
2026-10-27 22:30:06 +01:00
Kacpherek f83310a891 Shake a skeleton while it freezes into a stray
Skeleton.isShaking is its freezing conversion, synced as DATA_STRAY_CONVERSION_ID; Horizon never sent or read it, so the shake never showed.
2026-10-27 22:30:06 +01:00
Kacpherek 3fe8aa76b2 Build a golem whenever a carved pumpkin is placed
CarvedPumpkinBlock.onPlace runs on every placement; /setblock, /fill, /clone and piston moves never completed a golem.
2026-10-27 22:30:05 +01:00
Kacpherek f351bcb06b Keep a map's banner and frame markers when it is saved
Horizon wrote empty banners and frames lists, so a vanilla map lost its markers on the first save. They are kept as read; a locked copy keeps the banner markers (MapItemSavedData.locked).
2026-10-27 22:29:58 +01:00
Kacpherek bbe656e4ce Use the monsters_burn window for sunlight burning
Undead burned whenever sky darkness was below 4, including the End and storms with a bright sky. Vanilla's Mob.isSunBurnTick requires the gameplay/monsters_burn attribute: overworld only, true from tick 23460 to 12542 (day.json).
2026-10-27 22:29:48 +01:00
Kacpherek 1861c82434 Make TNT, TNT minecarts and End crystal blasts honour the drop decay rules
They always used plain destruction. Vanilla's ExplosionInteraction.TNT picks decay or plain destruction from tnt_explosion_drop_decay, and BLOCK (End crystals, the dragon respawn spikes) from block_explosion_drop_decay (ServerLevel.getDestroyType, EndCrystal.java:102, DragonRespawnStage.java:64).
2026-10-27 22:29:48 +01:00
Kacpherek dc75e77eb1 Pick a parent's variant at random for pig, cow and chicken babies
Pig, Cow and Chicken.getBreedOffspring take either parent's variant with equal odds; Horizon always took the initiator's.
2026-10-27 22:29:41 +01:00
Kacpherek 94e8c85fd0 Roll a natural salmon's size
Salmon.finalizeSpawn picks small 30, medium 50 or large 15; natural spawns kept variant 0, so every salmon rendered small.
2026-10-27 22:29:29 +01:00
Kacpherek 28c30923f4 Play the sound variant of pigs, cows, chickens, wolves and cats
Vanilla plays the mob's sound variant set, the baby set for babies (Pig/Cow/Chicken/Wolf/Cat.getSoundSet); Horizon always played the classic adult sounds and never gave a natural spawn a variant. Tame cats also purreow one time in four (Cat.getAmbientSound).
2026-10-27 22:29:29 +01:00
Ludovic MarieandClaude Opus 5.5 44448d941f Merge #2706 into #2927: the unchanged-block skip reads the column cache
#2706 lands first; with #2927's surface column cache the check reads the
cached state instead of decoding the section.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 22:29:15 +01:00
Kacpherek 8ba30576d1 Back up an unreadable player file and fall back to its .dat_old
A player file that failed to load was written over with no copy. Vanilla copies it to <uuid>_corrupted_<time>.dat and loads <uuid>.dat_old (PlayerDataStorage.load).
2026-10-27 22:29:07 +01:00
Kacpherek 8f26771053 Fall back to level.dat_old and keep an unreadable level.dat
An unreadable level.dat was treated as a new world and overwritten. Vanilla reads level.dat_old instead and moves the bad file to level.dat_corrupted_<time> (LevelStorageSource.restoreLevelDataFromOld).
2026-10-27 22:29:07 +01:00
Kacpherek 2b74cfae4a Drop a beehive with its bees when it is broken in creative
BeehiveBlock.playerWillDestroy drops the hive item with bees and honey_level when drops are prevented and block_drops is on; creative breaks dropped nothing.
2026-10-27 22:29:06 +01:00
Ludovic MarieandClaude Opus 5.5 2ee238e5f1 Merge #2931 into #2924: both add vanilla 26.3 options to options_ui.rs
The controls list and the mouse list sit side by side; #2931 lands
first.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 22:28:53 +01:00
Kacpherek 60a4edaef9 Chain a hanging sign under a ceiling hanging sign
CeilingHangingSignBlock.useItemOn passes for a hanging sign item on the DOWN face; Horizon opened the editor instead of placing the next sign.
2026-10-27 22:28:29 +01:00
Ludovic MarieandClaude Opus 5.5 5096b41100 Merge main into #2886: offline names need an owner since main
Keep main's owner check (play_as takes the store, Sign out updates
the owner state) with the branch's load-mutate-save and skin cleanup.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 22:28:22 +01:00
Kacpherek e9eaa66542 Offer the iron golem's poppy for 400 game ticks, not 800
OfferFlowerGoal.start sets tick to adjustedTickDelay(400), which is 200 goal ticks. Horizon counted 400 goal ticks, so the offer lasted twice as long.
2026-10-27 22:28:16 +01:00
Kacpherek 33921460cc Save the world border in the overworld's data folder
Horizon wrote data/minecraft/world_border.dat at the world root, which vanilla ignores. Vanilla's ServerLevel keeps it in each dimension's data folder; the old root file is still read as a fallback.
2026-10-27 22:28:04 +01:00
Kacpherek 2c2aa41619 Clear a door's lower half when the tool would not drop it
DoorBlock.playerWillDestroy removes the bottom half for creative or !hasCorrectToolForDrops (iron door by hand); only creative was handled.
2026-10-27 22:27:59 +01:00
Ludovic Marie 50ebc9de60 Merge remote-tracking branch 'origin/main' into cg24/f2875
# Conflicts:
#	README.md
2026-10-27 22:27:57 +01:00
Kacpherek f164427cba Use the full not_scary_for_pufferfish list for the puff trigger
Turtles, guardians, tadpoles, nautiluses and sulfur cubes no longer puff a pufferfish, and axolotls do, as in Pufferfish.PUFFER_SCARY_MOB and the tag.
2026-10-27 22:27:50 +01:00
Ludovic MarieandClaude Opus 5.5 9fcfdaa1ea Portal arrivals: rustfmt, and name vanilla's portal ticket
resolve_arrivals keeps its own doc comment; the area an arrival keeps is
vanilla's TicketType.PORTAL, which has no ENTITY type.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 22:27:47 +01:00
Kacpherek b77301b6eb Let an allay wander up to 16 blocks from its player
StayCloseToTarget only pulls an allay back once its target is 16 blocks away (closerThan(tooFar)); Horizon pulled it back beyond 4.
2026-10-27 22:27:44 +01:00
Ludovic MarieandClaude Opus 5.5 3be828d012 Merge main into #2813: reach first, then spawn protection, then the border
handleBlockBreakAction refuses a dig out of reach before it looks at spawn
protection; ServerLevel.mayInteract (spawn protection, then the world
border) comes after.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 22:27:32 +01:00
Kacpherek e08adcd31c Anger nearby piglins when an ender chest is opened
EnderChestBlock.useWithoutItem calls PiglinAi.angerNearbyPiglins(true); the ender chest branch skipped the generic container path that does it.
2026-10-27 22:27:31 +01:00
feeldev d480ce24dd fix(client): book text runs and copies its click commands and shows its hover text
BookViewScreen.handleClickEvent: a run_command click closes the book
(closeContainerOnServer, then clickCommandAction with no screen after it)
and runs the command; copy_to_clipboard copies and leaves the book open;
suggest_command does nothing, as BookViewScreen has no insertText. The
page's words show their show_text hover event as a tooltip
(HoveredTextEffects.TOOLTIP_AND_CURSOR), as the chat does.
2026-10-27 16:27:30 -05:00
Kacpherek 830a9d3d84 Give the skeleton horse vanilla's water drag
SkeletonHorse.getWaterSlowDown is 0.96; Horizon used the 0.8 default so it swam too slowly.
2026-10-27 22:27:21 +01:00
Kacpherek 6f0d56512d Stop mules from breeding
Mule inherits AbstractHorse.canMate, which is false; only horses and donkeys (Horse/Donkey.canMate) find a partner.
2026-10-27 22:27:21 +01:00
Kacpherek 482444afc4 Roll vanilla's donkey, mule and zombie horse attributes on spawn
A natural donkey or mule rolled a horse jump; vanilla rolls only health and leaves jump at 0.5. A zombie horse uses its own jump 0.5-0.7 and speed (9+3 rolls)/42.16 (AbstractChestedHorse, ZombieHorse).
2026-10-27 22:27:09 +01:00
Ludovic MarieandClaude Opus 5.5 46c84a8b24 Merge main into #2891: main has run_command click events since
Keep main's ClickEvent::RunCommand and click_command (now pub(super) for
the book screen) instead of the branch's copies.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 22:27:03 +01:00
Ludovic MarieandClaude Opus 5.5 8237c15741 Merge bubble-column-entities (#2723) into flowing-water-mobs
Both add a LAN check at the same place.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 22:26:58 +01:00
Ludovic Marie ae82c1b693 Merge remote-tracking branch 'origin/main' into cg24/f2891 2026-10-27 22:26:51 +01:00
Ludovic MarieandClaude Opus 5.5 501e543105 Creative on a server: the off hand's edits reach the server too
CreativeInventoryListener sends every InventoryMenu slot that changes,
the off hand (slot 45) included; only the 36 inventory slots were sent,
so Key F's off-hand stack stayed on this client (single player runs on
its server thread too).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 22:26:47 +01:00
Kacpherek 7711bfcdc5 Fail a dialog whose body or input cannot be read
Dialog.DIRECT_CODEC rejects the whole dialog for an unreadable body or input, a single option with no or two initial entries, or a text default longer than its limit. Horizon dropped the bad part and showed the rest.
2026-10-27 22:26:43 +01:00
Kacpherek 2ac08efcb2 Draw dialog sliders, dropdowns, item bodies and dialog lists
The dialog screen skipped number range and single option inputs, item bodies and the entries of a dialog_list, so a server menu showed only its Back button. Draw them as DialogScreen does: a slider and a cycle button, the item widget with its description, and one button per listed dialog (external title, opens it). The client now keeps the server's dialog tags to resolve a #tag list.
2026-10-27 22:26:43 +01:00
Kacpherek 559b83626f Open links from dialog buttons through the link confirmation
DialogScreen hands an open_url to Screen.clickUrlAction (Web Links, Prompt on Links, ConfirmLinkScreen over the screen the dialog leaves). A suggest_command has no screen to take text; change_page is logged as an error.
2026-10-27 22:26:43 +01:00
Kacpherek c2915208fd List built-in packs before folder packs in the pack screen
PackRepository keeps its sources in order (built-ins, then resourcepacks/), each sorted by id. Horizon sorted every id together.
2026-10-27 22:26:43 +01:00
Kacpherek 04bc30606d Take a texture's .mcmeta only from its own pack or a higher one
FallbackResourceManager ignores a .mcmeta from a pack below the one that supplies the file. Horizon used the top .mcmeta of any pack, so an animation could run on a pack's static PNG.
2026-10-27 22:26:43 +01:00
Kacpherek 6893bcff88 Cap server command suggestions at 1000
ServerCommandSuggestionsProvider keeps the first 1000 suggestions of a request; Horizon sent them all.
2026-10-27 22:26:43 +01:00
Kacpherek aae98d3db5 Send vanilla's error text for an invalid login name
There is no multiplayer.disconnect.invalid_name key. Vanilla's validState throws and Connection.exceptionCaught sends disconnect.genericReason with the exception.
2026-10-27 22:26:43 +01:00
Ludovic MarieandClaude Opus 5.5 6bb648db1e Controls: the three switches nothing reads yet show as unavailable
Operator Items Tab, Quit Shortcuts and Control-click are stored and saved
like vanilla's, but no code reads them yet: grey them out like the other
placeholders instead of offering a switch that does nothing.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 22:26:24 +01:00
Ludovic MarieandClaude Opus 5.5 b4de9dc23c Leave the current's push to main's
main pushes mobs with the fluid current since #2554 (kacpherek, in
update_fluids, with Entity.isPushedByFluid: fish, axolotls, turtles,
frogs and swimming drowned stay put); with both, a mob was pushed twice and
the water creatures were pushed too. The flow-push LAN check stays: it
covers main's push.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 22:26:24 +01:00
Ludovic MarieandClaude Opus 5.5 4a4cad6864 Merge main into flowing-water-mobs
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 22:26:24 +01:00
Kacpherek 0d7f842b09 Hurt the dragon with a wind charge
AbstractWindCharge.onHitEntity hurts whatever it hits for 1, the dragon's parts included (crystals are not hit); Horizon only looked at ordinary mobs.
2026-10-27 22:26:23 +01:00
Kacpherek a1dfcbf2b3 Remove the dragon's flame cloud when it takes off
DragonSittingFlamingPhase.end discards the cloud it made; Horizon left it to last its full 200 ticks after the phase ended.
2026-10-27 22:26:23 +01:00
Kacpherek 5703fdfdc4 Let slimes, ghasts and calm piglins not block a bed
ServerPlayer.startSleepInBed looks for Monster entities that isPreventingPlayerRest; slimes, ghasts, phantoms and unangry zombified piglins are not. Horizon counted every hostile-category mob.
2026-10-27 22:26:23 +01:00
Kacpherek 57494e6871 Slow mobs in cobwebs again
WebBlock.entityInside makes a mob stuck at 0.25/0.05/0.25 (0.5/0.25/0.5 with Weaving) and ends its fall; spiders and withers ignore it. The block-effects pass replaced the earlier cobweb code and dropped it.
2026-10-27 22:26:23 +01:00
Kacpherek ad8ef0b7e8 Wither a skull's target by difficulty, and only on a hit
WitherSkull.onHitEntity adds Wither for 10 s on Normal, 40 s on Hard and nothing on Easy, only when the hit hurt; Horizon always gave 10 s.
2026-10-27 22:26:23 +01:00
Kacpherek f9fca5688d Give witches their resistance to magic
Witch.getDamageAfterMagicAbsorb takes 85% off #witch_resistant_to (magic, indirect_magic, sonic_boom, thorns) and all of the witch's own damage; Horizon had no such rule.
2026-10-27 22:26:23 +01:00
Kacpherek 4156a6ac93 Aim a mob's crossbow by the world difficulty
RangedAttackMob.rangedAttackUncertainty is 14 - 4 * difficulty; Horizon always used the Normal value 6.
2026-10-27 22:26:23 +01:00
Kacpherek 6c3defa821 Hit with a hoglin or zoglin every 40 ticks, 15 for a baby
HoglinAi and Zoglin use MeleeAttack.create(40) for adults and create(15) for babies; Horizon used the 20-tick default.
2026-10-27 22:26:23 +01:00
Kacpherek fece4c6a76 Run function commands at function-permission-level
Function lines parsed at whatever level the caller had, so a tick or load function did nothing in a world without cheats and an owner's function could run /stop. Vanilla compiles every function line at the library's permission (function-permission-level, 2 elsewhere).
2026-10-27 22:26:19 +01:00
Kacpherek a0b19a4820 Stop a dedicated server whose tick runs past max-tick-time
max-tick-time was read and never used. Vanilla's ServerWatchdog (DedicatedServer.initServer, when the value is above 0) logs and writes a crash report once a tick is that far past its due time, then exits with status 1.
2026-10-27 22:26:19 +01:00
Kacpherek 02b1d1f430 Pause on lost focus only after 500 ms without it
Horizon paused the moment the window lost focus. Vanilla's pauseIfInactive waits until focus has been missing for more than 500 ms, so a brief focus steal does not pause.
2026-10-27 22:26:19 +01:00
Ludovic MarieandClaude Opus 5.5 75c149afde Leave the bubble column push to main's
main pushes mobs in a bubble column since #2555 (kacpherek, Entity's
applyEffectsFromBlocks after the move); with both, a mob got the push
twice. The bubble-column LAN check stays: it covers main's push.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 22:26:17 +01:00
Ludovic MarieandClaude Opus 5.5 8057d27511 Worn armour: only a dyeable layer takes the stack's dye
getColorForLayer returns white for a layer without Dyeable, whatever
dyed_color the stack has.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 22:26:12 +01:00
Ludovic MarieandClaude Opus 5.5 6f785d1a28 Merge main into bubble-column-entities
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 22:26:11 +01:00
Ludovic MarieandClaude Opus 5.5 466fef4486 Remote eating: the sound plays for items without consume particles too
Consumable.emitParticlesAndSounds gates only the particles on
hasConsumeParticles; the sound always plays.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 22:26:02 +01:00
Ludovic MarieandClaude Opus 5.5 e1d825312f Read the flat world's horizon from WorldGen's preset
main's dark disc and void fog read WorldGen.flat, which is now the preset
(seen by sirrayi's review).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 22:25:52 +01:00
Ludovic MarieandClaude Opus 5.5 5ea61ed3d8 Merge main into world-types
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 22:25:45 +01:00
Kacpherek b910cdd257 Draw block and item display entities on clients
A client only had an arm for text displays. DisplayRenderer draws a block display's model or an item display's item under the same billboard and transformation, with the brightness override or the light at the entity.
2026-10-27 22:25:37 +01:00
Kacpherek 3934833369 Send text, block and item display data to clients
Summoned displays reached clients as bare entities, so a text display showed nothing. Vanilla syncs Display's entries 8 to 22 and each type's own (text 23 to 27, block state 23, item and context 23 and 24) from the saved compound.
2026-10-27 22:25:37 +01:00
Kacpherek 4e814a152a Make every wolf beg for a bone or its food
BegGoal.playerHoldingInteresting is a bone or Wolf.isFood for any wolf. Horizon had untamed wolves beg only for bones and tamed wolves only for food.
2026-10-27 22:25:20 +01:00
Kacpherek 3f378dda96 Make a chased child villager run away in tag
PlayTagWithOtherKids always chased the first friend. Vanilla flees to a village spot (LandRandomPos 20, 8, speed 0.6) when a friend chases it, otherwise chases the kid with one to five chasers, else any friend.
2026-10-27 22:25:02 +01:00
Ludovic MarieandClaude Opus 5.5 231b309634 Name the forced terrain ring where the protos are kept
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 22:24:48 +01:00
Ludovic MarieandClaude Opus 5.5 70068076f2 Keep set_baby's doc comment on set_baby
The two new functions went in between it and its comment.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 22:24:39 +01:00
Ludovic MarieandClaude Opus 5.5 2466ccec98 Write and read the anger only for the NeutralMobs that keep it
anger_end_time and angry_at are NeutralMob's: here the wolf, iron golem,
polar bear, enderman and zombified piglin. The bee keeps its own anger and
a piglin's is a brain memory. Every other mob wrote anger_end_time: -1,
which vanilla never writes for them, and a bee's value from a vanilla save
was replaced by it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 22:24:33 +01:00
Ludovic MarieandClaude Opus 5.5 d3121cea67 Merge main into neutral-mob-anger
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 22:24:27 +01:00
Kacpherek 59b67af6a8 Send villagers to the dropped items they want
NearestItemSensor was a no-op and GoToWantedItem never started for villagers. Vanilla scans 32 blocks every 20 ticks for the nearest item the villager wants and sees, and walks to it at 0.5 when it is within 4 blocks.
2026-10-27 22:23:43 +01:00
Ludovic MarieandClaude Opus 5.5 f1a8cc6a0d Merge main into join-without-rescanning-registries
main moved hover_rows out of chat.rs and added UiState::new; the resource
id cache sits after it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 22:22:46 +01:00
Ludovic MarieandClaude Opus 5.5 e34f01715c Merge main into pack-prompt-keeps-pointer
main's draw_toasts takes the F1 flag now; the prompt still hides the
HUD, sign editor and book under it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 22:22:25 +01:00
Kacpherek 621473f60e Send the ominous item spawner to guests and draw its floating item
The spawner existed only on the server, so a client never saw the item grow and turn. Vanilla tracks it with DATA_ITEM; the client draws OminousItemSpawnerRenderer and runs tickClient's particles.
2026-10-27 22:21:29 +01:00
Kacpherek 1b469a542f Let villagers with bone meal grow crops
UseBonemeal never started: it computed the bone meal count and returned false. Vanilla picks a crop below max age around the villager every 10 ticks, holds bone meal and grows it, then picks again 40 ticks later, for up to 80 cycles.
2026-10-27 22:21:24 +01:00
Kacpherek 2b2dc1929f Show tipped and spectral arrows to guests and clients
A server sent every arrow as a plain one. Vanilla sends a spectral arrow as its own type and a tipped arrow's ID_EFFECT_COLOR, and the client draws arrow_spectral.png and sheds the effect swirls (Arrow.tick) and sparkles (SpectralArrow.tick).
2026-10-27 22:20:33 +01:00
Kacpherek 567e92e2a2 Let villagers harvest torchflower crops
TorchflowerCropBlock is a CropBlock with max age 1. It was missing from the villagers' crop lists, so farmers planted torchflower seeds but never harvested them.
2026-10-27 22:20:12 +01:00
Kacpherek 5388064d88 Ring the bell for real when a villager rings it
RingBell calls BellBlock.attemptToRing: the bell shakes, the sound plays and the block_change game event fires. Horizon only played the sound.
2026-10-27 22:19:17 +01:00
Kacpherek 052dac817a Drop a guest that misses a keep-alive for 15 seconds
The server waited 30 s for the answer. Vanilla disconnects with a timeout when the last keep-alive is still pending 15 s after it was sent; 30 s is only the read timeout.
2026-10-27 22:19:12 +01:00
Kacpherek 20ffd8e538 Tell villagers within 32 blocks that a bell was rung
BellBlockEntity.updateEntities sets HEARD_BELL_TIME on living entities within 32 blocks. Nothing set it, so a rung bell never sent villagers into the hide activity.
2026-10-27 22:18:52 +01:00
asownerytandClaude Opus 5.5 d321b656f6 Horses, donkeys, mules and llamas sync their rolled max health
finalizeSpawn's randomizeAttributes, setOffspringAttributes and a loaded
save wrote the rolled max health (and a horse's speed) straight into the
mob's stats instead of the attribute bases, so the server never sent them
and a rider's client drew the type's 53 health: 26 hearts. They are bases
now (AttributeInstance.setBaseValue), synced like vanilla's.

Fixes #2869

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 10:14:00 +13:00
Kacpherek 5dd3bf02d3 Tick thrown tridents on a server that has no player of its own
tick_tridents quit when there was no local player, so a server never moved, hit with, stuck or returned a trident. It now finds the owner and pickers among the level's players (guests), returns Loyalty tridents to the thrower and lets guests pick them up; the spawn packet carries the thrower's id, as Projectile.getAddEntityPacket does.
2026-10-27 22:08:18 +01:00
Kacpherek c3e19973cf Fire item_used_on_block for a compass used on a lodestone
Vanilla fires ITEM_USED_ON_BLOCK when a compass locks onto a lodestone (ServerPlayerGameMode via useOn).
2026-10-27 22:08:13 +01:00
Kacpherek 7aaa4b433b Fire avoid_vibration when a sneaking step is ignored
Vanilla's VibrationSystem fires AVOID_VIBRATION for a sneaking player whose step a sculk sensor or a warden ignores; a shrieker cannot trigger it.
2026-10-27 22:08:13 +01:00
Kacpherek 3cf601f760 Fire kill_mob_near_sculk_catalyst for the player who hurt the mob
Vanilla's SculkCatalystBlockEntity fires it for the player who last hurt a mob whose experience a catalyst takes.
2026-10-27 22:08:13 +01:00
Kacpherek 39327fca33 Fire allay_drop_item_on_block when an allay drops at the liked block
Vanilla's AllayAi.throwItem fires ALLAY_DROP_ITEM_ON_BLOCK for the liked player with the block below the deposit.
2026-10-27 22:08:12 +01:00
Kacpherek 6489cdf829 Fire player_interacted_with_entity for leashing, feeding and repairing
Vanilla's PlayerInteractTrigger fires after a successful item use on an entity: leashing, feeding a baby sniffer, repairing wolf armour. Item predicates now check the damage component, and guests' interactions fire it too.
2026-10-27 22:08:12 +01:00
Kacpherek 1727f2acf1 Fire player_sheared_equipment when wolf armour is sheared off
Vanilla's Mob.shearItem fires PLAYER_SHEARED_EQUIPMENT with the armour and the mob. A wolf's armour now counts as its body slot, with its durability, for entity and item predicates.
2026-10-27 22:08:04 +01:00
Kacpherek 079963f27e Fire bee_nest_destroyed with the bees left inside
Vanilla's BeehiveBlock.playerDestroy fires BEE_NEST_DESTROYED after emptying the hive, with the tool and the bees still inside; a silk touch break keeps them in.
2026-10-27 22:07:25 +01:00
Kacpherek 3c72bf2d79 Fire spear_mobs when a spear stabs several mobs in one use
Vanilla's KineticWeapon fires SPEAR_MOBS with the number of living entities stabbed in the use, so stabbing five or more completes the spear advancement.
2026-10-27 22:07:25 +01:00
Kacpherek 16cda1637b Fire lightning_strike and channeled_lightning for the right players
Vanilla's LightningBolt fires LIGHTNING_STRIKE when it is discarded, for players within 256 blocks, counting the fires it set and the mobs it missed as bystanders, and CHANNELED_LIGHTNING for the player who channelled it. The lightning rod advancement no longer passes for every bolt.
2026-10-27 22:07:24 +01:00
Kacpherek 7bfc737d81 Fire item_durability_changed when a player's item takes damage
Vanilla's ItemStack.applyDamage fires ITEM_DURABILITY_CHANGED for the owning player, with the stack as it was before the damage. Tools, weapons, armour, elytra, shield, mace, brush and the warped fungus on a stick now fire it, which lets ride_strider complete. The durability and delta ranges are not read; no advancement uses them.
2026-10-27 22:06:27 +01:00
Kacpherek 2e8c8e1dc7 Fire effects_changed when the player's effects change
Vanilla's ServerPlayer.onEffectAdded, onEffectUpdated and onEffectsRemoved fire EFFECTS_CHANGED; the axolotl is the source of the Regeneration it gives (Axolotl.applySupportingEffects). This lets all_effects, all_potions and kill_axolotl_target complete. Changes are seen once per tick, so one undone within the tick does not fire.
2026-10-27 22:06:27 +01:00
Kacpherek 727bd7f2dc Fire nether_travel when the player returns from the Nether
Vanilla's ServerPlayer keeps enteredNetherPosition when the player goes into the Nether and triggerDimensionChangeTriggers fires NETHER_TRAVEL with it on the way back to the Overworld. This lets fast_travel complete.
2026-10-27 22:05:42 +01:00
Kacpherek e54cf96271 Fire ride_entity_in_lava while riding a vehicle through lava
Vanilla's ServerPlayer.trackEnteredOrExitedLavaOnVehicle fires RIDE_ENTITY_IN_LAVA each tick the vehicle is in lava, measured from where the ride entered it. This lets ride_strider_in_overworld_lava complete.
2026-10-27 22:05:24 +01:00
Kacpherek 47bb5c668f Fire fall_after_explosion for a fall after a wind charge blast
Vanilla's ServerPlayer.onExplosionHit remembers the blast and trackStartFallingPosition fires FALL_AFTER_EXPLOSION when the player then falls. This lets who_needs_rockets complete.
2026-10-27 22:05:16 +01:00
Kacpherek a90e3bc36d Fire fall_from_height when a fall from high up ends
Vanilla's ServerPlayer.resetFallDistance fires FALL_FROM_HEIGHT with the position the fall started from (trackStartFallingPosition). This lets fall_from_world_height complete.
2026-10-27 22:05:06 +01:00
Kacpherek 3dc958611c Fire recipe_crafted when a crafting, stonecutter or smithing result is taken
Vanilla's ServerPlayer.triggerRecipeCrafted passes the recipe and the stacks that went in; ResultSlot, StonecutterMenu and SmithingMenu award it. This lets trim_with_any_armor_pattern and craft_decorated_pot_using_only_sherds complete, and smithing and stonecutter results now unlock their recipe too.
2026-10-27 22:02:48 +01:00
asowneryt 513aed64c5 Merge upstream main into fix/2802-projectile-death-message 2026-10-28 10:02:41 +13:00
Kacpherek 33c15a6d79 Fire player_generates_container_loot when a loot roll is opened
Vanilla's RandomizableContainer.unpackLootTable fires it for chests, trapped chests and chest minecarts, and BrushableBlockEntity does for the first brush of suspicious sand or gravel. This lets loot_bastion and salvage_sherd complete; opening marks the roll unpacked so it fires once.
2026-10-27 22:02:31 +01:00
Kacpherek 305ffc4e39 Send End portal frame fill effects to nearby players 2026-10-27 22:02:24 +01:00
Kacpherek 42388d2512 Swap items with shelves and powered shelf chains 2026-10-27 22:02:24 +01:00
Kacpherek cac7fa95fd Show the End gateway spawn burst and sound 2026-10-27 22:02:24 +01:00
Kacpherek 2deaab2602 Render items stored in shelves 2026-10-27 22:02:24 +01:00
Kacpherek 1db835fa0c Apply arrow weapon knockback enchantments 2026-10-27 22:02:19 +01:00
Kacpherek ad61c30f46 Award target hit and honey slide advancements 2026-10-27 22:02:19 +01:00
Kacpherek f35ee16412 Apply blindness fog and hide the blocked sky 2026-10-27 22:02:19 +01:00
Kacpherek 797482183c Show tutorial hints and track their progress 2026-10-27 22:02:18 +01:00
Kacpherek 5216403562 Enable the high contrast block outline option 2026-10-27 22:02:18 +01:00
Kacpherek 60c5d9fefd Apply falling stalactite damage and landing sounds 2026-10-27 22:02:18 +01:00
Kacpherek ca28a0edad Show items revealed by brushing suspicious blocks 2026-10-27 22:02:18 +01:00
Kacpherek 7ab2b20bd5 Let mouse buttons toggle cinematic camera 2026-10-27 22:02:17 +01:00
Kacpherek 2aea6138b7 Allow extended keyboard keys in bindings 2026-10-27 22:02:17 +01:00
Kacpherek dddcc99726 Start with HDR off after a start that died switching it on
HDR output now writes horizon-hdr-pending before it is switched on and removes it after the first presented HDR frame. A start that finds the file turns hdr off in horizon-graphics.txt and logs it, so a native crash in HDR output costs one start, not every start.
2026-10-27 22:02:16 +01:00
Kacpherek f6209003de Fire summoned_entity for built golems and the respawned dragon
Vanilla fires SUMMONED_ENTITY in CarvedPumpkinBlock.spawnGolemInWorld for players within 5 blocks of the new golem, and in EnderDragonFight.setRespawnStage(END) for the new dragon's fight players. This lets summon_iron_golem and respawn_dragon complete.
2026-10-27 22:02:13 +01:00
Kacpherek 736842d40f Unlock the Distortion Effects slider
The option was greyed out and always 100 percent. The slider now moves and saves screenEffectScale, which vanilla's Options.screenEffectScale scales camera shakes and the nausea effects by.
2026-10-27 22:01:26 +01:00
Kacpherek ba37fa75d6 Add the Directional Audio and Music Toast sound rows
Vanilla's SoundOptionsScreen has both next to Show Subtitles and Music Frequency. Nothing in Horizon's audio code reads them yet, so they are greyed out. Audio Device is left out: Horizon opens the default output and cannot switch it.
2026-10-27 22:01:14 +01:00
Kacpherek e9fbcfef78 Add the Online options page with Allow Server Listing
Vanilla's OnlineOptionsScreen has Allow Server Listing, which ClientInformation carries to servers, and the notification and presence options. Horizon has no friends list or Realms, so those three rows are greyed out.
2026-10-27 22:00:52 +01:00
Kacpherek 206b7acee4 Add the Skin Customisation options page and hide skin layers
The seven skin layers and Main Hand were saved but had no page. Vanilla's SkinCustomizationScreen lists them; the layers hide on the player model and reach servers in ClientInformation (Options.createInformation), and other players' layers follow Avatar.DATA_PLAYER_MODE_CUSTOMISATION.
2026-10-27 22:00:31 +01:00
Kacpherek 222dc28be9 Open the saddle and armour screen on a tame nautilus
Shift + use on a tame nautilus returned without opening anything. Vanilla AbstractNautilus.mobInteract opens NautilusInventoryScreen (saddle and body armour, no chest), and E while riding it opens the same screen. The mount menu now covers nautilus with its slot layout, texture, armour sprite and saddle sound.
2026-10-27 21:59:47 +01:00
Kacpherek 98d0c6d1a3 Show the server's Code of Conduct and wait for the answer
Horizon accepted every server's Code of Conduct without showing it. The text now opens on a screen with Acknowledge and Disconnect, and the server waits for the answer. The accepted text's hash is kept per server in servers.dat, as vanilla does (acceptedCodeOfConduct, String.hashCode).
2026-10-27 21:59:47 +01:00
Kacpherek 81be26d686 Accept flat_all_dimensions as a dedicated level-type
26.3's world presets include minecraft:flat_all_dimensions, so the dedicated server's level-type list accepts it. It still generates normal terrain.
2026-10-27 21:59:46 +01:00
Kacpherek 1a8d0ef420 Make unknown loot conditions and number providers agree
Unknown loot conditions fail and unknown number providers give 0 in both loot engines. The block engine used to pass conditions and give 1. location_check stays true there, as it has no world access at the offset.
2026-10-27 21:59:46 +01:00
Kacpherek 41f60562fd Add /datapack create
/datapack create <id> <description> fell into the disable branch and replied with an empty unknown-pack key. It now writes <world>/datapacks/<id>/ with a data folder and a pack.mcmeta, and replies with vanilla's success and failure keys (DataPackCommand.createPack).
2026-10-27 21:59:46 +01:00
Kacpherek 1deb3bd36c Send decorated pot sherds to LAN guests
Decorated pots showed no sherds for LAN guests. A pot is not a block entity here, so the hub and the chunk encoder read the saved tag for it and send the sherds, loot table and item (DecoratedPotBlockEntity.getUpdateTag, saveCustomOnly). Inserting into a pot marks it for update, and a client stores a pot update with the chunk.
2026-10-27 21:59:46 +01:00
Kacpherek 5b41d9ebf3 Light glow frame maps at 15728850
A map in a glow item frame is drawn at light 15728850 (sky 15, block 13), not full bright, as ItemFrameRenderer does.
2026-10-27 21:59:46 +01:00
Kacpherek afd40fdbfa Add the structure set's locate_offset to /locate
/locate adds the set's locate_offset (buried treasure's [9, 0, 9]) to the chunk corner, as StructurePlacement.getLocatePos does.
2026-10-27 21:59:46 +01:00
Kacpherek 511f2c42d8 Apply loot set_components so trial armour keeps its trim
Trial chamber equipment tables put a copper trim on chainmail and iron armour with set_components (SetComponentsFunction), which Horizon ignored. The trim now stays on the mob's worn armour, is saved with the mob and goes onto its drops.
2026-10-27 21:59:46 +01:00
Kacpherek 136b352e4f Give 15 vanilla damage types their own /damage kind
/damage fell back to the Generic kind for cramming, falling_anvil, falling_block, fireworks, generic_kill, lightning_bolt, out_of_world, outside_border, sonic_boom, spit, stalagmite, starve, sulfur_cube_hot, wind_charge and wither. Each now has its own kind and follows the vanilla damage type tags. Creaking, Warden and Wither ask "bypasses invulnerability" instead of "is Generic".
2026-10-27 21:59:46 +01:00
Kacpherek 2ad7b68710 Send the trial spawner mob to LAN guests
Trial spawners never showed their mob for LAN guests. The update tag now carries the spawn data the mob is drawn from, as TrialSpawnerStateData does. next_mob_spawns_at is left out: the client never reads it and it depends on the block state.
2026-10-27 21:59:46 +01:00
Kacpherek 52f3dcc5ef Use christmas chest textures on 24-26 December
Normal and trapped chests use the christmas sprites on 24-26 December, as ChestRenderer does, with the local date read once. minecraft:local_time reads the system time zone instead of UTC, so the Christmas chest item switches on the local date.
2026-10-27 21:59:46 +01:00
Kacpherek 2607d06f94 Use a saved world's preset for new chunks
New chunks always used minecraft:normal, so saved amplified, large-biome and single-biome worlds got normal terrain. The preset is now read from the world's overworld generator (noise settings and biome source), and far decoration uses it too.
2026-10-27 21:59:46 +01:00
Kacpherek ff1b01616f List every online player in /list
/list printed a fixed "1 of a max of 8" with only the host. It now lists the host of a single-player or LAN world first, then the placed guests in join order, with the real count and the hub's max, as ListPlayersCommand does. /list uuids shows each name with its UUID. A single-player owner is already a guest of the in-process server and is not listed twice.
2026-10-27 21:59:46 +01:00
Kacpherek 203fb2db89 Send the vault display item to LAN guests
Vaults never showed their display item for LAN guests. The update tag now carries the shared data that holds it (VaultBlockEntity.getUpdateTag). A guest's update keeps the turn the client has built up, since the tag does not carry it.
2026-10-27 21:59:46 +01:00
Kacpherek 7d99d23aef Fix beacon beam distance and cull at render distance
The beam width used x and y instead of the horizontal distance, so it was wrong for beacons to the north or south. Beams beyond render distance x 16 are skipped, as BeaconRenderer.shouldRender does. A beacon with Levels 0 still draws no beam.
2026-10-27 21:59:46 +01:00
Kacpherek 2f7edd1524 Send banner patterns to LAN guests
Banners showed no pattern layers for LAN guests and the single-player window. The update tag now carries the patterns, the custom name and the components, as BannerBlockEntity.getUpdateTag (saveWithoutMetadata) does.
2026-10-27 21:59:46 +01:00
Kacpherek b8f0cde900 Fix the Metal HDR colour space lookup that crashed macOS at start
wgpu-hal's Metal backend read the CoreGraphics colour space names (kCGColorSpaceITUR_2100_PQ and others) as if dlsym returned the string, but it returns the address of the global that holds it. Configuring a PQ swapchain passed that address to CGColorSpaceCreateWithName, which faulted with no Rust panic. Read through the pointer.
2026-10-27 21:59:16 +01:00
Kacpherek 732be56485 Launcher: show what installed versions take on disk
The Versions page measures each installed version's folder on a background thread and shows the size on its row and the total under the title, so players can see where 20 GB went.
2026-10-27 21:59:07 +01:00
Kacpherek ab955d66c4 Launcher: prune after each install and add a Settings switch
The window runs the same cleanup as the command line once an install or update is saved, and Settings gets a 'Delete old versions automatically' switch (on by default), translated to French and Russian.
2026-10-27 21:58:52 +01:00
Ludovic Marie 1feb28c045 Merge pull request #2750 from horizon-rust/refactor/horizon-client-input
🤖 Agent: Merged by Ludovic's AI agent (orchestrator), crate split route (c): head 64c174118 (main merged in, no rebase) passed the full tools/pr_ready.py on the Mac mini (2327 workspace tests, 398 Python, launcher 205; parity --changed 130 run, 0 failed) plus command_exec and save_roundtrip --fresh; verify green.
2026-10-27 21:58:06 +01:00
asownerytandClaude Opus 5.5 c4348be8d2 Check the killing blow of player_killed_entity: Return to Sender needs the ghast's fireball
KilledTrigger.TriggerInstance.matches tests killing_blow, a
DamageSourcePredicate, against the damage that killed the entity.
Horizon ignored it, so any ghast kill awarded Return to Sender (and any
breeze kill Blowback, any skeleton kill from 50 blocks Sniper Duel).

Fixes #2832

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 09:56:46 +13:00
Kacpherek 8fdcb37ddd Ask for a backup before upgrading a world that needs file fixes
Opening a world saved before DataVersion 4899 upgraded it in place with no prompt. Vanilla asks first (BackupStatus.FILE_FIXING_REQUIRED): Create Backup and Load, I Know What I'm Doing!, or Cancel. The backup reuses Make Backup, then the world opens, as EditWorldScreen does.
2026-10-27 21:56:00 +01:00
Kacpherek 694568a89c Show the multiplayer safety notice on the first Multiplayer click
Horizon opened the server list at once and never read skipMultiplayerWarning. Vanilla TitleScreen shows SafetyScreen first, with a do-not-show-again box saved to options.txt, unless that option is set.
2026-10-27 21:56:00 +01:00
Kacpherek 7f6e8b634e Spin the level view and draw the Nausea swirl for the player
Nausea never reached the camera or the HUD: the swirl input was hard-coded to 0 and the level projection had no portal or Nausea spin. Add vanilla's spin (max of portal and Nausea intensity, scaled by Distortion Effects squared) and the Nausea blend fade (150 in, 20 out). Vanilla: GameRenderer.renderLevel, LocalPlayer.tickSpinningEffect, MobEffects NAUSEA blend.
2026-10-27 21:56:00 +01:00
Kacpherek e9b71c288a Launcher: delete old release versions after an install or update
Each version is about 790 MB and an update adds one. After an install the launcher now deletes every release version except the newest two, the selected one, the ones instances pin and any the game runs from. Turn it off with prune-old-versions, change the count with keep-versions.
2026-10-27 21:55:05 +01:00
AndriiandClaude Sonnet 5.5 db27568929 Merge upstream main into fix/sweep-attack-2764
Fold upstream's player sweep (Sweep, sweep_players) into mace::sweep_damage;
keep melee_damage in the dragon attack; keep enchanted_damage next to
enchant_damage_bonus.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-27 21:54:35 +01:00
Kacpherek 9c9ce0b435 Keep a server item's block entity data for the op warning
minecraft:block_entity_data stayed raw bytes after the item sync, so a command block or spawner from a server had no data for the op warning or a spawner's tooltip. It is read as TypedEntityData (the block entity type, then its data) and given the id the saved component carries.
2026-10-27 21:52:10 +01:00
Kacpherek af41d0325c Read a server item's container contents for its tooltip
minecraft:container stayed raw bytes after the item sync, so a shulker box from a server showed no items in its tooltip. It is now read as ItemContainerContents (an optional template per slot), the way the saved container is.
2026-10-27 21:52:10 +01:00
Kacpherek e21957efd7 Send a server's own components back for unedited creative stacks
The creative slot packet rebuilt every stack from the few components Horizon models, so moving an item with a container, unbreakable, lore or a kept raw component sent the server a stripped copy. A stack read from the wire now keeps its patch, and it goes back as it came while nothing but the count has changed.
2026-10-27 21:52:10 +01:00
Ludovic Marie 2e69ac9886 Merge remote-tracking branch 'origin/main' into refactor/be-layer-deps 2026-10-27 21:49:00 +01:00
Kacpherek 6790913e0b Send creative slot stacks with length-prefixed components
SetCreativeModeSlot uses ItemStack.OPTIONAL_UNTRUSTED_STREAM_CODEC, which puts each component value behind its byte length. Horizon wrote the plain codec, so a vanilla server read the first byte of every component as a length and kicked the player with a DecoderException.
2026-10-27 21:48:00 +01:00
asownerytandClaude Opus 5.5 c309d331ad Harnesses run offscreen on a Linux without vscreen
Without vscreen, save_roundtrip, world_reopen and spawn_loot stopped on
FileNotFoundError, and lan_e2e's games opened windows on the real
desktop: unfocused, the host paused before its world loaded and waited
at "Connecting to the server..." until wait-ready gave up. They now run
offscreen there, as on macOS, and lan_e2e's games also skip the pause
(as HORIZON_E2E_HEADLESS does).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 09:47:06 +13:00
Kacpherek 7acfc655e8 Match vanilla's op warning on item tooltips
The warning was missing its bold first line and skipped spawn eggs of op-only entities. Vanilla: ItemStack.OP_NBT_WARNING, BlockItem and SpawnEggItem.shouldPrintOpWarning (non-block items never warn).
2026-10-27 21:45:45 +01:00
Kacpherek d485c688d6 Let armor stands press pressure plates and tripwires
A non-marker armor stand is a LivingEntity, so it counts for the mob and weighted plates and trips wires. ArmorStand.isIgnoringBlockTriggers() is only true for markers.
2026-10-27 21:45:44 +01:00
asownerytandClaude Opus 5.5 deea1a5b83 entity_model: make the bat's animation states accessible before setting them
VanillaEntityAnimOracle.batHistory set Bat.flyAnimationState and
restAnimationState through getField(..).set without setAccessible, which
Java refuses for a final field on every JDK: the bat case failed with
IllegalAccessException and took the harness down with it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 09:45:11 +13:00
Ludovic Marie 93d77962ee Merge pull request #2936 from horizon-rust/chore/horizon-rust-links
🤖 Agent: Merged by Ludovic's AI agent (orchestrator): links after the move to horizon-rust, the mod index's new address, and the release-notes fix that makes verify pass on main again. pr_ready --quick and the full Python suite pass locally.
2026-10-27 21:43:18 +01:00
Kacpherek 47ca0312df Skip mod recipe tables until the world resources are loaded
On a slow start the title is drawn while the world resources still load. A running mod that reads recipes built its tables every title frame and hit the unloaded resources, which panicked. Fixes #2928.
2026-10-27 21:38:40 +01:00
Ludovic MarieandClaude Opus 5.5 c9fa1c8115 Format the mod registry after the URL change
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 21:31:47 +01:00
Ludovic MarieandClaude Opus 5.5 d2b0347777 Point links at the horizon-rust organization
The repositories moved from ludovic111 to the horizon-rust organization.
GitHub redirects repository URLs but not GitHub Pages, so the mod index
moved to https://horizon-rust.github.io/horizon-mods/: the launcher and
the game read it there. The launcher still accepts icons and files from
the old addresses, which a mirror keeps serving for launchers released
before the move.

Release notes thanked ludovic111 once the repository's owner became the
organization (verify failed on main): the maintainer is now named.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 21:28:04 +01:00
feeldev 78c028321c fix(client): the first-person arm and the player models wear the player's own skin
The empty first-person arm drew Steve's skin always; it now uses the local
player's skin, as the inventory's model does (PlayerRenderer.renderRightHand
reads AbstractClientPlayer.getSkin). Player models without a downloaded skin
wear their profile's default skin (DefaultPlayerSkin.get), as the player
list does. In single player the integrated server is offline-mode and sends
no textures, so the signed-in account's own profile is fetched from the
services (Minecraft.profileFuture) and gives the local player its skin.

Fixes #2921
2026-10-27 15:21:32 -05:00
Ludovic Marie 64c174118a Merge remote-tracking branch 'origin/main' into refactor/horizon-client-input 2026-10-27 21:13:23 +01:00
Ludovic Marie f21e09510e Merge pull request #2910 from ludovic111/feat/triage-commands
🤖 Agent: Merged by Ludovic's AI agent (orchestrator) after review: comment commands for the trusted reviewers (/close, /reopen, /label, /unlabel), from main's code only, same list and pause as /nightly.
2026-10-27 21:11:21 +01:00
ElyTheCat 127434c46c Options: add Java 26.3 mouse settings 2026-10-27 23:21:09 +03:30
RVZO6andClaude Opus 5.5 32a0a11fed Keep a move behind a dig and its arm swing for the next tick
The hub hands a dig (handlePlayerAction) to the game after its pass, and
a move queued right behind it waited for the next tick so it was not
checked against the block the player had just broken. A creative break
sends the arm swing (Punch) between the dig and the move, so the move
was handled in the same pass: the block was still there, the move was
refused and the player sent back. Digging straight down, the player
sank into the block, was put back on top of it a tick later, then fell.

Any move left in the queue now waits, with what is queued before it,
as vanilla handles the packets in order. Traced with horizon-ctl in a
creative world: the player rose back after 6 to 7 of 8 breaks before,
after none now.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 15:47:14 -04:00
Hector Eccles 20d7d111e5 Merge branch 'ludovic111:main' into perf/terrain-rustgen-ports 2026-10-27 15:39:08 -04:00
gemakfy 7b40465f85 Fix creative inventory off-hand swap crash on Key F 2026-10-27 22:38:19 +03:00
Hector EcclesandOpenCode fb769e55e0 Worldgen: rustfmt the terrain performance changes
Co-Authored-By: OpenCode (DeepSeek V4.1 Flash) <[email protected]>
2026-10-27 15:25:06 -04:00
Ludovic Marie 27c3559421 Merge pull request #2879 from leifiyoo/fix/keychain-long-secrets
Keep long Microsoft sign-in secrets in several keychain items
2026-10-27 20:14:06 +01:00
Ludovic Marie a91711afaf Merge pull request #2382 from kacpherek/fix/parrot-follow-sit
🤖 Agent: Merged by Ludovic's AI agent (orchestrator). rdbtCVS's requested changes are addressed (he said not to wait for his re-review); main is merged in after the crate split, and this head is proven in union/cg19-r5 (clippy, 1695 tests, 121/121 harnesses, mob_ai). Thanks @kacpherek!
2026-10-27 20:06:07 +01:00
Ludovic Marie 4df2461b65 Merge pull request #2408 from kacpherek/fix/bundle-dump-on-use
🤖 Agent: Merged by Ludovic's AI agent (Claude, contrib-gate-19) after the contribution gate. Proven in union/cg19-r2 and union/cg19-r3, then merged with main again after the crate split and proven in union/cg19-r5 (clippy, tests, 121/121 harnesses). Thanks @kacpherek.
2026-10-27 20:05:25 +01:00
Ludovic Marie 35540bda73 Merge pull request #2898 from ludovic111/feat/launcher-mojang-download
🤖 Agent: Merged by Ludovic's AI agent (orchestrator). Proven by the real run with Ludovic's own sign-in on the Mac mini (download from Mojang, prepare, play, sign-out refused, keychain clean; see #2892).
2026-10-27 20:01:57 +01:00
Ludovic Marie f66e6a944e Merge pull request #2896 from ludovic111/feat/launcher-owner-screen
🤖 Agent: Merged by Ludovic's AI agent (orchestrator). Proven by the real run with Ludovic's own sign-in on the Mac mini (download from Mojang, prepare, play, sign-out refused, keychain clean; see #2892).
2026-10-27 20:01:49 +01:00
Hector EcclesandOpenCode 01d0170821 Worldgen: keep the skipped aquifer decisions' pending fluid flag
The fill no longer calls compute_substance for positive density or for air
above the aquifer's skip height. Both old paths cleared the aquifer's pending
fluid update; reproduce that side effect so the terrain-fill probe's
should_schedule_fluid_update matches vanilla (it caught the difference).

Co-Authored-By: OpenCode (DeepSeek V4.1 Flash) <[email protected]>
2026-10-27 15:01:42 -04:00
Ludovic MarieandClaude Opus 5.5 f6807fe7cd refactor: spawner, trial spawner and beehive logic as extension traits
Move-only step B8 of the horizon-client crate split. The spawner, trial
spawner and beehive data live in chunk::block_entity since the
BlockEntity cut; their behaviour needs mobs, so it stays above. Once the
data is in its own crate, Rust allows no inherent impl outside it, so
each impl block becomes an extension trait in the same module, with the
same method bodies:

- beehive: OccupantExt, BeehiveExt
- mobs::spawner: SpawnDataExt, SpawnerExt
- mobs::trial_spawner: TrialConfigExt, TrialSpawnerExt

The Default impls of Spawner, TrialConfig and TrialSpawner (plain field
values) move next to their structs, since a foreign trait impl must sit
with its type. Callers add one `use` for the trait.

No behaviour change.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 20:00:59 +01:00
Ludovic MarieandClaude Opus 5.5 a34a41e1b5 refactor: move the furnace's recipe logic out of containers
Move-only step B7 of the horizon-client crate split. `impl Furnace`
needed `crafting::Recipes`, the one gameplay module containers still
reached. Its four methods (set_input, recipe_experience, input_clicked,
tick) move unchanged to a new `furnace` module, as the extension trait
`FurnaceCooking` implemented for the Furnace data, with the two helpers
only they use (remainder, total_time). Callers add one `use`.

containers now depends only on modules of the render/world layer
(skins and tab_list included), so it can move into that crate whole.

No behaviour change.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 19:59:55 +01:00
Ludovic MarieandClaude Opus 5.5 9861a1ba04 refactor: cut vault, command_block, sculk and brewing loose from gameplay
Move-only step B6 of the horizon-client crate split. These four modules
only reached gameplay modules for small helpers, so after this they depend
only on the render/world layer and can move into its crate whole, impls
included.

- save::float moves to horizon_client_core::nbt_value (save re-exports it).
- mobs::spawner::normalize_id moves to horizon_client_core::identifier
  (spawner re-exports it).
- containers::Providers moves to its own module, providers.rs
  (containers re-exports it).
- vault, command_block and sculk import number/text/float, item_nbt,
  Who and scoreboard from their real homes instead of through save and
  mobs; brewing imports Providers from providers.
- The vault, spawner and trial_spawner harnesses record the client-core
  files the moved helpers now live in.
- villager_brain's `impl Poi` (max_tickets, valid_range) moves next to
  the type in poi_sections, and its two methods become pub.

No behaviour change.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 19:59:32 +01:00
Ludovic Marie 26547a5f29 Merge pull request #2860 from ludovic111/refactor/block-entity-data
🤖 Agent: Merged by Ludovic's AI agent (orchestrator). The full pr_ready ran on this exact merge with main: clippy -D warnings, 1340 tests, 369 Python tests, 78 affected harnesses (77 passed, 0 failed), and lan_e2e --check chest passes in-frame and on the server thread. Verify is green here. Crate split, priority 1.
2026-10-27 19:58:54 +01:00
Ludovic Marie d9ab241406 Merge remote-tracking branch 'origin/main' into refactor/horizon-client-input 2026-10-27 19:48:28 +01:00
Ludovic Marie 41c2c2205c Merge pull request #2892 from ludovic111/feat/launcher-owner-gate
🤖 Agent: owner gate (Ludovic's decision 2026-10-27). Unit and CLI/MCP tests pass, and the real run with Ludovic's sign-in passed (Play as lu4ovic; after sign-out Play refused; keychain clean). Results posted on the PR. #2896 and #2898 are stacked on it.
2026-10-27 19:46:54 +01:00
Hector EcclesandOpenCode e71f98adde Worldgen: write positive-density fill runs through the section once
A positive-density run is exactly the default block, so the fill no longer
calls set_block_raw and the settled heightmap checks per block. One palette
lookup per section segment writes the run, its non-air count follows the air
blocks it replaced, and the heightmap is updated at the run's top only, as
rustgen's fill_initial_solid_cell does for uniform cells.

Noise 1.74 -> 1.57 ms/chunk one thread and 2.27 -> 2.10 at 13; terrain +4.4%
(min) and +1.2% (min) at 13. Block snapshots identical.

Co-Authored-By: OpenCode (DeepSeek V4.1 Flash) <[email protected]>
2026-10-27 14:39:52 -04:00
ElyTheCat 80df20837b Options: add Java 26.3 controls 2026-10-27 22:09:31 +03:30
Ludovic Marie 43134f4e41 Merge branch 'feat/launcher-owner-screen' into feat/launcher-mojang-download 2026-10-27 19:32:19 +01:00
Ludovic Marie 85c8276e06 Merge branch 'feat/launcher-owner-gate' into feat/launcher-owner-screen 2026-10-27 19:32:19 +01:00
Ludovic Marie 95bf2811fc Merge remote-tracking branch 'origin/main' into feat/launcher-owner-gate 2026-10-27 19:32:19 +01:00
masa-masa-0314andZCode 2092c5a2e8 Worlds: take the save's icon.png from the rendered frame
GameRenderer.tryTakeScreenshotIfNeeded saves a world icon the Select
World list never had: while a local world is on screen without one, the
frame is grabbed after the level and the hand (before the HUD), centre
cropped to a square, averaged down to 64x64 and written to the save's
icon.png once a second until it sticks. The list already drew the icon
when a save had one; nothing ever created it.

Co-Authored-By: ZCode <[email protected]>
2026-10-28 02:29:39 +08:00
Ludovic MarieandClaude Sonnet 5.5 d87b081c34 Trusted reviewers triage on GitHub: /close, /reopen, /label, /unlabel
One list (.github/trusted-reviewers.txt) now governs /nightly and the triage commands.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-27 19:21:08 +01:00
Hector EcclesandOpenCode e319dd578f Worldgen: resolve each surface column once into a reused buffer
The surface walk read every column from paletted sections at least twice (the
top-down pass and the stone-depth look-behind). Column now keeps the resolved
ids for the read range, refilled into one buffer per chunk and updated by
set_block, as rustgen's SurfaceSystem does with read_column.

Surface 1.46 -> 1.41 ms/chunk one thread, 4.19 -> 3.89 at 13; terrain +1.4%
(min 2.6%) and +3.7% at 13. Block snapshots identical.

Co-Authored-By: OpenCode (DeepSeek V4.1 Flash) <[email protected]>
2026-10-27 14:20:16 -04:00
Ludovic Marie cf62fe270b Merge pull request #2679 from kacpherek/feat/spectator-menu
🤖 Agent: Merged by Ludovic's AI agent (Claude, contrib-gate-19) after the contribution gate. Checked against vanilla 26.3 and proven in union/cg19-r2 and union/cg19-r3. After this batch it was merged with main again and proven in union/cg19-r4 (clippy, 1726 tests, 121/121 harnesses). Thanks @kacpherek.
2026-10-27 19:14:58 +01:00
Hector EcclesandOpenCode 0f596d9e3c Worldgen: one material worker per carving pass
Every restored dirt block under a carved cell built a fresh
MaterialDensitySources and MaterialConditions, compiled the whole surface
rule and allocated its bindings. A carver now builds one worker per pass and
moves only the 1x1x1 bindings to the cell, as rustgen's CarvingContext does.

Carvers 0.44 -> 0.40 ms/chunk on one thread and 0.62 -> 0.49 at 13 threads;
terrain +0.6% one thread and +0.98% (median) at 13. Block snapshots identical.

Co-Authored-By: OpenCode (DeepSeek V4.1 Flash) <[email protected]>
2026-10-27 14:14:48 -04:00
Ludovic MarieandClaude Opus 5.5 b784b15b8a Merge main into fix/parrot-follow-sit
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 19:11:21 +01:00
Ludovic MarieandClaude Opus 5.5 38b508e5a7 Merge main into fix/bundle-dump-on-use
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 19:11:21 +01:00
Ludovic Marie de24a16ef4 Merge pull request #2349 from kacpherek/fix/campfire-douse-client
🤖 Agent: Merged by Ludovic's AI agent (Claude, contrib-gate-19) after the contribution gate. Checked against vanilla 26.3 and proven in union/cg19-r2 and union/cg19-r3. After this batch it was merged with main again and proven in union/cg19-r4 (clippy, 1726 tests, 121/121 harnesses). Thanks @kacpherek.
2026-10-27 19:11:16 +01:00
Ludovic Marie 9b53749263 Merge pull request #2688 from kacpherek/fix/mob-looping-sounds
🤖 Agent: Merged by Ludovic's AI agent (Claude, contrib-gate-19) after the contribution gate. Checked against vanilla 26.3 and proven in union/cg19-r2 and union/cg19-r3. After this batch it was merged with main again and proven in union/cg19-r4 (clippy, 1726 tests, 121/121 harnesses). Thanks @kacpherek.
2026-10-27 19:09:25 +01:00
Ludovic Marie cdccd03c7d Merge pull request #2685 from kacpherek/fix/mob-state-sync-3
🤖 Agent: Merged by Ludovic's AI agent (Claude, contrib-gate-19) after the contribution gate. Checked against vanilla 26.3 and proven in union/cg19-r2 and union/cg19-r3. After this batch it was merged with main again and proven in union/cg19-r4 (clippy, 1726 tests, 121/121 harnesses). Thanks @kacpherek.
2026-10-27 19:09:21 +01:00
Ludovic Marie 27943eb6c7 Merge pull request #2683 from kacpherek/fix/magma-lava-jump-cobweb
🤖 Agent: Merged by Ludovic's AI agent (Claude, contrib-gate-19) after the contribution gate. Checked against vanilla 26.3 and proven in union/cg19-r2 and union/cg19-r3. After this batch it was merged with main again and proven in union/cg19-r4 (clippy, 1726 tests, 121/121 harnesses). Thanks @kacpherek.
2026-10-27 19:09:17 +01:00
Ludovic Marie 28fbc7ff47 Merge pull request #2666 from kacpherek/fix/fishing-hook-players
🤖 Agent: Merged by Ludovic's AI agent (Claude, contrib-gate-19) after the contribution gate. Checked against vanilla 26.3 and proven in union/cg19-r2 and union/cg19-r3. After this batch it was merged with main again and proven in union/cg19-r4 (clippy, 1726 tests, 121/121 harnesses). Thanks @kacpherek.
2026-10-27 19:09:13 +01:00
Ludovic Marie 33ced209a0 Merge pull request #2663 from kacpherek/fix/remote-swing-hand-duration
🤖 Agent: Merged by Ludovic's AI agent (Claude, contrib-gate-19) after the contribution gate. Checked against vanilla 26.3 and proven in union/cg19-r2 and union/cg19-r3. After this batch it was merged with main again and proven in union/cg19-r4 (clippy, 1726 tests, 121/121 harnesses). Thanks @kacpherek.
2026-10-27 19:09:08 +01:00
Ludovic Marie 8155baa6f3 Merge pull request #2403 from kacpherek/fix/cat-morning-gift
🤖 Agent: Merged by Ludovic's AI agent (orchestrator). rdbtCVS's requested changes are addressed (he said not to wait for his re-review), and this head (main merged in) is proven in union/cg19-r4 (clippy, 1726 tests, 121/121 harnesses, mob_ai). Thanks @kacpherek!
2026-10-27 19:06:53 +01:00
Ludovic Marie 423a87cf68 Merge pull request #2397 from kacpherek/fix/parrot-cookie
🤖 Agent: Merged by Ludovic's AI agent (orchestrator). rdbtCVS's requested changes are addressed (he said not to wait for his re-review), and this head (main merged in) is proven in union/cg19-r4 (clippy, 1726 tests, 121/121 harnesses, mob_ai). Thanks @kacpherek!
2026-10-27 19:06:18 +01:00
Hector EcclesandOpenCode 7671219f1a Worldgen: skip the aquifer for solid and high air blocks in the fill
The fill called compute_substance for every block although the aquifer only
does work for blocks below its skip height. Positive density always yields the
default block, and above both the skip height and the fluid level a
non-positive density is air, so the fill now decides both cases itself.

One-thread terrain 4.30 -> 4.08 s (noise 1.95 -> 1.75 ms/chunk); 13-thread
noise 2.51 -> 2.27 ms/chunk. Block snapshots are identical.

Co-Authored-By: OpenCode (DeepSeek V4.1 Flash) <[email protected]>
2026-10-27 14:04:59 -04:00
Ludovic Marie ff8a13c99a Merge remote-tracking branch 'origin/main' into refactor/block-entity-data 2026-10-27 19:03:59 +01:00
Ludovic Marie 0e1fcaa3cc Merge pull request #2310 from kacpherek/fix/2289-shield-pose
🤖 Agent: Merged by Ludovic's AI agent (Claude, contrib-gate-19) after the contribution gate. Checked against vanilla 26.3 and proven in union/cg19-r2 and union/cg19-r3. After this batch it was merged with main again and proven in union/cg19-r4 (clippy, 1726 tests, 121/121 harnesses). Thanks @kacpherek.
2026-10-27 19:03:28 +01:00
Kacpherek 22cab3eeb3 Open windowed after the window system kills the game in fullscreen
When winit's event loop ends with ExitFailure (on Linux, a compositor
dropping the connection after a protocol error) while fullscreen is on,
turn fullscreen off in options.txt before the error goes on, so a
fullscreen the system can't show doesn't crash every launch (#2904).
2026-10-27 19:02:20 +01:00
Ludovic Marie 1b45824beb Merge pull request #2731 from kacpherek/feat/spear-lunge
🤖 Agent: Merged by Ludovic's AI agent (Claude, contrib-gate-19) after the contribution gate. Checked against vanilla 26.3 and proven in union/cg19-r2 and union/cg19-r3. After this batch it was merged with main again and proven in union/cg19-r4 (clippy, 1726 tests, 121/121 harnesses). Thanks @kacpherek.
2026-10-27 19:01:42 +01:00
Ludovic Marie a5053a898c Merge pull request #2215 from ludovic111/refactor/horizon-items
🤖 Agent: Merged by Ludovic's AI agent (orchestrator). The full pr_ready ran on this exact merge with main: clippy -D warnings, 2325 workspace tests, 365 Python tests, launcher 197, and 116 affected harnesses (114 passed, 0 failed). Verify is green here. rdbtCVS approved. Crate split, priority 1.
2026-10-27 19:01:21 +01:00
Ludovic Marie 980807b0f5 Merge pull request #2895 from ludovic111/feat/nightly-prune-builds
🤖 Agent: Merged by Ludovic's AI agent (orchestrator) after review: scheduled cleanup of nightly/build-* branches older than 7 days (never the newest or a published nightly's), as Ludovic decided.
2026-10-27 18:39:20 +01:00
Ludovic MarieandClaude Opus 5.5 610c2f3c10 launcher: Download and prepare, from Mojang, for an account that owns the game
With the owner gate in place, a signed-in owner no longer needs another
launcher first (brief mc-download-1, Ludovic 2026-10-27).

- The Play page's Resources row offers an owner "Download and prepare"
  and "From my install"; without an owner it asks to sign in first.
- `horizon-launcher prepare --download` does the same.
- Every file from Mojang must have the size and sha1 Mojang lists.
- The download shows its progress and has Cancel; files already whole
  are kept, so the next try goes on where it stopped.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 18:33:53 +01:00
Ludovic MarieandClaude Opus 5.5 7afb41a128 launcher: the owner gate's screen, once for offline players and on a refused Play
A player without an account that owns Minecraft: Java Edition sees one
screen when the launcher opens (once) and whenever Play is refused: why
Horizon needs the account, like other launchers, that worlds, offline
names and settings stay where they are, and Sign in, which runs the
device-code sign-in on the same screen. Not now closes it; Home keeps
its Sign in row, which now opens this screen.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 18:25:30 +01:00
Ludovic MarieandClaude Opus 5.5 f6e96e484c Nightly: the scheduled run deletes nightly/build-* branches older than 7 days
Ludovic's decision (2026-10-27). `nightly.py prune-builds` runs in the
scheduled `pick` only, with main's code, and lists each deletion in the run
summary. It deletes only branches named nightly/build-<date>-<n> older than
7 days, never the newest build, and never one a published `nightly` or
nightly-<date> release points to. nightly-state and every other branch are
left alone. tests/test_nightly.py pins it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 18:23:53 +01:00
Ludovic MarieandClaude Opus 5.5 ce64c617e9 launcher: Play needs a Microsoft account that owns Minecraft: Java Edition
Like other launchers, the launcher now starts the game only while an
account in it owns Java Edition (Ludovic's decision, 2026-10-27).

- account::launch is the owner gate: it asks the Minecraft services
  again (entitlements and profile, after the token renewal), the
  selected account first, until one owns the game. Each answer is kept
  in the keychain item beside the tokens. No answer (no network) keeps
  the last one; "doesn't own" blocks at once.
- Every Play goes through it: the window (Home, instances, packs),
  `horizon-launcher play` and `modpack play`, the MCP's play.
- Offline names are added and played only beside an owner; signing the
  last owner out keeps them, unplayable.
- Home shows "Sign in with a Microsoft account that owns Minecraft:
  Java Edition to play" with Sign in; the "coming soon" sign-in strings
  are gone.
- The game binary, harnesses, CI and the servers are unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 18:16:55 +01:00
Hector EcclesandDeepSeek V4.1 Flash 783c25e9bc Draw chorus plants in End LOD terrain
Distant End terrain now estimates chorus plants from the biome's own
chorus_plant feature: the stem height and the age-5 tip follow the real
feature, while its placement modifiers give the density. Real chorus also
survives the refined, visited and saved far-column summaries, which
previously dropped every non-solid block.

Vanilla reference: ChorusPlantFeature and ChorusFlowerBlock.generatePlant,
already ported in horizon-world.

Co-Authored-By: DeepSeek V4.1 Flash <[email protected]>
2026-10-27 13:15:22 -04:00
socksproxandDevin 9b6faeb8cf launcher: the game's made-up name can be rolled for a new one
The game's own name (horizon-player.txt) isn't a kept offline name, so
Forget never applied to it and there was no way to be rid of it. Settings
> Account shows it with "New name", which writes a fresh PlayerNNNNN to
every existing game home — the default and each instance's — so the
player stays the same (new) player in its worlds, as PLAYER_FILES' copies
do.

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-27 18:13:55 +01:00
Ludovic MarieandClaude Opus 5.5 1cfb4def89 Merge main into fix/mob-state-sync-3
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 18:12:13 +01:00
Ludovic MarieandClaude Opus 5.5 b2fd94f49f Merge main into fix/parrot-follow-sit
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 18:11:55 +01:00
Ludovic MarieandClaude Opus 5.5 b8eac21b6f Merge main into fix/mob-looping-sounds
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 18:11:22 +01:00
Ludovic MarieandClaude Opus 5.5 fad711f55f Merge main into feat/spear-lunge
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 18:11:22 +01:00
Ludovic MarieandClaude Opus 5.5 4bc514829c Merge main into fix/magma-lava-jump-cobweb
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 18:11:21 +01:00
Ludovic MarieandClaude Opus 5.5 b8ae7233e0 Merge main into feat/spectator-menu
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 18:11:20 +01:00
Ludovic MarieandClaude Opus 5.5 5001e3f57d Merge main into fix/fishing-hook-players
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 18:11:20 +01:00
Ludovic MarieandClaude Opus 5.5 f731ca1737 Merge main into fix/bundle-dump-on-use
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 18:11:19 +01:00
Ludovic MarieandClaude Opus 5.5 b124beafeb Merge main into fix/remote-swing-hand-duration
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 18:11:19 +01:00
Ludovic MarieandClaude Opus 5.5 c7f448c523 Merge main into fix/campfire-douse-client
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 18:11:18 +01:00
Ludovic MarieandClaude Opus 5.5 c7937c5c20 Merge main into fix/2289-shield-pose
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 18:11:18 +01:00
Ludovic MarieandClaude Opus 5.5 ed652da572 Merge main into fix/cat-morning-gift
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 18:11:17 +01:00
Ludovic MarieandClaude Opus 5.5 b28797c534 Merge main into fix/parrot-cookie
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 18:11:17 +01:00
Ludovic Marie 42ecf0f673 Merge pull request #2889 from ludovic111/chore/nightly-reviewer-online5277
🤖 Agent: Merged by Ludovic's AI agent (orchestrator): reviewer list fix (Online5277 instead of levzzz5154), as Ludovic asked.
2026-10-27 18:07:58 +01:00
socksproxandDevin (SWE-2) e36cd17dd1 Books: page click events work, run_command ones included
Two gaps left every link in a written book dead. view_pages only parsed
compound page components, so the list form ComponentSerialization also
takes (and servers behind ViaVersion send) fell back to plain text and
lost its click events. And run_command was never parsed into a
ClickEvent, so even a parsed page's command links did nothing.

The book's click handler now follows Screen.defaultHandleClickEvent:
open_url through the link flow, run_command sent unsigned (client mods
first, as clickCommandAction), suggest_command into the chat box,
copy_to_clipboard, with chat's own click match gaining run_command the
same way.

Co-Authored-By: Devin (SWE-2) <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-27 18:06:53 +01:00
Ludovic Marie cf2e9d1bc0 Merge remote-tracking branch 'origin/main' into refactor/horizon-items 2026-10-27 18:03:10 +01:00
Ludovic Marie f8062e8fee Merge pull request #2583 from levzzz5154/fix/pot-back-face-culling
🤖 Agent: Merged by Ludovic's AI agent after the contribution gate (wave 5, cg20 union C, Mac mini), after #2296; main merged in since. Thanks @levzzz5154!
2026-10-27 18:02:10 +01:00
Ludovic Marie 7f760b8eb3 Merge pull request #2730 from kacpherek/fix/2714-windburst
🤖 Agent: Merged by Ludovic's AI agent (Claude, contrib-gate-19) after the contribution gate. Checked against vanilla 26.3 and proven in union/cg19-r2 (all round-2 PRs together: clippy, 1722 tests, 135 harnesses, lan_e2e in-frame and on the server thread) and union/cg19-r3 (the same PRs merged with today's main: 96/96 harnesses). Thanks @kacpherek.
2026-10-27 17:58:46 +01:00
Ludovic Marie 8baf3cad9e Merge pull request #2727 from kacpherek/fix/2710-backwards-body-yaw
🤖 Agent: Merged by Ludovic's AI agent (Claude, contrib-gate-19) after the contribution gate. Checked against vanilla 26.3 and proven in union/cg19-r2 (all round-2 PRs together: clippy, 1722 tests, 135 harnesses, lan_e2e in-frame and on the server thread) and union/cg19-r3 (the same PRs merged with today's main: 96/96 harnesses). Thanks @kacpherek.
2026-10-27 17:58:42 +01:00
Ludovic Marie 05f36b054c Merge pull request #2726 from kacpherek/fix/2709-dirtpath
🤖 Agent: Merged by Ludovic's AI agent (Claude, contrib-gate-19) after the contribution gate. Checked against vanilla 26.3 and proven in union/cg19-r2 (all round-2 PRs together: clippy, 1722 tests, 135 harnesses, lan_e2e in-frame and on the server thread) and union/cg19-r3 (the same PRs merged with today's main: 96/96 harnesses). Thanks @kacpherek.
2026-10-27 17:58:38 +01:00
Ludovic Marie d89a8025f9 Merge pull request #2724 from kacpherek/fix/2717-flying-fall-distance
🤖 Agent: Merged by Ludovic's AI agent (Claude, contrib-gate-19) after the contribution gate. Checked against vanilla 26.3 and proven in union/cg19-r2 (all round-2 PRs together: clippy, 1722 tests, 135 harnesses, lan_e2e in-frame and on the server thread) and union/cg19-r3 (the same PRs merged with today's main: 96/96 harnesses). Thanks @kacpherek.
2026-10-27 17:58:34 +01:00
Senjin fbab7d29a5 Merge remote-tracking branch 'origin/main' into hdr-tiers
# Conflicts:
#	crates/horizon-client/src/entity.wgsl
2026-10-27 17:57:13 +01:00
Ludovic MarieandClaude Opus 5.5 674596103f nightly: Online5277 is the third trusted reviewer, not levzzz5154
Ludovic asked for his top three contributors by merged pull requests: kacpherek, rdbtCVS and Online5277.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 17:56:36 +01:00
Ludovic Marie 43d8bd01cd Merge pull request #2725 from kacpherek/fix/2702-toggle-sprint-ctrl-drop
🤖 Agent: Merged by Ludovic's AI agent (Claude, contrib-gate-19) after the contribution gate. Checked against vanilla 26.3 and proven in union/cg19-r2 (all round-2 PRs together: clippy, 1722 tests, 135 harnesses, lan_e2e in-frame and on the server thread) and union/cg19-r3 (the same PRs merged with today's main: 96/96 harnesses). Thanks @kacpherek.
2026-10-27 17:55:00 +01:00
Ludovic Marie 18156172a3 Merge pull request #2728 from kacpherek/fix/2715-riptide
🤖 Agent: Merged by Ludovic's AI agent (Claude, contrib-gate-19) after the contribution gate. Checked against vanilla 26.3 and proven in union/cg19-r2 (all round-2 PRs together: clippy, 1722 tests, 135 harnesses, lan_e2e in-frame and on the server thread) and union/cg19-r3 (the same PRs merged with today's main: 96/96 harnesses). Thanks @kacpherek.
2026-10-27 17:53:16 +01:00
masa-masa-0314andZCode e6c1a91a65 creative: rebuild the search state when the screen opens
CreativeModeInventoryScreen is a fresh screen per open (init() makes a new
search box and a zeroed scrollbar; only selectedTab is static), but Horizon
keeps one Creative for the game's lifetime, so the last search stayed in
the box after closing the inventory. Clear the box, refocus it on the
search tab and reset the scrollbar at both open paths (local and remote).

Fixes #2873.

Co-Authored-By: ZCode <[email protected]>
2026-10-28 00:53:07 +08:00
Ludovic Marie 4c49c45a04 Merge pull request #2373 from kacpherek/fix/raid-recruit-needs-goal
🤖 Agent: Merged by Ludovic's AI agent (Claude, contrib-gate-19) after the contribution gate. Checked against vanilla 26.3 and proven in union/cg19-r2 (all round-2 PRs together: clippy, 1722 tests, 135 harnesses, lan_e2e in-frame and on the server thread) and union/cg19-r3 (the same PRs merged with today's main: 96/96 harnesses). Thanks @kacpherek.
2026-10-27 17:51:15 +01:00
Ludovic Marie 7bd8747909 Merge pull request #2369 from kacpherek/fix/air-bubble-pop-sound
🤖 Agent: Merged by Ludovic's AI agent (Claude, contrib-gate-19) after the contribution gate. Checked against vanilla 26.3 and proven in union/cg19-r2 (all round-2 PRs together: clippy, 1722 tests, 135 harnesses, lan_e2e in-frame and on the server thread) and union/cg19-r3 (the same PRs merged with today's main: 96/96 harnesses). Thanks @kacpherek.
2026-10-27 17:51:11 +01:00
Ludovic Marie 40e66ea972 Merge pull request #2365 from kacpherek/fix/give-overflow-drop
🤖 Agent: Merged by Ludovic's AI agent (Claude, contrib-gate-19) after the contribution gate. Checked against vanilla 26.3 and proven in union/cg19-r2 (all round-2 PRs together: clippy, 1722 tests, 135 harnesses, lan_e2e in-frame and on the server thread) and union/cg19-r3 (the same PRs merged with today's main: 96/96 harnesses). Thanks @kacpherek.
2026-10-27 17:51:07 +01:00
Ludovic Marie 0acee0a978 Merge pull request #2686 from kacpherek/fix/wither-peaceful-raider-cap-trap-difficulty
🤖 Agent: Merged by Ludovic's AI agent (Claude, contrib-gate-19) after the contribution gate. Checked against vanilla 26.3 and proven in union/cg19-r2 (all round-2 PRs together: clippy, 1722 tests, 135 harnesses, lan_e2e in-frame and on the server thread) and union/cg19-r3 (the same PRs merged with today's main: 96/96 harnesses). Thanks @kacpherek.
2026-10-27 17:51:02 +01:00
Ludovic Marie 324239ea84 Merge pull request #2662 from kacpherek/fix/village-siege
🤖 Agent: Merged by Ludovic's AI agent (Claude, contrib-gate-19) after the contribution gate. Checked against vanilla 26.3 and proven in union/cg19-r2 (all round-2 PRs together: clippy, 1722 tests, 135 harnesses, lan_e2e in-frame and on the server thread) and union/cg19-r3 (the same PRs merged with today's main: 96/96 harnesses). Thanks @kacpherek.
2026-10-27 17:50:58 +01:00
socksproxandDevin 8cfc13fe2c launcher: the account list follows changes made outside the window
accounts.json was read once at start-up, so an offline name added or
forgotten through the CLI or another window left the running window
without the name's row (and its Forget button) until relaunch. Page
switches and opening the account chip's menu read it again, as the
game's made-up name already was.

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-27 17:49:50 +01:00
Ludovic Marie 93c4acf169 Merge pull request #2661 from kacpherek/fix/chat-click-hover-events
🤖 Agent: Merged by Ludovic's AI agent (Claude, contrib-gate-19) after the contribution gate. Checked against vanilla 26.3 and proven in union/cg19-r2 (all round-2 PRs together: clippy, 1722 tests, 135 harnesses, lan_e2e in-frame and on the server thread) and union/cg19-r3 (the same PRs merged with today's main: 96/96 harnesses). Thanks @kacpherek.
2026-10-27 17:48:44 +01:00
Ludovic Marie 2f8c781f04 Merge pull request #2658 from kacpherek/fix/death-message-styled
🤖 Agent: Merged by Ludovic's AI agent (Claude, contrib-gate-19) after the contribution gate. Checked against vanilla 26.3 and proven in union/cg19-r2 (all round-2 PRs together: clippy, 1722 tests, 135 harnesses, lan_e2e in-frame and on the server thread) and union/cg19-r3 (the same PRs merged with today's main: 96/96 harnesses). Thanks @kacpherek.
2026-10-27 17:48:40 +01:00
Ludovic Marie 3cdd63e01d Merge pull request #2657 from kacpherek/fix/attack-speed-effects
🤖 Agent: Merged by Ludovic's AI agent (Claude, contrib-gate-19) after the contribution gate. Checked against vanilla 26.3 and proven in union/cg19-r2 (all round-2 PRs together: clippy, 1722 tests, 135 harnesses, lan_e2e in-frame and on the server thread) and union/cg19-r3 (the same PRs merged with today's main: 96/96 harnesses). Thanks @kacpherek.
2026-10-27 17:48:35 +01:00
Ludovic Marie aace479a0c Merge pull request #2656 from kacpherek/fix/other-player-poses
🤖 Agent: Merged by Ludovic's AI agent (Claude, contrib-gate-19) after the contribution gate. Checked against vanilla 26.3 and proven in union/cg19-r2 (all round-2 PRs together: clippy, 1722 tests, 135 harnesses, lan_e2e in-frame and on the server thread) and union/cg19-r3 (the same PRs merged with today's main: 96/96 harnesses). Thanks @kacpherek.
2026-10-27 17:48:31 +01:00
Ludovic Marie d54cab9565 Merge pull request #2653 from kacpherek/fix/2628-enderman-mouth
🤖 Agent: Merged by Ludovic's AI agent (Claude, contrib-gate-19) after the contribution gate. Checked against vanilla 26.3 and proven in union/cg19-r2 (all round-2 PRs together: clippy, 1722 tests, 135 harnesses, lan_e2e in-frame and on the server thread) and union/cg19-r3 (the same PRs merged with today's main: 96/96 harnesses). Thanks @kacpherek.
2026-10-27 17:48:27 +01:00
Ludovic Marie a3c05538ca Merge pull request #2121 from kacpherek/fix/1938-ghast-dismount
🤖 Agent: Merged by hand by Ludovic's AI agent (orchestrator) after the contribution gate: proven in cg21's union2 (union/cg21-kacpherek-2 077c10835: 80/82 harnesses on playhorizon, lan_e2e 9/9 in-frame and server-thread). CI parity failed only on remote_fireball/remote_fireworks, which fail on bare main on playhorizon's runner too and pass on the Mac mini.
2026-10-27 17:45:41 +01:00
Ludovic Marie fddd6b3864 Merge pull request #2649 from kacpherek/fix/sleep-status-overlay
🤖 Agent: Merged by Ludovic's AI agent (Claude, contrib-gate-19) after the contribution gate. Checked against vanilla 26.3 and proven in union/cg19-r2 (all round-2 PRs together: clippy, 1722 tests, 135 harnesses, lan_e2e in-frame and on the server thread) and union/cg19-r3 (the same PRs merged with today's main: 96/96 harnesses). Thanks @kacpherek.
2026-10-27 17:44:09 +01:00
Ludovic Marie ee217acc19 Merge pull request #2407 from kacpherek/fix/spawn-protection
🤖 Agent: Merged by Ludovic's AI agent (Claude, contrib-gate-19) after the contribution gate. Checked against vanilla 26.3 and proven in union/cg19-r2 (all round-2 PRs together: clippy, 1722 tests, 135 harnesses, lan_e2e in-frame and on the server thread) and union/cg19-r3 (the same PRs merged with today's main: 96/96 harnesses). Thanks @kacpherek.
2026-10-27 17:44:05 +01:00
Ludovic Marie 18510648ea Merge pull request #2406 from kacpherek/fix/flying-climbable
🤖 Agent: Merged by Ludovic's AI agent (Claude, contrib-gate-19) after the contribution gate. Checked against vanilla 26.3 and proven in union/cg19-r2 (all round-2 PRs together: clippy, 1722 tests, 135 harnesses, lan_e2e in-frame and on the server thread) and union/cg19-r3 (the same PRs merged with today's main: 96/96 harnesses). Thanks @kacpherek.
2026-10-27 17:44:01 +01:00
Ludovic Marie f65a4e45cf Merge pull request #2389 from kacpherek/fix/weather-height-adjusted-temperature
🤖 Agent: Merged by Ludovic's AI agent (Claude, contrib-gate-19) after the contribution gate. Checked against vanilla 26.3 and proven in union/cg19-r2 (all round-2 PRs together: clippy, 1722 tests, 135 harnesses, lan_e2e in-frame and on the server thread) and union/cg19-r3 (the same PRs merged with today's main: 96/96 harnesses). Thanks @kacpherek.
2026-10-27 17:43:57 +01:00
Ludovic Marie 28c105fa9b Merge pull request #2388 from kacpherek/fix/death-screen-quit-confirm
🤖 Agent: Merged by Ludovic's AI agent (Claude, contrib-gate-19) after the contribution gate. Checked against vanilla 26.3 and proven in union/cg19-r2 (all round-2 PRs together: clippy, 1722 tests, 135 harnesses, lan_e2e in-frame and on the server thread) and union/cg19-r3 (the same PRs merged with today's main: 96/96 harnesses). Thanks @kacpherek.
2026-10-27 17:43:52 +01:00
Ludovic Marie 2cd77f6d49 Merge pull request #2684 from kacpherek/fix/turtle-goals
🤖 Agent: Merged by Ludovic's AI agent (Claude, contrib-gate-19) after the contribution gate. Checked against vanilla 26.3 and proven in union/cg19-r2 (all round-2 PRs together: clippy, 1722 tests, 135 harnesses, lan_e2e in-frame and on the server thread) and union/cg19-r3 (the same PRs merged with today's main: 96/96 harnesses). Thanks @kacpherek.
2026-10-27 17:41:22 +01:00
Ludovic Marie a045782e04 Merge pull request #2677 from kacpherek/fix/mob-event-particles-to-clients
🤖 Agent: Merged by Ludovic's AI agent (Claude, contrib-gate-19) after the contribution gate. Checked against vanilla 26.3 and proven in union/cg19-r2 (all round-2 PRs together: clippy, 1722 tests, 135 harnesses, lan_e2e in-frame and on the server thread) and union/cg19-r3 (the same PRs merged with today's main: 96/96 harnesses). Thanks @kacpherek.
2026-10-27 17:41:18 +01:00
Ludovic Marie c5f0414a0a Merge pull request #2676 from kacpherek/fix/golem-shears-uncertainty
🤖 Agent: Merged by Ludovic's AI agent (Claude, contrib-gate-19) after the contribution gate. Checked against vanilla 26.3 and proven in union/cg19-r2 (all round-2 PRs together: clippy, 1722 tests, 135 harnesses, lan_e2e in-frame and on the server thread) and union/cg19-r3 (the same PRs merged with today's main: 96/96 harnesses). Thanks @kacpherek.
2026-10-27 17:41:14 +01:00
Ludovic Marie 2c82bbee6a Merge pull request #2673 from kacpherek/fix/server-thread-no-local-player
🤖 Agent: Merged by Ludovic's AI agent (Claude, contrib-gate-19) after the contribution gate. Checked against vanilla 26.3 and proven in union/cg19-r2 (all round-2 PRs together: clippy, 1722 tests, 135 harnesses, lan_e2e in-frame and on the server thread) and union/cg19-r3 (the same PRs merged with today's main: 96/96 harnesses). Thanks @kacpherek.
2026-10-27 17:41:10 +01:00
socksproxandDevin a7e3bcf79f launcher: a forgotten offline name is gone for good
Forget removed the name from accounts.json but left skins/<name>.png and
skins/<name>.cape.png behind, the skin panel's file pick could still
record a look for a name that no longer existed, and the account rows'
Play as / Play offline / Sign out saved the window's stale account list,
which could write a name forgotten elsewhere (CLI, another window) back.

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-27 17:41:07 +01:00
Ludovic Marie 0529ae8250 Merge pull request #2412 from kacpherek/fix/ambient-bush-eyeblossom-moss-effects
🤖 Agent: Merged by Ludovic's AI agent (orchestrator). rdbtCVS's requested changes are addressed (he said on #2215 not to wait for his re-review), and this head is proven in union/cg19-r2 and union/cg19-r3 (clippy, 1722 tests, 135 + 96 harnesses, lan_e2e). Thanks @kacpherek!
2026-10-27 17:38:40 +01:00
Ludovic Marie 1c876168bd Merge pull request #2402 from kacpherek/fix/parrot-imitate-dance
🤖 Agent: Merged by Ludovic's AI agent (orchestrator). rdbtCVS's requested changes are addressed (he said on #2215 not to wait for his re-review), and this head is proven in union/cg19-r2 and union/cg19-r3 (clippy, 1722 tests, 135 + 96 harnesses, lan_e2e). Thanks @kacpherek!
2026-10-27 17:38:23 +01:00
Ludovic Marie 48a3d8d175 Merge pull request #2394 from kacpherek/fix/parrot-shoulder
🤖 Agent: Merged by Ludovic's AI agent (orchestrator). rdbtCVS's requested changes are addressed (he said on #2215 not to wait for his re-review), and this head is proven in union/cg19-r2 and union/cg19-r3 (clippy, 1722 tests, 135 + 96 harnesses, lan_e2e). Thanks @kacpherek!
2026-10-27 17:38:17 +01:00
Ludovic Marie f8040cb977 Merge pull request #2393 from kacpherek/fix/wolf-armor
🤖 Agent: Merged by Ludovic's AI agent (orchestrator). rdbtCVS's requested changes are addressed (he said on #2215 not to wait for his re-review), and this head is proven in union/cg19-r2 and union/cg19-r3 (clippy, 1722 tests, 135 + 96 harnesses, lan_e2e). Thanks @kacpherek!
2026-10-27 17:38:13 +01:00
Ludovic Marie 1af3b2ebd9 Merge pull request #2375 from kacpherek/fix/warden-sonic-boom-knockback
🤖 Agent: Merged by Ludovic's AI agent (orchestrator). rdbtCVS's requested changes are addressed (he said on #2215 not to wait for his re-review), and this head is proven in union/cg19-r2 and union/cg19-r3 (clippy, 1722 tests, 135 + 96 harnesses, lan_e2e). Thanks @kacpherek!
2026-10-27 17:38:09 +01:00
Ludovic Marie 1e8396db70 Merge pull request #2363 from kacpherek/fix/tp-facing-feet
🤖 Agent: Merged by Ludovic's AI agent (orchestrator). rdbtCVS's requested changes are addressed (he said on #2215 not to wait for his re-review), and this head is proven in union/cg19-r2 and union/cg19-r3 (clippy, 1722 tests, 135 + 96 harnesses, lan_e2e). Thanks @kacpherek!
2026-10-27 17:38:05 +01:00
Ludovic Marie 6a031aa651 Merge pull request #2682 from kacpherek/feat/spear-use
🤖 Agent: Merged by Ludovic's AI agent (Claude, contrib-gate-19) after the contribution gate. Checked against vanilla 26.3 and proven in union/cg19-r2 (all round-2 PRs together: clippy, 1722 tests, 135 harnesses, lan_e2e in-frame and on the server thread) and union/cg19-r3 (the same PRs merged with today's main: 96/96 harnesses). Thanks @kacpherek.
2026-10-27 17:36:52 +01:00
Ludovic Marie 98d53642fa Merge pull request #2387 from kacpherek/fix/recovery-compass-death-spot
🤖 Agent: Merged by Ludovic's AI agent (Claude, contrib-gate-19) after the contribution gate. Checked against vanilla 26.3 and proven in union/cg19-r2 (all round-2 PRs together: clippy, 1722 tests, 135 harnesses, lan_e2e in-frame and on the server thread) and union/cg19-r3 (the same PRs merged with today's main: 96/96 harnesses). Thanks @kacpherek.
2026-10-27 17:35:56 +01:00
Ludovic Marie 60bcab5d61 Merge pull request #2380 from kacpherek/fix/comparator-reads-item-frame
🤖 Agent: Merged by Ludovic's AI agent (Claude, contrib-gate-19) after the contribution gate. Checked against vanilla 26.3 and proven in union/cg19-r2 (all round-2 PRs together: clippy, 1722 tests, 135 harnesses, lan_e2e in-frame and on the server thread) and union/cg19-r3 (the same PRs merged with today's main: 96/96 harnesses). Thanks @kacpherek.
2026-10-27 17:35:50 +01:00
Ludovic Marie c083b9ed02 Merge pull request #2376 from kacpherek/fix/trial-mob-equipment
🤖 Agent: Merged by Ludovic's AI agent (Claude, contrib-gate-19) after the contribution gate. Checked against vanilla 26.3 and proven in union/cg19-r2 (all round-2 PRs together: clippy, 1722 tests, 135 harnesses, lan_e2e in-frame and on the server thread) and union/cg19-r3 (the same PRs merged with today's main: 96/96 harnesses). Thanks @kacpherek.
2026-10-27 17:35:46 +01:00
Ludovic Marie 1ada2e33c8 Merge pull request #2348 from kacpherek/fix/silverfish-merge-and-wake-friends
🤖 Agent: Merged by Ludovic's AI agent (Claude, contrib-gate-19) after the contribution gate. Checked against vanilla 26.3 and proven in union/cg19-r2 (all round-2 PRs together: clippy, 1722 tests, 135 harnesses, lan_e2e in-frame and on the server thread) and union/cg19-r3 (the same PRs merged with today's main: 96/96 harnesses). Thanks @kacpherek.
2026-10-27 17:35:42 +01:00
Ludovic Marie 2a6bf0fae5 Merge pull request #2342 from kacpherek/fix/skeleton-arrow-difficulty
🤖 Agent: Merged by Ludovic's AI agent (Claude, contrib-gate-19) after the contribution gate. Checked against vanilla 26.3 and proven in union/cg19-r2 (all round-2 PRs together: clippy, 1722 tests, 135 harnesses, lan_e2e in-frame and on the server thread) and union/cg19-r3 (the same PRs merged with today's main: 96/96 harnesses). Thanks @kacpherek.
2026-10-27 17:35:37 +01:00
Ludovic Marie 5f14c125ed Merge pull request #2659 from kacpherek/fix/sweep-hits-players
🤖 Agent: Merged by Ludovic's AI agent (Claude, contrib-gate-19) after the contribution gate. Checked against vanilla 26.3 and proven in union/cg19-r2 (all round-2 PRs together: clippy, 1722 tests, 135 harnesses, lan_e2e in-frame and on the server thread) and union/cg19-r3 (the same PRs merged with today's main: 96/96 harnesses). Thanks @kacpherek.
2026-10-27 17:33:29 +01:00
Ludovic Marie 152afb5529 Merge pull request #2357 from kacpherek/fix/shulker-bullet-levitation
🤖 Agent: Merged by Ludovic's AI agent (Claude, contrib-gate-19) after the contribution gate. Checked against vanilla 26.3 and proven in union/cg19-r2 (all round-2 PRs together: clippy, 1722 tests, 135 harnesses, lan_e2e in-frame and on the server thread) and union/cg19-r3 (the same PRs merged with today's main: 96/96 harnesses). Thanks @kacpherek.
2026-10-27 17:32:34 +01:00
Ludovic Marie 3972efa333 Merge pull request #2338 from kacpherek/fix/enderman-angry-speed
🤖 Agent: Merged by Ludovic's AI agent (Claude, contrib-gate-19) after the contribution gate. Checked against vanilla 26.3 and proven in union/cg19-r2 (all round-2 PRs together: clippy, 1722 tests, 135 harnesses, lan_e2e in-frame and on the server thread) and union/cg19-r3 (the same PRs merged with today's main: 96/96 harnesses). Thanks @kacpherek.
2026-10-27 17:32:30 +01:00
Ludovic Marie 4c348837e4 Merge pull request #2335 from kacpherek/fix/frost-walker
🤖 Agent: Merged by Ludovic's AI agent (Claude, contrib-gate-19) after the contribution gate. Checked against vanilla 26.3 and proven in union/cg19-r2 (all round-2 PRs together: clippy, 1722 tests, 135 harnesses, lan_e2e in-frame and on the server thread) and union/cg19-r3 (the same PRs merged with today's main: 96/96 harnesses). Thanks @kacpherek.
2026-10-27 17:32:25 +01:00
Ludovic Marie 521a761745 Merge pull request #2328 from kacpherek/fix/arrows-bells-buttons
🤖 Agent: Merged by Ludovic's AI agent (Claude, contrib-gate-19) after the contribution gate. Checked against vanilla 26.3 and proven in union/cg19-r2 (all round-2 PRs together: clippy, 1722 tests, 135 harnesses, lan_e2e in-frame and on the server thread) and union/cg19-r3 (the same PRs merged with today's main: 96/96 harnesses). Thanks @kacpherek.
2026-10-27 17:32:21 +01:00
Ludovic Marie bf11ca8ac9 Merge pull request #2324 from kacpherek/fix/plates-mobs-items
🤖 Agent: Merged by Ludovic's AI agent (Claude, contrib-gate-19) after the contribution gate. Checked against vanilla 26.3 and proven in union/cg19-r2 (all round-2 PRs together: clippy, 1722 tests, 135 harnesses, lan_e2e in-frame and on the server thread) and union/cg19-r3 (the same PRs merged with today's main: 96/96 harnesses). Thanks @kacpherek.
2026-10-27 17:32:17 +01:00
Ludovic Marie df08ffda39 Merge pull request #2882 from ludovic111/fix/parity-host-excludes-locally
🤖 Agent: playhorizon's CI runner now skips the harnesses its host entry excludes (remote_fireball, remote_fireworks fail there on bare main from CPU starvation); pr_ready clean, unit test added.
2026-10-27 17:27:52 +01:00
Ludovic Marie 7d3aeb0315 Merge pull request #2399 from kacpherek/fix/wolf-spares-tamed-wolves
🤖 Agent: Merged by Ludovic's AI agent (orchestrator). rdbtCVS's requested changes are addressed (he said on #2215 not to wait for his re-review), and this head is proven in union/cg19-r2 and union/cg19-r3 (clippy, 1722 tests, 135 + 96 harnesses, lan_e2e). Thanks @kacpherek!
2026-10-27 17:27:35 +01:00
Ludovic Marie d4b7ef198c Merge pull request #2359 from kacpherek/fix/piglins-hunt-adult-hoglins
🤖 Agent: Merged by Ludovic's AI agent (orchestrator). rdbtCVS's requested changes are addressed (he said on #2215 not to wait for his re-review), and this head is proven in union/cg19-r2 and union/cg19-r3 (clippy, 1722 tests, 135 + 96 harnesses, lan_e2e). Thanks @kacpherek!
2026-10-27 17:26:04 +01:00
Ludovic Marie 6b011e436d Merge pull request #2356 from kacpherek/fix/trident-client-flight
🤖 Agent: Merged by Ludovic's AI agent (orchestrator). rdbtCVS's requested changes are addressed (he said on #2215 not to wait for his re-review), and this head is proven in union/cg19-r2 and union/cg19-r3 (clippy, 1722 tests, 135 + 96 harnesses, lan_e2e). Thanks @kacpherek!
2026-10-27 17:26:00 +01:00
Ludovic Marie d98450a575 Merge pull request #2355 from kacpherek/fix/respawn-radius
🤖 Agent: Merged by Ludovic's AI agent (orchestrator). rdbtCVS's requested changes are addressed (he said on #2215 not to wait for his re-review), and this head is proven in union/cg19-r2 and union/cg19-r3 (clippy, 1722 tests, 135 + 96 harnesses, lan_e2e). Thanks @kacpherek!
2026-10-27 17:25:56 +01:00
Ludovic Marie d860b9bfc0 Merge pull request #2343 from kacpherek/fix/endermite-lifetime
🤖 Agent: Merged by Ludovic's AI agent (orchestrator). rdbtCVS's requested changes are addressed (he said on #2215 not to wait for his re-review), and this head is proven in union/cg19-r2 and union/cg19-r3 (clippy, 1722 tests, 135 + 96 harnesses, lan_e2e). Thanks @kacpherek!
2026-10-27 17:25:51 +01:00
Ludovic Marie 7f4ae40cc5 Merge pull request #2326 from kacpherek/fix/mob-cactus-berry-damage
🤖 Agent: Merged by Ludovic's AI agent (orchestrator). rdbtCVS's requested changes are addressed (he said on #2215 not to wait for his re-review), and this head is proven in union/cg19-r2 and union/cg19-r3 (clippy, 1722 tests, 135 + 96 harnesses, lan_e2e). Thanks @kacpherek!
2026-10-27 17:25:47 +01:00
Ludovic Marie 7a53c8833b Merge pull request #2346 from kacpherek/fix/brush-suspicious-blocks
🤖 Agent: Merged by Ludovic's AI agent (Claude, contrib-gate-19) after the contribution gate. Checked against vanilla 26.3 and proven in union/cg19-r2 (all round-2 PRs together: clippy, 1722 tests, 135 harnesses, lan_e2e in-frame and on the server thread) and union/cg19-r3 (the same PRs merged with today's main: 96/96 harnesses). Thanks @kacpherek.
2026-10-27 17:25:17 +01:00
Ludovic Marie f8e7517393 Merge pull request #2254 from kacpherek/fix/construct-beacon-trigger
🤖 Agent: Merged by Ludovic's AI agent (orchestrator): main merged in with the advancements resolution union2 proved; verify green. Thanks @kacpherek!
2026-10-27 17:24:47 +01:00
Ludovic Marie 42cbbb565e Merge pull request #2099 from kacpherek/fix/2087-pot-top
🤖 Agent: Read against the vanilla 26.3 sources (Decorated pot: draw one face of its top and bottom planes) and proven in contrib-gate-21's union of 72 kacpherek PRs: 95 parity harnesses on playhorizon and the Mac mini, clippy, 1534 tests, lan_e2e 23 checks in-frame and with --server-thread. Thanks @kacpherek!
2026-10-27 17:17:48 +01:00
Ludovic Marie 4c7ada0d60 Merge pull request #2506 from masa-masa-0314/fix/animated-inventory-blocks
🤖 Agent: Merged by Ludovic's AI agent (orchestrator). Proven in union C (122/122 delta harnesses); its CI parity failed only on remote_fireball, a known load flake on playhorizon's runner that fails on bare main there too. Thanks @masa-masa-0314!
2026-10-27 17:11:38 +01:00
Ludovic MarieandClaude Opus 5.5 1788bf8134 Merge origin/main into fix/construct-beacon-trigger
advancements.rs keeps main's enchanted-item trigger (#2233) and the PR's
construct-beacon trigger.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 17:11:08 +01:00
sirrayiandDevin 62bb83857c Update rustls-webpki to 0.103.16 and webpki-roots to 1.0.10 (RUSTSEC-2026-0382, -0381)
cargo deny check advisories flags rustls-webpki 0.103.15 (distrusted emSign intermediates may be accepted, RUSTSEC-2026-0382) and webpki-roots 1.0.9 (bundles the distrusted emSign ECC Root CA - G3, RUSTSEC-2026-0381). Both are patch releases; every HTTPS request of the game, the launcher, horizon-server and the telemetry server goes through them (ureq -> rustls). horizon-mod-registry's lock already has the fixed versions.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-27 16:10:51 +00:00
SenjinandClaude Opus 5.5 be26c6db88 HDR: log what the display offers, and when HDR turns on or off
The log already names the adapter and the surface format; a report of HDR
trouble now also shows whether the surface offers HDR10 or scRGB, and the
output, swapchain format and nits it turned on with.

Part of #1441.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 16:57:42 +01:00
leifiyooandClaude Haiku 5.5 8d2b298253 Keep long Microsoft sign-in secrets in several keychain items
Credential Manager takes at most 2560 bytes per item. The secrets of a
Microsoft sign-in (the refresh token and the Minecraft token) are longer,
so saving them failed on Windows with "password encoded as UTF-16 is
longer than platform limit of 2560 chars" and the sign-in did not finish.

On macOS and Windows a secret is now split over the items `<uuid>`,
`<uuid>.1`, `<uuid>.2`... of at most 1000 UTF-8 bytes each (under 2560
bytes of UTF-16). Reads join the pieces, a write removes the leftovers
of a longer secret, and sign-out removes all of them. Existing single
items keep working.

Co-Authored-By: Claude Haiku 5.5 <[email protected]>
2026-10-27 16:53:00 +01:00
Ludovic MarieandClaude Opus 5.5 9914a8af41 Name the wither's client smoke tick client_smoke
#2685 adds wither::client_tick for the side heads' turning on a client;
a distinct name lets both land without a clash.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 16:47:06 +01:00
Ludovic Marie 9972feb710 Merge pull request #2235 from kacpherek/fix/instant-effects-rounding-splash-distance
🤖 Agent: Read against the vanilla 26.3 sources (Round instant potion effects and measure splash range from the body) and proven in contrib-gate-21's union of 72 kacpherek PRs: 95 parity harnesses on playhorizon and the Mac mini, clippy, 1534 tests, lan_e2e 23 checks in-frame and with --server-thread; main merged in since, clippy clean. Thanks @kacpherek!
2026-10-27 16:44:03 +01:00
Ludovic MarieandClaude Opus 5.5 e776eb1716 Merge main into fix/magma-lava-jump-cobweb
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 16:42:33 +01:00
Ludovic MarieandClaude Opus 5.5 78e635fe3e Merge main into fix/remote-swing-hand-duration
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 16:42:33 +01:00
Ludovic MarieandClaude Opus 5.5 5e582420ed Merge main into fix/2714-windburst
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 16:42:33 +01:00
Ludovic MarieandClaude Opus 5.5 3ff797d73d Merge main into fix/2710-backwards-body-yaw
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 16:42:33 +01:00
Ludovic MarieandClaude Opus 5.5 1d52fe8101 Merge main into fix/2709-dirtpath
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 16:42:33 +01:00
Ludovic MarieandClaude Opus 5.5 2487d29318 Merge main into fix/2717-flying-fall-distance
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 16:42:33 +01:00
Ludovic MarieandClaude Opus 5.5 3b8a8eeb91 Merge main into fix/2702-toggle-sprint-ctrl-drop
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 16:42:33 +01:00
Ludovic MarieandClaude Opus 5.5 063b6ee2b7 Merge main into fix/2715-riptide
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 16:42:33 +01:00
Ludovic MarieandClaude Opus 5.5 4414c3c1af Merge main into fix/bundle-dump-on-use
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 16:42:32 +01:00
Ludovic MarieandClaude Opus 5.5 6cf5b046fa Merge main into fix/2289-shield-pose
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 16:42:32 +01:00
Ludovic MarieandClaude Opus 5.5 d6ecd23a6e Merge main into fix/sleep-status-overlay
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 16:42:32 +01:00
Ludovic Marie 12a8782268 Merge pull request #2864 from ludovic111/feat/nightly-launcher-text
🤖 Agent: verified (fmt, launcher lib tests 197 passed, fr/ru strings); #2859 merged, so the text is true now. Queued at 99ce9afd6.
2026-10-27 16:40:36 +01:00
Ludovic Marie 028f608816 Merge pull request #2859 from ludovic111/feat/nightly-reviewers-wire
🤖 Agent: Merged by Ludovic's orchestrator after review (nightly-2 wiring: /nightly for the trusted reviewers; nightly builds get their own cache key, never save it, and stay off the release runner).
2026-10-27 16:38:12 +01:00
Ludovic MarieandClaude Opus 5.5 0f8f8fce11 Merge main into fix/mob-state-sync-3
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 16:38:11 +01:00
Ludovic MarieandClaude Opus 5.5 18b317186a Merge main into fix/turtle-goals
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 16:37:54 +01:00
Ludovic MarieandClaude Opus 5.5 708db4627e Merge main into fix/fishing-hook-players
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 16:37:33 +01:00
Ludovic MarieandClaude Opus 5.5 e93299fb80 Merge main into fix/death-message-styled
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 16:37:25 +01:00
Ludovic MarieandClaude Opus 5.5 e12d4c9d07 Merge main into fix/ambient-bush-eyeblossom-moss-effects
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 16:37:19 +01:00
Ludovic MarieandClaude Opus 5.5 505d17a425 Merge main into fix/parrot-shoulder
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 16:37:07 +01:00
Ludovic MarieandClaude Opus 5.5 2d89e9ae5f Merge main into fix/recovery-compass-death-spot
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 16:36:59 +01:00
Ludovic MarieandClaude Opus 5.5 14470343d1 Merge main into fix/air-bubble-pop-sound
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 16:36:44 +01:00
Ludovic MarieandClaude Opus 5.5 9d9389310d Merge main into feat/spectator-menu
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 16:36:43 +01:00
Ludovic MarieandClaude Opus 5.5 8649edaf16 Merge main into fix/village-siege
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 16:36:42 +01:00
Ludovic MarieandClaude Opus 5.5 3a8706d0d7 Merge main into fix/chat-click-hover-events
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 16:36:42 +01:00
Ludovic MarieandClaude Opus 5.5 0762f49f20 Merge main into fix/cat-morning-gift
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 16:36:41 +01:00
Ludovic MarieandClaude Opus 5.5 d980703786 Merge main into fix/other-player-poses
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 16:36:41 +01:00
Ludovic MarieandClaude Opus 5.5 efd7b94c15 Merge main into fix/2628-enderman-mouth
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 16:36:41 +01:00
Ludovic MarieandClaude Opus 5.5 9f2e4485ad Merge main into fix/sweep-hits-players
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 16:36:41 +01:00
Ludovic MarieandClaude Opus 5.5 bbd5c1c833 Merge main into fix/parrot-follow-sit
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 16:36:40 +01:00
Ludovic MarieandClaude Opus 5.5 639ac0e4c6 Merge main into fix/parrot-imitate-dance
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 16:36:40 +01:00
Ludovic MarieandClaude Opus 5.5 76e3a0f7b7 Merge main into fix/wolf-armor
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 16:36:40 +01:00
Ludovic MarieandClaude Opus 5.5 47e6541731 Merge main into fix/warden-sonic-boom-knockback
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 16:36:39 +01:00
Ludovic MarieandClaude Opus 5.5 361020fc23 Merge main into fix/tp-facing-feet
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 16:36:39 +01:00
Ludovic MarieandClaude Opus 5.5 417ee432c5 Merge main into fix/silverfish-merge-and-wake-friends
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 16:36:39 +01:00
Ludovic MarieandClaude Opus 5.5 accd2718ac Merge main into fix/skeleton-arrow-difficulty
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 16:36:38 +01:00
Ludovic MarieandClaude Opus 5.5 854bcb158f random_tick probe: a fresh suspicious block's reset check changes nothing
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 16:35:08 +01:00
Ludovic MarieandClaude Opus 5.5 1bd74d1b2f Skip the world spawn trip's teleport when the respawn already put the player there
Vanilla teleports a respawning player once; the second, identical move
showed up in the dedicated_server, pearl_death and cursor_death harnesses.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 16:35:07 +01:00
Ludovic MarieandClaude Opus 5.5 967a9363ce entity_death_loot: give wolves their gear in the body slot
Horizon now keeps a wolf's body armour in its head slot, as for the other
BODY types.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 16:35:07 +01:00
levzzz b901415c80 Merge remote-tracking branch 'upstream/main' into fix/forceload-rings 2026-10-27 20:32:05 +05:00
leifiyooandClaude Haiku 5.5 de286f236f Count down Riptide's spin on a server's copy of a player
The host runs a guest's trident release on its copy of the guest, which
sets the spin for 20 ticks. The copy never ran the client's movement
tick, so the count never went down: the copy stayed in the spin pose
(eye 0.4), and every arrow or projectile the guest shot was spawned from
the leg until something else reset it.

LivingEntity.aiStep counts autoSpinAttackTicks down on the server too.
Count it down in the guest's server tick, before updatePlayerPose.

Co-Authored-By: Claude Haiku 5.5 <[email protected]>
2026-10-27 16:30:53 +01:00
leifiyooandClaude Haiku 5.5 e7f5201c1f Client: worn dyed leather armour shows its dye colour
EquipmentLayerRenderer.getColorForLayer takes a dyeable layer's colour from
the stack's minecraft:dyed_color and falls back to the asset's undyed colour.
The player's own armour and LAN guests' armour, plus armour stands, used only
the undyed colour, so dyed leather looked brown.

Co-Authored-By: Claude Haiku 5.5 <[email protected]>
2026-10-27 16:30:53 +01:00
ElyTheCat c06a7d4511 Linux: OpenGL can present to the window
wgpu-hal's GLES backend takes its EGL display from the display handle the
wgpu instance was created with. Horizon created every instance without one
(`new_without_display_handle`), so on Linux EGL fell back to
`EGL_MESA_platform_surfaceless`: those configs cannot make a window surface,
the GL adapter reported every surface as incompatible (`gl not compatible with
provided surface`), and Auto never reached its OpenGL fallback.

Give the instance the window's X11 or Wayland display handle, and offer the
OpenGL choice in Options > Graphics on Linux (DirectX 12 stays Windows-only).

Tested on Intel HD 4000 with Mesa crocus (OpenGL 4.2), on X11 and Wayland:
graphicsApi:opengl, HORIZON_GRAPHICS_API=opengl and WGPU_BACKEND=gl each run
and present, and Auto falls back to OpenGL when Vulkan has no ICD.
2026-10-27 18:57:55 +03:30
Ludovic Marie ed10d376fa Merge pull request #2858 from ludovic111/feat/nightly-set
🤖 Agent: Merged by Ludovic's orchestrator after review (nightly-2: the /nightly set library; tools/ci/ is hand-merged).
2026-10-27 16:27:51 +01:00
Ludovic MarieandClaude Opus 5.5 8d5424408c Nightly: nothing an unreviewed build leaves reaches a release
Orchestrator's review of #2859: a nightly can hold pull requests nobody
reviewed yet, and their build scripts run in `build`. game-build.yml gets a
`trusted` input, true only from release.yml:

- the Rust cache (GitHub-hosted Linux and Windows): its own key for an
  untrusted build (`nightly-<name>`), never saved; a release restores only
  what a release saved;
- macOS: an untrusted build's exporters and build jobs run on the runner
  that already runs pull requests' parity jobs (macOS + horizon-game), never
  the release runner (MAC_RUNNER) nor parity-main's;
- a trusted build doesn't read the machine's shared sccache, which pull
  requests' jobs write (runner-env.sh: HORIZON_NO_SCCACHE).

tests/test_nightly.py pins all three.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 16:26:51 +01:00
Ludovic MarieandClaude Opus 5.5 99ce9afd63 Launcher: the Nightly setting says reviewers pick its pull requests
nightly-2: a Nightly can include pull requests not reviewed yet, picked by
trusted reviewers (docs/release-flow.md "Nightly channel"). The Settings row
says so, in English, French and Russian.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 16:20:32 +01:00
levzzzandminimax-m3.1-flash da877fe1db Give a forced chunk vanilla's rings
A `/forceload`ed chunk holds a FORCED ticket at level 31, and vanilla's
level rises one per ring, so the chunk itself and the two rings around it
are level chunks with their light, the third ring has its features and the
fourth only its terrain. Horizon took the forced chunk's reach from the
caller's margin instead, so only the chunk itself ever finished and its
neighbours stayed proto chunks.

The reach now follows the ticket levels: the full ring is two chunks out,
the features ring three and the terrain ring four, and the protos of the
terrain ring are kept so the features ring can be decorated from them. A
decorated proto chunk is saved as `minecraft:initialize_light`, the status
vanilla gives a chunk that has its features but no light.

Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 20:18:42 +05:00
masa-masa-0314andZCode 91d401aa8a Subtitles: a server's client ticks the overlay too
Single player joins its integrated server as a client, so `Minecraft.tick`
takes the `tick_remote` path and never reaches `tick_mobs`, where the
subtitle overlay's tick had its only call. The overlay therefore never
started listening to the sound engine: no sound notified it, and the
closed captions stayed empty with "Show Subtitles" on or off.

Call `tick_subtitles` from `tick_remote_level` beside `tick_hud`, as the
local path does.

Co-Authored-By: ZCode (Z.ai) <[email protected]>
2026-10-27 23:17:06 +08:00
levzzzandminimax-m3.1-flash a7d0586d95 Merge upstream/main into fix/armadillo-roll-ball
Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 20:16:53 +05:00
Ludovic MarieandClaude Opus 5.5 21ac444f28 refactor: block entity data moves down beside the chunk
Step A of the horizon-client crate split. `Chunk.block_entities` held
`containers::BlockEntity`, whose payloads were defined across containers,
brewing, signs, sculk, mobs::spawner, mobs::trial_spawner, vault, beehive
and command_block, so `chunk` reached most of the gameplay code.

The enum and every payload type now live in `chunk/block_entity.rs` as
plain data (definitions and derives). Every impl block stays in its
feature module, and each module re-exports its types under the old path,
so callers don't change. Also:

- `mobs::Who` moves to `horizon_client_core::who` (the vibration `Info`
  names it); mobs re-exports it.
- `skins::NameOrId` moves beside `SkullSkin`, which holds it; skins
  re-exports it.
- `Spawner.next`, the `Next` enum and `Selector.current` become
  `pub(crate)`, since their struct now lives in another module.
- The vault, spawner and trial_spawner harnesses record
  `chunk/block_entity.rs`.

No behaviour change.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 16:16:27 +01:00
Ludovic MarieandClaude Opus 5.5 7efd8ef33a Nightly tests: the scheduled refresh, the notes and the repo's reviewers list
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 16:15:14 +01:00
Ludovic Marie c6633ea1bb Merge branch 'feat/nightly-set' into feat/nightly-reviewers-wire 2026-10-27 16:14:58 +01:00
Ludovic MarieandClaude Opus 5.5 6c0e262aa0 nightly_set tests: the repo's list file is checked with it (next PR)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 16:14:39 +01:00
Ludovic Marie 1e7e2ffb27 Merge remote-tracking branch 'origin/main' into refactor/horizon-items 2026-10-27 16:14:13 +01:00
Ludovic MarieandClaude Opus 5.5 c9990e43e9 nightly_set tests: the scheduled refresh and the notes move with nightly.py (next PR)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 16:14:01 +01:00
Ludovic MarieandClaude Opus 5.5 79eddec09c Nightly: trusted reviewers pick the pull requests with /nightly
Ludovic's decision (2026-10-27, from kacpherek's proposal under #2779): the
Nightly is where contributors test pull requests before the gate.
kacpherek, rdbtCVS and levzzz5154 (.github/nightly-reviewers.txt) and
Ludovic pick them with `/nightly 2640 2642`, `add`, `remove`, `clear`,
`status` or `dry-run` in any issue or pull request.

- nightly-command.yml (issue_comment, main's copy): checks who asked, saves
  the set on nightly-state, starts `nightly build`, answers "🤖 Nightly: …".
  It never checks out or merges a pull request.
- nightly-build.yml: `pick` builds main + the set (newest heads and merged or
  closed ones dropped at the scheduled run), pushes it as
  nightly/build-<date>-<n>, records it in builds.jsonl; build, sign and
  publish are unchanged; `answer` tells the thread how it ended.
- Notes say a Nightly can include pull requests not reviewed yet, picked by
  trusted reviewers, and list each with its author and who picked it.
- docs/release-flow.md: who can pick, how, how Ludovic removes or pauses them.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 16:11:14 +01:00
Ludovic MarieandClaude Opus 5.5 0ff1df58a2 nightly_set.py: trusted reviewers' picks for the Nightly (library)
The pieces of nightly-2 (`/nightly 2640 2642`): parsing a comment, the
reviewers list (.github/nightly-reviewers.txt, with a `paused` line), the
refusals (paths CODEOWNERS gives to Ludovic, closed or merged PRs, more than
30, more than 6 builds a day), the set and its commands (Ludovic's remove
keeps reviewers from adding a PR back), the nightly-state branch (pushed with
retries when another run saved first), and the assembly: each PR head merged
onto main in order with `git merge-tree --write-tree`, objects only, so no
file, hook or filter of a pull request reaches the runner. Equal inputs give
the same commit. A conflict names the two pull requests (or main) and the
paths.

Nothing calls it yet: the next PR wires it into nightly-build.yml and a
`nightly command` workflow.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 16:11:13 +01:00
Ludovic Marie 58beb98327 Merge pull request #2233 from kacpherek/fix/enchanted-item-trigger
🤖 Agent: Read against the vanilla 26.3 sources (Fire the enchanted_item advancement trigger at the enchanting table) and proven in contrib-gate-21's union of 72 kacpherek PRs: 95 parity harnesses on playhorizon and the Mac mini, clippy, 1534 tests, lan_e2e 23 checks in-frame and with --server-thread; main merged in since, clippy clean. Thanks @kacpherek!
2026-10-27 16:08:02 +01:00
AndriiandClaude Sonnet 5.5 8e36842891 Merge upstream main into fix/sweep-attack-2764
Drops the player-victim sweep in favour of #2659 and uses the
enchantment's sweeping_damage_ratio and upstream's particle queue.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-27 16:04:16 +01:00
levzzzandminimax-m3.1-flash 7b841af095 Merge the dyed armour colour fix onto upstream/main
Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 20:01:00 +05:00
levzzzandminimax-m3.1-flash 033c89a1b2 A dyed leather armour keeps its colour
The worn armour was always tinted with the colour leather has when it is
undyed, so a dyed leather piece showed the undyed brown; the trim of
another player's armour was not drawn at all, because only the item id
of the worn pieces was kept.

The vanilla's EquipmentLayerRenderer tints a dyeable layer with the
stack's dye colour (DataComponents.DYED_COLOR) and the colour it has
when undyed otherwise, and draws the trim as a layer over the armour.
The worn pieces keep their stack now, so the dye colour and the trim
reach the renderer (#2774).

Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 20:00:53 +05:00
sirrayiandDevin 0580626784 Server: refuse digging and using blocks outside the world border
ServerLevel.mayInteract also requires WorldBorder.isWithinBounds(pos): START_DESTROY_BLOCK outside the border starts no dig, and UseItemOn uses nothing there (the block updates are still sent back). Part of #2743.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-27 14:58:38 +00:00
sirrayiandDevin 6143e258ab Release notes: a Contributors heading and a rule before the disclaimer
The "Thanks to ..." line read as part of the section before it (often Known issues), on GitHub and on playhorizon.dev/releases. It now has its own heading, and a horizontal rule sets the disclaimer apart. Fixes #2314.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-27 14:55:35 +00:00
Ludovic Marie 96f84abfeb Merge pull request #2256 from kacpherek/fix/axe-frees-copper-golem-statue
🤖 Agent: Read against the vanilla 26.3 sources (An axe turns an unoxidized copper golem statue back into a golem) and proven in contrib-gate-21's union of 72 kacpherek PRs: 95 parity harnesses on playhorizon and the Mac mini, clippy, 1534 tests, lan_e2e 23 checks in-frame and with --server-thread; main merged in since, clippy clean. Thanks @kacpherek!
2026-10-27 15:50:37 +01:00
Claude Codeandminimax-m3.1-flash 2dc154ff7f The structure pieces ask isSolidRender, not the collision cube
The legacy structure pieces asked "is the collision shape a full block"
for the blocks around them, when they choose a chest's facing
(StructurePiece.reorient) and when the mineshaft checks its floor. That
is a different set of blocks than vanilla's BlockState.isSolidRender:
glass, leaves, ice, a spawner and a barrier are full collision blocks
but do not draw as solid, and soul sand and mud draw as solid without
a full collision shape.

The pieces now ask the blocks' own facts, the flag the worldgen already
keeps for every state (BlockProperty::SolidRender, which the exporter
writes from state.isSolidRender()), the way the monster room's own
reorient does. The chest's facing is decided in one function that takes
the blocks around the chest, so the rule can be checked against
vanilla's.

Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 19:50:19 +05:00
Ludovic MarieandClaude Opus 5.5 0879f3dbd2 parity: a host's exclude list also holds on its own CI runner
playhorizon's runner fails remote_fireball and remote_fireworks even alone and
at bare main (3b19c386c: 2 of 2 runs and the retry), while the Mac mini passes
them; every fork PR whose parity lands there stopped on them. The host's
exclude list only kept harnesses off workers sent there, not off the runner's
own jobs: platform_skips now adds the local host's exclude, skipped with its
reason unless named. The two harnesses join playhorizon's list; the Mac
mini's runner and the nightly audit (macOS) keep running them.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 15:37:04 +01:00
levzzz f6e63d252d Merge remote-tracking branch 'upstream/main' into HEAD 2026-10-27 19:32:30 +05:00
AndriiandClaude Sonnet 5.5 4f13db42b0 Share the sweep arc options and the crit burst helper
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-27 15:32:04 +01:00
levzzzandminimax-m3.1-flash f507a067cd The breeze's wind and the wind charge scroll their textures
Vanilla draws the breeze's wind layer and the wind charge with
`RenderTypes.breezeWind`, whose texture scrolls with the entity's age: by
0.02 of the texture per tick for the breeze's wind layer and 0.03 for the
wind charge. Horizon drew both with a still texture, so the breeze's gust
and the wind charge did not move.

The draw now takes a scroll offset and wraps the vertex's U inside the
texture, as the texture matrix of the vanilla's render type does.

Fixes #2760

Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 19:30:29 +05:00
Ludovic Marie 27af1879d7 Merge pull request #2124 from kacpherek/fix/block-animate-gaps
🤖 Agent: Read against the vanilla 26.3 sources (Block ambient effects: levers, repeaters, dripstone, wither roses, End gateways, lightning rods) and proven in contrib-gate-21's union of 72 kacpherek PRs: 95 parity harnesses on playhorizon and the Mac mini, clippy, 1534 tests, lan_e2e 23 checks in-frame and with --server-thread. Thanks @kacpherek!
2026-10-27 15:28:43 +01:00
levzzzandminimax-m3.1-flash 84d204327f A neutral mob's anger survives the save and the reload
Vanilla keeps a neutral mob's anger in the entity tag: `NeutralMob.
addPersistentAngerSaveData` writes the `anger_end_time`, the absolute
game time the anger ends at, and the `angry_at` the target, and
`readPersistentAngerSaveData` reads them back, so a mob that is unloaded
and loaded again is still after whoever hurt it.

Horizon kept the anger as a countdown in memory only, so it was lost
with the chunk. The save now writes the anger end time and the target
with the vanilla's keys, and the load gives the mob back the anger that
is left of it and its target.

Fixes #2784

Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 19:26:34 +05:00
Ludovic Marie 096814ade9 Merge pull request #2675 from kacpherek/fix/dolphin-goals
🤖 Agent: Merged main in (conflicts resolved, see the merge commit); proven in union R1b (branch union/cg19-r1b): clippy -D warnings, tests, the harnesses it can affect, lan_e2e in-frame and on the server thread.
2026-10-27 15:24:54 +01:00
Ludovic Marie d177ad48e5 Merge pull request #2360 from kacpherek/fix/dragon-egg-attack
🤖 Agent: Merged main in (conflicts resolved, see the merge commit); proven in union R1b (branch union/cg19-r1b): clippy -D warnings, tests, the harnesses it can affect, lan_e2e in-frame and on the server thread.
2026-10-27 15:21:13 +01:00
levzzz 28f906c37d Merge remote-tracking branch 'upstream/main' into HEAD 2026-10-27 19:21:10 +05:00
levzzzandminimax-m3.1-flash f8f2f904bc A summoned or egg-spawned mob is not persistent
Vanilla never calls `Mob.setPersistenceRequired` for a mob it spawns: the
spawn egg's offspring, the spawn egg on a block and `/summon` all leave
the mob to the despawn rule, and only the NBT can make it persistent.
Horizon marked every one of them persistent, so they never despawn and
they are left out of the mob cap.

The three paths now spawn the mob like before, without the persistence
flag, and the command execution corpus checks it against vanilla: a
summoned zombie is not selected by `nbt={PersistenceRequired:1b}`.

Fixes #2787

Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 19:21:08 +05:00
Alexander KrylovandHermes Agent c3fb1656e6 Fonts: Pixelify Sans gets the Cyrillic О and П it was missing
Upstream maps U+043E (о) and U+043F (п) and not their capitals, so a Russian
heading drew О and П from the fallback font: they came out smooth and taller
than the pixel letters around them (screenshots in the PR).

Both come from the font's own drawing. О is the Latin O's glyph under the
Cyrillic codepoint, which is the same round shape this font gives о: its
lowercase is the O with the top shifted down. П is that weight's lowercase п
with everything above the baseline lifted to the cap height the font uses there
(Н's top), 175 units in SemiBold, 177 in Medium and 171 in Bold, since each
weight draws its own metrics. Both now sit on the bbox of the O and Н of their
own weight.

О keeps the O's hinting instructions; П is a new outline with none, which a
pixel design does not need at the sizes the launcher draws it. GDEF, GPOS and
GSUB are byte-identical to the files they replace.

Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
2026-10-27 17:19:10 +03:00
Ludovic Marie 21123aaa3c Merge pull request #2670 from kacpherek/fix/chat-options-page
🤖 Agent: Merged main in (conflicts resolved, see the merge commit); proven in union R1b (branch union/cg19-r1b): clippy -D warnings, tests, the harnesses it can affect, lan_e2e in-frame and on the server thread.
2026-10-27 15:17:32 +01:00
Ludovic Marie 65096c5b2e Merge pull request #2374 from kacpherek/fix/effect-icons-over-screens
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 15:17:28 +01:00
Ludovic Marie 9948e9d3ee Merge pull request #2372 from kacpherek/fix/nether-map-arrow-spin
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 15:17:24 +01:00
socksproxandDevin (SWE-2 High) b98633b677 Play the player's own bites while eating on a remote server
On a remote (non-Horizon) server the use-item tick spawned the crumb
particles but never played the consumable's eat sound, so only the burp
at the end was heard. Call emit_consume for the periodic emit, as
tick_used_item does on the player's own server, so the bites sound too.

Fixes #2747.

Co-Authored-By: Devin (SWE-2 High) <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-27 15:14:34 +01:00
levzzzandminimax-m3.1-flash 3c7942d2bd Test that a hit rolls the armadillo up into a ball
An armadillo hit by a living thing curls up: Armadillo.actuallyHurt
rolls it up, Armadillo.rollUp plays entity.armadillo.roll and switches
the state to ROLLING, and ArmadilloState.shouldHideInShell then shows
the shell instead of the armadillo. The roll itself is on main (#2535),
but nothing held it in place, so this test pins it: a hit rolls the
armadillo up, the roll plays its sound and the model shows the shell
(#2759).

Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 19:14:31 +05:00
Ludovic Marie 3c3884ff38 Merge pull request #2245 from kacpherek/fix/crossbow-advancement-triggers
🤖 Agent: Merged by hand by Ludovic's AI agent (orchestrator) after the contribution gate: proven in cg21's union2 (union/cg21-kacpherek-2 077c10835: 80/82 harnesses on playhorizon, lan_e2e 9/9 in-frame and server-thread). CI parity failed only on remote_fireball/remote_fireworks, which fail on bare main on playhorizon's runner too and pass on the Mac mini.
2026-10-27 15:14:14 +01:00
Ludovic Marie 30c58b6591 Merge pull request #2371 from kacpherek/fix/wolf-meat-heal
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 15:12:12 +01:00
Ludovic Marie fd2265dc19 Merge pull request #2370 from kacpherek/fix/johnny-vindicator-targets-raiders
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 15:12:08 +01:00
Ludovic Marie 84dcd156f8 Merge pull request #2368 from kacpherek/fix/raid-no-action-time
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 15:12:04 +01:00
Ludovic Marie 5ca6bd7de6 Merge pull request #2367 from kacpherek/fix/xp-clear-spawnpoint-targets
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 15:11:59 +01:00
Ludovic Marie 2a6b4c0ae6 Merge pull request #2366 from kacpherek/fix/clone-move-overlap
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 15:11:55 +01:00
Ludovic MarieandClaude Opus 5.5 d5aa3f6e42 Name the cat relax goal's owner check is_owner_of
The tame mob goals (#2382, #2399) add an is_owner(m, w) to this module.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 15:11:34 +01:00
Ludovic MarieandClaude Opus 5.5 fbc7c628f6 Merge main into fix/give-overflow-drop
/give's overflow throw takes main's third argument (GiveCommand:
player.drop(copy, false)).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 15:10:11 +01:00
Ludovic Marie c20565ee49 Merge pull request #2364 from kacpherek/fix/dragon-strafe-line-of-sight
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 15:08:51 +01:00
levzzzandminimax-m3.1-flash 65bca4ce98 net: skip a compressed frame that carries no packet
A server may send a compressed frame whose uncompressed length is 0 and
which holds no packet: `ConnReader::recv` read the packet id from the
empty body and failed with "needed 1 bytes, 0 left", which ended the
connection. Vanilla skips such a frame: its PacketDecoder only decodes a
packet from a readable buffer, so an empty frame produces no packet and
the connection stays up.

A frame with no bytes at all is a corrupted frame in vanilla ("Frame
length cannot be zero"), so the frame splitter now says that instead of
failing later with a missing byte.

Fixes #2739

Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 19:08:49 +05:00
Ludovic Marie 4e9c68e3da Merge pull request #2358 from kacpherek/fix/shulker-closed-armor
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 15:08:46 +01:00
Ludovic Marie 712144297a Merge pull request #2325 from kacpherek/fix/phantom-difficulty
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 15:08:41 +01:00
Ludovic Marie 651ac2cfd5 Merge pull request #2674 from kacpherek/fix/air-bubbles-eye-height
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 15:08:38 +01:00
Ludovic Marie 2853015f0e Merge pull request #2231 from kacpherek/fix/step-sound-above
🤖 Agent: Read against the vanilla 26.3 sources (Play the step sound of the block above the floor for the player) and proven in contrib-gate-21's union of 72 kacpherek PRs: 95 parity harnesses on playhorizon and the Mac mini, clippy, 1534 tests, lan_e2e 23 checks in-frame and with --server-thread. Thanks @kacpherek!
2026-10-27 15:08:34 +01:00
Ludovic MarieandClaude Opus 5.5 0914229800 A parrot saves its owner in main's Variant arm
A second parrot arm was unreachable, so the variant was never saved.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 15:07:40 +01:00
Ludovic MarieandClaude Opus 5.5 e350b87b3c End portal exits pass no spawn finder
main's item and mob End portal exit calls end_arrival, which now takes the
respawn search's finder; in the End it only builds the platform.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 15:07:40 +01:00
Ludovic Marie 6567f95aa3 Merge pull request #2672 from kacpherek/fix/smithing-template-tooltip
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 15:05:51 +01:00
Ludovic Marie 1361ae8338 Merge pull request #2671 from kacpherek/fix/chat-minor
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 15:05:47 +01:00
Ludovic Marie d4412d160f Merge pull request #2655 from kacpherek/fix/book-sign-author
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 15:05:44 +01:00
Ludovic Marie 38ddc48cab Merge pull request #2654 from kacpherek/fix/host-sees-invisible-guests
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 15:05:40 +01:00
Ludovic Marie ef0d62e47e Merge pull request #2652 from kacpherek/fix/leave-bed-screen
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 15:05:35 +01:00
AndriiandClaude Sonnet 5.5 574f3587ed Hit boats and carts with the full attack damage, sweep victims in section order
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-27 15:05:34 +01:00
Ludovic Marie 6f4b1bf4c2 Merge pull request #2651 from kacpherek/fix/hotbar-pop-bundle-bar
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 15:02:59 +01:00
Ludovic Marie 691341b2b0 Merge pull request #2650 from kacpherek/fix/pvp-axe-disables-shield
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 15:02:55 +01:00
Ludovic Marie 83979b38d6 Merge pull request #2648 from kacpherek/fix/local-server-checks
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 15:02:51 +01:00
Ludovic Marie b189f1b44c Merge pull request #2647 from kacpherek/fix/creative-hotbar-save-load
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 15:02:48 +01:00
Ludovic Marie 958db5f40f Merge pull request #2646 from kacpherek/fix/2630-husk-hunger-shield
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 15:02:44 +01:00
Ludovic Marie 6b9d3309cc Merge pull request #2644 from kacpherek/fix/remote-break-item-change
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 15:00:02 +01:00
Ludovic Marie c5d737b536 Merge pull request #2643 from kacpherek/fix/2633-dragon-breath-colour
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 14:59:58 +01:00
Ludovic Marie 57fe750035 Merge pull request #2642 from kacpherek/fix/pick-item-server
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 14:59:54 +01:00
Ludovic Marie 3849e37393 Merge pull request #2640 from kacpherek/fix/hopper-entity-containers
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 14:59:48 +01:00
Ludovic Marie 9b091ed657 Merge pull request #2418 from kacpherek/fix/announce-player-achievements
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 14:59:43 +01:00
iLostName de89e758cc remote: a server's client doesn't load the loot tables on its first tick 2026-10-27 14:57:27 +01:00
Ludovic Marie 777e0e2eb5 Merge pull request #2416 from kacpherek/fix/riptide-lift-collision
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 14:56:37 +01:00
Ludovic Marie 04ecdee7a2 Merge pull request #2415 from kacpherek/fix/op-permission-level
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 14:56:33 +01:00
Ludovic Marie 6ddc3226d4 Merge pull request #2410 from kacpherek/fix/enforce-whitelist
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 14:56:29 +01:00
Ludovic Marie 1903865cd5 Merge pull request #2409 from kacpherek/fix/flying-edge-backoff
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 14:56:25 +01:00
Ludovic Marie d5d54a8775 Merge pull request #2405 from kacpherek/fix/lan-join-leave-host-chat
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 14:56:21 +01:00
Ludovic Marie c27e2d2c2d Merge pull request #2404 from kacpherek/fix/sulfur-cube-archetype-physics
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 14:52:58 +01:00
iLostName 66b705cec3 sounds: list each namespace's sound files once instead of a lookup per sound 2026-10-27 14:52:56 +01:00
Ludovic Marie d463d793a9 Merge pull request #2398 from kacpherek/fix/lodestone-compass-use
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 14:52:54 +01:00
Ludovic Marie f10425675c Merge pull request #2395 from kacpherek/fix/horse-armor-protects
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 14:52:50 +01:00
Ludovic Marie aa8ab4253d Merge pull request #2392 from kacpherek/fix/loot-set-name
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 14:52:46 +01:00
Ludovic Marie fa56070097 Merge pull request #2390 from kacpherek/fix/block-loot-copy-components
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 14:52:41 +01:00
AndriiandClaude Sonnet 5.5 ee6b5bf585 Match Player.attack more closely: enchant bonus, magic crit, no frame sweep
The main hit takes getEnchantedDamage on the real attack damage, a dragon
part is never a critical hit, and the enchanted-hit burst is shown when the
enchantments add damage. Vehicles are hurt before the sweep and sounds.
Frames, paintings and cushions end the attack in skipAttackInteraction, so
they no longer sweep. Swept armor stands are knocked back.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-27 14:49:05 +01:00
Ludovic Marie dd5863edc2 Merge pull request #2385 from kacpherek/fix/lightning-sky-flash
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 14:48:05 +01:00
Ludovic Marie 37cabdcb02 Merge pull request #2381 from kacpherek/fix/hud-text-above-bars
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 14:48:00 +01:00
Ludovic Marie a40ef4eee0 Merge pull request #2379 from kacpherek/fix/sculk-catalyst-nonplayer-kills
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 14:47:56 +01:00
iLostName 5f5a43fd54 focus loss doesn't open the pause menu over the pack prompt or a server dialog 2026-10-27 14:47:53 +01:00
Ludovic Marie 914a6956e9 Merge pull request #2378 from kacpherek/fix/sky-body-rain-brightness
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 14:47:51 +01:00
Ludovic Marie 698b49e3a5 Merge pull request #2362 from kacpherek/fix/shulker-peaceful
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 14:47:46 +01:00
sirrayiandDevin 690033b71b port-pack: keep pack-index file names and jar entries inside the output
A pack index's file name (the last /-part of an mrpack path, a CurseForge or modlist jar name) was joined onto jars/ unchecked: on Windows ..\..\x.dll or C:\x escaped it. Jar entries were checked for .. only between /. Refuse names with \, : or that are . or ..

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-27 13:45:29 +00:00
sirrayiandDevin c0bf77f3c3 Telemetry server: count IPv6 clients by /64 in the rate limits
An IPv6 client rotating addresses inside its /64 got a fresh bucket for every address, so the per-address limits on batches, live pings, hub votes, reports and suggestions didn't hold, and the bucket maps grew until the daily upkeep. Key them by /64, as horizon-relay already does.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-27 13:45:27 +00:00
Ludovic Marie ec067f1e39 Merge pull request #2353 from kacpherek/fix/piglin-hurt-drops-admired-item
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 14:42:07 +01:00
Ludovic Marie 6081a66629 Merge pull request #2352 from kacpherek/fix/portal-pigmen-cooldown
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 14:42:03 +01:00
Ludovic Marie ee69413a68 Merge pull request #2351 from kacpherek/fix/ice-melts-when-mined
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 14:41:59 +01:00
Ludovic Marie fc4e7af854 Merge pull request #2344 from kacpherek/fix/creeper-fall-swell
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 14:41:54 +01:00
Ludovic Marie 0e069c31d7 Merge pull request #2243 from kacpherek/fix/bee-flower-effects
🤖 Agent: Read against the vanilla 26.3 sources (Bees get an effect from a wither rose or open eyeblossom instead of breeding) and proven in contrib-gate-21's union of 72 kacpherek PRs: 95 parity harnesses on playhorizon and the Mac mini, clippy, 1534 tests, lan_e2e 23 checks in-frame and with --server-thread; main merged in since, clippy clean. Thanks @kacpherek!
2026-10-27 14:41:50 +01:00
levzzzandminimax-m3.1-flash f9ca0a76cf The death message names the cause's item when it has a custom name
`DamageSource.getLocalizedDeathMessage` uses the `<id>.item` message when
the cause holds an item with a custom name, with the item's name as a
third argument: "Steve was shot by Zombie using My Sword". Horizon always
used the two-argument message, so the item was never named.

The mob's main hand item now keeps its custom name, and the death message
picks the message and its arguments from it.

Fixes #2802

Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 18:38:37 +05:00
sirrayiandDevin 4fea5fafe5 Dedicated server: apply server.properties rate-limit
rate-limit was parsed but never used. Average each player's packets a second as Connection.tickSecond does and kick past the limit with disconnect.exceeded_packet_rate, as RateKickingConnection does.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-27 13:36:52 +00:00
sirrayiandDevin 780abb9832 SNBT: bound nesting depth, keep nan/inf words as strings
Deeply nested [[[... in a command, function, command block or old world overflowed the stack and aborted the process; both SNBT parsers now stop at 512 levels. Bare words such as nan, infd or Infinity were parsed as floats by Rust's parser (and became JSON null in text components); only digits make a number.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-27 13:36:51 +00:00
sirrayiandDevin 2812cf201d Protocol: don't panic on malformed chunk sections, compressed frames or legacy pings
A server sending a palette wider than 64 bits divided by zero in storage_len (and 33-64 bits overflowed shifts); SimpleBitStorage allows at most 32. A non-canonical data-length VarInt in a compressed frame was misread. An empty first read in the legacy ping handler indexed past the end.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-27 13:36:51 +00:00
sirrayiandDevin ce848af067 Zip: don't trust an entry's declared sizes for allocation
A server resource pack whose zip64 sizes claim ~2^63 bytes aborted the client on Vec::with_capacity. Cap the capacity at the archive's length and the inflate limit at 1 GiB, with saturating arithmetic.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-27 13:36:51 +00:00
sirrayiandDevin d3be8dbf05 horizon-server: bound HTTP request and header lines
read_line had no limit, and the read timeout applies per read, so a client streaming bytes without a newline to the API or the web panel grew the supervisor's memory without bound. Lines longer than 8 KiB now get a 400.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-27 13:36:51 +00:00
sirrayiandDevin 9bbc54bd4c Server: refuse block actions out of a player's reach
Breaking and using blocks (START/STOP_DESTROY_BLOCK, UseItemOn) were carried out for any position a client sent, so a modified client could dig or place anywhere in loaded chunks. Check isWithinBlockInteractionRange(pos, 1.0) and the hit point as vanilla's handleBlockBreakAction and handleUseItemOn do.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-27 13:36:50 +00:00
Ludovic Marie fbbff39e07 Merge pull request #2341 from kacpherek/fix/fishing-rain-biome
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 14:36:42 +01:00
Ludovic Marie 49c2000de3 Merge pull request #2339 from kacpherek/fix/fishing-pickup-delay
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 14:36:37 +01:00
Ludovic Marie f561d4845b Merge pull request #2336 from kacpherek/fix/zombie-drowned-conversion-stops
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 14:36:31 +01:00
Ludovic Marie d6c4e5a027 Merge pull request #2333 from kacpherek/fix/farmer-requested-items
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 14:36:26 +01:00
Ludovic Marie ad271604af Merge pull request #2262 from kacpherek/fix/2209-flat-world-horizon
🤖 Agent: Read against the vanilla 26.3 sources (Flat worlds: no black horizon, the dark disc and void fog use the flat horizon) and proven in contrib-gate-21's union of 72 kacpherek PRs: 95 parity harnesses on playhorizon and the Mac mini, clippy, 1534 tests, lan_e2e 23 checks in-frame and with --server-thread. Thanks @kacpherek!
2026-10-27 14:36:22 +01:00
levzzzandminimax-m3.1-flash 1232745064 Keep a traveller that waits for an End gateway's exit
The traveller was dropped from the queue before the exit was resolved, so a
failure to resolve it lost the entity for good. The traveller now waits and
the failure is retried, with the reason in the log.

Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 18:34:07 +05:00
AndriiandClaude Sonnet 5.5 52c667f423 Sweep from stands, frames and dragon parts, and hurt other players
- start a sweep (and the attack sounds, with a living target's critical
  hit) when a punch lands on an armor stand, an item frame or a dragon part
- spare the entity the attack hit, and swing the arm for the stands and
  frames, which did not
- hurt the other players in the sweep box through the pvp rule and teams,
  for the host and for guests, with the 0.4 knockback; queued like critical
  hits until the hub is back in place

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-27 14:33:16 +01:00
Ludovic Marie 9e6cd2ed70 Merge pull request #2803 from ludovic111/test/server-check-text-busy
🤖 Agent: verify passed; test-only fix for the horizon-server ETXTBSY flake that failed #2215. Queued by crate-split-1.
2026-10-27 14:28:25 +01:00
levzzzandminimax-m3.1-flash 3382b666ce Add the audit that drives a cow and a stack through an End gateway
The script plays the End in a scratch home: it places a gateway beside the
spawn platform, puts a cow and a seven-diamond stack in it, and checks that
both leave it and land at the gateway's exit with their data, in the
server-thread and the in-frame singleplayer.

Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 18:27:43 +05:00
Ludovic Marie a813c4a755 Merge pull request #2669 from kacpherek/fix/toast-stack-f1
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 14:26:19 +01:00
iLostName 86474679db server packs: the prompt takes the open screen's place, as PackConfirmScreen does 2026-10-27 14:26:17 +01:00
Ludovic Marie e8e7254710 Merge pull request #2668 from kacpherek/fix/chat-history-restricted
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 14:26:14 +01:00
Ludovic Marie 639100ac95 Merge pull request #2667 from kacpherek/fix/passenger-tracking-range
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 14:26:09 +01:00
Ludovic Marie 9cb12d984b Merge pull request #2665 from kacpherek/fix/mount-hud
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 14:26:05 +01:00
Ludovic Marie b211519e79 Merge pull request #2660 from kacpherek/fix/sleep-messages
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 14:25:58 +01:00
sleeperandClaude Opus 5.5 dcb7476d12 Keep a player entity's profile name after its list entry goes
A player entity keeps the profile it was created with
(RemotePlayer.getGameProfile), whose name is its scoreboard name, even
once ClientboundPlayerInfoRemovePacket drops it from the player list.
Servers rely on that to hide their NPCs (fake players) from the list
after spawning them on a team, as Hypixel SkyBlock does with
collisionRule never. Horizon read the name from the list, so an NPC
removed from it lost its team and pushed the local player again.

Remote::level_player_names keeps the name each player entity spawned
with; Game::remote_player_name and remote_player_names (teams, name tags,
glowing) read it, falling back to the list entry.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 13:23:38 +00:00
levzzz 152334eee3 Merge remote-tracking branch 'upstream/main' into fix/screen-cursor-centre 2026-10-27 18:23:26 +05:00
Ludovic MarieandClaude Opus 5.5 c4a39757a1 Merge main into feat/spear-lunge
enchant_effects.rs: Lunge's post_piercing_attack effects beside main's
ApplyMobEffect.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 14:21:38 +01:00
socksproxandDevin (SWE-2 High) 4fdc93257f Let portal arrivals tick in their destination without a player
An item or mob sent through a portal into a playerless dimension waited
forever: resolve_arrivals needed lit chunks, the level never streamed any,
and levels_to_tick skipped it entirely, so its Age stayed frozen until
someone followed and it was picked up intact (#1528). Vanilla's portal
move lands the entity under an ENTITY ticket, which keeps the chunk
entity-ticking while it ages and despawns on time.

Pending arrivals now add their landing chunk to the level's streaming
extra areas and keep their destination level in levels_to_tick, so it
lights, resolves, and ticks them just as if a player stood there.

Co-Authored-By: Devin (SWE-2 High) <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-27 14:21:24 +01:00
Ludovic Marie 23d7e03a5b Merge pull request #2540 from kacpherek/fix/sync-mob-variants
🤖 Agent: Merged by hand by Ludovic's AI agent (orchestrator) after the contribution gate. rdbtCVS's change request is addressed; a re-review was asked, with no new objection since. Proven in cg19's union R1/R1b (union/cg19-r1b 6ec974ab3); main merged in since, CI green at this head.
2026-10-27 14:21:20 +01:00
Ludovic Marie 9a8e0e36a9 Merge pull request #2401 from kacpherek/fix/armor-stand-pose
🤖 Agent: Merged by hand by Ludovic's AI agent (orchestrator) after the contribution gate. rdbtCVS's change request is addressed; a re-review was asked, with no new objection since. Proven in cg19's union R1/R1b (union/cg19-r1b 6ec974ab3); main merged in since, CI green at this head.
2026-10-27 14:21:12 +01:00
Ludovic Marie d68895d0b1 Merge pull request #2340 from kacpherek/fix/cave-spider-husk-difficulty
🤖 Agent: Merged by hand by Ludovic's AI agent (orchestrator) after the contribution gate. rdbtCVS's change request is addressed; a re-review was asked, with no new objection since. Proven in cg19's union R1/R1b (union/cg19-r1b 6ec974ab3); main merged in since, CI green at this head.
2026-10-27 14:21:07 +01:00
Ludovic MarieandClaude Opus 5.5 7690ad27c2 Merge main into fix/mob-looping-sounds
main's Drown event (entity event 67) stays beside this branch's entity
event doc.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 14:19:47 +01:00
Ludovic Marie 59ed6869e3 Merge pull request #2801 from ludovic111/feat/nightly-game-label
🤖 Agent: verified with pr_ready --quick (fmt, clippy, 1443 tests green); nightly label screenshot in the body. Queued at 2af3308.
2026-10-27 14:19:33 +01:00
Ludovic Marie 1ac4bcbe9b Merge pull request #2690 from kacpherek/fix/mob-ambient-particles
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 14:19:27 +01:00
Ludovic Marie 9b487bdfe9 Merge pull request #2687 from kacpherek/feat/shelf-use
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 14:19:23 +01:00
Ludovic Marie 1ec2b888aa Merge pull request #2680 from kacpherek/fix/spider-jockey-hoglin-toss
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 14:19:19 +01:00
Ludovic Marie dbb396e599 Merge pull request #2678 from kacpherek/fix/skeleton-freezes-to-stray
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 14:19:12 +01:00
levzzzandminimax-m3.1-flash 67e0d54062 tests: give the capture fixture the mouse position helper
The fixture compiles `Game::capture` on its own, so the helper it now
calls has to be part of it: the test extracts it from the client, like it
extracts the methods, and the call inside the fixture loses the crate
path.

The fixture also asserts the grab path now: grabbing the pointer puts
the mouse at the window's centre, as `MouseHandler.grabMouse` does.

Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 18:18:07 +05:00
Ludovic MarieandClaude Opus 5.5 74da2fe6b7 Merge main into fix/mob-state-sync-3
- lan.rs shared flags: on fire, a drowned's swimming, invisible and main's
  glowing in one byte.
- The piglin's zombification immunity (16) goes into its existing arm (the
  shared arm could no longer reach it); the villager's unhappy counter
  beside main's mob_freeze_meta.
- remote_mob_data.rs: imports unioned.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 14:17:14 +01:00
asownerytandClaude Opus 5.5 53cb3a19d1 Name the projectile in the death message when it has no owner
AbstractArrow.onHitEntity makes an ownerless arrow (or trident) its own
causing entity, and fireballs, spit and shulker bullets are the
DamageSource's direct entity. Horizon recorded no entity for an ownerless
projectile's hit, so DamageSource.getLocalizedDeathMessage's one-argument
form showed the raw two-placeholder template (%1$s was shot by %2$s).

Fixes #2802

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 02:16:56 +13:00
Eshayat Al-WasiuandCodex 84390f6dc1 Match server deletion input to button row width
Co-Authored-By: Codex (GPT-6.1) <[email protected]>
2026-10-27 19:16:53 +06:00
Eshayat Al-WasiuandCodex 9d191c28f2 Narrow server deletion confirmation input
Co-Authored-By: Codex (GPT-6.1) <[email protected]>
2026-10-27 19:16:53 +06:00
Eshayat Al-WasiuandCodex 5d42108a98 Place deletion confirmation help below input
Co-Authored-By: Codex (GPT-6.1) <[email protected]>
2026-10-27 19:16:53 +06:00
Eshayat Al-WasiuandCodex 1972ef3bef Use shared settings row styling for server deletion
Co-Authored-By: Codex (GPT-6.1) <[email protected]>
2026-10-27 19:16:53 +06:00
Eshayat Al-WasiuandCodex e8145b07fa Remove redundant server deletion confirmation hint
Co-Authored-By: Codex (GPT-6.1) <[email protected]>
2026-10-27 19:16:53 +06:00
Eshayat Al-WasiuandCodex 5e7295a631 Align server deletion controls with the settings rows
Co-Authored-By: Codex (GPT-6.1) <[email protected]>
2026-10-27 19:16:52 +06:00
Eshayat Al-WasiuandCodex 761a770174 Emphasize the server name in the delete confirmation
Co-Authored-By: Codex (GPT-6.1) <[email protected]>
2026-10-27 19:16:52 +06:00
Eshayat Al-WasiuandCodex 7f0a61954b Add a confirmed server delete action to launcher settings
Move stopped servers to the system trash after name confirmation. Refuse active supervisor locks and background services, and refresh the sidebar after removal.

Co-Authored-By: Codex (GPT-6.1) <[email protected]>
2026-10-27 19:16:52 +06:00
Ludovic MarieandClaude Opus 5.5 966c38092d Merge main into fix/turtle-goals
- turtle.rs: these goals and navigation beside main's TurtleMoveControl
  and its tests; the doc says what is still not ported.
- ai.rs: the turtle keeps its own register arm, so main's call from the
  generic arm goes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 14:14:49 +01:00
levzzzandminimax-m3.1-flash 826147d716 Merge upstream/main into fix/shield-blocking-pose
Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 18:14:14 +05:00
levzzzandminimax-m3.1-flash a8903cba07 Merge upstream/main into fix/shulker-body-yaw
Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 18:14:07 +05:00
levzzzandminimax-m3.1-flash 0b4fa473e7 Test the shield's blocking pose and its use duration
A shield's use animation is the BLOCK one: AvatarRenderer.getArmPose
raises the arm for it and Item.getUseDuration gives it 72000 ticks, so
the arm stays up while it is used. The fix (kacpherek's #2310) sets the
blocking flag for the other players and gives the shield its use
duration; these tests pin both ends: the arm goes across the body while
the shield is used, and the shield's use duration is vanilla's 72000
ticks, not the 0 an item without an use animation gets (#2289).

Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 18:13:31 +05:00
Ludovic Marie 1e3a9d53a7 Merge pull request #2386 from kacpherek/fix/mace-smash-knockback-players
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 14:10:14 +01:00
Ludovic Marie e5cd63b608 Merge pull request #2337 from kacpherek/fix/shears-wear
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 14:10:04 +01:00
sleeperandClaude Opus 5.5 045cabb7f2 Find other players' teams by profile name
Entity.getTeam looks a team member up by getScoreboardName, which is a
player's profile name (Player.getScoreboardName) and any other entity's
UUID. Game::mob_team, mob_teams and player_allies used the UUID for every
entity a server sent, players included, so another player's team was
never found. On a server that turns collisions off with a collisionRule
never team (Hypixel SkyBlock), the other players still pushed the local
player; the same lookup feeds allies, friendly fire, glowing colours and
the team checks explosions make.

Game::scoreboard_name now picks the profile name for another player (from
the player list, via Game::remote_player_name) and the UUID otherwise.

lan_e2e gains a team-push check: the host pushes the guest without a
team, and doesn't once the host's team has collisionRule never.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 13:10:03 +00:00
Ludovic Marie 06522c11db Merge pull request #2334 from kacpherek/fix/cat-spawner
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 14:09:56 +01:00
Ludovic MarieandClaude Opus 5.5 6d4b5c345f Merge main into feat/spear-use
lan.rs: the spear's kinetic hits beside main's cushion break particles.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 14:07:35 +01:00
Ludovic MarieandClaude Opus 5.5 8dcb4289a9 Merge main into fix/golem-shears-uncertainty
creatures.rs: shearing a snow golem or a bogged beside main's armadillo
brushing.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 14:06:35 +01:00
Ludovic MarieandClaude Opus 5.5 6df1dfd4da Merge main into fix/fishing-hook-players
Hook targets and pulls beside main's guest explosions and particles; imports
unioned.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 14:05:55 +01:00
iLostName 560aa61e91 server packs: the download prompt keeps the pointer and stops the walking behind it 2026-10-27 14:05:45 +01:00
Ludovic MarieandClaude Opus 5.5 98ec18fcfe Merge main into fix/chat-click-hover-events
chat.rs: the run-command click beside main's page links and link prompt.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 14:05:24 +01:00
Ludovic MarieandClaude Opus 5.5 be0a93271f test: retry ETXTBSY when horizon-server tests run a script they just wrote
engine::tests::the_check_runs_the_program failed on CI (verify of #2215)
with "can't start: Text file busy (os error 26)". The test writes a shell
script and runs it at once; when another test thread forks to spawn a
process while the script's write handle is open, the child keeps that
handle until it execs, and running the script meanwhile fails with
ETXTBSY on Linux. The two engine tests that do this now retry that one
error (up to 50 times, 20 ms apart). Test-only change.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 14:04:41 +01:00
Ludovic MarieandClaude Opus 5.5 683ff6f73a Merge main into fix/sweep-hits-players
The guests' sweeps (damage to the other players) beside main's guest
explosions and particles; both flush at the end of a guest's turn.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 14:04:31 +01:00
Ludovic Marie 7bdfe942a4 Merge pull request #2779 from ludovic111/feat/nightly-channel
🤖 Agent: Merged by hand by Ludovic's AI agent (orchestrator) after reading the workflows: the union build runs without secrets, a separate job only hashes and signs with a channel-tagged message (release launchers refuse nightly signatures, #2792), and the workflows always run from main. The release dry run on this head (40504185084) is green on every job.
2026-10-27 14:02:39 +01:00
Ludovic MarieandClaude Opus 5.5 0ea243a1c7 Merge main into fix/sync-mob-variants
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 14:01:44 +01:00
Ludovic MarieandClaude Opus 5.5 9dd27667f2 Merge main into fix/armor-stand-pose
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 14:01:35 +01:00
Ludovic MarieandClaude Opus 5.5 8709026be3 Merge main into fix/cave-spider-husk-difficulty
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 14:01:25 +01:00
Ludovic MarieandClaude Opus 5.5 2af3308892 Nightly builds name themselves on the title screen and in reports
A game built by the nightly workflow (HORIZON_NIGHTLY="<date> <sha>") draws
"Nightly <date> · <sha>" in the title screen's top-left corner, reports it as
its version in crash reports, and logs it as the log's first line, so a bug
report that carries the log names the build. Telemetry gets the version the
launcher installs it as (nightly-<date>-<sha>). Releases and source builds
are unchanged; only the "Horizon <version>" log line is new for them.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 14:01:12 +01:00
Ludovic MarieandClaude Opus 5.5 a791368257 Merge main into fix/2628-enderman-mouth
- lan.rs: the enderman's DATA_CREEPY after main's warden, sniffer and
  shulker data.
- remote_mob_data.rs: the creepy flag beside main's arms.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 14:01:07 +01:00
Ludovic Marie 2b910fa16d Merge pull request #2793 from ludovic111/test/win-screen-temp-dirs
🤖 Agent: test-only fix for the win_screen flake (two parallel tests shared one temp dir; each call now gets its own). pr_ready --quick green (1329 tests). Queued once pr_ready was green, as the orchestrator asked; merge after verify passes.
2026-10-27 14:00:30 +01:00
Ludovic MarieandClaude Opus 5.5 af548f5ae8 Merge main into fix/cat-morning-gift
- PlayerView: sleeping and the sleep timer beside main's passenger flag.
- creatures.rs: DropGift's item motion uses nextDouble, as the ItemEntity
  constructor does; SetBlock takes main's setBlockAndUpdate.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 13:59:04 +01:00
Ludovic MarieandClaude Opus 5.5 1e741d1528 Merge main into fix/parrot-imitate-dance
game.rs: jukebox songs for the guests beside main's cushion breaks and
mining cracks (level events 2019, 2020).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 13:57:19 +01:00
Ludovic MarieandClaude Opus 5.5 8fad68ab3e Merge main into fix/parrot-cookie
creatures.rs: the player's eye position beside main's acting UUID.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 13:56:34 +01:00
Ludovic MarieandClaude Opus 5.5 32c8def114 Merge main into fix/parrot-shoulder
- creatures.rs: shoulder times beside main's guest particles, the parrot
  shoulder event beside main's Infested, parrot food beside main's acting UUID.
- entity_nbt.rs: a parrot saves its Owner and Sitting in main's Variant arm
  (a second arm was unreachable).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 13:55:55 +01:00
Ludovic Marie 4483c2613c Merge pull request #2792 from ludovic111/feat/nightly-launcher
🤖 Agent: merging the opt-in Nightly setting (off by default, own instance); fmt, clippy -D warnings, launcher lib tests and horizon-release-sign tests pass, including the release channel refusing nightly signatures.
2026-10-27 13:55:38 +01:00
Ludovic MarieandClaude Opus 5.5 7c1055df97 Merge main into fix/wolf-armor
- Taming takes main's acting_player_uuid (the host gets a UUID made for
  it); acting_owner goes.
- Wolf armour is the body slot (6) on main (head_is_body): lan.rs sends
  its cracks and components there, and remote.rs reads them from slot 6.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 13:54:47 +01:00
Ludovic MarieandClaude Opus 5.5 d25648f729 Merge main into fix/comparator-reads-item-frame
game.rs GameLevel: the item frames beside main's cart signals and arrow
boxes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 13:52:12 +01:00
Ludovic MarieandClaude Opus 5.5 70dd250ae6 Merge main into fix/warden-sonic-boom-knockback
- creatures.rs: mob_hits_mob takes main's hit handling (infested
  silverfish, other events left in the outbox), so a sonic boom and a melee
  hit share it.
- mobs.rs: SonicBoom beside main's Shift.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 13:51:40 +01:00
Ludovic MarieandClaude Opus 5.5 c6276dbbde Merge main into fix/trident-client-flight
- trident.rs: main's thrower guest (player_hurt_entity) beside the owner id
  a server sends.
- remote.rs: the trident and main's firework rocket both take a server
  move at once; both take its velocity.
- remote_projectiles.rs: main's smoke trails stay; the trident flies by its
  own client tick instead of main's interpolation step.
- tests/test_remote_motion.py: both fields.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 13:50:45 +01:00
Ludovic MarieandClaude Opus 5.5 a0b4721c5b Merge main into fix/campfire-douse-client
remote.rs: main's command block editor opens on a reacting block; the
campfire douse prediction stays its own branch.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 13:49:34 +01:00
Ludovic Marie d136f966f8 Merge remote-tracking branch 'origin/main' into refactor/horizon-items 2026-10-27 13:49:06 +01:00
Ludovic MarieandClaude Opus 5.5 1f457ff81e Merge main into fix/silverfish-merge-and-wake-friends
spawner.rs: main's mob_poof (entity event 20 to the clients) and
poof_particles split.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 13:49:02 +01:00
levzzzandminimax-m3.1-flash b85ce82160 Merge kacpherek's PR #2310: the shield goes up in first person and other players show the blocking pose
Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 17:48:33 +05:00
AndriiandClaude Sonnet 5.5 49ce3e6d5a Sweep in vanilla's order and against everything it hits
- play the sweep sound before the victims are hurt, then draw the arc
- hurt armor stands (not marker ones) in the sweep box through the same
  hit a direct punch gives them
- run the victims' damage through getEnchantedDamage with the real base
- sweep around a boat or a minecart that takes the hit, which only played
  the sound before

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-27 13:48:20 +01:00
Ludovic MarieandClaude Opus 5.5 9663d6f53e Merge main into fix/mob-cactus-berry-damage
mobs.rs: main's DryOut and piston deltas beside this branch's sweet berry
bush and stuck multiplier.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 13:48:12 +01:00
Ludovic MarieandClaude Opus 5.5 0d554de016 parity: game_loop records mouse, window and keymapping from the input crate
The harness hashed crates/horizon-client/src/{mouse,window,keymapping}.rs,
which this branch moves to horizon-client-input, so CI's parity job failed
with FileNotFoundError before running it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 13:48:09 +01:00
Ludovic MarieandClaude Opus 5.5 1bddc392b2 Merge main into fix/plates-mobs-items
- game.rs: the plate and tripwire mobs and other entities beside main's
  detector rail signal and arrow-pressed buttons.
- projectiles.rs: main's trident is 0.5 x 0.5 (EntityTypes.TRIDENT).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 13:47:37 +01:00
Ludovic Marie 3b19c386cf Merge pull request #2354 from kacpherek/fix/dragon-kill-credit
🤖 Agent: Merged by hand by Ludovic's AI agent (orchestrator) after the contribution gate. rdbtCVS's change request is addressed at this head; a re-review was asked hours ago, with no new objection since. Proven in cg19 union R1 bb29ce68e (union/cg19-r1: main c6c2ec5f9 + 83 PRs) Mac mini: 98 se.
2026-10-27 13:46:08 +01:00
levzzzandminimax-m3.1-flash 00df6c646f The shulker's body stays square to its block while the head turns
Shulker.recreateFromPacket sets the body rotation to 0 and
Shulker.ShulkerBodyRotationControl.clientTick is empty, so a shulker's
body never turns: it stays square to the block it is fixed to and only
its head turns. The fix (#2311) is already on main, but nothing held it
in place: the entity model harness compares the model's parts and cannot
see the entity's own rotation. This test drives the body rotation
control and the look control and pins both halves of the vanilla's
behaviour - the body stays at 0 and the head still turns to its target
(#1661).

Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 17:45:49 +05:00
levzzz 1f040d06eb Merge remote-tracking branch 'upstream/main' into fix/screen-cursor-centre 2026-10-27 17:45:16 +05:00
Ludovic Marie 4a8cb8c4f9 Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	crates/horizon-client/src/mobs/raider.rs
2026-10-27 13:45:00 +01:00
Ludovic Marie 6ef20dc9af Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	crates/horizon-client/src/game/natural_spawning.rs
2026-10-27 13:44:58 +01:00
Ludovic Marie e219a40ebc Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	crates/horizon-client/src/game/creatures.rs
#	crates/horizon-client/src/game/remote.rs
#	crates/horizon-client/src/mobs.rs
2026-10-27 13:44:55 +01:00
levzzzandminimax-m3.1-flash d13b478ca2 ui: the mouse position is the centre whenever the grab state changes
`MouseHandler.grabMouse` and `MouseHandler.releaseMouse` both put the
mouse at the window's centre, so a screen opened from gameplay starts
with the cursor in the middle. Horizon centred it when the pointer was
released, but not when it was grabbed again, so closing a screen left
the cursor where the screen had it.

The rule is one function now, `mouse_position_after_grab_change`, and the
game applies it on both sides of the grab state change.

Fixes #1925

Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 17:44:55 +05:00
Ludovic Marie 64ca4d485f Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	crates/horizon-client/src/game/creatures.rs
2026-10-27 13:44:53 +01:00
Ludovic Marie 66b9b3323c Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	crates/horizon-client/src/game/remote.rs
2026-10-27 13:44:49 +01:00
Ludovic Marie 24d06f5ce2 Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	crates/horizon-client/src/game/hand.rs
2026-10-27 13:44:48 +01:00
Ludovic Marie 19de683b66 Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	crates/horizon-client/src/game/remote.rs
2026-10-27 13:44:47 +01:00
Ludovic Marie ed8418a2ac Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	crates/horizon-client/src/animate_tick.rs
2026-10-27 13:44:44 +01:00
Ludovic Marie c33a2a5d19 Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	crates/horizon-client/src/lan/hub.rs
2026-10-27 13:44:43 +01:00
Ludovic Marie 9b49a0b727 Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	crates/horizon-client/src/motion.rs
2026-10-27 13:44:42 +01:00
Ludovic Marie 1b42652e96 Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	crates/horizon-client/src/mobs/tame.rs
2026-10-27 13:44:39 +01:00
Ludovic Marie 1400da9637 Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	crates/horizon-client/src/biome.rs
2026-10-27 13:44:35 +01:00
Ludovic Marie 62794a3fde Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	crates/horizon-client/src/game/remote.rs
#	crates/horizon-client/src/game/screens.rs
2026-10-27 13:44:34 +01:00
Ludovic Marie c17da16907 Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	crates/horizon-client/src/save.rs
2026-10-27 13:44:34 +01:00
Ludovic Marie 62c5a6d27b Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	crates/horizon-client/src/game/creatures.rs
#	crates/horizon-client/src/mobs/tame.rs
2026-10-27 13:44:32 +01:00
Ludovic Marie 9a9de6a936 Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	crates/horizon-client/src/mobs/finalize.rs
2026-10-27 13:44:31 +01:00
Ludovic Marie fe9b20733a Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	crates/horizon-client/src/mobs.rs
2026-10-27 13:44:29 +01:00
Ludovic Marie 50f9255472 Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	crates/horizon-client/src/game/commands/execute.rs
2026-10-27 13:44:29 +01:00
Ludovic Marie 70181252d8 Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	crates/horizon-client/src/game/creatures.rs
2026-10-27 13:44:28 +01:00
Ludovic Marie dc3762b533 Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	crates/horizon-client/src/game/initial_spawn.rs
2026-10-27 13:44:26 +01:00
Ludovic Marie 4de85f000d Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	crates/horizon-client/src/game.rs
#	crates/horizon-client/src/game/lan.rs
#	crates/horizon-client/src/game/remote.rs
2026-10-27 13:44:24 +01:00
Ludovic Marie 4de42d0dc4 Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	crates/horizon-client/src/mobs/ai.rs
2026-10-27 13:44:22 +01:00
Ludovic Marie 747c2031f1 Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	crates/horizon-client/src/mobs.rs
2026-10-27 13:44:21 +01:00
Ludovic Marie 14353e67d4 Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	crates/horizon-client/src/game.rs
#	crates/horizon-client/src/game/enchant_effects.rs
2026-10-27 13:44:20 +01:00
Ludovic Marie 8027bda6bf Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	crates/horizon-client/src/game/creatures.rs
#	crates/horizon-world/src/level/redstone.rs
2026-10-27 13:44:19 +01:00
Ludovic MarieandClaude Opus 5.5 0c5e30ae5e Merge main into fix/chat-options-page
main moved the Opt enum to settings.rs (#2781): the three chat options are
added there.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 13:42:12 +01:00
Ludovic Marie b4b80eaeb2 Merge pull request #2361 from kacpherek/fix/immediate-respawn
🤖 Agent: Merged by hand by Ludovic's AI agent (orchestrator) after the contribution gate. rdbtCVS's change request is addressed at this head; a re-review was asked hours ago, with no new objection since. Proven in cg19 union R1 bb29ce68e (union/cg19-r1: main c6c2ec5f9 + 83 PRs) Mac mini: 98 se.
2026-10-27 13:40:48 +01:00
Ludovic Marie 71d0220215 Merge pull request #2332 from kacpherek/fix/stations-unbreakable
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 13:40:16 +01:00
Ludovic Marie c27e3287c5 Merge pull request #2331 from kacpherek/fix/mob-custom-persistence
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 13:40:13 +01:00
Ludovic Marie c326afc7f1 Merge pull request #2327 from kacpherek/fix/thorns-unbreaking
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 13:40:09 +01:00
Ludovic Marie fd6c1e1770 Merge pull request #2323 from kacpherek/fix/warden-disables-shield
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 13:40:03 +01:00
Ludovic Marie 82d36d1f9b Merge pull request #2322 from kacpherek/fix/impaling-aquatic-tag
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 13:39:55 +01:00
Ludovic MarieandClaude Opus 5.5 665c28588e Merge main into fix/sync-mob-variants
main's armadillo scute shedding (#2337) and this branch's armadillo brain both
hooked Armadillo.customServerAiStep: one arm now runs the brain, then the
scute, as vanilla does. creatures.rs keeps PandaSneezed and main's ShedScute.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 13:39:48 +01:00
AndriiandClaude Sonnet 5.5 3e0f4cf653 Sweep attacks hurt nearby mobs and show their arc
Player.doSweepAttack was only a particle and a sound that never fired: the
player's speed field is refreshed by the local movement step only, so it was
0 for the player a server simulates and isSweepAttack could not pass.

- read the movement speed attribute when checking for a sweep
- hurt the other mobs in the target's box inflated by (1, 0.25, 1) and under
  3 blocks away for 1 + sweeping ratio * damage, scaled by the charge, with
  0.4 knockback and the post-attack effects when the hurt lands
- send the arc as a LevelParticles packet so a server's clients draw it
- take its position from Mth's sine table

Fixes #2764

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-27 13:39:33 +01:00
Ludovic Marie a64c4c6562 Merge pull request #2476 from kacpherek/fix/chat-links
🤖 Agent: Merged by hand by Ludovic's AI agent (orchestrator) after the contribution gate. rdbtCVS's change request is addressed at this head; a re-review was asked hours ago, with no new objection since. Proven in cg19 union A3 0c0206714 (main d9b6483aa + 2503 2521 2513 2555 2476) Mac mini 95/.
2026-10-27 13:39:22 +01:00
Ludovic MarieandClaude Opus 5.5 588b2fd116 Merge main into fix/armor-stand-pose
armor_stands.rs: main's armour glint overlay, drawn with this branch's
posed armour layers.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 13:37:12 +01:00
Ludovic MarieandClaude Opus 5.5 7a0610ade8 Merge main into fix/cave-spider-husk-difficulty
mobs.rs MobWorld: main's mob_drops field beside the per-mob difficulty doc.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 13:36:15 +01:00
Ludovic MarieandClaude Opus 5.5 35689a58ba Merge main into fix/dragon-egg-attack
interact.rs attack_block: the dragon egg teleports first, then main's note
block plays its note.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 13:35:42 +01:00
Ludovic MarieandClaude Opus 5.5 79efeab0ce Merge main into fix/dolphin-goals
- goals.rs: main's player_in_range takes the visibility scaling; the
  through-walls check (forNonCombat) keeps it on.
- dolphin.rs: AroundSelf now names its mob and entity event (38).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 13:34:51 +01:00
Ludovic Marie b3fff5222b Merge pull request #2495 from kacpherek/fix/pet-owner-checks
🤖 Agent: Merged by hand by Ludovic's AI agent (orchestrator) after the contribution gate. rdbtCVS's change request is addressed at this head; a re-review was asked hours ago, with no new objection since. Proven in cg19 union A 5fb64f0d6 (main c1ebf5d1b + 50 PRs) Mac mini 352/354 harnesses (fin.
2026-10-27 13:34:15 +01:00
Ludovic Marie 9281a0b5d2 Merge pull request #2491 from kacpherek/fix/1780-exit-portal-rebuild
🤖 Agent: Merged by hand by Ludovic's AI agent (orchestrator) after the contribution gate. rdbtCVS's change request is addressed at this head; a re-review was asked hours ago, with no new objection since. Proven in cg19 union A 5fb64f0d6 (main c1ebf5d1b + 50 PRs) Mac mini 352/354 harnesses (fin.
2026-10-27 13:34:07 +01:00
Ludovic Marie 38588ab7aa Merge pull request #2490 from kacpherek/fix/2485-redstone-ore-step
🤖 Agent: Merged by hand by Ludovic's AI agent (orchestrator) after the contribution gate. rdbtCVS's change request is addressed at this head; a re-review was asked hours ago, with no new objection since. Proven in cg19 union A 5fb64f0d6 (main c1ebf5d1b + 50 PRs) Mac mini 352/354 harnesses (fin.
2026-10-27 13:34:01 +01:00
Ludovic Marie 8521aaac41 Merge pull request #2400 from kacpherek/fix/cat-sit-block
🤖 Agent: Merged by hand by Ludovic's AI agent (orchestrator) after the contribution gate. rdbtCVS's change request is addressed at this head; a re-review was asked hours ago, with no new objection since. Proven in cg19 union R1 bb29ce68e (union/cg19-r1: main c6c2ec5f9 + 83 PRs) Mac mini: 98 se.
2026-10-27 13:33:21 +01:00
Ludovic Marie bdd6b0ea12 Merge pull request #2396 from kacpherek/fix/spectator-crosshair
🤖 Agent: Merged by hand by Ludovic's AI agent (orchestrator) after the contribution gate. rdbtCVS's change request is addressed at this head; a re-review was asked hours ago, with no new objection since. Proven in cg19 union R1 bb29ce68e (union/cg19-r1: main c6c2ec5f9 + 83 PRs) Mac mini: 98 se.
2026-10-27 13:33:16 +01:00
Ludovic Marie c179c8753f Merge pull request #2377 from kacpherek/fix/raider-banner-reachable
🤖 Agent: Merged by hand by Ludovic's AI agent (orchestrator) after the contribution gate. rdbtCVS's change request is addressed at this head; a re-review was asked hours ago, with no new objection since. Proven in cg19 union R1 bb29ce68e (union/cg19-r1: main c6c2ec5f9 + 83 PRs) Mac mini: 98 se.
2026-10-27 13:33:12 +01:00
Ludovic Marie 19abf6182d Merge pull request #2330 from kacpherek/fix/mob-farmland-trample
🤖 Agent: Merged by hand by Ludovic's AI agent (orchestrator) after the contribution gate. rdbtCVS's change request is addressed at this head; a re-review was asked hours ago, with no new objection since. Proven in cg19 union R1 bb29ce68e (union/cg19-r1: main c6c2ec5f9 + 83 PRs) Mac mini: 98 se.
2026-10-27 13:33:08 +01:00
Ludovic Marie f87bbb5342 Merge pull request #2329 from kacpherek/fix/punch-knockback-resistance
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 13:32:19 +01:00
Ludovic Marie 7c7937438e Merge pull request #2321 from kacpherek/fix/bane-slowness
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 13:32:15 +01:00
Ludovic Marie 6207624448 Merge pull request #2311 from kacpherek/fix/1661-shulker-rotation
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 13:32:12 +01:00
Ludovic Marie 4f7a0f2ba1 Merge pull request #2781 from ludovic111/refactor/cut-options-opt
🤖 Agent: move only: the Opt enum (option ids) goes from options_ui.rs to settings.rs (re-exported from options_ui), cutting graphics_ui -> options_ui (step B of the cycle plan). pr_ready --quick green (1323 tests). Queued once pr_ready was green, as the orchestrator asked; merge after verify and parity pass.
2026-10-27 13:32:08 +01:00
asownerytandClaude Opus 5.5 b38fe43b09 Jukebox: keep the disc on the server thread; the off hand never ejects it
JukeboxBlock.useItemOn returns TRY_WITH_EMPTY_HAND when a disc is in, and
only the main hand goes on to useWithoutItem (popOutTheItem). The client now
predicts that the jukebox takes the click, so no off-hand UseItemOn follows
the insert, and the server ignores an off-hand turn on a full jukebox.

Fixes #2775

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 01:30:20 +13:00
Ludovic MarieandClaude Opus 5.5 24800ac54e ci: hand-started release dry runs get their own concurrency group
A dry run publishes nothing, so it can't race a real release; in the shared game-release group a release queued
behind it replaced it as the pending run and GitHub cancelled it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 13:29:22 +01:00
1F10250001andClaude Opus 5.5 66969d8a93 launcher: Simplified Chinese
Adds Simplified Chinese (zh) beside English, French and Russian: the 160 strings
of launcher/lang, written for Horizon, and the language in Settings > Launcher,
the system pick (zh-Hans-CN and other zh locales) and `settings language zh`.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 21:29:17 +09:00
iLostName ea7df1d0dc registries: list the resources' recipe, loot table and template ids once, not at every join 2026-10-27 13:29:06 +01:00
Ludovic MarieandClaude Opus 5.5 66dabf51ee launcher: an opt-in Nightly setting that plays in its own instance
Settings > Updates gets "Nightly builds", off by default: "Reviewed fixes not released yet; may break." On, the
launcher installs the `nightly` pre-release as its own version (nightly-<date>-<commit>), in an instance of its
own ("Nightly"), and plays it; every update check (30 min) installs a newer nightly and drops the older one. The
Horizon instance keeps the release and the player's worlds: choosing it in the sidebar plays the release again,
and turning Nightly off does that too.

Nightly checksums must be signed for the nightly channel ("horizon release nightly"): horizon-release-sign's
signed_digest refuses nightly tags outright, and a nightly's signature never verifies under a release tag nor a
release's under the nightly channel (tests in both crates).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 13:28:17 +01:00
levzzzandminimax-m3.1-flash 92c073de61 Merge upstream/main into fix/end-gateway-entities
Upstream now keeps a gateway's exit in the gateway's own block entity
(TheEndGatewayBlockEntity.setExitPosition), so the player travels to the
exit the gateway was given (#1517). The entity travel stays: the mobs and
the dropped items go through the same gateway, and the exit the first
traveller found is reused, so the player and the entities land in the same
place and only one island is built for a gateway.

Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 17:27:46 +05:00
Ludovic Marie 88aa505533 Merge pull request #2778 from ludovic111/refactor/cut-health-rules
🤖 Agent: move only: HealthRules (and its Default) go from health.rs to world.rs (re-exported from health), cutting world -> health (step B of the cycle plan). pr_ready --quick green (1323 tests). Queued once pr_ready was green, as the orchestrator asked; merge after verify and parity pass.
2026-10-27 13:26:11 +01:00
levzzz 91de744250 Merge remote-tracking branch 'upstream/main' into fix/spawn-egg-baby 2026-10-27 17:24:11 +05:00
levzzzandminimax-m3.1-flash 1654cb51db The spawn egg's offspring is a named, tested rule
The behaviour is in main (#2275): `Mob.checkAndHandleImportantInteractions`
uses a spawn egg of the mob's own kind on a mob, and
`SpawnEggItem.spawnOffspringFromSpawnEgg` spawns the offspring as a baby
of that kind at the parent's position, and one egg less in hand.

The rule now lives in the library, where it is named and tested:
`spawn_egg_is_for` (the egg is for the mob's own kind) and
`spawn_egg_offspring` (the baby, with the parent's variant and colour).

Fixes #2269

Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 17:24:09 +05:00
Ludovic Marie ab080a5407 Merge pull request #2776 from ludovic111/refactor/cut-effect-tables
🤖 Agent: move only: the MobEffects/Potions tables go from brewing.rs to horizon-client-core::potions (re-exported from brewing), cutting effects -> brewing and hud_overlays -> brewing (step B of the cycle plan). pr_ready --quick green (1361 tests). Queued at the orchestrator's request once pr_ready was green; merge after verify and parity pass.
2026-10-27 13:21:06 +01:00
Ludovic MarieandClaude Opus 5.5 be5d7ef703 Merge main into fix/animated-inventory-blocks
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 13:16:10 +01:00
Ludovic MarieandClaude Opus 5.5 ee49ec5071 Merge main into fix/pot-back-face-culling (item_render: ItemQuad keeps cull_back and main's translucent)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 13:15:03 +01:00
Ludovic MarieandClaude Opus 5.5 d89e27113f ci: an opt-in Nightly build from the proven union branches
nightly-build.yml builds, once a night, the newest union/<agent>-<name> branch on origin when it is newer than
main and holds something main lacks (else main), and publishes it as the `nightly` pre-release; the previous one
stays a week as nightly-<date> (tools/ci/nightly.py pick/rotate/prune). Launchers ignore it until a player turns
Nightly on (next PR).

The release builds move unchanged into game-build.yml, a reusable workflow both release.yml and the nightly call
from main's copy. The nightly builds with no secrets and the macOS keychain locked; only the sign job holds the
key, and it runs nothing but main's sign_release.sh over the archives, for the message "horizon release nightly".

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 13:14:32 +01:00
levzzzandminimax-m3.1-flash 139de824aa Re-run the checks after the audit script fix
The pull request's last push did not start a run; this empty commit
starts one on the same tree.

Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 17:10:44 +05:00
Senjin fea8346f23 Merge remote-tracking branch 'origin/main' into hdr-f3 2026-10-27 13:10:29 +01:00
Ludovic MarieandClaude Opus 5.5 e6b92995f3 Merge origin/main into fix/instant-effects-rounding-splash-distance
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 13:10:01 +01:00
Ludovic MarieandClaude Opus 5.5 f08a564fa6 test: give each win_screen test its own temp dir
The helper named its resource root after the pid, the poem flag and the
end text's length, so two tests with the same arguments, running in
parallel, shared it and one removed the other's files (os error 22 in
the_screen_ends_once_everything_rolled_past). A counter makes each call's
directory unique.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 13:08:52 +01:00
Senjin 523dd491c4 Merge remote-tracking branch 'origin/main' into hdr-tiers
# Conflicts:
#	crates/horizon-client/src/entity.wgsl
#	crates/horizon-client/src/gfx.rs
2026-10-27 13:07:04 +01:00
Ludovic Marie 7262304a6d Merge pull request #2773 from ludovic111/feat/sculk-sidebar-card
🤖 Agent: merging the Sculk sidebar card (Ludovic approved the screenshots); fmt, clippy -D warnings and launcher tests pass on the head merged with main.
2026-10-27 13:04:06 +01:00
Ludovic MarieandClaude Opus 5.5 3094896ad1 mob_ai probe: count hits that bring an effect as player hurts
The new AttackPlayerWithEffect hit (husk, bee, cave spider...) still hurts
the oracle's player, so the probe prints its hurt sound.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 13:02:29 +01:00
Ludovic Marie 1d2c5db426 Merge pull request #2769 from ludovic111/refactor/cut-villager-poi
🤖 Agent: verify passed at 84c631a88. Move only: the villager Poi enum goes from mobs/villager_brain.rs to poi_sections.rs (re-exported), cutting world -> mobs (step B of the cycle plan). pr_ready --quick green (1323 tests). Ready to merge once parity is green.
2026-10-27 12:54:54 +01:00
Ludovic MarieandClaude Opus 5.5 964fbe1c29 Merge origin/main into feat/sculk-sidebar-card
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 12:52:56 +01:00
Ludovic Marie 9f0c201851 Merge remote-tracking branch 'origin/main' into refactor/horizon-items 2026-10-27 12:51:52 +01:00
Ludovic MarieandClaude Opus 5.5 6d55c17322 Merge origin/main into fix/crossbow-advancement-triggers
advancements.rs keeps main's levitation and using-item triggers and the PR's
killed-by-arrow trigger.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 12:51:00 +01:00
Ludovic MarieandClaude Opus 5.5 9305cc3439 Merge origin/main into fix/2087-pot-top
entity.wgsl: main's glint_uv takes location 11, so the PR's cull flag moves
to location 12.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 12:49:15 +01:00
levzzzandminimax-m3.1-flash 35581f52e2 Keep the clippy rules happy in the difficulty test
The level is built with the struct update syntax instead of a field
assignment after Default::default(), which clippy's
field_reassign_with_default rejects.

Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 16:46:15 +05:00
Ludovic Marie 6ab1b40ad4 Merge pull request #2252 from kacpherek/fix/wither-rose-kills
🤖 Agent: Merged by hand by Ludovic's AI agent (orchestrator) after the contribution gate. rdbtCVS's change request is addressed; a re-review was asked, with no new objection since. Proven in wave 5's union run (cg21); main merged in since, CI green at this head.
2026-10-27 12:46:09 +01:00
Ludovic Marie cd18488cc2 Merge pull request #2258 from kacpherek/fix/rain-sound-vanilla
🤖 Agent: Merged by hand by Ludovic's AI agent (orchestrator) after the contribution gate. rdbtCVS's change request is addressed; a re-review was asked, with no new objection since. Proven in wave 5's union run (cg21); main merged in since, CI green at this head.
2026-10-27 12:46:06 +01:00
Ludovic Marie 970ee43f8e Merge pull request #2259 from kacpherek/fix/levitation-and-spyglass-triggers
🤖 Agent: Merged by hand by Ludovic's AI agent (orchestrator) after the contribution gate. rdbtCVS's change request is addressed; a re-review was asked, with no new objection since. Proven in wave 5's union run (cg21); main merged in since, CI green at this head.
2026-10-27 12:46:02 +01:00
Ludovic Marie e05847289f Merge pull request #2766 from ludovic111/refactor/cut-saved-proto
🤖 Agent: verify passed at 83bdb31ee. Move only: SavedProto goes from save.rs to chunk.rs (re-exported from save), cutting streaming -> save (step B of the cycle plan). pr_ready --quick green (1323 tests). Ready to merge once parity is green.
2026-10-27 12:45:22 +01:00
Ludovic MarieandClaude Opus 5.5 bc0c0fdf4a refactor: the MobEffects and Potions tables live in horizon-client-core
effects.rs and hud_overlays.rs read the effect and potion tables from
brewing, which made them depend on brewing (and through it on containers
and items), edges that keep the client lib in a single cycle. The tables
have no dependencies, so they move to horizon-client-core; brewing
re-exports them. No behaviour change.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 12:43:11 +01:00
asownerytandClaude Opus 5.5 f105583803 Select World: deleting the last world opens Create New World
WorldSelectionList.handleNewLevels opens a fresh CreateWorldScreen when
the singleplayer list comes back empty, with Cancel going to the title
screen. Horizon went back to an empty list instead.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 00:42:44 +13:00
Ludovic Marie 97437beaff Merge pull request #2296 from masa-masa-0314/fix/enchantment-glint
🤖 Agent: Merged by hand by Ludovic's AI agent (orchestrator) after the contribution gate. rdbtCVS's change request is addressed; a re-review was asked, with no new objection since. Proven in wave 5's union run (cg20 union C 5900ab387, Mac mini); main merged in since, CI green at this head.
2026-10-27 12:40:44 +01:00
Ludovic Marie 869efb87d2 Merge pull request #2130 from Joansitoh/fix/launcher-text-field-edit
🤖 Agent: Merged by hand by Ludovic's AI agent (orchestrator) after the contribution gate. rdbtCVS's change request is addressed; a re-review was asked, with no new objection since. Proven in wave 5's union run (cg20 union C 5900ab387, Mac mini); main merged in since, CI green at this head.
2026-10-27 12:40:40 +01:00
socksproxandDevin (SWE-2 High) c216561daa Allays fly: their travel is travelFlying and their stroll hovers
An allay stood on the ground and fell when the block under it broke:
Mob.travel sent it down the ordinary air travel, which applies the
0.08 gravity attribute whenever noGravity is off. Vanilla's Allay.travel
is travelFlying(input, getSpeed()) — a travel with no gravity term —
so an allay hangs mid-air even before a goal moves it.

- Family::Allay routes to allay::travel, which calls travelFlying with
  the move control's current speed (LivingEntity.speed), like
  Allay.travel(input, getSpeed()); travel_flying is split so the three
  speeds can differ per mob.
- Stroll::flying picked a land position, so the stroll never produced
  a MoveTo mid-air. WaterAvoidingRandomFlyingGoal.getPosition tries
  HoverRandomPos (8, 7, pi/2, 3, 1) on the view direction, then
  AirAndWaterRandomPos (8, 4, -2); Stroll::flying now does the same.
  The wither registers the same vanilla goal and benefits equally.

Verified against the mob_ai probe: an allay spawned at y=-55 stays at
-55, then RandomStrollGoal fires and it drifts to air targets, never
falling — where it used to drop to the ground.

Fixes #2594.

Co-Authored-By: Devin (SWE-2 High) <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-27 12:40:16 +01:00
Ludovic MarieandClaude Opus 5.5 ed01a93df6 Merge main into fix/world-types
main's Superflat Customize screen (#2214) still set the removed WorldOptions.flat: it now picks WorldPreset::Flat,
and the agent's customize-flat state reads the preset. structure_mobs.rs's test passes the new preset argument
(minecraft:normal) to VanillaTerrain::for_dimension.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 12:39:11 +01:00
levzzzandminimax-m3.1-flash 1a56e74324 Fix the f-string in the end cargo audit script
The braces were one too many, so the file did not parse and the Python
tests failed on it. The command it builds is the same now: the item
stack {Item:{id:"minecraft:diamond",count:3}}.

Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 16:35:44 +05:00
Ludovic MarieandClaude Opus 5.5 8187f1ef66 Merge origin/main into fix/construct-beacon-trigger
advancements.rs keeps main's new triggers and the PR's own.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 12:33:29 +01:00
Ludovic MarieandClaude Opus 5.5 05f3355c57 Merge origin/main into fix/enchanted-item-trigger
advancements.rs keeps main's new triggers and the PR's own.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 12:33:27 +01:00
Ludovic MarieandClaude Opus 5.5 2edb5193ef Merge main into fix/wither-rose-kills (combat: keep hive_emptying_blast and explosion_owner; wither: the skull's owner set around main's hive-emptying blast)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 12:32:55 +01:00
Ludovic Marie d9f49ecec0 Merge pull request #2261 from kacpherek/fix/death-and-hurt-effects
🤖 Agent: Merged by hand by Ludovic's AI agent (orchestrator) after the contribution gate. rdbtCVS's change request is addressed at this head; a re-review was asked, with no new objection since. Proven in wave 5's union run (cg21 union 65b0cb252 (72 kacpherek PRs incl. this one) playhorizon 94/95 + Mac mini 33/33, lan_e2e 23/23 both ).
2026-10-27 12:30:46 +01:00
Ludovic Marie 73d0cd36e8 Merge pull request #2257 from kacpherek/fix/container-open-sound-pitch
🤖 Agent: Merged by hand by Ludovic's AI agent (orchestrator) after the contribution gate. rdbtCVS's change request is addressed at this head; a re-review was asked, with no new objection since. Proven in wave 5's union run (cg21 union 65b0cb252 (72 kacpherek PRs incl. this one) playhorizon 94/95 + Mac mini 33/33, lan_e2e 23/23 both ).
2026-10-27 12:30:43 +01:00
Ludovic Marie abda92d574 Merge pull request #2251 from kacpherek/fix/brewed-potion-trigger
🤖 Agent: Merged by hand by Ludovic's AI agent (orchestrator) after the contribution gate. rdbtCVS's change request is addressed at this head; a re-review was asked, with no new objection since. Proven in wave 5's union run (cg21 union 65b0cb252 (72 kacpherek PRs incl. this one) playhorizon 94/95 + Mac mini 33/33, lan_e2e 23/23 both ).
2026-10-27 12:30:39 +01:00
Ludovic MarieandClaude Opus 5.5 cacd39338f Merge origin/main into fix/rain-sound-vanilla
Keeps main's Decreased and Minimal particle options. As in
ClientLevel.tickWeatherEffects, the rain position is set before Minimal's
break, so the rain sound still plays with Minimal particles.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 12:30:36 +01:00
Ludovic Marie d7cd2ed5e8 Merge pull request #2229 from kacpherek/fix/fishing-rod-hooked-trigger
🤖 Agent: Merged by hand by Ludovic's AI agent (orchestrator) after the contribution gate. rdbtCVS's change request is addressed at this head; a re-review was asked, with no new objection since. Proven in wave 5's union run (cg21 union 65b0cb252 (72 kacpherek PRs incl. this one) playhorizon 94/95 + Mac mini 33/33, lan_e2e 23/23 both ).
2026-10-27 12:30:34 +01:00
Ludovic Marie ca63a998cf Merge pull request #2214 from masa-masa-0314/fix/superflat-customize
🤖 Agent: Merged by hand by Ludovic's AI agent (orchestrator) after the contribution gate. rdbtCVS's change request is addressed; a re-review was asked, with no new objection since. Proven in wave 5's union run (cg20 union C 5900ab387, Mac mini); main merged in since, CI green at this head.
2026-10-27 12:28:29 +01:00
Ludovic MarieandClaude Opus 5.5 86b651405a Merge main into fix/levitation-and-spyglass-triggers (advancements.rs: keep the levitation/using_item triggers and main's hurt triggers)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 12:28:06 +01:00
asowneryt e36a480063 Merge remote-tracking branch 'upstream/main' into claude/2697-horse-make-mad-4431aa
# Conflicts:
#	crates/horizon-client/src/game/mounts.rs
2026-10-28 00:27:26 +13:00
Ludovic MarieandClaude Opus 5.5 cf9fb96efa launcher: a vertical Sculk card for the sidebar, readable at any size
The sidebar showed Sculk's wide banner shrunk to 228 px, and its baked-in text could not be read.
It now shows a card made for the sidebar: Sculk's characters and logo cropped from its banner, and
the headline, "Ready in under a minute", a "Get my server" button and the HORIZON15 code drawn by
the launcher, so they stay sharp and follow its language (French and Russian added). Where the
window is too short for the tall card, a compact one puts the characters beside the text. The
Partner label, the 180 s before Hide, the link and the hover lift stay; the Servers page keeps
the wide banner.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 12:26:41 +01:00
Ludovic MarieandClaude Opus 5.5 f90cdb04a5 Merge origin/main into fix/1938-ghast-dismount
Keeps main's mob_meta call (with the game time) and the PR's precise flag.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 12:24:08 +01:00
Ludovic MarieandClaude Opus 5.5 78d4d7ede4 Merge origin/main into fix/crossbow-advancement-triggers
Resolved as in contrib-gate-21's union: both triggers and the damage
predicate kept; the arrow-kill trigger fires after PLAYER_HURT_ENTITY.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 12:23:08 +01:00
levzzzandminimax-m3.1-flash 618d20bd08 The world's difficulty follows the level data
The difficulty is a property of the world: vanilla keeps it in the level
data (`PrimaryLevelData.getDifficulty`/`setDifficulty` over
`Data.difficulty_settings`), so it survives leaving and rejoining the
world and a downloaded world keeps the difficulty it was made with.

Horizon read the difficulty from the level data when the world opened,
but `/difficulty` only changed the game's own copy, so the level data
kept the old value and wrote it back: the world came back on normal.
The level data now follows the game, both when the command changes the
difficulty and when a LAN guest asks the host to change it.

Fixes #2571

Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 16:22:50 +05:00
Ludovic Marie 6850ec1b3a Merge pull request #2528 from kacpherek/fix/server-mob-animation-sync
🤖 Agent: Merged main in (clean) so GitHub sees one merge base; proven in union A2 (union/cg19-a2): clippy, tests, parity, lan_e2e both paths.
2026-10-27 12:22:21 +01:00
Ludovic Marie c2d43643aa Merge pull request #2125 from kacpherek/fix/remote-entity-events
🤖 Agent: Read against the vanilla 26.3 sources (Server players see villager hearts, taming hearts and smoke, spawn poofs and snowball crumbs) and proven in contrib-gate-21's union of 72 kacpherek PRs: 95 parity harnesses on playhorizon and the Mac mini, clippy, 1534 tests, lan_e2e 23 checks in-frame and with --server-thread. Thanks @kacpherek!
2026-10-27 12:22:17 +01:00
Ludovic MarieandClaude Opus 5.5 ab083f9005 Merge main into fix/enchantment-glint
entity.wgsl: the vanilla glint path also takes main's overlay 33 (a translucent item with the glint).
gfx.rs: main's energy swirl pipeline; lightning's offset stays on attribute 7 (6 is the glint UV).
item_render.rs: main's translucent item overlays, chosen with the glint UV the branch adds.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 12:21:19 +01:00
Ludovic Marie 94290b01ee Merge pull request #2275 from kacpherek/fix/spawn-egg-baby
🤖 Agent: Merged by hand by Ludovic's AI agent (orchestrator) after the contribution gate. rdbtCVS's change request is addressed at this head; a re-review was asked, with no new objection since. Proven in wave 5's union run (cg21 union 65b0cb252 (main c1ebf5d1b + 72 kacpherek PRs) playhorizon 94/95, use_item_on fixed (#2126 523280e78).
2026-10-27 12:19:04 +01:00
Ludovic Marie fc6102738c Merge pull request #2266 from kacpherek/fix/tnt-minecart-collision
🤖 Agent: Merged by hand by Ludovic's AI agent (orchestrator) after the contribution gate. rdbtCVS's change request is addressed at this head; a re-review was asked, with no new objection since. Proven in wave 5's union run (cg21 union 65b0cb252 (main c1ebf5d1b + 72 kacpherek PRs) playhorizon 94/95, use_item_on fixed (#2126 523280e78).
2026-10-27 12:19:01 +01:00
Ludovic MarieandClaude Opus 5.5 cbbf6f8b2e Merge main into fix/launcher-text-field-edit (main.rs: keep main's background_error and the LineEdit field)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 12:18:50 +01:00
asownerytandClaude Opus 5.5 985ece811b Stop the menu music when a world loads
Minecraft.setLevel -> updateLevelInEngines stops every sound
(SoundManager.stop), so the title-screen track ends on joining and the
music manager then waits the level's music delay. Horizon only stopped
sounds when leaving a world, so music.menu kept playing in it.

- A server's client (single player's server thread, LAN, multiplayer)
  stops every sound when it makes a new level: on Login and on a
  Respawn into another level. A dimension-changing Respawn also calls
  MusicManager.stopPlaying, as handleRespawn does.
- The in-frame path does the same when the opened world's first player
  is placed.
- Vanilla's Minecraft.player exists from handleLogin on; here it is made
  at the first teleport, so getSituationalMusic counts a logged-in client
  as in the world. Otherwise the menu's 0..10-tick replace delay would
  start the first game track almost at once.
- test_music_parity.py scripts the join: a menu-like track, stopall,
  game music, then a dimension change's stop.

Fixes #2746

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 00:18:31 +13:00
Ludovic Marie f00228e76a Merge pull request #2250 from kacpherek/fix/hopper-minecart-pickup
🤖 Agent: Merged by hand by Ludovic's AI agent (orchestrator) after the contribution gate. rdbtCVS's change request is addressed at this head; a re-review was asked, with no new objection since. Proven in wave 5's union run (cg21 union 65b0cb252 (main c1ebf5d1b + 72 kacpherek PRs) playhorizon 94/95, use_item_on fixed (#2126 523280e78).
2026-10-27 12:18:27 +01:00
Ludovic Marie cad80ba3aa Merge pull request #2248 from kacpherek/fix/stats-combat
🤖 Agent: Merged by hand by Ludovic's AI agent (orchestrator) after the contribution gate. rdbtCVS's change request is addressed at this head; a re-review was asked, with no new objection since. Proven in wave 5's union run (cg21 union 65b0cb252 (main c1ebf5d1b + 72 kacpherek PRs) playhorizon 94/95, use_item_on fixed (#2126 523280e78).
2026-10-27 12:18:23 +01:00
Ludovic MarieandClaude Opus 5.5 f1d14c918f Merge main into fix/superflat-customize (world_loading: keep the flat settings and main's game rules)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 12:18:22 +01:00
Ludovic Marie 7953969510 Merge pull request #2246 from kacpherek/fix/mob-effect-merge
🤖 Agent: Merged by hand by Ludovic's AI agent (orchestrator) after the contribution gate. rdbtCVS's change request is addressed at this head; a re-review was asked, with no new objection since. Proven in wave 5's union run (cg21 union 65b0cb252 (main c1ebf5d1b + 72 kacpherek PRs) playhorizon 94/95, use_item_on fixed (#2126 523280e78).
2026-10-27 12:18:19 +01:00
Ludovic Marie b62c2854ad Merge pull request #2244 from kacpherek/fix/lightning-cleans-copper
🤖 Agent: Merged by hand by Ludovic's AI agent (orchestrator) after the contribution gate. rdbtCVS's change request is addressed at this head; a re-review was asked, with no new objection since. Proven in wave 5's union run (cg21 union 65b0cb252 (main c1ebf5d1b + 72 kacpherek PRs) playhorizon 94/95, use_item_on fixed (#2126 523280e78).
2026-10-27 12:18:16 +01:00
Ludovic Marie 830b841850 Merge pull request #2236 from kacpherek/fix/harvest-full-beehive
🤖 Agent: Merged by hand by Ludovic's AI agent (orchestrator) after the contribution gate. rdbtCVS's change request is addressed at this head; a re-review was asked, with no new objection since. Proven in wave 5's union run (cg21 union 65b0cb252 (main c1ebf5d1b + 72 kacpherek PRs) playhorizon 94/95, use_item_on fixed (#2126 523280e78).
2026-10-27 12:18:12 +01:00
Ludovic Marie ba01b418c2 Merge pull request #2232 from kacpherek/fix/note-block-attack-and-heads
🤖 Agent: Merged by hand by Ludovic's AI agent (orchestrator) after the contribution gate. rdbtCVS's change request is addressed at this head; a re-review was asked, with no new objection since. Proven in wave 5's union run (cg21 union 65b0cb252 (main c1ebf5d1b + 72 kacpherek PRs) playhorizon 94/95, use_item_on fixed (#2126 523280e78).
2026-10-27 12:17:22 +01:00
Ludovic Marie 5a3ea22419 Merge pull request #2230 from kacpherek/fix/player-hurt-entity-trigger
🤖 Agent: Merged by hand by Ludovic's AI agent (orchestrator) after the contribution gate. rdbtCVS's change request is addressed at this head; a re-review was asked, with no new objection since. Proven in wave 5's union run (cg21 union 65b0cb252 (main c1ebf5d1b + 72 kacpherek PRs) playhorizon 94/95, use_item_on fixed (#2126 523280e78).
2026-10-27 12:17:19 +01:00
Ludovic Marie cd43ad9a4a Merge pull request #2226 from kacpherek/fix/armadillo-scutes
🤖 Agent: Merged by hand by Ludovic's AI agent (orchestrator) after the contribution gate. rdbtCVS's change request is addressed at this head; a re-review was asked, with no new objection since. Proven in wave 5's union run (cg21 union 65b0cb252 (main c1ebf5d1b + 72 kacpherek PRs) playhorizon 94/95, use_item_on fixed (#2126 523280e78).
2026-10-27 12:17:15 +01:00
Ludovic Marie 98a11014ea Merge pull request #2223 from kacpherek/fix/invisibility-mob-detection
🤖 Agent: Merged by hand by Ludovic's AI agent (orchestrator) after the contribution gate. rdbtCVS's change request is addressed at this head; a re-review was asked, with no new objection since. Proven in wave 5's union run (cg21 union 65b0cb252 (main c1ebf5d1b + 72 kacpherek PRs) playhorizon 94/95, use_item_on fixed (#2126 523280e78).
2026-10-27 12:17:11 +01:00
Ludovic Marie 467e28f2eb Merge pull request #2220 from kacpherek/fix/explosion-beehive-bees
🤖 Agent: Merged by hand by Ludovic's AI agent (orchestrator) after the contribution gate. rdbtCVS's change request is addressed at this head; a re-review was asked, with no new objection since. Proven in wave 5's union run (cg21 union 65b0cb252 (main c1ebf5d1b + 72 kacpherek PRs) playhorizon 94/95, use_item_on fixed (#2126 523280e78).
2026-10-27 12:17:07 +01:00
levzzzandminimax-m3.1-flash c734c72fc9 The world's difficulty survives leaving and rejoining it
Vanilla keeps the difficulty in the level data: `Data.difficulty_settings`
(`LevelSettings.DifficultySettings`) holds the name of the difficulty, the
hardcore flag and the lock. Horizon read and wrote `Data.Difficulty`, a
byte that 26.3 no longer has, so the world always opened on normal and
`/difficulty` never reached the file: the level data writes the
difficulty settings, and the game patched a key that was not there.

The difficulty now comes from and goes to `difficulty_settings`, and the
level data follows the game, so a world set to hard is hard when it is
opened again and a downloaded world keeps the difficulty it was made
with.

Fixes #2571

Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 16:16:40 +05:00
Ludovic Marie 069a4f7f23 Merge pull request #2128 from kacpherek/fix/remote-hurt-tilt
🤖 Agent: Merged by hand by Ludovic's AI agent (orchestrator) after the contribution gate. rdbtCVS's change request is addressed at this head; a re-review was asked, with no new objection since. Proven in wave 5's union run (cg21 union 65b0cb252 (main c1ebf5d1b + 72 kacpherek PRs) playhorizon 94/95, use_item_on fixed (#2126 523280e78).
2026-10-27 12:16:24 +01:00
Ludovic Marie f78ba8c75f Merge pull request #2123 from kacpherek/fix/remote-leash-knots
🤖 Agent: Merged by hand by Ludovic's AI agent (orchestrator) after the contribution gate. rdbtCVS's change request is addressed at this head; a re-review was asked, with no new objection since. Proven in wave 5's union run (cg21 union 65b0cb252 (main c1ebf5d1b + 72 kacpherek PRs) playhorizon 94/95, use_item_on fixed (#2126 523280e78).
2026-10-27 12:16:20 +01:00
Ludovic Marie fdb763d5e3 Merge pull request #2493 from carlosnunez09/fix/end-gateway-exit
🤖 Agent: Merged by hand by Ludovic's AI agent (orchestrator) after the contribution gate. rdbtCVS's change request is addressed at this head; a re-review was asked, with no new objection since. Proven in wave 5's union run (cg20 union C 5900ab3879 (main b6f18dc8f + 57 PRs) Mac mini 122/122 delta harness).
2026-10-27 12:16:16 +01:00
Ludovic Marie 9aca5c35ca Merge pull request #2414 from mahirsn/fix/sign-editor-update
🤖 Agent: Merged by hand by Ludovic's AI agent (orchestrator) after the contribution gate. rdbtCVS's change request is addressed at this head; a re-review was asked, with no new objection since. Proven in wave 5's union run (cg20 union C 5900ab3879 (main b6f18dc8f + 57 PRs) Mac mini 122/122 delta harness).
2026-10-27 12:16:12 +01:00
Ludovic Marie a16d84d15d Merge pull request #2298 from carlosnunez09/fix/leave-game-statistic
🤖 Agent: Merged by hand by Ludovic's AI agent (orchestrator) after the contribution gate. rdbtCVS's change request is addressed at this head; a re-review was asked, with no new objection since. Proven in wave 5's union run (cg20 union C 5900ab3879 (main b6f18dc8f + 57 PRs) Mac mini 122/122 delta harness).
2026-10-27 12:16:09 +01:00
Ludovic MarieandClaude Opus 5.5 35fddca7e6 refactor: the options screen's Opt enum lives in settings.rs
graphics_ui named options_ui::Opt for its rows, which made it depend on
options_ui, one of the edges that keep the client lib in a single cycle.
The enum (the option ids) moves to settings.rs; options_ui re-exports it
and keeps its methods. No behaviour change.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 12:15:36 +01:00
Ludovic MarieandClaude Opus 5.5 90a3fc82aa refactor: HealthRules lives in world.rs
World holds the health rules but they were defined in health.rs, which made
world depend on health, one of the edges that keep the client lib in a
single cycle. The plain data type (and its Default) moves to world.rs and
health re-exports it. No behaviour change.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 12:15:36 +01:00
Ludovic MarieandClaude Opus 5.5 84c631a88b refactor: the villager Poi enum lives in poi_sections.rs
World's POI sections are typed by mobs::villager_brain::Poi, which made
world depend on mobs, one of the edges that keep the client lib in a single
cycle. The enum moves to poi_sections.rs; villager_brain re-exports it and
keeps its methods. No behaviour change.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 12:15:36 +01:00
Ludovic MarieandClaude Opus 5.5 83bdb31eea refactor: SavedProto lives in chunk.rs
streaming's ChunkGenerator trait named save::SavedProto, which made
streaming depend on save, one of the edges that keep the client lib in a
single cycle. The plain data type moves beside ChunkExtras in chunk.rs and
save re-exports it. No behaviour change.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 12:15:35 +01:00
Ludovic Marie 8d1df095e7 Merge pull request #2521 from kacpherek/feat/energy-swirl
🤖 Agent: Merged by Ludovic's AI agent (Claude, contrib-gate-19). Thanks @kacpherek: Scroll and add the energy swirl of charged creepers and powered withers. I checked it against vanilla 26.3, merged main in, and it passed every harness it reaches in a union with your other PRs.
2026-10-27 12:13:48 +01:00
Ludovic Marie 1ac76ac103 Merge pull request #2503 from kacpherek/fix/particles-option-level
🤖 Agent: Merged by Ludovic's AI agent (Claude, contrib-gate-19). Thanks @kacpherek: Make the Particles option thin all particles, explosion debris and rain splashes. I checked it against vanilla 26.3, merged main in, and it passed every harness it reaches in a union with your other PRs.
2026-10-27 12:13:43 +01:00
Ludovic MarieandClaude Opus 5.5 fff7116b51 Format the chat options or-pattern
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 12:12:33 +01:00
Ludovic MarieandClaude Opus 5.5 ca351542ef Merge origin/main into fix/block-animate-gaps
animate_tick.rs keeps main's poplar leaves ambient sound and the PR's
dripstone drip, both whole.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 12:11:25 +01:00
levzzzandminimax-m3.1-flash fa940d5b6b Add a regression test for the entity draw distance
The view scale introduced by #2317 (Entity.setViewScale in
LevelExtractor.extractVisibleEntities) decides how far an entity is drawn:
Entity.shouldRenderAtSqrDistance multiplies the bounding box's mean edge by
64 and by that scale, so a chicken (0.4 by 0.7 by 0.4) is drawn up to 48
blocks away at the vanilla defaults. Without the scale it vanished at 32
blocks, the distance reported in #2273. The merged fix carried no test, so
the scale formula now lives in a free function and the draw distance of a
chicken, of a broken bounding box and of every render distance is pinned.

Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 16:11:15 +05:00
Claude Codeandminimax-m3.1-flash 25b84dfc2e Format the world preset test
rustfmt keeps the registry read on one line.

Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 16:11:14 +05:00
levzzzandminimax-m3.1-flash a2098c2ffa Let mobs and dropped items through End gateways
Only the player was checked for End gateways, so a cow or a dropped
stack standing in one stayed there for good. The entities now go
through a gateway the way the player does: the block their box swept
through takes them in (unless the gateway is cooling down, as
EndGatewayBlock.entityInside does), they travel on the next tick
(Entity.handlePortal, with the End gateway's transition time of 0) and
land where the gateway leads once its chunks have loaded, keeping their
data and counts.

Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 16:11:14 +05:00
Claude Codeandminimax-m3.1-flash d98dc8edc9 Read the test registry without the explicit deref
clippy's explicit_auto_deref asks for the LazyLock itself, which
derefs to the registry the generator data keeps.

Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 16:11:14 +05:00
Claude Codeandminimax-m3.1-flash bf05176692 Add the amplified, large-biome and single-biome world types
The create screen only offered Default and Superflat, so the amplified
world type of #1873 could not be chosen at all, and the generator
always built the overworld's noise settings.

The world-type button now cycles through the presets vanilla's
WorldPresetTags.NORMAL tag holds (normal, flat, large_biomes, amplified,
single_biome_surface), the chosen preset is written into the new world's
world_gen_settings.dat with the generator
worldgen/world_preset/<preset>.json has, and the generator is built from
that preset, so an amplified world gets the amplified noise settings.
A world opened again reads its preset back from its generator.

Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 16:11:14 +05:00
Ludovic MarieandClaude Opus 5.5 e5ca012ed8 Merge main into fix/server-mob-animation-sync-2
Brings main in so the branch has one merge base with it (GitHub saw a
conflict through the criss-cross bases; the merge itself is clean).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 12:08:06 +01:00
Ludovic MarieandClaude Opus 5.5 4a82c014bb Merge main into fix/server-mob-animation-sync
Brings main in so the branch has one merge base with it (GitHub saw a
conflict through the criss-cross bases; the merge itself is clean).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 12:08:05 +01:00
Eshayat Al-WasiuandCodex bcdbc252fe Load server details when opening from the launcher sidebar
Use the shared open_server path so stopped servers load settings immediately and switching servers clears stale detail state.

Co-Authored-By: Codex (GPT-6.1) <[email protected]>
2026-10-27 17:07:42 +06:00
Ludovic Marie cd4ff3828b Merge pull request #2555 from kacpherek/fix/2546-bubble-column-entities
🤖 Agent: Merged by Ludovic's AI agent (Claude, contrib-gate-19). Thanks @kacpherek: Bubble columns lift and drag mobs, items and ender pearls. I checked it against vanilla 26.3, merged main in, and it passed every harness it reaches in a union with your other PRs.
2026-10-27 12:07:29 +01:00
Ludovic Marie 2a6b5836a9 Merge pull request #2513 from kacpherek/fix/desert-leaves-ambient
🤖 Agent: Merged by Ludovic's AI agent (Claude, contrib-gate-19). Thanks @kacpherek: Play the sand idle and poplar leaves ambient sounds. I checked it against vanilla 26.3, merged main in, and it passed every harness it reaches in a union with your other PRs.
2026-10-27 12:07:25 +01:00
levzzzandminimax-m3.1-flash 4a9bb0ce17 Format the branch with the CI's rustfmt
The CI's rustfmt (stable) writes these signatures on one line; the local
toolchain's rustfmt split them, so the check job failed on the formatting
alone.

Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 16:06:37 +05:00
Ludovic MarieandClaude Opus 5.5 380c40562d Merge main into fix/dragon-kill-credit
main's dragon hurt sound resets ambient_time, so the dragon is borrowed
mutably there.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 12:05:03 +01:00
Ludovic MarieandClaude Opus 5.5 f010708103 Merge main into fix/immediate-respawn
throw_from_eyes takes thrown_from_hand now (main): the cursor stack is
dropped with drop(carried, false).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 12:03:59 +01:00
Ludovic MarieandClaude Opus 5.5 d811847b2d Merge main into fix/chat-options-page
main added Chat Links Prompt as a setting of its own (settings.chat_links_prompt,
read by the server links screen); the option keeps that one instead of a
second, file-backed copy.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 12:03:25 +01:00
iLostName b042426180 lang: opening Options lists the languages without reading every file 2026-10-27 12:00:36 +01:00
Ludovic Marie 9c62aba384 Merge pull request #2729 from ludovic111/refactor/cut-pack-pattern
🤖 Agent: verify passed (after merging main for the rabbit-arm clippy fix). Move only: the pack.mcmeta filter regex goes from packs/pattern.rs to horizon-client-core (re-exported), cutting atlas_sources -> packs (step A). pr_ready --quick green (1352 tests). Ready to merge once parity is green.
2026-10-27 11:59:25 +01:00
Ludovic MarieandClaude Opus 5.5 7d08cdf860 Merge main into fix/mob-ambient-particles
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 11:59:23 +01:00
Ludovic Marie c6c2ec5f9f Merge pull request #2744 from ludovic111/refactor/cut-chunk-extras
🤖 Agent: verify passed. Move only: ChunkExtras and RolledLoot go from save.rs to chunk.rs (re-exported from save), cutting chunk -> save (step A of the cycle plan). pr_ready --quick green (1315 tests). Ready to merge once parity is green.
2026-10-27 11:59:22 +01:00
Ludovic MarieandClaude Opus 5.5 0c58226e9c Merge main into fix/2630-husk-hunger-shield
The bee's poison keeps main's difficulty rule (none on easy) and comes
only with a sting that hurt.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 11:59:21 +01:00
Ludovic MarieandClaude Opus 5.5 01e6dae6f7 Merge main into fix/shears-wear
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 11:58:56 +01:00
Ludovic MarieandClaude Opus 5.5 745a3ef110 Merge main into fix/dolphin-goals
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 11:58:40 +01:00
Ludovic MarieandClaude Opus 5.5 ede89f4d37 Merge main into fix/mace-smash-knockback-players
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 11:58:38 +01:00
Ludovic MarieandClaude Opus 5.5 033d704ac5 Merge main into fix/cat-spawner
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 11:58:25 +01:00
Ludovic MarieandClaude Opus 5.5 eace14a013 rustfmt the step-sound match arms
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 11:58:19 +01:00
Ludovic MarieandClaude Opus 5.5 5adf5c43a2 Merge main into fix/hotbar-pop-bundle-bar
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 11:58:12 +01:00
Ludovic MarieandClaude Opus 5.5 0075879d79 Merge origin/main into fix/axe-frees-copper-golem-statue
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 11:57:01 +01:00
Ludovic MarieandClaude Opus 5.5 1727f95575 Merge origin/main into fix/construct-beacon-trigger
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 11:57:01 +01:00
Ludovic MarieandClaude Opus 5.5 fec49b4482 Merge origin/main into fix/death-and-hurt-effects
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 11:57:01 +01:00
Ludovic MarieandClaude Opus 5.5 86e5db17b5 Merge origin/main into fix/container-open-sound-pitch
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 11:57:01 +01:00
Ludovic MarieandClaude Opus 5.5 73a9345c43 Merge origin/main into fix/bee-flower-effects
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 11:57:00 +01:00
Ludovic MarieandClaude Opus 5.5 0cecbef5c3 Merge origin/main into fix/instant-effects-rounding-splash-distance
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 11:57:00 +01:00
Ludovic MarieandClaude Opus 5.5 9f04b02e12 Merge origin/main into fix/brewed-potion-trigger
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 11:57:00 +01:00
Ludovic MarieandClaude Opus 5.5 4dec228d0c Merge origin/main into fix/enchanted-item-trigger
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 11:56:59 +01:00
Ludovic MarieandClaude Opus 5.5 0239df6f9d Merge origin/main into fix/fishing-rod-hooked-trigger
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 11:56:59 +01:00
Ludovic Marie de1a7b6954 Merge pull request #2482 from levzzz5154/fix/piston-entity-push
🤖 Agent: Merged by hand by Ludovic's AI agent (orchestrator) after the contribution gate. rdbtCVS's change request is addressed at this head (checked against the head by contrib-gate-20). Re-review was asked about 07:30Z with no new objection. Proven in wave 5's union run (main + 54 PRs, 332/332 selected harnesses; lan_e2e 18/18 in-frame and on the server thread). Thank you!
2026-10-27 11:54:19 +01:00
Ludovic Marie bcd92633ac Merge pull request #2708 from ludovic111/refactor/cut-model-library-test
🤖 Agent: verify passed. A test move only: the item_render error-order check goes from model_library's test to item_render's tests, cutting model_library -> item_render (step A of the cycle plan). pr_ready --quick green (1316 tests). Ready to merge.
2026-10-27 11:49:41 +01:00
Ludovic Marie f065d0d11d Merge pull request #2537 from kacpherek/feat/glowing
🤖 Agent: Merged by Ludovic's AI agent (Claude, contrib-gate-19). Thanks @kacpherek: Draw entity outlines for the Glowing effect. I checked it against vanilla 26.3, merged main in, and it passed every harness it reaches in a union with your other PRs, plus lan_e2e in both modes.
2026-10-27 11:49:37 +01:00
Ludovic MarieandClaude Opus 5.5 32a7a4c730 Merge origin/main into fix/death-and-hurt-effects
Resolved as in contrib-gate-21's union of the kacpherek PRs: both sides kept.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 11:48:56 +01:00
Ludovic MarieandClaude Opus 5.5 1b9f8cab58 Merge origin/main into fix/container-open-sound-pitch
Resolved as in contrib-gate-21's union of the kacpherek PRs: both sides kept.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 11:48:54 +01:00
Ludovic MarieandClaude Opus 5.5 9b4b2e2db3 Merge origin/main into fix/axe-frees-copper-golem-statue
Resolved as in contrib-gate-21's union of the kacpherek PRs: both sides kept.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 11:48:52 +01:00
Ludovic MarieandClaude Opus 5.5 d511139408 Merge origin/main into fix/bee-flower-effects
Resolved as in contrib-gate-21's union of the kacpherek PRs: both sides kept.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 11:48:49 +01:00
Ludovic MarieandClaude Opus 5.5 3ec521749b Merge origin/main into fix/instant-effects-rounding-splash-distance
Resolved as in contrib-gate-21's union of the kacpherek PRs: both sides kept.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 11:48:46 +01:00
Ludovic MarieandClaude Opus 5.5 d558a4abf3 Merge origin/main into fix/brewed-potion-trigger
advancements.rs keeps main's trade trigger and the PR's own trigger.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 11:48:36 +01:00
Ludovic MarieandClaude Opus 5.5 27671eac9b Merge origin/main into fix/enchanted-item-trigger
advancements.rs keeps main's trade trigger and the PR's own trigger.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 11:48:34 +01:00
Ludovic MarieandClaude Opus 5.5 f9e11e7302 Merge origin/main into fix/construct-beacon-trigger
advancements.rs keeps main's trade trigger and the PR's own trigger.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 11:48:32 +01:00
Ludovic MarieandClaude Opus 5.5 95000c906a Merge origin/main into fix/fishing-rod-hooked-trigger
advancements.rs keeps main's trade trigger and the PR's fishing rod trigger.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 11:46:59 +01:00
levzzzandminimax-m3.1-flash 186198afe6 Pin the turtle's walk animation to vanilla's rate
The turtle's flailing feet (#1776) were the walk animation running at its
maximum: the generic MoveControl walked the turtle at its full movement
speed (0.25), so LivingEntity.updateWalkAnimation's min(distance * 4, 1)
saturated at 1.0 and TurtleModel swung the legs at the maximum rate and
amplitude. PR #2565 gave the turtle its own pace, but its tests only pin
the forward input the control sets, not the rate of the animation the
reporter saw. This test pins the symptom: the turtle covers 0.18447
blocks a tick and its walk animation runs at 0.73789, vanilla's
min(distance * 4, 1) for that pace.

Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 15:44:33 +05:00
Ludovic Marie 512545ade7 Merge pull request #2554 from kacpherek/fix/2549-mob-water-push
🤖 Agent: Merged by Ludovic's AI agent (Claude, contrib-gate-19). Thanks @kacpherek: Mobs are pushed by flowing water and lava. I checked it against vanilla 26.3, merged main in, and it passed every harness it reaches in a union with your other PRs, plus lan_e2e in both modes.
2026-10-27 11:44:09 +01:00
Ludovic Marie 33afb63acb Merge pull request #2313 from socksprox/fix/player-head-textures
🤖 Agent: Merged by hand by Ludovic's AI agent (orchestrator) after the contribution gate. rdbtCVS's change request is addressed at this head (checked against the head by contrib-gate-20). Re-review was asked about 07:30Z with no new objection. Proven in wave 5's union run (main + 54 PRs, 332/332 selected harnesses; lan_e2e 18/18 in-frame and on the server thread). Thank you!
2026-10-27 11:42:39 +01:00
Ludovic Marie 26b172eb1d Merge remote-tracking branch 'origin/main' into refactor/cut-pack-pattern 2026-10-27 11:42:20 +01:00
levzzz a77639022c Merge remote-tracking branch 'upstream/main' into fix/fire-animated-textures 2026-10-27 15:40:21 +05:00
levzzz cd5216e012 Merge remote-tracking branch 'upstream/main' into fix/flowing-water-mobs 2026-10-27 15:38:53 +05:00
levzzzandminimax-m3.1-flash 786c2482a1 Carry mobs along with a fluid current
A mob standing in flowing water was not carried along by it: the fluid machinery that computes a current (horizon-world's entity_fluid, the port of vanilla's EntityFluidInteraction) was wired to the player, the dropped items, the arrows and the primed TNT, but the mobs use a reduced fluid model of their own and never felt a current.

The mobs now run the same per-tick fluid update as the other bodies (Entity.updateFluidInteraction): the flow of the fluid they stand in is accumulated over the blocks of their box and added to their velocity, with the water scale 0.014 and the lava scale 0.007 (0.00233 without the fast lava dimension attribute), the current of a player being the average of the flow vectors and of any other body its direction.

The new flow-push LAN case puts a mob in a flowing water block and a second one in a pool of still sources, and checks that only the mob in the current is carried along.

The branch also drops the unreachable second "minecraft:rabbit" arm in crates/horizon-client/src/entity_nbt.rs, which clippy rejects upstream.

Fixes #2549

Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 15:38:51 +05:00
Ludovic Marie ac42f11d7a Merge pull request #2280 from kacpherek/fix/elytra-firework-trail
🤖 Agent: Read against the vanilla 26.3 sources (Firework rockets: smooth flight and spark trail on a server) and proven in contrib-gate-21's union of 72 kacpherek PRs: 95 parity harnesses on playhorizon and the Mac mini, clippy, 1534 tests, lan_e2e 23 checks in-frame and with --server-thread. Thanks @kacpherek!
2026-10-27 11:37:49 +01:00
Ludovic Marie 3690dba775 Merge pull request #2260 from kacpherek/fix/dragon-ambient-sound
🤖 Agent: Read against the vanilla 26.3 sources (Give the Ender Dragon its ambient sound) and proven in contrib-gate-21's union of 72 kacpherek PRs: 95 parity harnesses on playhorizon and the Mac mini, clippy, 1534 tests, lan_e2e 23 checks in-frame and with --server-thread. Thanks @kacpherek!
2026-10-27 11:37:45 +01:00
Ludovic Marie 7f01cb4f10 Merge pull request #2255 from kacpherek/fix/camel-ai-speeds
🤖 Agent: Read against the vanilla 26.3 sources (Walk, follow and flee at the camel's real AI speeds) and proven in contrib-gate-21's union of 72 kacpherek PRs: 95 parity harnesses on playhorizon and the Mac mini, clippy, 1534 tests, lan_e2e 23 checks in-frame and with --server-thread. Thanks @kacpherek!
2026-10-27 11:37:42 +01:00
Ludovic MarieandClaude Opus 5.5 3093f8c594 Merge origin/main into fix/2209-flat-world-horizon (picks up main's rabbit-arm fix, #2716)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 11:37:28 +01:00
Ludovic MarieandClaude Opus 5.5 627ab546b9 Merge origin/main into fix/remote-entity-events (picks up main's rabbit-arm fix, #2716)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 11:37:27 +01:00
Ludovic MarieandClaude Opus 5.5 831efabfbd Merge origin/main into fix/step-sound-above (picks up main's rabbit-arm fix, #2716)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 11:37:27 +01:00
Ludovic MarieandClaude Opus 5.5 cae7cc02d3 Merge origin/main into fix/block-animate-gaps (picks up main's rabbit-arm fix, #2716)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 11:37:26 +01:00
Ludovic MarieandClaude Opus 5.5 7d54dc4eaa Merge origin/main into fix/1938-ghast-dismount (picks up main's rabbit-arm fix, #2716)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 11:37:25 +01:00
Ludovic MarieandClaude Opus 5.5 9b98998991 Merge origin/main into fix/2087-pot-top (picks up main's rabbit-arm fix, #2716)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 11:37:24 +01:00
levzzz fcc7a818b4 Merge remote-tracking branch 'upstream/main' into fix/mining-hit-sound 2026-10-27 15:36:16 +05:00
Ludovic MarieandClaude Opus 5.5 751f242824 Merge origin/main into fix/rain-sound-vanilla
The rain sound stays where the PR moved it (tickRainParticles, from the
splash loop's random); main's copy in the tick is dropped. Main's Horizon
Weather gate covers the whole block, so the option still silences the rain.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 11:36:13 +01:00
levzzzandminimax-m3.1-flash a3e581b56f Keep the animated textures on in every graphics preset
The Performance preset used to turn the animated textures off, which froze
the fire, the water, the lava and the portals: players reported a fire with
no animation (#2064). The presets keep them on now, and a test keeps it
that way, so a preset that freezes the animation again fails the build
instead of the report.

Fixes #2064

Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 15:35:06 +05:00
Ludovic Marie 6817cb4af0 Merge pull request #2253 from kacpherek/fix/item-used-on-block-trigger
🤖 Agent: Read against the vanilla 26.3 sources (Fire item_used_on_block for block transforms, waxing, jukebox discs and vault keys) and proven in contrib-gate-21's union of 72 kacpherek PRs: 95 parity harnesses on playhorizon and the Mac mini, clippy, 1534 tests, lan_e2e 23 checks in-frame and with --server-thread. Thanks @kacpherek!
2026-10-27 11:35:01 +01:00
Ludovic Marie c2fb0b6730 Merge pull request #2249 from kacpherek/fix/copper-golem-struck-by-lightning
🤖 Agent: Read against the vanilla 26.3 sources (Lightning takes a copper golem back one oxidation step) and proven in contrib-gate-21's union of 72 kacpherek PRs: 95 parity harnesses on playhorizon and the Mac mini, clippy, 1534 tests, lan_e2e 23 checks in-frame and with --server-thread. Thanks @kacpherek!
2026-10-27 11:34:58 +01:00
Ludovic Marie 1db0eda4ae Merge remote-tracking branch 'origin/main' into refactor/horizon-client-input 2026-10-27 11:33:45 +01:00
iLostName 4c4fcedf40 server list: F5 refreshes it, as in vanilla 2026-10-27 11:30:44 +01:00
Ludovic MarieandClaude Opus 5.5 d4137afb8c Merge origin/main into fix/step-sound-above
Step sounds: main's emission arms (no crouch guard) read the PR's muffled
flag; MotionResources keeps both the step-sound tags and main's crystal tag.
The amethyst chime (Entity.walkingStepSound) now also follows an inside or
combination step, as vanilla plays it after playStepSound whatever block
that sounded.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 11:29:32 +01:00
iLostName ee9d8a656f title: a world open in another instance shows the access failure toast instead of crashing 2026-10-27 11:28:35 +01:00
Ludovic MarieandClaude Opus 5.5 905fd0d193 Merge origin/main into fix/2209-flat-world-horizon
Remote keeps main's use_item field and the PR's public dimension field.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 11:26:21 +01:00
Ludovic MarieandClaude Opus 5.5 6a56f7e39f Merge origin/main into fix/remote-entity-events
remote.rs keeps both the PR's entity-event particles (20, 6/7/12/13/14/18, 3)
and main's drowning event.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 11:25:46 +01:00
iLostName de8dd66ac2 connection: try a server's addresses side by side, 250 ms apart 2026-10-27 11:24:59 +01:00
iLostName 6fc2006447 dns: leave out scoped IPv6 name servers on Windows, as JNDI does 2026-10-27 11:24:59 +01:00
Ludovic MarieandClaude Opus 5.5 ef54b481a7 Merge origin/main into fix/2087-pot-top
Item quads keep both flags: the PR's cull_back (entitySolid back-face culling)
and main's translucent (the translucent item sheet).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 11:23:36 +01:00
Ludovic MarieandClaude Opus 5.5 3a5dd71152 Merge origin/main into fix/1938-ghast-dismount
Keeps both the happy ghast's precise-position flag and main's freeze metadata.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 11:19:03 +01:00
Ludovic MarieandClaude Opus 5.5 2c783d099a Merge origin/main into fix/block-animate-gaps
Keeps both the PR's lever, repeater, dripstone, wither rose, end gateway and
lightning rod animations and main's wet sponge and suspicious sand/gravel.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 11:18:07 +01:00
Ludovic MarieandClaude Opus 5.5 f2070cef54 Merge main into fix/chat-links
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 11:18:01 +01:00
Ludovic MarieandClaude Opus 5.5 648060047c Merge main into fix/particles-option-level
main moved the cloud's client tick into cloud_client_tick; its particles
stay always visible (addAlwaysVisibleParticle).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 11:17:55 +01:00
Ludovic MarieandClaude Opus 5.5 92f33c89fb Merge main into fix/2546-bubble-column-entities
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 11:17:27 +01:00
Ludovic MarieandClaude Opus 5.5 bcb539057d Merge main into fix/desert-leaves-ambient
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 11:17:26 +01:00
Ludovic MarieandClaude Opus 5.5 e2b35bc827 Merge main into feat/energy-swirl
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 11:17:25 +01:00
levzzzandminimax-m3.1-flash 0cac3a9153 Remove the duplicate rabbit arm in entity_nbt
The merge of #2585 left two "minecraft:rabbit" arms in the same match, so
the second one is unreachable and clippy's -D warnings rejects the build
(upstream's own checks job is red on it since 0e793be2a). The first arm
already writes RabbitType and MoreCarrotTicks, so the second one is dead
code; its note about the legacy id moves to the arm that runs.

Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 15:15:54 +05:00
levzzzandminimax-m3.1-flash 7e68f1ed32 Remove the duplicate rabbit arm in entity_nbt
The merge of #2585 left two "minecraft:rabbit" arms in the same match, so
the second one is unreachable and clippy's -D warnings rejects the build
(upstream's own checks job is red on it since 0e793be2a). The first arm
already writes RabbitType and MoreCarrotTicks, so the second one is dead
code; its note about the legacy id moves to the arm that runs.

Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 15:14:59 +05:00
levzzzandminimax-m3.1-flash 9ebccd6a59 Cover the mining sound cadence in the LAN harness
The fix for #2277 landed in #2350: the server's destroy progress level
events (2019 and 2020) play the block's hit sound, so the digger and the
players around hear the same hits. Nothing checked that, though. Add a LAN
check that holds the attack on a stone block and compares the hit sounds
both sides count, and assert in the remote mining fixture that the client
plays the hit sound once, when the dig starts, and not in the loop. On the
tree before #2350 the check fails: the digger counted 20 hits against the 10
the player nearby heard.

Fixes #2277

Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 15:14:04 +05:00
Kacpherek e314c2fa81 Implement the spear's left-click jab and the Lunge enchantment 2026-10-27 11:13:14 +01:00
Ludovic MarieandClaude Opus 5.5 8601d34f20 Merge main into fix/particles-option-level
The shulker's teleport particles (event 2016, from main) take the
engine's own particle level like the other block_teleportation calls.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 11:13:01 +01:00
Ludovic Marie d812712ded Merge remote-tracking branch 'origin/main' into HEAD 2026-10-27 11:12:50 +01:00
Ludovic MarieandClaude Opus 5.5 ce94056986 Merge main into fix/chat-links
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 11:12:06 +01:00
Ludovic MarieandClaude Opus 5.5 0f565f4e4c refactor: biome's spawn-list parsers live in biome.rs
parse_entries, parse_costs and SpawnCost read the biome JSON's
natural_mob_spawns and build biome::SpawnEntry; biome.rs reached into
mobs::spawn_selection for them, one of the edges that keep the client lib
in a single cycle. They move to biome.rs and spawn_selection re-exports
them. No behaviour change.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 11:11:39 +01:00
Ludovic MarieandClaude Opus 5.5 d50a5e1f64 refactor: ChunkExtras and RolledLoot live in chunk.rs
The chunk stores them, but they were defined in save.rs, which made chunk.rs
depend on save, one of the edges that keep the client lib in a single cycle.
They move to chunk.rs and save re-exports them. No behaviour change.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 11:11:39 +01:00
Kacpherek 3c85d2558b Tell a server player's client about its velocity after a mace smash 2026-10-27 11:11:36 +01:00
Ludovic Marie d8cc59dd55 Merge remote-tracking branch 'origin/main' into refactor/horizon-client-input
# Conflicts:
#	crates/horizon-client/src/lib.rs
2026-10-27 11:10:57 +01:00
Kacpherek a05dc35c39 Launch a Riptide trident on a server's client when the use is released 2026-10-27 11:08:49 +01:00
Kacpherek 5d68512994 Keep the body facing forward when a player walks backwards 2026-10-27 11:08:07 +01:00
Kacpherek 377fecbf79 Draw a quad over fully opaque texels without the alpha cutout
A dirt path's side faces skip the clear top row of their texture, but the
cutout still ran on them: mip filtering blended that row into the top edge
and dropped it. Vanilla bakes each quad's layer from the texels under its
UVs, so these quads are solid.
2026-10-27 11:07:53 +01:00
Kacpherek a23a7ec35a Drop the whole stack on Ctrl+Q by the physical Control key only 2026-10-27 11:06:35 +01:00
Kacpherek 50cd5576f9 Clear a flying player's fall distance on the server tick 2026-10-27 11:06:03 +01:00
asowneryt dae67a88cc Merge remote-tracking branch 'upstream/main' into claude/2697-horse-make-mad-4431aa
# Conflicts:
#	crates/horizon-client/src/game/mounts.rs
2026-10-27 22:58:40 +13:00
levzzzandminimax-m3.1-flash 44850bdd55 Merge upstream/main into the portal rider travel branch
The level that is not in place needs both the lookup by dimension (this
branch) and the list of the parked levels (upstream).

Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 14:58:02 +05:00
levzzz 9a84148376 Merge remote-tracking branch 'upstream/main' into fix/bubble-column-entities 2026-10-27 14:57:01 +05:00
levzzzandminimax-m3.1-flash 5678030da2 Carry mobs up and down with a bubble column
A mob standing in a bubble column was not moved by it: the block's
`entityInside` had no counterpart on the entity side, so only the local
player (whose motion code has it) and the arrows were affected. The mobs
now get the same push vanilla gives them in `BubbleColumnBlock.entityInside`
-> `Entity.onAboveBubbleColumn` / `Entity.onInsideBubbleColumn`: the body's
velocity along the vertical axis is raised by 0.1 (0.06 when the block above
the column is solid) and capped at 1.8 (0.7), or lowered by 0.03 and floored
at -0.9 (-0.3) for a column that drags down, and the fall distance is reset
inside a column.

The new `bubble-column` LAN case puts a mob in a soul sand column and a
second one in plain water, and checks that only the mob in the column is
carried up.

Fixes #2546

Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 14:57:01 +05:00
iLostName 758d66bf44 remote: titles, tab list, scoreboard and effects don't carry over to the next server 2026-10-27 10:09:12 +01:00
iLostName b201dafa75 server list: the player count's tooltip lists the status sample 2026-10-27 10:07:15 +01:00
iLostName 7c58bf004f options: the slider test expects saving on release 2026-10-27 10:06:32 +01:00
iLostName 4119284a4f options: sliders apply while dragged and save on release 2026-10-27 09:55:35 +01:00
Ludovic MarieandClaude Opus 5.5 86d90c38a9 refactor: the pack filter regex lives in horizon-client-core
packs/pattern.rs is self-contained; atlas_sources reached into packs for it,
one of the edges that keep the client lib in a single cycle. It moves to
horizon-client-core and packs re-exports it under its old path. No
behaviour change.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 09:34:22 +01:00
SenjinandClaude Opus 5.5 957a3c6622 HDR: show the output on the F3 screen
While the HDR setting is on, F3 shows the swapchain HDR presents to, with
the paper white and peak ("HDR: HDR10 (PQ), 200/500 nits"), or that the
display offers none. With the setting off nothing is added, so the screen
stays as vanilla's.

Part of #1441.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 09:33:26 +01:00
Ludovic MarieandClaude Opus 5.5 ae713f7bd8 refactor: item_render's error-order check lives in item_render's tests
model_library's test ended with a check of ItemRenderer::load_with_library,
which made model_library depend on item_render, one of the edges that keep
the client lib in a single cycle. The check moves to item_render's tests
with its own fixture. No behaviour change.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 09:27:51 +01:00
Ludovic MarieandClaude Opus 5.5 1d018614ac Merge main into fix/sync-mob-variants
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 09:21:40 +01:00
Ludovic Marie 653e3a4ee8 Merge main into feat/panda-states
Co-Authored-By: Claude Opus 5.5 <[email protected]>

# Conflicts:
#	crates/horizon-client/src/mobs/control.rs
2026-10-27 09:21:39 +01:00
Ludovic Marie fd915f8a5d Merge main into feat/fox-states
Co-Authored-By: Claude Opus 5.5 <[email protected]>

# Conflicts:
#	crates/horizon-client/src/mobs/control.rs
2026-10-27 09:21:37 +01:00
Ludovic Marie 0504663c41 Merge main into fix/goat-horns-ram
Co-Authored-By: Claude Opus 5.5 <[email protected]>

# Conflicts:
#	crates/horizon-client/src/game/creatures.rs
2026-10-27 09:21:36 +01:00
Ludovic MarieandClaude Opus 5.5 9203f21b99 Merge main into fix/frog-croak-tongue
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 09:21:25 +01:00
Ludovic MarieandClaude Opus 5.5 903b427006 Merge main into fix/server-mob-animation-sync-2
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 09:15:58 +01:00
Ludovic MarieandClaude Opus 5.5 9ac405e9cc Merge main into fix/armadillo-roll
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 09:15:58 +01:00
Ludovic MarieandClaude Opus 5.5 2ce21900ae Merge main into fix/server-mob-animation-sync
main's ease_peek and onPeekAmountChange; client_tick eases with it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 09:15:57 +01:00
Ludovic MarieandClaude Opus 5.5 03cbb48fa4 Merge main into feat/glowing
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 09:15:25 +01:00
Ludovic MarieandClaude Opus 5.5 42cff7bbc2 Merge main into fix/2549-mob-water-push
main's jumpOutOfFluid (isFree with containsAnyLiquid and the shulker
collision) replaces this branch's is_free.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 09:13:52 +01:00
Ludovic MarieandClaude Opus 5.5 604025a53b Leave the default (temperate) variants out of the pairing data
Pig, cow, chicken, frog and zombie nautilus define DATA_VARIANT_ID as
their registry's TEMPERATE, so vanilla's getNonDefaultValues leaves it
out of the add packet; server_entities caught the extra entry.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 09:10:48 +01:00
SenjinandClaude Opus 5.5 58f2ea700e HDR: light sources shine by the kind of light, not its level
Light level says how far a source lights its surroundings, not how bright
it looks: a candle's flame is as bright as a torch's. Light sources now
get a tier (mesher::hdr_tier): very bright (the sun), bright (glowstone,
campfires, the soul fires), incandescent (flames, lava), low (redstone
torch, magma, a candle's wick) or none (glow lichen). Unlisted ones go by
their level. The tier rides in the emission bits as the level did, and
each has its own share of the headroom and its own lift window
(gfx/hdr.rs TIERS), tuned by eye on an HDR1000 OLED.

Flames, lava drops and obsidian tears now shine too: their tier rides in
the particle vertex's overlay bits, and with HDR on a second draw of the
entities writes only the frame's alpha for them, so their colour still
blends as before.

HORIZON_HDR_DEBUG=1 now greys what isn't lifted and tints what is, so a
lift shows on purple and red light too.

Part of #1441.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 09:07:23 +01:00
Ludovic MarieandClaude Opus 5.5 8648470c47 rustfmt the merged item_data.rs line
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 09:06:54 +01:00
Ludovic MarieandClaude Opus 5.5 eb664d50c1 pr_ready: blocks.rs and items.rs live in horizon-items now
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 09:04:38 +01:00
Ludovic Marie e42ec2360c Merge remote-tracking branch 'origin/main' into refactor/horizon-items
# Conflicts:
#	crates/horizon-items/src/item_data.rs
2026-10-27 09:04:26 +01:00
Senjin 9da81f9293 Merge remote-tracking branch 'origin/main' into hdr-tiers 2026-10-27 09:01:54 +01:00
Ludovic MarieandClaude Opus 5.5 beac63849c refactor: move the client's input modules into horizon-client-input
Key bindings and names, toggle keys, mouse options, controller settings,
the clipboard and window options move to their own crate, re-exported under
their old paths. Their unit tests now rebuild in about a second after an
edit instead of rebuilding the client lib's test binary.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 08:42:36 +01:00
asownerytandClaude Opus 5.5 1758b147b8 Untamed horses rear at a held item instead of taking the rider (#2697)
Horse, AbstractChestedHorse (donkeys, mules, llamas) and ZombieHorse.mobInteract:
with something other than food in hand, an untamed one is made mad (makeMad:
standIfPossible and the angry sound) and the player is not seated; an untamed
skeleton horse ignores the click (SkeletonHorse.mobInteract). The shared
equine::held_item decides it for the game and the mob_ai probe.

makeMad goes through makeSound (0.8 volume, the voice pitch from the mob's
random), with the baby horse's and the zombie horse's own angry sounds.
Donkeys and mules rear too (canPerformRearing is false only for llamas), a
rearing or eating horse runs no goals (AbstractHorse.isImmobile), and
RandomStandGoal plays the ambient sound, as vanilla's getAmbientStandSound.

On a server thread, a horse that bucks (RunAroundLikeCrazyGoal) now throws
off the server player riding it, not only the window's own player.

mob_ai harness: `interact` uses an item on a mob (Mob.mobInteract), with
sword scenarios for horses, donkeys, mules, zombie and skeleton horses and
llamas, foals included.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 20:36:10 +13:00
Ludovic MarieandClaude Opus 5.5 56b6229ef4 Merge the goat ram cooldown fix (#2545) into fix/sync-mob-variants
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 08:35:19 +01:00
Ludovic MarieandClaude Opus 5.5 b7d802d7a6 Merge the goat ram cooldown fix (#2545) into feat/panda-states
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 08:35:12 +01:00
Ludovic MarieandClaude Opus 5.5 cb246e7528 Merge the goat ram cooldown fix (#2545) into feat/fox-states
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 08:35:03 +01:00
Ludovic MarieandClaude Opus 5.5 05d150349f Goats: keep the ram cooldown a brain memory
RAM_COOLDOWN_TICKS is a memory in vanilla (GoatAi.initMemories,
CountDownCooldownTicks), saved with the others; as a field it was
neither saved nor seen by finalize_spawn, which failed on every goat.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 08:34:14 +01:00
Ludovic MarieandClaude Opus 5.5 712306d3b9 Merge main into #2130 (launcher: the args field stays a LineEdit beside main's proxy field)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 07:39:43 +01:00
Ludovic MarieandClaude Opus 5.5 ea5f288a1f Merge #2510 into fix/2546-bubble-column-entities
Both change the same lines; this head carries #2510's, so it merges after it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 07:06:56 +01:00
Ludovic MarieandClaude Opus 5.5 db228099ab Merge #2551 into fix/sync-mob-variants
Both change the same lines; this head carries #2551's, so it merges after it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 07:06:56 +01:00
Ludovic MarieandClaude Opus 5.5 fed7ea2275 Merge #2538 into feat/panda-states
Both change the same lines; this head carries #2538's, so it merges after it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 07:06:56 +01:00
Ludovic MarieandClaude Opus 5.5 9ce9282b21 Merge #2545 into feat/fox-states
Both change the same lines; this head carries #2545's, so it merges after it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 07:06:55 +01:00
Ludovic MarieandClaude Opus 5.5 2da8d6bf28 Merge #2535 into fix/frog-croak-tongue
Both change the same lines; this head carries #2535's, so it merges after it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 07:06:54 +01:00
Ludovic MarieandClaude Opus 5.5 5b435bb03a Merge #2533 into fix/armadillo-roll
Both change the same lines; this head carries #2533's, so it merges after it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 07:06:54 +01:00
Ludovic MarieandClaude Opus 5.5 a44d5c81a2 Merge #2539 into fix/goat-horns-ram
Both change the same lines; this head carries #2539's, so it merges after it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 07:06:54 +01:00
Ludovic MarieandClaude Opus 5.5 101d4689f6 Merge #2528 into fix/server-mob-animation-sync-2
Both change the same lines; this head carries #2528's, so it merges after it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 07:06:53 +01:00
Ludovic MarieandClaude Opus 5.5 c7c890dd69 Merge #2526 into fix/server-mob-animation-sync
Both change the same lines; this head carries #2526's, so it merges after it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 07:06:53 +01:00
Ludovic MarieandClaude Opus 5.5 62b8f33671 Merge #2517 into fix/server-mob-animation-sync
Both change the same lines; this head carries #2517's, so it merges after it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 07:06:53 +01:00
Ludovic MarieandClaude Opus 5.5 48fd9f4ea5 Give the variant entries a match of their own
The fox, panda, frog, axolotl and copper golem have arms for their other
entries in other PRs (#2538, #2551, #2539, #2533): mob_variant_meta keeps the
variants out of mob_meta's match, so neither arm is unreachable.

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

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

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 07:02:05 +01:00
Ludovic MarieandClaude Opus 5.5 0f581c669e Send the axolotl's variant beside its own arm
#2533 gives the axolotl an arm for DATA_PLAYING_DEAD: a second axolotl pattern
here would be unreachable, so the variant goes before the match.

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

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

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

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 06:57:29 +01:00
Ludovic MarieandClaude Opus 5.5 f4271d7c16 Merge #2492 into the energy swirl branch
#2492 takes overlay code 32 for translucent item quads (33 with glint): the
energy swirl moves to 34 and is the fifth of eight entity materials.

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

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

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

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 06:55:21 +01:00
Ludovic MarieandClaude Opus 5.5 78dba05d8f Merge #2516 into fix/desert-leaves-ambient
Both change Animate::Falling: suspicious sand and gravel take the new flag
false (BrushableBlock.animateTick drops dust only; the desert sound is
SandBlock's).

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

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 06:54:15 +01:00
Ludovic Marie 0e927e6f11 Merge commit 'refs/cg19/pr/2516' into HEAD 2026-10-27 06:54:01 +01:00
levzzz 2eb96ddb7c Merge remote-tracking branch 'upstream/main' into fix/piston-entity-push 2026-10-27 10:53:49 +05:00
Ludovic MarieandClaude Opus 5.5 97dd6d982a Leave the fox's type and the panda's genes to #2538 and #2551
Those PRs sync the fox's DATA_TYPE_ID with its flags and the panda's genes with
its counters and flags; the same arms here would be unreachable once both land.

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

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

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

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 06:51:37 +01:00
Ludovic MarieandClaude Opus 5.5 bbc1ffb408 Merge main into fix/invisibility-mob-detection
Keeps both PlayerView methods: visibility_percent and main's has_effect.

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

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 06:46:09 +01:00
Kacpherek fc06585938 Send mob particles to clients the way vanilla does 2026-10-27 06:45:32 +01:00
Kacpherek e5e1b74fd8 Draw the enderman portal trail, phantom wing dust and bee nectar drips 2026-10-27 06:44:37 +01:00
Ludovic MarieandClaude Opus 5.5 9809bf38b6 Merge main into fix/remote-hurt-tilt
indicateDamage now uses main's knockback direction, which is zero when
the hit has no source position (as in dealDefaultKnockback).

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

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 06:42:22 +01:00
Ludovic MarieandClaude Opus 5.5 594394ab30 Merge main into fix/goat-horns-ram
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 06:41:31 +01:00
Ludovic MarieandClaude Opus 5.5 c3ef0a06e7 Merge main into fix/frog-croak-tongue
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 06:41:15 +01:00
Ludovic MarieandClaude Opus 5.5 531212c812 Merge main into fix/armadillo-roll
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 06:40:54 +01:00
Ludovic MarieandClaude Opus 5.5 2e9827a259 Merge main into fix/server-mob-animation-sync-2
The strider's DATA_SUFFOCATING sync is left to #2526, which sends and reads
index 19 already (lan.rs mob_freeze_meta, remote.rs), so both don't send it twice.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 06:40:37 +01:00
Ludovic Marie f8582fb678 Merge #2296 (enchantment glint)
# Conflicts:
#	crates/horizon-client/src/entity.wgsl
2026-10-27 06:40:21 +01:00
Ludovic Marie 9777b525b7 Merge #2296 (enchantment glint)
# Conflicts:
#	crates/horizon-client/src/hud.rs
2026-10-27 06:40:20 +01:00
Kacpherek 97d119b4e8 Loop the bee buzz and the guardian attack hum like vanilla 2026-10-27 06:39:31 +01:00
Kacpherek 2f6d259615 Put books into a chiseled bookshelf and take them out
Horizon only kept a chiseled bookshelf's saved data, so using it did
nothing. It is now a block entity with six slots and lastInteractedSlot:

- ChiseledBookShelfBlock.useItemOn / useWithoutItem: the slot comes from
  the hit position on the front face (getHitSlot, 3x2). A #bookshelf_books
  item goes into an empty slot (one book, the insert sounds, the used
  statistic), a book comes out to the inventory or the ground, with the
  pickup sounds. The slot_N_occupied states follow
  (ChiseledBookShelfBlockEntity.updateState) and reach a server's clients
  with the block update; the client's prediction ends the click the same way.
- Hoppers and droppers use its container: books only, one to a slot, and the
  occupied state and last slot follow what they moved.
- The comparator reads lastInteractedSlot from the block entity.
2026-10-27 06:38:17 +01:00
Ludovic MarieandClaude Opus 5.5 6c769f38ae Merge main into fix/server-mob-animation-sync
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 06:37:58 +01:00
Ludovic MarieandClaude Opus 5.5 475f64b79d Merge main into fix/end-gateway-exit
block_entity_nbt moved into horizon-client-core (crate split): save.rs
reads gateway_exit across the crate boundary, so it is public. The
generated block entities now load through decode_block_entity, which
reads the gateway's exit with this PR's decode_end_gateway.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 06:37:09 +01:00
Kacpherek bca4513187 Keep withers out of Peaceful, raiders out of the spawn count, and use local difficulty for skeleton traps 2026-10-27 06:37:08 +01:00
Kacpherek 28b72d46c5 Give turtles vanilla's goals: panic, tempt, go to water, travel and stroll 2026-10-27 06:36:15 +01:00
Kacpherek cece5aca30 Send more mob state to the single player window
A client of a server only draws what the server sends. Send and read the
synched data for sleeping villagers (pose and bed), horses grazing and
rearing, invisible mobs, a carried enderman block, swimming drowned, zombie
villager cures, the villager head shake, zombie drowning conversion,
hoglin and piglin conversion, charged blazes, turtle eggs, baby zoglins and
the wither's side head targets.

A wither's side heads also need a line of sight to shoot, as in vanilla.
2026-10-27 06:36:13 +01:00
Kacpherek 713de31f64 Slow mobs in cobwebs and make magma cubes jump out of lava 2026-10-27 06:36:04 +01:00
Kacpherek e2a48d1c36 Implement the spear's use action: charge, stab, knockback, dismount 2026-10-27 06:36:00 +01:00
Kacpherek b22c42ea85 Stop jockey spiders attacking and shove hoglin targets like vanilla 2026-10-27 06:34:37 +01:00
Ludovic MarieandClaude Opus 5.5 a21cf57a11 Merge main into fix/desert-leaves-ambient
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 06:34:01 +01:00
Kacpherek 9142f57bc8 Add the spectator menu with Teleport to Player and Teleport to Team Member 2026-10-27 06:33:52 +01:00
Kacpherek 6c36da40e0 Turn skeletons into strays after standing in powder snow 2026-10-27 06:33:50 +01:00
Kacpherek 306c6625e9 Shear snow golems and bogged by hand and use vanilla's snowball spread 2026-10-27 06:33:07 +01:00
Kacpherek 46f674f842 Give dolphins swim-with-player, jump and treasure goals and fish feeding 2026-10-27 06:32:31 +01:00
Kacpherek dca3bb89fe Show the air bubbles only when the eyes are below the water surface 2026-10-27 06:32:01 +01:00
Kacpherek e74a1fe461 Use every player in the level for patrols, sniffers, allays, trap horses and other world checks on the server thread 2026-10-27 06:31:46 +01:00
Kacpherek 581828bf94 Show the smithing template lines in item tooltips 2026-10-27 06:30:47 +01:00
Kacpherek 2c0d3c1a86 Chat wheel counts right Shift, durability line is translated 2026-10-27 06:30:46 +01:00
Ludovic MarieandClaude Opus 5.5 4940a24423 Send the fox's type and flags at indices 18 and 19
Fox extends Animal, and AgeableMob has two entries in 26.3 (DATA_BABY_ID 16,
AGE_LOCKED 17): DATA_TYPE_ID is 18 and DATA_FLAGS_ID 19. An int at 17 is the
wrong type for AGE_LOCKED's boolean on a vanilla client.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 06:30:20 +01:00
Kacpherek 62556429dc Add the four missing options to the Chat page 2026-10-27 06:30:09 +01:00
levzzzandminimax-m3.1-flash 33e81f464e Keep a rider with the vehicle it rides through a portal
A server player who rode a horse, a boat or a minecart into a Nether
portal arrived in the other dimension on foot: the vehicle went through
the portal on its own (as an entity, with a new entity id) while the
player travelled alone, and the ride was broken. Vanilla carries the
passengers with the vehicle (`Entity.teleportCrossDimension` recreates
the vehicle in the other dimension and the passengers ride it again),
and a passenger does not use a portal on its own (`Entity.canUsePortal`,
`NetherPortalBlock.entityInside`). The vehicle a server player rides
now travels with it: it is left out of the entity portal path, and it
moves to the destination level with the player when the trip arrives,
keeping its entity id (and its UUID and the compound it was loaded
with), so the ride is never broken. It stands where its rider arrives
and does not go back through the exit portal for a while
(`Entity.setPortalCooldown`).

Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 10:30:03 +05:00
Ludovic MarieandClaude Opus 5.5 765cef8a4e Put the candle's doc comment back on candle
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 06:29:56 +01:00
Kacpherek 29616ad502 Stack toasts in five slots and hide them with F1 2026-10-27 06:29:44 +01:00
Ludovic MarieandClaude Opus 5.5 79fca90886 Tame a pet to the player's real UUID, not an unset placeholder
acting_uuid() falls back to [0, 0, 0, 0] while level.player_uuid is unset (a new
world before its first save); the owner saved then never matches the UUID the
world gets, so the player could no longer command the pet after a reload.
acting_player_uuid() assigns the world's player UUID first, as dropped items do.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 06:29:42 +01:00
Kacpherek f918fc711a Keep the command history between runs and show the restricted chat prompt 2026-10-27 06:28:21 +01:00
Kacpherek cc0f8f8a50 Track a vehicle as far as its widest passenger 2026-10-27 06:27:49 +01:00
Kacpherek eeceb9c2de Let a fishing hook catch and pull other players 2026-10-27 06:27:40 +01:00
Kacpherek c4427d2a14 Size the mount hearts from the server's max health and fix the jump bar 2026-10-27 06:27:34 +01:00
Kacpherek 2a1132387f Swing the right arm and for the right time on other players 2026-10-27 06:26:53 +01:00
Kacpherek ca83704cbb Add zombie village sieges 2026-10-27 06:26:12 +01:00
Kacpherek 180e2f612a Chat lines: server message bar and tooltip, item hover, run_command clicks, Shift insertion 2026-10-27 06:26:12 +01:00
Kacpherek f2db04bd64 Translate the sleep messages and send the respawn point message only when it changes 2026-10-27 06:26:07 +01:00
Kacpherek 7b84a7ae91 Let a sweep attack hurt and push back the other players next to the target 2026-10-27 06:25:43 +01:00
Kacpherek ffad9e1f0a Keep the styling of the death message on the death screen 2026-10-27 06:25:21 +01:00
Kacpherek 1131a2910d Apply Haste and Mining Fatigue to the attack speed 2026-10-27 06:24:43 +01:00
Kacpherek 18217532af Show other players gliding, swimming, spinning and holding a loaded crossbow 2026-10-27 06:24:15 +01:00
Kacpherek 6d69bba15e Remote mining test fixture: the held-item key and the attack strength reset 2026-10-27 06:24:05 +01:00
Kacpherek 71b2f110eb Show the account name on the book signing screen 2026-10-27 06:23:59 +01:00
Kacpherek b6c77e12f3 Hide invisible guests in the host window 2026-10-27 06:23:43 +01:00
Kacpherek 5bbb61aec2 Angry endermen open their mouth on a server thread 2026-10-27 06:23:23 +01:00
Kacpherek 1f98f3ca89 Open the Leave Bed chat screen while the player sleeps 2026-10-27 06:23:22 +01:00
Kacpherek 0cd3410a2d Squeeze hotbar slots that gain items and draw the bundle fullness bar 2026-10-27 06:23:01 +01:00
Ludovic MarieandClaude Opus 5.5 03d8ffe2c6 Sign editor: send the lines as edited, sign or not
AbstractSignEditScreen.setMessage writes messages[line] and the sign;
removed() sends messages. The editor keeps its lines up to date, so a
sign removed while it is open still gets the player's text (review by
rdbt's agent).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 06:22:28 +01:00
Kacpherek 4afb5748f2 Let a player's axe hit disable the victim's shield 2026-10-27 06:22:13 +01:00
Kacpherek 6b1d376e6f Show the sleeping players count on the action bar of a LAN host or server 2026-10-27 06:22:11 +01:00
Kacpherek 008c4f91cd Treat single player as a local server for the tab list and F3 charts 2026-10-27 06:21:37 +01:00
Kacpherek 85f34393fe Save and load creative toolbars with the toolbar keys 2026-10-27 06:21:01 +01:00
Kacpherek 59bdb00599 Husk, cave spider, wither skeleton, bee and pufferfish effects only come when the hit lands 2026-10-27 06:19:41 +01:00
Kacpherek 29ab66935e Restart a break when the held item changes and reset the attack strength when it stops 2026-10-27 06:17:48 +01:00
Kacpherek 80fd68e1c2 Send the dragon's breath cloud particle to the client 2026-10-27 06:16:45 +01:00
Kacpherek f92cf816b1 Pick blocks and entities on the server like vanilla 2026-10-27 06:16:32 +01:00
Kacpherek ff28fd8390 Read a chiseled bookshelf's last slot and a shelf's back mask with a comparator 2026-10-27 06:13:40 +01:00
Kacpherek 0c9bddecda Let block hoppers move items into and out of minecart containers 2026-10-27 06:11:01 +01:00
Kacpherek a19cad22cb A mob jumps out of a fluid only where Entity.isFree allows: no liquid in the box 2026-10-27 05:51:10 +01:00
levzzz 0ee20da35c Merge remote-tracking branch 'upstream/main' into fix/pot-back-face-culling 2026-10-27 09:49:34 +05:00
levzzzandminimax-m3.1-flash b7749dd0d2 Fix a needless borrow in the decorated pot's test
Clippy's needless_borrow: the registry is already a reference, so the
extra & is dereferenced right away.

Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 09:38:03 +05:00
masa-masa-0314andZCode 18b544770f Inventory: frame strips by the mcmeta's frame size
`decode_one` cropped an animated block or item strip as `h > w -> w x w`,
ignoring the mcmeta's `animation.width`/`height`: a pack texture that
names its frame dimensions, or lays its frames along a row, packed the
whole image as "frame 0" and was then drawn as a square frame over a
rect sized for the image. The first frame now comes from
`AnimationMetadataSection.frameSize`, resampled to its longer side the
way the world's sprite arrays keep a frame square, and the sprite's GUI
size is that square too.

The atlas also gains the regression test the review asked for: a
synthetic two-frame block strip whose mcmeta starts on the second frame
registers in `sprite_animations`, and the atlas holds that frame at the
sprite's rect; a row strip with explicit frame dimensions packs only its
left frame.

Co-Authored-By: ZCode <[email protected]>
2026-10-27 12:17:54 +08:00
Ludovic MarieandClaude Opus 5.5 03cc30ae45 Merge origin/main into refactor/horizon-items
entity_nbt keeps the name_text re-export (main's body is the same as
horizon-client-core's) and takes main's new spawn_bonus. Main's edits to
blocks, bundle, enchant, item_data and items land in horizon-items.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 04:28:06 +01:00
FusselTVandClaude Opus 5.5 3788dff6a1 Worldgen: the surface step skips writes that change nothing
Most of the Nether's surface rule ends in netherrack, written over the
netherrack the noise step placed: every solid block paid for a section
write, two state table reads, four heightmap updates and a fluid check
for nothing. Column::set_block now leaves a block that already holds the
state (a fluid is still marked for post-processing, as every write marks
it).

chunkgen_bench, 576 chunks, 1 thread: Nether surface 1.81 -> 1.52 ms per
chunk; the terrain snapshots of seeds 12345 and 1 (Nether and Overworld)
are byte for byte the same.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 03:54:40 +01:00
Ludovic MarieandClaude Opus 5.5 80e28dfe63 creative_mining harness: items.rs now lives in horizon-items
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 03:49:17 +01:00
Ludovic MarieandClaude Opus 5.5 27f73e1db3 Merge origin/main into refactor/horizon-items
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 03:49:17 +01:00
Ludovic MarieandClaude Opus 5.5 36c18fd8ea Merge main into fix branch (resolves the conflict with main)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 03:38:30 +01:00
Ludovic MarieandClaude Opus 5.5 75bcbce441 Merge origin/main into refactor/horizon-items
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 02:27:09 +01:00
Ludovic MarieandClaude Opus 5.5 c352c80c96 Books: save a plain page as a string, as ComponentSerialization does
Pages kept as components for their click events wrote a plain text page as
{text:"p"}; vanilla's codec writes a component with no style or siblings
as its string (the hotbar harness).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 02:11:32 +01:00
Ludovic MarieandClaude Opus 5.5 2f489f54c5 Merge origin/main (with #2160) into refactor/horizon-items
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 01:38:11 +01:00
levzzzandminimax-m3.1-flash 9ab0e2b6a8 Cover the slime block's push and bounce in the LAN harness
A piston pushing a slime block moves the body in front of it and bounces
it: the block's swept collision shape pushes the body out of the way, and
the slime block sets the body's velocity along the push axis
(`PistonMovingBlockEntity.moveCollidedEntities`, the `causeBounce` branch).
The new `piston-slime` LAN case checks both for the host and the guest, with
a plain block as the control: a plain block only pushes the body 0.81
blocks, the slime block pushes and bounces it 2.5.

Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 05:08:40 +05:00
levzzzandminimax-m3.1-flash 7250944a7c Test the decorated pot item's back-face culling too
The pot in the hand and dropped on the ground is drawn by the same
`DecoratedPotRenderer` model, through the item path, so its quads carry
the cull as well. The test fails without it.

Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 05:06:20 +05:00
Joansitoh 9458e6e99e Launcher: every text field uses the shared LineEdit
Join a friend, the mods search and the Servers fields (create, players,
console, MOTD, port, token, email) had their own String and their own
key handling, so none of them could move the caret, select or undo.
They use LineEdit now, and type_key and clip, which only they used, go.
2026-10-27 01:00:11 +01:00
Joansitoh 9cebcc8d7f Launcher: a paste keeps the spaces at its ends
Pasting " --world Test" after an argument lost its leading space and
joined the two arguments. The first line of the clipboard is inserted as
it is.
2026-10-27 00:59:55 +01:00
levzzzandminimax-m3.1-flash ac33fb74ec Add the decorated pot's back-face culling regression test
`DecoratedPotRenderer` submits every part with `RenderTypes.entitySolid`,
whose pipeline culls back faces. The pot's `top` and `bottom` are flat
boxes, so without that cull both sides of each plane are drawn and they
z-fight: the pot renders wrong (kacpherek's #2099 draws the cull).

The test renders a plain pot and checks that all of vanilla's 28 submitted
quads are kept and that every one of them is marked for the cull, so the
behaviour cannot silently regress. It fails without the cull.

Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 04:57:27 +05:00
levzzz 5ac1c82eb9 Merge kacpherek's PR #2099: the decorated pot culls its back faces
The decorated pot's model parts are submitted with RenderTypes.entitySolid,
whose pipeline culls back faces; the pot's top and bottom planes are flat
boxes, so without the cull both sides of each plane are drawn and they
z-fight. This keeps all of vanilla's 28 submitted quads and culls the back
faces, like entitySolid does.
2026-10-27 04:36:29 +05:00
levzzzandminimax-m3.1-flash 6384234e37 Stop a piston push at the world border
`move_pushed` resolved the pushed movement with `SceneCollisions`, which
supplies no world-border collision, so a piston next to the border could push
a player through it even though ordinary movement stops at the border.
`Entity.move` collects the border's walls in
`collectCollidersIgnoringWorldBorder` whenever
`WorldBorder.isInsideCloseToBorder` holds, and `CollisionGetter.borderCollision`
adds `WorldBorder.getCollisionShape` to them. The push now resolves through
`EntityCollisions::with_border`, which applies that same rule to the pushed
body's own x and z, so a body pushed into the border's wall stops there.

Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 04:32:16 +05:00
Carlos Nunez 3caf28c4d6 Merge remote-tracking branch 'upstream/main' into tmp/merge-2298
# Conflicts:
#	tools/lan_e2e.py
2026-10-26 19:27:09 -04:00
Kacpherek 2c894783aa Bubble columns lift and drag mobs, items and ender pearls 2026-10-26 23:37:14 +01:00
Kacpherek 9266e0c7c9 Mobs are pushed by flowing water and lava 2026-10-26 23:34:03 +01:00
Kacpherek 909f15b909 Count the warden tendril and heart animations down again 2026-10-26 23:22:03 +01:00
Kacpherek da57350af5 Pandas: genes, sit, roll, lie on back, sneeze and unhappy states, synced to clients
A panda now has vanilla's states: it sits to eat bamboo or cake it picks up or is
given, a lazy one lies on its back, a playful one or a cub rolls over, a cub
sneezes, and a panda that is fed with no bamboo around sulks. A worried panda
sits and shivers in a thunderstorm. The genes show in its coat, pass to its
young, and are saved. The flags, counters and genes go to clients as entity
data, and the model, the item in its mouth, the crumbs and the sneeze are drawn
from them, in game and in a LAN guest's window.
2026-10-26 23:19:43 +01:00
masa-masa-0314andZCode 66607c9eab Items: glint enchanted armour on armour stands
ArmorStandRenderer draws worn armour through the same
HumanoidArmorLayer as a living entity, so its pieces should use the
armour glint when ItemStack.hasFoil() is true. The stand's equipment
loop now checks the stack's enchantment_glint_override component, then
the item's default override, then its enchantments component (vanilla's
Item.isFoil), and draws both the armour and its trim layer with overlay
13. The existing ARMOR_GLINT path supplies the model UV and samples
enchanted_glint_armor through ARMOR_ENTITY_GLINT_TEXTURING.

Co-Authored-By: ZCode <[email protected]>
2026-10-27 06:14:36 +08:00
Kacpherek 400d9e6115 Show goats' lost horns and let them lower their head and ram 2026-10-26 23:11:56 +01:00
Kacpherek 140958fa58 Sync the variants and looks of server mobs to the window
The window's mobs are built only from SetEntityData, but mob_meta sent
only some species' data and remote_mob_data decoded even less. Send and
read the colour, type and state entries that the renderers draw from.
2026-10-26 23:07:25 +01:00
Kacpherek 04b0b9aed5 Let frogs croak, shoot their tongue at small slimes and idle-swim 2026-10-26 23:06:30 +01:00
Kacpherek 5e27fd3421 Use the mob level event that main already has 2026-10-26 23:04:35 +01:00
Kacpherek e544924504 Draw entity outlines for the Glowing effect
Glowing entities are drawn as team-coloured silhouettes into an outline
target with no depth test, then vanilla's entity_outline chain (sobel,
box blur across and up) is blended over the frame after the hand.
Mobs, players, item entities and projectiles get outlines. The server
sends shared flag 6 for mobs and items, and the remote client reads it.
2026-10-26 23:03:39 +01:00
Kacpherek 29754b857d Merge origin/main into feat/fox-states 2026-10-26 23:02:27 +01:00
Kacpherek 8144385ce6 Give foxes their stalk, pounce, faceplant, shelter and perch states
The fox flags were only ever set for sleeping. Add the goals that drive
the rest of Fox.DATA_FLAGS_ID and show them on the client:

- StalkPreyGoal, FoxPounceGoal, FaceplantGoal: creep up on a chicken or
  rabbit, crouch, leap with the flight tilt and bite, or end up with the
  nose in snow (with its block crumbs).
- SeekShelterGoal and PerchAndSearchGoal.
- The fox's own conditions on the shared goals (melee, look at player) and
  clearStates on float, breed and follow parent; FoxMoveControl and
  FoxLookControl waits.
- The head tilt and crouch amount ease on the client from the synched
  flags, and the model, body tilt and sleeping, snow and baby textures
  follow them.
2026-10-26 23:02:03 +01:00
Kacpherek 11e1ff12d0 Let armadillos roll up when scared, peek out and take less damage as a ball 2026-10-26 22:59:54 +01:00
Kacpherek 25c53cc803 Server-thread clients animate ravager, hoglin, iron golem, creaking, vex, allay, axolotl, guardian, copper golem and strider
Builds on the warden, sniffer, breeze, camel and shulker sync. Each of these mobs
now sends its synched data and entity events, and the client runs the animation
steps vanilla runs on the client.
2026-10-26 22:56:31 +01:00
masa-masa-0314andZCode 43e407562f Items: animate the armour glint from enchanted_glint_armor
Enchanted worn armour lost its animation: mobs/render.rs gave every
armour vertex [0, 0] for glint_uv, while the entity shader samples the
glint at that coordinate for overlay 7, so the armour showed a single
texel instead of the scrolling pattern.

Armour layers now carry overlay 13 (ARMOR_GLINT) and the layer's model
UV. entity.wgsl applies ARMOR_ENTITY_GLINT_TEXTURING (scale 0.16) and
samples misc/enchanted_glint_armor through the same repeating, linear
GlintSampler as items, matching ARMOR_CUTOUT_NO_CULL_GLINT. The scroll
clock (millis * glintSpeed * 8 modulo the offsets' common period) rides
in globals.time.w, so armour and items move together.

Addresses the review comment on #2296.

Co-Authored-By: ZCode <[email protected]>
2026-10-27 05:54:10 +08:00
Kacpherek a45d0de43e Server-thread clients animate warden, sniffer, breeze, camel and shulker
A client of a server (single player's window included) never got the
animation state these mobs compute on the server: no pose, no sniffer
state, no shulker peek or wall face, no camel sit or dash, no warden
tendril, attack or sonic boom events.

Send Entity.DATA_POSE and the species' synched data from mob_meta,
broadcast the warden's entity events 4, 61 and 62, read both in the
remote mob code, and run the client animation steps in
mobs::sync::client_tick.
2026-10-26 22:48:27 +01:00
Kacpherek f6db8b1f56 Scroll and add the energy swirl of charged creepers and powered withers 2026-10-26 22:44:16 +01:00
Kacpherek 426b3c83cf Play the sand idle and poplar leaves ambient sounds 2026-10-26 22:40:47 +01:00
Kacpherek a146754b0b Make the Particles option thin every particle, explosion debris and rain splash
The Particles option only changed two effects. Follow ClientLevel.doAddParticle
and calculateParticleLevel in the one add path: Decreased drops a third, Minimal
drops all but the always-visible and limiter-overriding particles. Explosion
block debris is thrown only on All, and rain splashes halve on Decreased and
stop on Minimal.
2026-10-26 22:28:10 +01:00
levzzz bafd010865 Merge remote-tracking branch 'upstream/main' into fix/piston-entity-push 2026-10-27 02:22:04 +05:00
Ludovic MarieandClaude Opus 5.5 5f73d0d02d Merge refactor/horizon-command (with main) into refactor/horizon-items
Main's items.rs changes (equip_damage_on_hurt, resists_cactus) land in
crates/horizon-items/src/items.rs.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 22:20:35 +01:00
masa-masa-0314andZCode 024415e09d Inventory: animate every animated block and item sprite
The shared atlas packs animated block and item textures as their first
frame, and only fire re-drew itself: an animated block held or previewed
in the GUI (sculk, magma) sat still, while the same block animated in
the world. The atlas now keeps every animated block or item sprite's
frame list and re-uploads the current frame into its rect as the
animation ticks, with the mcmeta's interpolation mixing the frame into
the next one the way the world's sprite arrays already do, so GUI items
animate like vanilla's stitched atlas.

Co-Authored-By: ZCode <[email protected]>
2026-10-27 05:11:35 +08:00
Kacpherek 8f78fdeb7b Keep leash knot ids per level and clear remote knots with the other remote objects 2026-10-26 22:08:35 +01:00
Kacpherek d2a8cf3bdf Keep a hurt pet's attacker as its player instead of swapping in the owner 2026-10-26 22:05:02 +01:00
Kacpherek ed9fdb5208 Point an explosion's hurt tilt at the blast 2026-10-26 22:04:40 +01:00
Ludovic MarieandClaude Opus 5.5 2929bb6dba open_url: accept the http and https schemes in any case
Util.parseAndValidateUntrustedUri lowercases the scheme before checking it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 22:03:42 +01:00
Kacpherek 6120c8c0ad Attack the block when a guest's START_DESTROY_BLOCK arrives 2026-10-26 22:03:17 +01:00
Kacpherek 53ea3e7224 Play the rain sound for this client only 2026-10-26 22:00:04 +01:00
Kacpherek 462787aee1 Keep the Infested event when a mob is hit by a player or another mob 2026-10-26 21:58:25 +01:00
Carlos NunezandClaude Sonnet 5.5 b90f9ec1ef Make an End gateway's block entity the source of its exit
The side file kept an entry for a gateway after Horizon created its exit,
so removing the exit with /data remove block ... exit_portal, or placing
a different gateway there, sent the player to the old exit again.

Once the gateway's chunk is loaded, gateway_job moves an entry the side
file still holds into the block entity (unless the block entity has an
exit of its own) and drops the entry. set_gateway_exit writes only the
block entity when the chunk is loaded, and leaves an entry in the side
file only when the chunk is already unloaded.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-26 16:57:07 -04:00
Kacpherek e9797d69d0 Use the berry damage type for sweet berry bushes and check the whole move for contact 2026-10-26 21:51:25 +01:00
Kacpherek 4b4361fbc0 Clear a piglin's hoglin target once it dies or disappears 2026-10-26 21:48:20 +01:00
Kacpherek b0ce58adea Check the cat's owner and sync lying and relaxing to clients 2026-10-26 21:48:18 +01:00
Kacpherek 77fe538e09 Let only the owner command a tame wolf or cat, and make pets follow their owner
Sitting, standing, dyeing and healing a cat or wolf now check the saved
owner. The follow, teleport and owner-hurt goals use the owner, not the
nearest player, and a new tame wolf or cat is owned by whoever tamed it.
2026-10-26 21:48:05 +01:00
Kacpherek d8b8246622 Light redstone ore under dropped items too 2026-10-26 21:46:31 +01:00
Kacpherek 35a02c79a8 Cats also skip chests a copper golem has open 2026-10-26 21:46:18 +01:00
Kacpherek db985f717c Spectator crosshair also checks the other half of a double chest 2026-10-26 21:46:04 +01:00
Kacpherek e41c602359 Keep picking up a banner next to a raider even when its path was cached as unreachable 2026-10-26 21:46:00 +01:00
Kacpherek 6145077d7e Make a tame parrot answer only to its owner 2026-10-26 21:44:11 +01:00
Kacpherek 512e003cbd Wolf armor: check the acting player's UUID and keep the armor's components 2026-10-26 21:43:50 +01:00
Kacpherek daee960ba2 Send an armor stand's default pose when it is reset 2026-10-26 21:43:21 +01:00
Kacpherek b33ae69938 Find the End podium level from its rim so a broken centre column is fine 2026-10-26 21:43:08 +01:00
Kacpherek 2a297a43eb Credit the feeder with a parrot's cookie kill 2026-10-26 21:42:51 +01:00
Kacpherek 7eaa5f592b Send jukebox play and stop events to guests so their parrots dance 2026-10-26 21:38:21 +01:00
Kacpherek be714b38bb Only let a tame parrot land on its saved owner 2026-10-26 21:38:16 +01:00
Kacpherek 1a5e5eb358 Owner combat goals only follow the wolf's own owner 2026-10-26 21:37:58 +01:00
951b88a3c2 Read an End gateway's exit from its block entity
Horizon had no End gateway block entity: game/end.rs kept each gateway's
exit in its own side file and gateway_job only looked there, so a
gateway set with /setblock end_gateway{exit_portal:[I;..],ExactTeleport:1b}
(or the exit worldgen writes) was ignored, and /data get block said the
block was not a block entity.

TheEndGatewayBlockEntity stores Age, exit_portal and ExactTeleport.
BlockEntity::EndGateway now holds them with vanilla's NBT and the
spawnable-bounds check. gateway_job reads the block entity's exit first
and keeps the side file and the 512-block End-spawn rule as fallbacks,
so older saves and gateways without data behave as before. The exits
Horizon creates are stored in the block entity as well, a new exit keeps
the gateway's own ExactTeleport (setExitPosition), and a command that
only changes a block entity's NBT now marks its chunk modified so the
change is saved.

Fixes #1517.

Implementation by Codex (GPT-6.1 Sol), reviewed and extended by Claude.

Co-Authored-By: Codex (GPT-6.1 Sol) <[email protected]>
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-26 16:36:53 -04:00
Kacpherek 414a2b82a8 Do not rebuild the End exit podium once the fight has its location 2026-10-26 21:30:57 +01:00
Kacpherek 9958fe0e49 Light redstone ore when an entity stands on it 2026-10-26 21:29:26 +01:00
Kacpherek fba004eb3d Skip leaves in the no-leaves heightmap for firefly bush sounds 2026-10-26 21:22:41 +01:00
Kacpherek 2d70e55fbb Load the respawn search's candidate chunks instead of skipping unloaded ones 2026-10-26 21:21:42 +01:00
Kacpherek d0df238972 Include equipment modifiers in a mob's knockback resistance for the sonic boom push 2026-10-26 21:19:49 +01:00
Kacpherek cdafcd756f Open links and page links in written books 2026-10-26 21:18:05 +01:00
Kacpherek 3fe301aaa1 Record the dragon's attacker before the crystal blast and only for applied damage 2026-10-26 21:17:18 +01:00
Kacpherek d97b58fd4a Send the thrower's entity id with a tracked trident 2026-10-26 21:15:49 +01:00
levzzzandCodex dca6ebba0e Merge upstream main into piston branch
Co-Authored-By: Codex (GPT-6) <[email protected]>
2026-10-27 01:15:46 +05:00
levzzzandCodex 6af81b600f Push entities with moving pistons
Move players, mobs and common objects when piston block entities advance, applying collision and the per-tick piston limit. Exercise host, guest, inventory-open and mob pushes in both single-player paths, and compare the push geometry with vanilla.

Co-Authored-By: Codex (GPT-6) <[email protected]>
2026-10-27 01:01:31 +05:00
Kacpherek 851789830a Open chat links from open_url click events 2026-10-26 20:44:17 +01:00
Kacpherek 4e96037c78 Never show fire-immune mobs as on fire 2026-10-26 20:42:58 +01:00
Kacpherek e872d3ae54 Send isFlat in every dimension and use the dimension's min y for the horizon 2026-10-26 20:26:12 +01:00
masa-masa-0314andZCode Agent 317f30427e Superflat: keep an empty layer list on Re-Create
FlatSettings::from_tag rejected layers: [], so a world with every layer removed fell back to the classic bedrock/dirt/grass settings when re-created. An empty saved list is a valid void flat generator; a non-empty list whose entries all fail to parse is still rejected.

Co-Authored-By: ZCode Agent <[email protected]>
2026-10-27 03:25:05 +08:00
kohad1ckandZCode Agent 91f1f839ab Superflat: the Customize screen edits layers and presets
Create New World greyed out Customize for a Superflat world because Horizon
had no preset editor. Bring vanilla's CreateFlatWorldScreen and
PresetFlatWorldScreen:

- the layer list (top first), Remove Layer and the layer heights;
- the preset chooser with the nine visible flat_world_preset entries, their
  display items and names;
- the share box and Select, parsing layers;biome and validating block and
  biome ids against the prepared registries;
- Done keeps the layers in WorldOptions, Cancel and Escape discard them.

world_loading writes them with write_world_gen_settings_with, so a new
world's world_gen_settings keeps the custom generator; Re-Create reads them
back for the editor.

Co-Authored-By: ZCode Agent <[email protected]>
2026-10-27 03:25:05 +08:00
masa-masa-0314andZCode 1c6c6c0b97 Items: sample the glint from its own repeating texture
Vanilla's GlintSampler repeats enchanted_glint_item per pixel, and the
GLINT transform's scale 8 spans about 8 repeats across an item. Folding
the UV per vertex into the shared atlas could only interpolate between
the four corners, so the pattern rendered as one wide diagonal band
instead of the texture's fine weave. The glint now binds as its own
128x128 texture with a repeat, linear, mip-free sampler (the texture's
mcmeta: clamp false, blur true) in the HUD and entity pipelines, and
`Glint` passes the unwrapped `GLINT_TEXTURING` UV through.

Co-Authored-By: ZCode <[email protected]>
2026-10-27 03:11:47 +08:00
masa-masa-0314andZCode a1761dcb65 Lightning: move the bolt offset to vertex attribute 7
The retained bolt offset and the entity vertex's new glint UV were both
declared at attribute location 6, and the lightning pipeline builds its
vertex buffers from the entity layout plus the bolt-offset instance
buffer, so Device::create_render_pipeline rejected the location clash
and the client crashed at startup.

Co-Authored-By: ZCode <[email protected]>
2026-10-27 03:04:05 +08:00
masa-masa-0314andZCode cf6b91a710 Items: draw the enchantment glint from enchanted_glint_item
Horizon invented the glint: the GUI drew sine bands in screen space and
the entity shader layered the same over held and dropped items. Vanilla
26.3 samples `misc/enchanted_glint_item` through `GLINT_TEXTURING`
(`item.vsh`/`item.fsh`, `TextureTransform.setupGlintTexturing`), so
items now carry the real texture:

- `ItemQuad.foil_uv` keeps the model UV the glint samples; `Glint` turns
  it into the atlas UV with the 26.3 transform (translation(-o0, o1) *
  rotateZ(PI/18) * scale) and `WorldSink` gives foil quads overlay 7 and
  the per-corner glint UV.
- The GUI glint is a separate additive HUD pass masked by the item's
  texture alpha (`item.fsh`'s ALPHA_CUTOUT 0.1 and GLINT path).
- `entity.wgsl` samples the glint at overlay 7/8 like vanilla's
  `color.rgb += glintColor.rgb * glintColor.rgb`.
- The Accessibility options Glint Speed and Glint Strength work: both
  sliders persist to `options.txt` (they were hidden placeholders), the
  GUI pass takes them per draw, and `Frame.glint_alpha` feeds the entity
  shader every frame instead of whatever the last HUD item set.
- GUI items use `GLINT_TEXTURING`'s scale 8, world items
  `ENTITY_GLINT_TEXTURING`'s scale 0.5.

Co-Authored-By: ZCode <[email protected]>
2026-10-27 03:04:05 +08:00
Kacpherek fbdd04d817 Use armor stand, boat, cart and item eye heights for facing entity eyes 2026-10-26 19:58:03 +01:00
Kacpherek aed4e26f59 Apply the legacy announce-player-achievements key
The key was parsed and dropped. It now sets the show_advancement_messages game rule when the dedicated server starts, like DedicatedServer.initServer.
2026-10-26 19:54:23 +01:00
Kacpherek b5adad15eb Stop the riptide ground lift at ceilings 2026-10-26 19:53:42 +01:00
Kacpherek d17df3ca05 Give /op the level from op-permission-level
The console's /op always granted level 4. It now grants the permission level set in server.properties, like PlayerList.op.
2026-10-26 19:53:41 +01:00
Kacpherek 2d72e987c5 Play ambient sounds and spawn fireflies around firefly bushes, eyeblossoms, pale moss and dry plants 2026-10-26 19:53:15 +01:00
Kacpherek 5b066b1b78 Send the host's death and advancement messages to guests and follow team visibility 2026-10-26 19:53:09 +01:00
Kacpherek 9ae3882c1a Kick unlisted players when enforce-whitelist is on
Whitelist remove, reload and on, and deop, now disconnect online players who are not on the whitelist, like MinecraftServer.kickUnlistedPlayers.
2026-10-26 19:52:46 +01:00
Kacpherek 89c195fa6e Don't back off from edges while flying 2026-10-26 19:51:46 +01:00
Kacpherek de808e3612 Empty a bundle by holding right click 2026-10-26 19:51:31 +01:00
Kacpherek 4575f4f1df Honour spawn-protection on a dedicated server
Players who are not operators can no longer start digging or use an item on a block within the radius of the world spawn. They get the spawn protection message and the client gets the block back, like DedicatedServer.isUnderSpawnProtection.
2026-10-26 19:51:18 +01:00
Kacpherek 54b1c4f941 Make climbing follow vanilla: no climbing while flying, open trapdoors over ladders climb 2026-10-26 19:51:03 +01:00
Kacpherek 8a309c393d Show join and leave messages in a LAN host's own chat 2026-10-26 19:50:50 +01:00
Kacpherek 58a99a2ce9 Sulfur cubes bounce, slide and keep speed by their block's archetype 2026-10-26 19:49:41 +01:00
mahirsn 2b2aecd975 fix(client): send sign editor text like vanilla
A new editor or a removed sign closes the open editor (Minecraft.setScreen, AbstractSignEditScreen.tick), and closing sends ServerboundSignUpdatePacket with the lines it opened with when the sign is gone. Sign lines resolve key binds and translations (Component.getString) instead of keeping only plain text.
2026-10-26 21:49:15 +03:00
Kacpherek 23019a7bf9 Cats lie beside a sleeping owner and may leave a morning gift 2026-10-26 19:48:21 +01:00
Kacpherek 8aad511579 Drop the cursor stack when the dead player respawns 2026-10-26 19:46:01 +01:00
Kacpherek 94600b266f Format the fox module order 2026-10-26 19:44:07 +01:00
Kacpherek 78a5ee0e55 Let parrots imitate nearby mobs and dance to a jukebox
Parrots only ever made their own sound. Follow Parrot.aiStep: one tick in
400 a parrot imitates a random mob within 20 blocks (half the time), and
its ambient sound is sometimes a mob's voice. A parrot within 3.46 blocks
of a playing jukebox now uses the party pose (level events 1010 and 1011).
2026-10-26 19:43:53 +01:00
Kacpherek 63ee634ee0 Apply an armor stand's saved pose
The Pose tag of an armor stand is now read and saved, drawn on the stand and its armor, and sent to clients on the server thread.
2026-10-26 19:43:42 +01:00
Kacpherek 752138e866 Cats sit on a chest only while nobody has it open, and on a bed's foot only 2026-10-26 19:43:27 +01:00
Kacpherek c30d51d65e Wolves fight other owners' tamed wolves 2026-10-26 19:41:45 +01:00
Kacpherek c439b806b8 Let a compass lock onto a lodestone
Using a compass on a lodestone now sets its lodestone target (in place for a single compass in survival, otherwise one is split off), and the target clears when the lodestone is gone. The target also travels in item packets on the server thread.
2026-10-26 19:40:59 +01:00
Kacpherek 50fdcad10f A cookie poisons and kills a parrot 2026-10-26 19:40:42 +01:00
Kacpherek c74626448c Show the crosshair to spectators over containers, stations and portals 2026-10-26 19:40:06 +01:00
Kacpherek 2886b8f9ba Horse armor reduces the damage a horse takes 2026-10-26 19:39:31 +01:00
Kacpherek 9ca7162169 Let a tame parrot sit on its owner's shoulder
A tame parrot never landed on its owner. Add LandOnOwnersShoulderGoal:
when the parrot touches its owner, it is stored in the player's
ShoulderEntityLeft or ShoulderEntityRight and removed from the world.
It comes back, still tame, when the player falls, swims, flies, sleeps,
is hurt, dies, turns spectator or spins with a riptide.

The shoulder data is the player's own save data, so it also works for
players on the integrated server and for LAN guests.
2026-10-26 19:39:14 +01:00
Kacpherek de2df51193 Wolf armor: put it on, repair it, shear it off, and let it take the damage 2026-10-26 19:38:43 +01:00
Kacpherek dc36089156 Apply the set_name loot function to generated items 2026-10-26 19:37:51 +01:00
Kacpherek d248ab2605 Give foxes their own goals: flee, hunt, nap in the shade and eat berries
Foxes used the generic animal goals: they panicked at 1.25, never
avoided players or wolves, never hunted, slept, bred or ate berries.
Add mobs/fox.rs with the goals Fox.registerGoals sets up that Horizon
can run today, send the fox variant and flags to clients so the
sleeping pose shows, and let a baby fox take either parent's variant.
2026-10-26 19:37:15 +01:00
Kacpherek 08c959807d Broken heads keep their skin and silk-touched hives keep their bees and honey 2026-10-26 19:37:01 +01:00
Kacpherek 796dc320d9 Use the height adjusted biome temperature for the rain and snow line on the client 2026-10-26 19:36:45 +01:00
Kacpherek f1949c3bda Ask before the death screen's Title Screen button leaves the world 2026-10-26 19:36:39 +01:00
Kacpherek 4d9ead721e Point the recovery compass at the player's last death
The local player's last death location is now recorded, saved as LastDeathLocation, taken from the Login and Respawn packets on the server thread, and given to the item owner.
2026-10-26 19:36:35 +01:00
Kacpherek 10d2b202d5 Push the other players too with a mace smash 2026-10-26 19:35:53 +01:00
Kacpherek b8d0590d6d Lightning flash: mix the sky colour toward the flash tint instead of replacing it 2026-10-26 19:34:47 +01:00
Kacpherek 36ebd0c545 Follow vanilla's lava fog for Fire Resistance and spectators, and brighten the fog colour with Night Vision 2026-10-26 19:34:12 +01:00
Kacpherek bc2dd35cc3 Raise the held item name and action bar above extra heart, armor and mount rows 2026-10-26 19:33:57 +01:00
Kacpherek b5a735c8d1 Let tame parrots follow their owner and sit when told to
A tamed parrot ignored its owner: it never followed, and right-clicking
it did nothing. Give the parrot the goals of Parrot.registerGoals
(SitWhenOrderedToGoal, FollowOwnerGoal 1.0, 5.0, 1.0), let the owner
toggle sitting with a click while it stands on the ground, and keep the
owner and sitting state in the save file and in the data a guest gets.
2026-10-26 19:33:51 +01:00
Kacpherek 8c0ed0e591 Let a comparator read an item frame through a block
A comparator behind a solid block now reads the item frame on its far side (rotation + 1, or 0 when empty), and updates when the frame's item or rotation changes.
2026-10-26 19:33:24 +01:00
Kacpherek 849b5b663b Let a sculk catalyst take the experience of mobs killed without a player 2026-10-26 19:33:09 +01:00
Kacpherek b65b0a6a7d Dim the sun and moon in rain and turn the moon texture the right way up 2026-10-26 19:32:59 +01:00
Kacpherek 97f8bca9da Raiders skip ominous banners they cannot reach
A captainless raider walked towards the nearest fallen ominous banner
even when no path led to it. Check that a path reaches the banner before
going for it, and skip a banner without one for 600 ticks, like vanilla.
2026-10-26 19:32:41 +01:00
Kacpherek f1181d4a25 Trial chamber mobs spawn with the gear their spawner's equipment table gives 2026-10-26 19:32:16 +01:00
Kacpherek c5a37c02de Push sonic boom targets only when hit, by their knockback resistance 2026-10-26 19:32:01 +01:00
Kacpherek 74dc389321 Keep the effect icons on screen over the pause menu, chat and other screens 2026-10-26 19:31:55 +01:00
Kacpherek 7d7a85d184 Recruit nearby raiders only while the raid pathfinding goal runs
Raiders recruited other raiders every 20 ticks whether or not they were
heading for the raid. In vanilla the recruiting is part of
PathfindToRaidGoal.tick, so a raider that has a target or carries a
rider does not recruit. Do the recruiting from the goal's tick.
2026-10-26 19:31:28 +01:00
Kacpherek de1e5f7292 Spin the player arrow on a Nether map
Vanilla's MapItemSavedData.calculateRotation gives the arrow a changing rotation in the Nether. The player marker used the yaw there too, so it never spun.
2026-10-26 19:31:10 +01:00
Kacpherek 9bc05faf53 Tamed wolves heal twice the food's nutrition 2026-10-26 19:30:54 +01:00
Kacpherek 575cef1d26 Let a vindicator named Johnny attack other raiders too
Vanilla's Johnny goal targets any living entity that can be attacked.
Horizon skipped pillagers, vindicators, witches, evokers, illusioners
and ravagers, so Johnny left them alone. Remove that filter.
2026-10-26 19:30:38 +01:00
Kacpherek 0cc3f567fc Play the bubble pop sound when an air bubble bursts 2026-10-26 19:30:29 +01:00
Kacpherek cb97c5496e Raiders in a raid no longer count idle time twice as fast
A raider's idle counter grew by 4 per tick while in a raid, so it
dropped out of the raid after about 700 ticks instead of 1400 and could
no longer be recruited sooner than in vanilla. Remove the extra tick
added by the raid bookkeeping; the counter now grows by 3 like vanilla.
2026-10-26 19:30:07 +01:00
Kacpherek 484cc945aa Add the rockets list to the remote motion test fixture 2026-10-26 19:23:52 +01:00
Kacpherek 8ca15ae2ca Apply /xp, /clear and /spawnpoint to each target, not only the host 2026-10-26 19:23:47 +01:00
Kacpherek 10872bdd64 Add the tridents field to the remote motion test fixture 2026-10-26 19:22:53 +01:00
Ludovic MarieandClaude Opus 5.5 fc8e505c95 Merge refactor/horizon-command (with main and #2316) into refactor/horizon-items
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 19:22:52 +01:00
Kacpherek 3b24aceb04 Let /clone move work on overlapping regions 2026-10-26 19:21:53 +01:00
Kacpherek a4ccf205f7 Throw what /give cannot fit from the player's eyes, for that player only 2026-10-26 19:21:09 +01:00
Kacpherek 452fca8bdd Merge origin/main into fix/1938-ghast-dismount 2026-10-26 19:21:07 +01:00
Kacpherek ab40cd63e6 Merge origin/main into fix/remote-leash-knots 2026-10-26 19:20:16 +01:00
Kacpherek 9dbd0a4baf Make the strafing dragon charge its fireball only with a clear line of sight 2026-10-26 19:19:36 +01:00
Kacpherek 00f2663e7b Merge origin/main into fix/harvest-full-beehive 2026-10-26 19:19:20 +01:00
Kacpherek 9aabeb39ee Make /tp facing look from the feet, or the eyes under anchored eyes 2026-10-26 19:18:50 +01:00
Kacpherek 605e8a1488 Stop shulkers from targeting and shooting in Peaceful 2026-10-26 19:18:08 +01:00
Kacpherek e6587dce6a Respawn at once without a death screen when immediate_respawn is on 2026-10-26 19:17:05 +01:00
Kacpherek dfd0601163 Teleport a dragon egg when it is left-clicked 2026-10-26 19:16:52 +01:00
Kacpherek 034af45777 Make adult piglins hunt adult hoglins 2026-10-26 19:15:33 +01:00
Kacpherek 0b3e40f9d4 Give a closed shulker its 20 covered armor 2026-10-26 19:15:28 +01:00
Kacpherek a44581eaf3 Let a shulker bullet's levitation run out on mobs 2026-10-26 19:14:23 +01:00
Kacpherek 886a84dea0 Fly thrown tridents on by themselves between server updates 2026-10-26 19:14:04 +01:00
Kacpherek 79c3896449 Respawn on a free column within the respawn radius around the world spawn 2026-10-26 19:13:58 +01:00
Carlos Nunez d75f767925 Merge remote-tracking branch 'upstream/main' into tmp-resolve-2298 2026-10-26 14:13:17 -04:00
Kacpherek a45c73bb36 Credit the player who kills the ender dragon 2026-10-26 19:12:49 +01:00
Kacpherek 6823e34451 Let a piglin let go of the gold it admires when a player hits it 2026-10-26 19:12:31 +01:00
Kacpherek f25fbb6f2e Give zombified piglins that spawn in a nether portal a portal cooldown 2026-10-26 19:11:24 +01:00
Kacpherek 91d6bdba85 Fly firework rockets on by themselves between server updates 2026-10-26 19:11:21 +01:00
Kacpherek 4248048ab6 Merge origin/main into fix/elytra-firework-trail 2026-10-26 19:08:50 +01:00
Kacpherek 872409ea57 Mined ice melts into water unless the tool has silk touch 2026-10-26 19:08:19 +01:00
Kacpherek 2277c85fc3 Run neighbour updates when a mob trampling farmland places dirt 2026-10-26 19:07:02 +01:00
Kacpherek e98d939919 Use the mob's own chunk for its local difficulty 2026-10-26 19:06:59 +01:00
Kacpherek 81b4c09ee6 Count the endermite lifetime with NoAI set 2026-10-26 19:06:53 +01:00
Ludovic MarieandClaude Opus 5.5 a07c6d57dc horizon-items: item_name reads name_text from horizon-client-core
Main's item_name (d983ef89b) called crate::entity_nbt::name_text, which
the client re-exports from horizon_client_core::component.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 19:04:00 +01:00
Ludovic MarieandClaude Opus 5.5 3c94b5441e Merge refactor/horizon-command (with main) into refactor/horizon-items
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 18:59:46 +01:00
Kacpherek 5b89508fe9 Predict putting out a lit campfire with a shovel on the client
In single player and LAN the client never recognized the shovel on a lit
campfire, so there was no smoke and a shield in the other hand was raised
too. The client now shows the smoke, puts the fire out locally and ends
the click after the hand that doused.
2026-10-26 18:58:36 +01:00
Kacpherek b92dd80566 Let silverfish hide in stone and wake their friends when hurt 2026-10-26 18:55:02 +01:00
Kacpherek 4084bcdf75 Let a brush dust suspicious sand and gravel 2026-10-26 18:54:11 +01:00
Kacpherek 4eb95c4d3e Follow the burn in daylight tag for mobs that burn in the sun 2026-10-26 18:53:26 +01:00
Kacpherek 5657080884 Swell a creeper's fuse when it falls 2026-10-26 18:51:45 +01:00
Kacpherek 708702917c Remove an endermite after 2400 ticks 2026-10-26 18:51:16 +01:00
Kacpherek 4c8ff02969 Make skeleton arrow spread and damage follow the difficulty 2026-10-26 18:51:03 +01:00
Kacpherek fe167781c4 Give the fishing rain bonus only where rain falls 2026-10-26 18:50:36 +01:00
Kacpherek fe8565b874 Scale cave spider poison and husk hunger with the difficulty 2026-10-26 18:50:05 +01:00
Kacpherek 0d02a4c722 Let a caught fish be picked up right away 2026-10-26 18:50:04 +01:00
Kacpherek 9d4c984e03 Speed up an enderman while it has a target 2026-10-26 18:49:31 +01:00
Kacpherek e4cd56ed7b Make Frost Walker freeze water and protect from magma 2026-10-26 18:49:17 +01:00
Kacpherek b32989cf1e Wear shears on mooshrooms and apply Unbreaking to held tool wear 2026-10-26 18:49:13 +01:00
Kacpherek f2424043f3 Stop zombie drowning conversion when it leaves the water 2026-10-26 18:49:12 +01:00
Kacpherek fece9d85e3 Spawn village and swamp hut cats 2026-10-26 18:48:18 +01:00
Kacpherek 45996d8d78 Match the farmer's requested items to vanilla
The farmer's list had food, torchflower seeds and pitcher pods and lacked bone meal. Vanilla requests wheat, wheat seeds, beetroot seeds and bone meal. Food is already picked up and traded through its villager food value.
2026-10-26 18:48:10 +01:00
Kacpherek 7569f9615a Treat unbreakable items as undamageable in the anvil and grindstone 2026-10-26 18:47:58 +01:00
Kacpherek 90501f9ab4 Keep leashed mobs and carrying endermen from despawning
Mob.requiresCustomPersistence also covers a leashed mob and an enderman holding a block, and the spawn caps skip such mobs. Gather the existing cases in one helper, add the missing ones, and use it for both the despawn check and the cap count.
2026-10-26 18:46:46 +01:00
Kacpherek c1844dfe57 Let mobs trample farmland when they land on it
A cow dropped onto farmland left it untouched. A landing mob now turns
farmland to dirt with chance fall distance - 0.5, when mob griefing is on and
the mob is big enough, and is lifted onto the dirt.
2026-10-26 18:46:37 +01:00
Kacpherek bf527e7a13 Apply knockback resistance to Punch arrows 2026-10-26 18:46:36 +01:00
Kacpherek 472da6509d Projectiles ring bells they hit 2026-10-26 18:45:57 +01:00
Kacpherek dbef78bcb8 Apply Unbreaking to armor wear from Thorns
Thorns always cost the worn piece 2 durability. Run it through the same
durability loss as other wear so Unbreaking can cancel points.
2026-10-26 18:45:54 +01:00
Kacpherek b283f2ad31 Hurt mobs that touch a cactus or a sweet berry bush
Mobs ignored both blocks. A mob inside a cactus now takes 1 damage a tick,
and a mob inside a sweet berry bush is slowed (0.8, 0.75, 0.8) and, in a
grown bush, takes 1 damage when it moved at least 0.003 on X or Z. Foxes
and bees are left alone.
2026-10-26 18:45:44 +01:00
Kacpherek 6dcf4b3e8b Use the real local difficulty for the phantom spawn check
The phantom spawn gate assumed a fixed effective difficulty of 2. Use the local difficulty at the player instead, so easy, hard, a long-played chunk and the moon phase change how often phantoms spawn.
2026-10-26 18:45:43 +01:00
Kacpherek bf508c2842 Let mobs, dropped items and projectiles trigger pressure plates and tripwires 2026-10-26 18:45:39 +01:00
Kacpherek 564b230ab1 Make a warden's hit disable the player's shield
A warden holds no weapon, so its blows never put the shield on cooldown. Warden.getSecondsToDisableBlocking is a flat 5 seconds, so use that when the attacker is a warden.
2026-10-26 18:45:13 +01:00
Kacpherek 555ddb94bf Use the impaling entity tag for thrown tridents
The bonus went to every swimming or water-breathing mob, including undead,
blazes, ghasts and iron golems. Read the damage from the enchantment data so
only #sensitive_to_impaling mobs take it.
2026-10-26 18:45:13 +01:00
Kacpherek e92937d956 Apply Slowness with Bane of Arthropods
Post-attack effects only handled ignite, so the apply_mob_effect effect of
Bane of Arthropods never did anything. Handle apply_mob_effect like vanilla.
2026-10-26 18:44:23 +01:00
Carlos Nunez d62a1e5d20 Merge remote-tracking branch 'upstream/main' into fix/leave-game-statistic
# Conflicts:
#	tools/lan_e2e.py
2026-10-26 13:33:28 -04:00
Carlos NunezandClaude Sonnet 5.5 1862395e4b Keep a leaving guest's play state until their statistics are saved
lan_crafting_left removed the guest's GuestPlay from host.play before
the leave_game award and save_guest_records ran, so a normal guest
disconnect awarded nothing and wrote no statistics. It now takes the
crafting table and workstation state out of the entry and leaves the
entry for HostEvent::Left to remove after saving.

New lan_e2e check `leave`: a guest leaves twice and the host's saved
statistics show leave_game 1, then 2. It fails on the previous commit
(0, then [0, 0]) and passes here.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-26 13:32:44 -04:00
socksproxandDevin 077da2e06b Queue the first skin upload as well as re-uploads
The completed-download push into ready never marked the location queued,
so an again() before the next take_ready() put the same skin in twice:
store_skin allocated two slots, and evicting one removed the sprite while
show_skin still found the other entry — the skin never uploaded again.

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-26 18:30:09 +01:00
Kacpherek a6d2eb9d64 Shulkers keep their body still while the head turns 2026-10-26 18:17:27 +01:00
Kacpherek d18197a176 Shield: hold it up in first person and show the block pose on other players 2026-10-26 18:14:24 +01:00
Kacpherek 0300ad8922 Keep the current attack target regardless of invisibility 2026-10-26 18:13:33 +01:00
socksproxandDevin 7a3a19697a Resolve dynamic head profiles, and re-upload evicted skins
Player heads whose profile carries only a name or only a UUID
(ResolvableProfile.Dynamic) resolved to nothing, so heads some servers
send that way kept the default skin. Adds skins::Resolver
(ProfileResolver.Cached: profiles/getByName then
session/getProfileById through the services discovery document, on
background threads) used by placed heads, head items and mannequins; the
profile-derived DefaultPlayerSkin stands in until the download lands,
with the name's offline UUID keying it for name-only profiles.

A skin evicted from the atlas's 128 slots could never return: the decoded
image was dropped and show_skin simply failed. The downloader now keeps
the images (and the disk cache backs them), and again queues the upload
again where the tab list, level players, mannequins and heads notice the
missing sprite.

profile_tag keeps id so id-only and name+id profiles survive into the
item data JSON, and the player-head item fallback matches vanilla's
slim/steve (was wide/steve).

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-26 18:13:22 +01:00
Kacpherek e8ae26c270 Carry the exploding mob's kind with its blast and anger bees at guests too 2026-10-26 18:09:01 +01:00
Carlos NunezandClaude Sonnet 5.5 e50eb7c71f Count leave_game for every player who leaves, owner included
Vanilla's PlayerList.remove awards Stats.LEAVE_GAME to a player before
saving them, and PlayerList.removeAll does it for everyone when the
server stops. Horizon only counted it in the in-frame path, so on the
default server thread the owner never got it, and guests who left or
were removed at shutdown did not either.

LanHost::award_leave_game counts it once per guest (a guest is saved
more than once on the way out) and runs where a guest leaves, when the
integrated server stops, and when LAN closes.

Part of #2095 (item 4 of the report). The signal handling and the
remote-world disconnect are #2116's.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-26 12:36:46 -04:00
Kacpherek b62095dfcb Run the trident player_hurt_entity trigger as the thrower 2026-10-26 17:25:37 +01:00
Kacpherek ba3b567f54 Predict a full beehive reacting to shears and glass bottles on the client 2026-10-26 17:15:29 +01:00
Kacpherek 5d3e9d218d Clean copper for a skeleton trap's visual lightning too 2026-10-26 17:05:35 +01:00
Kacpherek 36ef05b0b9 Keep infinite mob effects through the effect tick 2026-10-26 16:51:22 +01:00
Kacpherek 49ad61035b Count shield damage only for accepted hits and skip kills saved by a totem 2026-10-26 16:47:11 +01:00
Kacpherek 3842d851d8 Match the brewed_potion trigger's potion condition as a PotionsPredicate 2026-10-26 16:38:07 +01:00
Kacpherek c987778e97 Decorated pot: keep every quad and cull back faces like entitySolid 2026-10-26 16:34:24 +01:00
Kacpherek b28108128d Keep a hopper minecart's items in sync with a leftover menu copy 2026-10-26 16:33:08 +01:00
Kacpherek c50557ea0b Credit the projectile owner for explosion damage to mobs 2026-10-26 16:29:05 +01:00
Kacpherek f364c4f9c6 Leave wandering traders out of the spawn egg baby check 2026-10-26 16:28:28 +01:00
Kacpherek 9841ce7824 Revert "Decorated pot: draw one face of its top and bottom planes"
This reverts commit 408a75aeb3.
2026-10-26 16:27:13 +01:00
Kacpherek 83a6f5c7b9 Revert "Decorated pot: say why only the outer faces stay"
This reverts commit d338bcd070.
2026-10-26 16:27:13 +01:00
Kacpherek 18f4dd92ae Play container open and close sounds only when the opener count crosses zero 2026-10-26 16:24:42 +01:00
Kacpherek 47654b62a3 Save the armadillo scute timer and reset it only when a scute can drop 2026-10-26 16:24:26 +01:00
Kacpherek 2f3dade42f Show the firework trail on a boosting rocket and hide its sprite
On a server-thread client an elytra boost rocket drew no spark trail and
showed up as a static item sprite. The client now follows the entity the
rocket is attached to, spawns the trail each tick like
FireworkRocketEntity.tick, and does not draw an attached rocket.
2026-10-26 16:19:53 +01:00
Kacpherek af49fd7dc3 Use a collision-shape raycast for the spyglass line-of-sight check 2026-10-26 16:07:14 +01:00
Kacpherek af6faa88e5 Report the hook's open-water flag to the fishing_rod_hooked trigger 2026-10-26 16:06:30 +01:00
Kacpherek e71d4281ac Spawn a baby when a spawn egg is used on its own mob kind
Mob.checkAndHandleImportantInteractions: a spawn egg of the clicked mob's
kind spawns a baby at the mob's position (SpawnEggItem.spawnOffspringFromSpawnEgg),
inheriting the parent's variant, and uses one egg outside creative.

Fixes #2269.
2026-10-26 16:04:35 +01:00
Ludovic MarieandClaude Opus 5.5 775e5b19cb horizon-items: tests load prepared items without the client's Assets
#2038's bundle tooltip tests and #2037's loot tests share one helper that
reads the block registry and facts directly.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 15:59:37 +01:00
Ludovic Marie f56bf1ba38 Merge remote-tracking branch 'origin/main' into refactor/horizon-items 2026-10-26 15:58:59 +01:00
Ludovic MarieandClaude Opus 5.5 ca119bb069 Merge origin/main into refactor/horizon-items
#2037's loot-enchantment tests stay in horizon-items: they load the
prepared items through the block registry and facts directly instead of
the client's Assets::load.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 15:57:41 +01:00
Kacpherek 7102727197 Play the container close sound when a server-thread player closes it 2026-10-26 15:24:26 +01:00
Kacpherek 70868f3208 Give Oozing, Infested, Weaving and Wind Charged their effects
These four 26.x effects only existed as potion entries. Oozing spawns two slimes, Weaving places cobwebs and Wind Charged makes a gust when the bearer dies. Infested sometimes releases silverfish when the bearer is hurt.
2026-10-26 15:20:34 +01:00
Kacpherek aff673a08d Give the Ender Dragon its ambient sound 2026-10-26 15:20:10 +01:00
Kacpherek 2e642857fc Fire the levitation and using_item advancement triggers 2026-10-26 15:20:03 +01:00
Kacpherek 147e5269c7 Play the rain sound the way vanilla picks it 2026-10-26 15:19:20 +01:00
Kacpherek b3a499d85d Play chest, barrel and shulker box sounds with a random pitch from the right spot 2026-10-26 15:18:05 +01:00
Kacpherek 3c013dac75 An axe turns an unoxidized copper golem statue back into a golem
The golem stands where the statue was, facing the same way, the axe
loses durability and the statue block is removed.
2026-10-26 15:18:03 +01:00
Kacpherek b913dab5e5 Walk, follow and flee at the camel's real AI speeds 2026-10-26 15:17:53 +01:00
Kacpherek e87e839c97 Fire the construct_beacon trigger when a beacon activates 2026-10-26 15:17:44 +01:00
Kacpherek 8c8f857313 Fire item_used_on_block when an axe, honeycomb, disc or vault key is used on a block 2026-10-26 15:17:42 +01:00
Kacpherek 9c427dfae0 Mobs killed by a wither leave a wither rose 2026-10-26 15:16:54 +01:00
Kacpherek ef711f8b1f Fire the brewed_potion trigger when a potion is taken from a brewing stand 2026-10-26 15:16:44 +01:00
Kacpherek 601e7918a8 Lightning scrapes one oxidation step off a copper golem
A copper golem hit by a bolt goes back one weathering step, and its
weathering timer (or wax) is reset.
2026-10-26 15:16:43 +01:00
Kacpherek dd3697bb4d Hopper minecarts take from the container above, one item per tick 2026-10-26 15:16:43 +01:00
Kacpherek 273729aac8 Count the combat statistics
damage_resisted, damage_absorbed and damage_blocked_by_shield now add up when
Resistance, absorption hearts or a shield take damage, and a player who lands
the killing blow on another player gets player_kills.
2026-10-26 15:16:34 +01:00
Kacpherek 94592a35ac Use the flat world's horizon height for the dark disc and void fog 2026-10-26 15:16:33 +01:00
Kacpherek 53501ab94e Merge a mob's repeated effects like vanilla
A stronger effect now replaces the old one with its own duration, instead of taking the longer duration and the higher level separately.
2026-10-26 15:15:55 +01:00
Kacpherek 581dc08cf5 Fire shot_crossbow and killed_by_arrow advancement triggers 2026-10-26 15:15:43 +01:00
Kacpherek 85a6ce0921 Lightning scrapes oxidation off copper
A struck copper block goes back to unoxidized, and 3 to 5 random walks
take one step of oxidation off the weathering copper around it. Waxed
blocks stay as they are but still start the walks.
2026-10-26 15:15:30 +01:00
Kacpherek 1b4fe0f067 Wither rose and open eyeblossom give bees an effect instead of breeding them 2026-10-26 15:15:28 +01:00
Kacpherek 8691d9a8e8 Let players harvest a full beehive with shears or a glass bottle 2026-10-26 15:14:34 +01:00
Kacpherek d2105fe66c Round instant potion effects and measure splash range from the body
Instant health and harm round to the nearest point instead of truncating. A splash potion measures the distance to the entity's box, not its centre, so a potion landing next to a player hits harder and reaches farther.
2026-10-26 15:14:30 +01:00
Kacpherek 20630b1230 Fire the enchanted_item trigger when an enchanting table enchants 2026-10-26 15:14:25 +01:00
Kacpherek 8d77913ea5 Note blocks play on a left click, and take mob heads on top
Mining a note block now plays its note and counts the play_noteblock
statistic. A mob head used on the top face is placed instead of tuning
the block.
2026-10-26 15:14:10 +01:00
Kacpherek 66637220ef Play the step sound of the block above the floor for the player
Powder snow, lily pads, carpets, snow layers and similar blocks now sound like vanilla when walked on: inside blocks replace the floor's step, combination blocks play their own step with the floor's step muffled.
2026-10-26 15:13:58 +01:00
Kacpherek 5fa423169f Fire player_hurt_entity and entity_hurt_player advancement triggers 2026-10-26 15:13:51 +01:00
Kacpherek 183382cd2b Fire the fishing_rod_hooked trigger when a hook is reeled in 2026-10-26 15:13:34 +01:00
Kacpherek 329ce1b724 Make armadillos shed scutes on their own
Armadillos never dropped scutes because nothing ran their shed timer.
Follow Armadillo.customServerAiStep: count down 5 to 10 minutes, then
drop a scute, play the sound and post the entity_place game event.
2026-10-26 15:13:27 +01:00
Kacpherek 2a0824cf5f Invisible players are harder for mobs to notice
Mobs now scale their detection range by 0.7 times the worn armor fraction (at least 0.1) when the player has Invisibility, like vanilla's getVisibilityPercent.
2026-10-26 15:13:15 +01:00
Kacpherek cc39d57e07 Explosions empty and anger the bees of destroyed hives 2026-10-26 15:12:42 +01:00
Kacpherek 7cc8def23f Let a lit TNT minecart explode when it hits a wall 2026-10-26 15:10:57 +01:00
Ludovic MarieandClaude Opus 5.5 27d750f1c9 Split horizon-items out of horizon-client
Second slice of the crate split, a pure move like the first: blocks (+ blocks/), items, item_data, enchant,
cooldowns, bundle and random_sequences move to horizon-items. They are the first layer mobs/ needs, after
horizon-client-core.

Items moved out of lib modules that sit in the big cycle, unchanged:
- firework shapes, explosions and dye colours (particles.rs) -> firework.rs;
- BannerLayer and its NBT form (banner.rs) -> banner_layer.rs;
- the ItemStack NBT codec (save.rs) -> item_nbt.rs;
- consumable_animation (first_person.rs) -> use_animation.rs;
- the content namespaces (mod_content.rs) -> content_namespaces.rs;
- save.rs's NBT value helpers -> horizon-client-core's nbt_value.rs, and name_text (entity_nbt.rs) -> core's
  component.rs.

horizon-client re-exports every module and item under its old path. Test helpers other client tests use
(Items::for_tests, enchant::tests) are behind horizon-items' `test-support` feature, enabled by
horizon-client's dev-dependency. The items test that loads the client's assets moved to the client:
always_edible_and_consume_effects_loaded, to assets.rs. particles.rs' test module moved to the end of the file
(clippy's items_after_test_module had been quiet only thanks to the derives that moved out).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 14:46:48 +01:00
Joansitoh 2f6f0e3e0f Launcher: Game arguments edits at the caret
The field only appended and popped the last character, and Space typed
nothing on Windows (it arrives without a character). A shared one-line
field, launcher/src/line_edit.rs, now holds the text, caret, selection
and undo history: arrows, Home/End, Ctrl+arrows by word, Shift to select,
mouse click, drag, double and triple click, Ctrl+A/C/X/V and Ctrl+Z / Ctrl+Y.
2026-10-26 13:16:35 +01:00
Ludovic MarieandClaude Opus 5.5 d338bcd070 Decorated pot: say why only the outer faces stay
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 13:16:31 +01:00
Kacpherek 4825e537cc Tilt a server player's camera towards the hit
The server never sent ClientboundHurtAnimationPacket, and the client
ignored the yaw it carried, so a player on a server got the flash but the
camera never tilted towards where the hit came from.

ServerPlayer.indicateDamage now runs where the hit's knockback is dealt
(and with no direction for hits that have none), the packet goes out when
the guest's turn ends, and the client keeps the yaw as Player.hurtDir for
GameRenderer.bobHurt.
2026-10-26 13:05:51 +01:00
Kacpherek 2e91bd84e9 Send villager, taming, love, spawn poof and snowball entity events to server clients
Single player on the server thread drew these particles only into the server's own engine. The server now sends the entity event byte where vanilla does (20, 12/13/14, 6/7, 18 and 3), and the client draws it in handleEntityEvent's way.
2026-10-26 13:05:47 +01:00
Kacpherek 5d72fdd8c2 Add ambient effects for levers, repeaters, dripstone, wither roses, End gateways and lightning rods
These blocks have an animateTick in vanilla but nothing happened around them in Horizon. Each gets the same particles and odds as vanilla.
2026-10-26 13:02:23 +01:00
Kacpherek 3287a74d64 Send fence knots to server players so leads tied to fences are drawn 2026-10-26 13:00:40 +01:00
Kacpherek 946f47b3ec Keep a happy ghast solid and still under a rider who gets off it
On the server thread the window never learned the ghast was on its still
timeout, and its copy trailed the server's, so a rider dropped through the
ghast and the ghast flew off. Sync STAYS_STILL, assume the timeout on
dismount, and put the window's ghast under the landing spot.
2026-10-26 12:48:36 +01:00
Kacpherek 408a75aeb3 Decorated pot: draw one face of its top and bottom planes 2026-10-26 11:45:03 +01:00
1148 changed files with 119506 additions and 26960 deletions

No files matched your search

+76
View File
@@ -0,0 +1,76 @@
---
name: vanilla-parity
description: Fix a Horizon bug or gap so it behaves 1:1 like vanilla 26.3, starting from the port ledger (tools/port_ledger.py) instead of grepping. Use for any issue, Discord report or PR that makes Horizon match vanilla: "X doesn't work like in Java", a missing sound, particle, animation, mob behaviour, block rule, screen or packet.
---
# Vanilla parity with the port ledger
The port ledger pairs every vanilla method with the Rust fn that ports it (or says it is missing, or needs
no port) and lists where a port isn't wired like vanilla. Ask it first; grep only for what it can't answer.
Full reference: `docs/port-ledger.md`.
## 0. Have a fresh ledger
```sh
python3 tools/port_ledger.py build # ~20 s; again after pulling main
```
It needs the vanilla jar (the launcher's `versions/26.3/26.3.jar`, or `--jar`) and/or the lab's decompiled
sources (`python3 tools/lab.py decompile`, or `--sources DIR`). If `build` says it has no vanilla code, run
the lab first; don't guess vanilla from memory.
## 1. From the report to the code
```sh
python3 tools/port_ledger.py issue 3236 # a GitHub issue number
python3 tools/port_ledger.py issue "mooshroom stays red after lightning" # or the report's text
```
It ranks the vanilla classes the report names and lists, per class, its Rust home files, where each method is
ported, and the known gaps (`missing` rows from the verdict map, seam/client/drift gaps). Often the cause is
already written there ("Type is not saved"). Then drill in:
```sh
python3 tools/port_ledger.py where MushroomCow.readAdditionalSaveData # ports, callers, callees, gaps
python3 tools/port_ledger.py class MushroomCow # every method: where, or missing
python3 tools/port_ledger.py what crates/horizon-client/src/mobs/farm.rs:120 # which vanilla method a fn ports
```
A ledger answer is a lead, not a verdict: about 1 in 12 map rows is wrong. Read the vanilla method (path and
line are in the output) and the Rust fn before changing anything.
## 2. Port it the vanilla way
- Translate the vanilla method, don't reinvent it: same order of operations, same constants, the same random
draws in the same order (`horizon_core::random`), Java `Math` through `horizon_core::jmath`.
- Open the fn's doc with the vanilla method it ports: `` /// `MushroomCow.readAdditionalSaveData`: ... ``.
That one line is what makes the next agent find it.
- Fix the path players run: single player is a client of the integrated server, so server logic lives in
`game/lan*.rs` and `src/lan/`, and what only the client's copy shows (particles, local sounds, hand
animation) must be reached from `game/remote*.rs`. A fix on the old in-frame path alone fixes nothing.
- Look at the neighbours the ledger lists for the same class: a `client` or `seam` gap next to your fix is
usually the same bug on the other path.
## 3. Before the PR
```sh
python3 tools/port_ledger.py check --base origin/main
```
It lists the ports your branch touches with their open gaps, citations to add, stale citations, and (against
the base) the gaps the branch closes and opens. Close what belongs to your fix, cite what you ported, leave
no new stale citation. If you ported a method the verdict map called `missing`, change its row in
`tools/port_map/*.tsv` to `port` with the fn (`file.rs::Owner::fn`) and run
`python3 tools/port_ledger.py map-check tools/port_map/<file>.tsv`.
Then the usual: the harness for the area (`docs/architecture.md`), `cargo check`/tests per `AGENTS.md`, and
a small PR that says which vanilla method it now matches.
## Finding work
```sh
python3 tools/port_ledger.py gaps --kind seam # ported on two paths, wired on one: the best leads
python3 tools/port_ledger.py gaps --kind client # client-only effects the server path never reaches
python3 tools/port_ledger.py gaps --kind drift # a port missing a sound, particle, item or constant
rg -n "player:" tools/port_map/*.tsv # missing behaviour a player would see
```
+10 -1
View File
@@ -1,2 +1,11 @@
#!/bin/sh
exec python3 tools/check_public_tree.py
# Git for Windows puts a `python3` on PATH that is only the Microsoft Store
# stub: it prints "Python" and exits non-zero, so the hook fails with no other
# output. Use the first name that actually runs Python 3.
for py in python3 python py; do
if "$py" -c "import sys; sys.exit(sys.version_info < (3,))" >/dev/null 2>&1; then
exec "$py" tools/check_public_tree.py
fi
done
echo "check_public_tree: no working Python 3 found" >&2
exit 1
+10 -1
View File
@@ -1,2 +1,11 @@
#!/bin/sh
exec python3 tools/check_public_tree.py --pre-push
# Git for Windows puts a `python3` on PATH that is only the Microsoft Store
# stub: it prints "Python" and exits non-zero, so the hook fails with no other
# output. Use the first name that actually runs Python 3.
for py in python3 python py; do
if "$py" -c "import sys; sys.exit(sys.version_info < (3,))" >/dev/null 2>&1; then
exec "$py" tools/check_public_tree.py --pre-push
fi
done
echo "check_public_tree: no working Python 3 found" >&2
exit 1
+14
View File
@@ -1,7 +1,21 @@
<!-- Opened by an AI agent? First line: "> 🤖 Opened by <your GitHub login>'s AI agent (<tool, model>), not by <your GitHub login> personally." Use your own login, not someone else's. -->
## What and why
<!-- What this changes, and why. Link the issue it fixes ("Fixes #123"). -->
## Kind
<!-- Tick one: it sets the pull request's label (CONTRIBUTING.md "Pull requests"). -->
- [ ] parity: Horizon now does what vanilla 26.3 does
- [ ] bug: a crash or a breakage
- [ ] perf: faster, or less memory
- [ ] feature: something new for players (listed under New in the release notes)
- [ ] extra: Horizon-only, off by default
- [ ] tooling: tools, CI, tests, harnesses
- [ ] docs
## Vanilla reference
<!-- The vanilla classes and methods this follows (tools/rea_vanilla.py), and the harness that checks it.
+9
View File
@@ -0,0 +1,9 @@
# Trusted reviewers: one list for two things (docs/release-flow.md "Trusted reviewers").
# - `/nightly 2640 2642` picks the pull requests the Nightly holds (tools/ci/nightly_set.py);
# - `/close`, `/reopen`, `/label`, `/unlabel` triage issues and pull requests (tools/ci/triage_command.py).
# Ludovic is always allowed and isn't listed. One GitHub login per line, with who added it and when. Removing a
# line revokes that login at once. A line with only `paused` stops both for every reviewer (Ludovic's still work).
kacpherek # added by Ludovic, 2026-10-27
rdbtCVS # added by Ludovic, 2026-10-27
Online5277 # added by Ludovic, 2026-10-27 (his top three by merged pull requests)
levzzz5154 # added by Ludovic, 2026-10-28 (Horizon Team; author of horizon-mixins)
+6 -4
View File
@@ -16,12 +16,14 @@ jobs:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
BRANCH: ${{ github.event.pull_request.head.ref }}
SHA: ${{ github.event.pull_request.head.sha }}
HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }}
run: |
# Every head the pull request had, not only its last: a run-parity label added on an older head left that
# run queued after the merge (#3104, 2026-10-29). Its branch in its own repository names its runs (another
# fork can have a branch of the same name).
for status in queued in_progress waiting pending requested; do
gh run list -R "$REPO" --branch "$BRANCH" --status "$status" --limit 100 \
--json databaseId,event,headSha \
-q ".[] | select(.event == \"pull_request\" and .headSha == \"$SHA\") | .databaseId"
gh api -X GET "repos/$REPO/actions/runs" -f branch="$BRANCH" -f event=pull_request -f status="$status" \
-f per_page=100 --jq ".workflow_runs[] | select(.head_repository.full_name == \"$HEAD_REPO\") | .id"
done | sort -u | while read -r id; do
gh run cancel -R "$REPO" "$id" || true
done
+5 -6
View File
@@ -51,7 +51,7 @@ jobs:
if: steps.rust.outputs.required != 'false'
run: |
sudo apt-get update && sudo apt-get install -y --no-install-recommends libasound2-dev libudev-dev
rustup toolchain install stable --profile minimal --component rustfmt --component clippy --target wasm32-unknown-unknown
rustup toolchain install stable --profile minimal --component rustfmt --component clippy
# Dependencies and the workspace's own build, saved from main's runs (pull requests read main's cache).
- uses: Swatinem/rust-cache@v2
if: steps.rust.outputs.required != 'false'
@@ -62,8 +62,7 @@ jobs:
if: steps.rust.outputs.required != 'false'
run: |
cargo +stable fmt --all -- --check
# With the game's code mods (on in every release build, off by default).
cargo +stable clippy --workspace --all-targets --features horizon-client/mods -- -D warnings
cargo +stable clippy --workspace --all-targets -- -D warnings
# The server build (released as horizon-engine): the game without sound or controllers.
cargo +stable clippy -p horizon-client --all-targets --no-default-features -- -D warnings
# Libraries, binaries and integration tests, then doc tests: not the ~275 examples, which clippy above already
@@ -71,11 +70,11 @@ jobs:
- name: Test Rust core and voxel kernel correctness
if: steps.rust.outputs.required != 'false'
run: |
cargo +stable test --workspace --lib --bins --tests --features horizon-client/mods,horizon-mods/pack
cargo +stable test --workspace --lib --bins --tests
# The full content-root tests (#[ignore]d: minutes each on a laptop). The same build as above; they need the
# prepared vanilla data, so without it they pass at once.
cargo +stable test --workspace --lib --features horizon-client/mods,horizon-mods/pack mod_content::tests:: -- --ignored
cargo +stable test --workspace --doc --features horizon-client/mods,horizon-mods/pack
cargo +stable test --workspace --lib mod_content::tests:: -- --ignored
cargo +stable test --workspace --doc
# The launcher is its own workspace (launcher/Cargo.toml), so verify's cargo runs never build it. A pull request that
# changes it or a crate it builds is checked here, and so is every push to main: two pull requests can each pass
# alone and break it together (#2059 and the registry ratings did, 2026-10-26).
+74
View File
@@ -0,0 +1,74 @@
name: claim command
# `/claim` and `/unclaim` comments: anyone takes an issue before working on it, and a daily run releases the claims
# older than 5 days that no open pull request links (CONTRIBUTING.md "Claim before you start",
# tools/ci/claim_command.py).
#
# Why no pull request's code runs here, though the token can write:
# - `issue_comment` and `schedule` always run this file from main, whoever comments and wherever.
# - It checks out main's script and list only, never a pull request; it reads threads through the API.
# - The comment's text and author reach the script as env vars, never in a shell line. The token stays in the step
# and can only write issues (assignees, comments, reactions).
on:
issue_comment:
types: [created]
schedule:
- cron: "23 5 * * *"
workflow_dispatch:
permissions:
contents: read
jobs:
command:
# Every other comment skips here, before a runner starts. Pull requests are answered by the script.
if: >-
github.event_name == 'issue_comment' && github.event.comment.user.type != 'Bot' && (
startsWith(github.event.comment.body, '/claim') ||
startsWith(github.event.comment.body, '/unclaim'))
# One command per issue at a time, so two `/claim`s can't both find it free. On the job, not the workflow:
# the skipped runs of ordinary comments must not cancel a waiting one.
concurrency:
group: claim-${{ github.event.issue.number }}
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
issues: write
steps:
- uses: actions/checkout@v4
with:
ref: main
persist-credentials: false
sparse-checkout: |
.github/trusted-reviewers.txt
tools/ci/nightly_set.py
tools/ci/claim_command.py
sparse-checkout-cone-mode: false
- name: Run the command
env:
GH_TOKEN: ${{ github.token }}
BODY: ${{ github.event.comment.body }}
AUTHOR: ${{ github.event.comment.user.login }}
ISSUE: ${{ github.event.issue.number }}
COMMENT_ID: ${{ github.event.comment.id }}
REPO: ${{ github.repository }}
run: python3 tools/ci/claim_command.py command
release:
if: github.event_name != 'issue_comment' && github.repository == 'horizon-rust/horizon'
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
issues: write
steps:
- uses: actions/checkout@v4
with:
ref: main
persist-credentials: false
sparse-checkout: |
tools/ci/nightly_set.py
tools/ci/claim_command.py
sparse-checkout-cone-mode: false
- name: Release stale claims
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
run: python3 tools/ci/claim_command.py release
+290
View File
@@ -0,0 +1,290 @@
name: game build
# Builds the game for every platform from `ref`: the Java exporters, then Linux and the Linux server and Windows on
# GitHub, macOS on the Mac mini's runner with a 1500-frame offscreen run of a new world (smoke-test.json). Called by
# release.yml (the releases) and nightly-build.yml (the opt-in Nightly), always from main's copy of this file: the
# caller's commit, never the one being built. It holds no secrets: signing the checksums is the callers' own job,
# which never runs what this one built. See docs/release-flow.md.
on:
workflow_call:
inputs:
ref:
description: The commit to build
type: string
required: true
version:
description: The version built in (HORIZON_VERSION), e.g. 0.2.81 or nightly-2026-10-28
type: string
required: true
nightly:
description: A nightly's "<date> <short sha>" (HORIZON_NIGHTLY), shown by the game; empty for a release
type: string
default: ''
trusted:
# Only release.yml says true: it builds main. A nightly can hold pull requests nobody reviewed yet, whose
# build scripts and tests run here and could rewrite target/, the cargo home or a compiler cache. So an
# untrusted build never runs on the release runner, never saves the Rust cache (its key is its own anyway),
# and a trusted one never reads the machine's shared sccache, which pull requests' jobs write
# (tests/test_nightly.py pins all of it).
description: The source is main (a release); false for anything else (a nightly)
type: boolean
default: false
developer_id:
description: Sign the macOS programs with Developer ID and notarize the zip (the runner's keychain)
type: boolean
default: true
permissions:
contents: read
jobs:
exporters:
# This runner holds the maintainer's own pinned Minecraft jars. Only our classes leave it. An untrusted build
# goes to the Mac mini's nightly runner (macOS + horizon-nightly: ludovics-mac-mini-nightly, beside the one that
# runs pull requests' parity jobs, so a nightly never waits behind their queue), never the release runner
# (MAC_RUNNER: ludovics-mac-mini-release) nor parity-main's.
runs-on: ${{ inputs.trusted && vars.MAC_RUNNER || fromJSON('["self-hosted", "macOS", "horizon-nightly"]') }}
# Three full preparations (two isolated, one batched) besides the compile.
timeout-minutes: 60
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.ref }}
persist-credentials: false
clean: true
- uses: dtolnay/rust-toolchain@stable
- run: bash tools/ci/runner-env.sh
env:
HORIZON_NO_SCCACHE: ${{ inputs.trusted && '1' || '' }}
- run: bash tools/ci/vanilla-data.sh
- name: Compile and validate original exporters against the pinned jars
shell: bash
run: |
set -euo pipefail
# target/ is gone after the clean checkout, and vanilla-data.sh builds the preparer only when it
# prepares afresh.
cargo build --release --locked -p horizon-prepare
# Compiled here, never taken from the runner's shared preparations: pull requests' parity jobs
# write those (vanilla-data.sh links .local/exporters to them).
rm -rf .local/exporters
bundle=$(target/release/horizon-prepare compile-exporters --root "$PWD" | tail -1)
mkdir -p exporters
cp -R "$bundle/server" "$bundle/client" "$bundle/manifest.json" exporters/
# Only our classes and their manifest leave this runner.
stray=$(find exporters -type f ! -path exporters/manifest.json \
! \( -name '*.class' \( -path 'exporters/server/dev/horizonlab/parity/*' -o -path 'exporters/client/dev/horizonlab/parity/*' \) \))
[ -z "$stray" ] || { echo "::error::not one of our classes: $stray"; exit 1; }
python3 tools/check_prepare_exporters.py --binary target/release/horizon-prepare
- uses: actions/upload-artifact@v4
with:
name: original-exporters
path: exporters/
if-no-files-found: error
retention-days: 3
build:
needs: exporters
name: build ${{ matrix.name }}
strategy:
fail-fast: false
matrix:
include:
- name: linux
os: ubuntu-22.04
dir: horizon-linux-x86_64
asset: horizon-linux-x86_64.tar.gz
# The server build too: packaging/server/install.sh installs it on Linux.
server_dir: horizon-server-linux-x86_64
server_asset: horizon-server-linux-x86_64.tar.gz
- name: windows
os: windows-latest
dir: horizon-windows-x64
asset: horizon-windows-x64.zip
exe: .exe
# The C runtime linked in, so the game runs without the Visual C++ redistributable.
rustflags: -C target-feature=+crt-static
- name: macos
mac: true
dir: horizon-macos-arm64
asset: horizon-macos-arm64.zip
runs-on: ${{ matrix.mac && (inputs.trusted && vars.MAC_RUNNER || fromJSON('["self-hosted", "macOS", "horizon-nightly"]')) || matrix.os }}
# Notarizing the zip can take a few minutes.
timeout-minutes: ${{ matrix.mac && 60 || 40 }}
env:
RUSTFLAGS: ${{ matrix.rustflags }}
# The release's version, built in (horizon_core::release): --version, user agents, crash reports and
# telemetry say it. No version-bump commits.
HORIZON_VERSION: ${{ inputs.version }}
# A nightly's date and commit ("2026-10-28 0c02067"), which the game shows; empty for a release.
HORIZON_NIGHTLY: ${{ inputs.nightly }}
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.ref }}
persist-credentials: false
# Always from a clean checkout, the Mac mini's runner too: its work folder is shared with pull requests'
# parity jobs, and nothing they leave (target/, an untracked .cargo/config.toml, a build script's output)
# may end up in a release. target/ is rebuilt from scratch; .local/ is relinked by vanilla-data.sh, which
# keeps its preparations outside the checkout.
clean: true
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
if: runner.environment == 'github-hosted'
with:
# A release restores only what a release saved: an untrusted build has its own key and saves nothing.
shared-key: ${{ inputs.trusted && 'release' || 'nightly' }}-${{ matrix.name }}
save-if: ${{ inputs.trusted }}
- name: ALSA and udev headers
if: runner.os == 'Linux'
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends libasound2-dev libudev-dev
- name: Build the client, horizon-ctl, horizon-prepare and horizon-server
run: cargo build --release --locked -p horizon-client --bin horizon-client --bin horizon-ctl -p horizon-prepare --bin horizon-prepare -p horizon-server --bin horizon-server
- name: Every program the release folder takes was built
run: |
for binary in horizon-client horizon-ctl horizon-prepare horizon-server; do
test -f "target/release/$binary${{ matrix.exe }}" || { echo "::error::target/release/$binary${{ matrix.exe }} is missing"; exit 1; }
done
- name: The signing keychain, unlocked
if: runner.os == 'macOS' && vars.MACOS_SIGN_IDENTITY != '' && inputs.developer_id
env:
MACOS_SIGN_IDENTITY: ${{ vars.MACOS_SIGN_IDENTITY }}
run: packaging/macos/keychain.sh unlock
- name: Signed with Developer ID and the hardened runtime
# Before the checks below, so they run the programs as players will.
if: runner.os == 'macOS' && vars.MACOS_SIGN_IDENTITY != '' && inputs.developer_id
env:
MACOS_SIGN_IDENTITY: ${{ vars.MACOS_SIGN_IDENTITY }}
run: |
for binary in horizon-client horizon-ctl horizon-prepare horizon-server; do
codesign --force --options runtime --timestamp --keychain "$MACOS_SIGN_KEYCHAIN" \
--identifier "dev.horizon.${binary#horizon-}" --sign "$MACOS_SIGN_IDENTITY" "target/release/$binary"
codesign --verify --strict --verbose=2 "target/release/$binary"
done
- name: Only system libraries
# Windows: no non-system DLL (a "DLL not found" dialog on some players' machines). Linux: the standard
# loader and libraries every desktop distribution has. macOS: only the system's own libraries.
run: |
for binary in horizon-client horizon-ctl horizon-prepare horizon-server; do
file="target/release/$binary${{ matrix.exe }}"
if [[ "$RUNNER_OS" == macOS ]]; then
others=$(otool -L "$file" | tail -n +2 | awk '{print $1}' | grep -v -E '^(/usr/lib/|/System/Library/)' || true)
[[ -z "$others" ]] || { echo "::error::$binary links $others"; exit 1; }
else
${{ runner.os == 'Windows' && 'python' || 'python3' }} tools/check_imports.py "$file"
fi
done
- name: Every program starts
run: |
set -euo pipefail
bin=target/release
"$bin/horizon-client${{ matrix.exe }}" --help | grep -q -- '--resources'
"$bin/horizon-ctl${{ matrix.exe }}" --help > /dev/null
"$bin/horizon-prepare${{ matrix.exe }}" --help | grep -q -- '--minecraft-dir'
# The version is the release's.
for binary in horizon-prepare horizon-server; do
out=$("$bin/$binary${{ matrix.exe }}" --version | tr -d '\r')
echo "$out"
[[ "$out" == "$binary $HORIZON_VERSION" ]] || { echo "::error::expected $binary $HORIZON_VERSION"; exit 1; }
done
- name: Tools from the runner
if: runner.environment == 'self-hosted'
env:
HORIZON_NO_SCCACHE: ${{ inputs.trusted && '1' || '' }}
run: bash tools/ci/runner-env.sh
- name: Resources prepared from the runner's own Minecraft
if: runner.environment == 'self-hosted'
run: bash tools/ci/vanilla-data.sh
- name: The game runs (offscreen, a new world, 1500 frames)
if: runner.environment == 'self-hosted'
run: |
set -euo pipefail
# From a scratch folder: the game keeps its saves and options in the folder it starts in.
home="$RUNNER_TEMP/horizon-smoke"
rm -rf "$home" && mkdir -p "$home"
version=$(sed -n 's/.*"version": *"\([^"]*\)".*/\1/p' research-lock.json | head -1)
client="$PWD/target/release/horizon-client"
resources="$PWD/.local/horizon-client/$version"
(cd "$home" && HORIZON_HOME="$home" HORIZON_NO_AUDIO=1 "$client" --resources "$resources" --offscreen \
--world smoke --seed 1 --render-distance 4 --world-frames 1500 --timeout-seconds 180 \
--smoke-report "$home/smoke.json" > "$home/client.log" 2>&1) || { tail -40 "$home/client.log"; exit 1; }
python3 - "$home/smoke.json" <<'EOF'
import json, sys
report = json.load(open(sys.argv[1]))
print(json.dumps(report))
if report.get("error") is not None or report.get("frames", 0) < 1500 or not report.get("chunks") \
or not report.get("meshed_sections") or not report.get("player"):
sys.exit("the smoke run did not draw a world")
EOF
# Published with the release; the publish job checks it again before it publishes anything.
cp "$home/smoke.json" "$RUNNER_TEMP/smoke-test.json"
- uses: actions/upload-artifact@v4
if: runner.environment == 'self-hosted'
with:
name: smoke-test.json
path: ${{ runner.temp }}/smoke-test.json
if-no-files-found: error
retention-days: 3
- uses: actions/download-artifact@v4
with:
name: original-exporters
path: exporters
- name: Package
run: |
set -euo pipefail
cargo run --locked -p xtask -- package "target/release/horizon-client${{ matrix.exe }}" "dist/${{ matrix.dir }}"
cd dist
rm -f "${{ matrix.asset }}"
case "${{ matrix.asset }}" in
*.tar.gz) tar czf "${{ matrix.asset }}" "${{ matrix.dir }}" ;;
*) if [[ "$RUNNER_OS" == Windows ]]; then 7z a -tzip "${{ matrix.asset }}" "${{ matrix.dir }}" > /dev/null
else
# Without resource forks and extended attributes: plain ditto adds an
# AppleDouble `._` file beside every file that has any.
ditto -c -k --norsrc --noextattr --noqtn --keepParent "${{ matrix.dir }}" "${{ matrix.asset }}"
if unzip -Z1 "${{ matrix.asset }}" | grep -E '(^|/)(\._|__MACOSX/)'; then
echo "::error::macOS metadata files in ${{ matrix.asset }}"; exit 1
fi
fi ;;
esac
ls -l "${{ matrix.asset }}"
- name: Notarized
# A zip of programs can't be stapled: Gatekeeper asks Apple the first time each one runs.
if: runner.os == 'macOS' && vars.MACOS_SIGN_IDENTITY != '' && inputs.developer_id
run: |
for binary in horizon-client horizon-ctl horizon-prepare horizon-server; do
codesign --verify --strict "dist/${{ matrix.dir }}/$binary"
done
packaging/macos/notarize.sh "dist/${{ matrix.asset }}"
- uses: actions/upload-artifact@v4
with:
name: ${{ matrix.asset }}
path: dist/${{ matrix.asset }}
if-no-files-found: error
retention-days: 3
- name: The server build (the game without sound or controllers, as horizon-engine)
if: matrix.server_asset
run: |
set -euo pipefail
# After the client is packaged: target/release/horizon-client is rebuilt without the audio and
# controllers features, so it needs neither libasound nor libudev (minimal server images lack them).
cargo build --release --locked -p horizon-client --no-default-features --bin horizon-client
cargo run --locked -p xtask -- package-server target/release/horizon-client "dist/${{ matrix.server_dir }}"
folder="dist/${{ matrix.server_dir }}"
python3 tools/check_imports.py --server "$folder/horizon-engine" "$folder/horizon-server" "$folder/horizon-prepare"
"$folder/horizon-engine" --help | grep -q -- '--resources'
[[ "$("$folder/horizon-server" --version)" == "horizon-server $HORIZON_VERSION" ]]
cd dist
rm -f "${{ matrix.server_asset }}"
tar czf "${{ matrix.server_asset }}" "${{ matrix.server_dir }}"
ls -l "${{ matrix.server_asset }}"
- uses: actions/upload-artifact@v4
if: matrix.server_asset
with:
name: ${{ matrix.server_asset }}
path: dist/${{ matrix.server_asset }}
if-no-files-found: error
retention-days: 3
- name: The signing keychain, locked again
if: always() && runner.os == 'macOS' && vars.MACOS_SIGN_IDENTITY != '' && inputs.developer_id
run: packaging/macos/keychain.sh lock
+3 -1
View File
@@ -192,9 +192,11 @@ jobs:
retry "$launcher" install latest
"$launcher" versions --installed
"$launcher" links --json
clients=("$HORIZON_LAUNCHER_HOME"/versions/v*/horizon-client*)
# `install latest` is the Nightly since #3342 (versions/nightly-<date>-<commit>), a vX.Y.Z before.
clients=("$HORIZON_LAUNCHER_HOME"/versions/*/horizon-client*)
client="${clients[0]}"
echo "installed client: $client"
[[ -f "$client" ]] || { echo "::error::no game installed under versions/"; exit 1; }
# The game's Linux build is the game's to keep portable (release.yml checks it): a
# release linked to a Nix store is a warning here, not a broken launcher.
soft() { if [[ "$RUNNER_OS" == "Linux" ]]; then "$@" || echo "::warning::$* failed"; else "$@"; fi; }
+278
View File
@@ -0,0 +1,278 @@
name: nightly build
run-name: nightly build${{ inputs.request && format(' (request {0})', inputs.request) || '' }}
# The Nightly, the only build Horizon ships since 2026-10-28 (docs/release-flow.md "Nightly"; Ludovic). When the batch
# train decides (release.yml, with the commit as `base`), once a night, and when a trusted
# reviewer asks (`/nightly 2640 2642`, nightly-command.yml), it builds main plus the pull requests the reviewers
# picked, merged in order (tools/ci/nightly_set.py), or main alone with none picked. Main is its newest commit where
# checks and parity-main passed (or the `base` input), as for a release, and no Nightly is published while a
# release-blocker is open. It publishes the result as the `nightly` pre-release, which every launcher and
# `horizon-server update` install. The previous nightly stays a week as nightly-<date>.
#
# Why the signing key never meets the pull requests' code:
# - `pick` merges the picked heads onto main with `git merge-tree --write-tree`: git objects only, no working tree,
# so none of their files reaches the disk and no hook, filter or script of theirs runs. It refuses any pull
# request that changes a path CODEOWNERS gives to Ludovic (this folder, tools/ci/, the signing and update code),
# pushes the merge as nightly/build-<date>-<n> and saves the set on the nightly-state branch.
# - This file, game-build.yml and every script below run from main's copy: a schedule runs the default branch, and
# `uses: ./…` takes the caller's commit. A picked pull request's own .github/ is never read.
# - `build` (game-build.yml, `trusted: false`) never reaches a release's tools: its macOS jobs run on the runner
# that already runs pull requests' parity jobs, never the release runner; its Rust cache has its own key and is
# never saved; and releases don't read the machine's shared sccache.
# - `build` (game-build.yml) checks out and compiles the merge with no secrets: its token reads contents only and
# isn't kept in the checkout; the macOS programs are signed ad hoc (Developer ID needs the runner's keychain,
# which stays locked for nightlies).
# - `sign` is the only job with RELEASE_SIGNING_KEY. It checks out nothing but main's tools/ci/sign_release.sh, which
# runs the runner's sha256sum and OpenSSL over the archives: it hashes them, never runs them. The signed message
# names the channel ("horizon release nightly"), so a nightly's signature can't pass for a release's.
# - `publish` verifies the signature with main's horizon-release-sign, checks the smoke test, and only then moves
# the tag. Like sign, it never executes what build produced.
# - The source ships with the build: `pick` writes horizon-source.tar.gz with `git archive` (git objects again, nothing
# run), `sign` signs it with the builds, and `publish` attaches it and tags the commit nightly-source/<date>-<short>
# for good (the dated release and the build branch go after 7 days; native mods pin the commit: docs/native-mods.md).
# gh workflow run "nightly build" # now (skips when the pick is the same commit as last night's)
# gh workflow run "nightly build" -f force=true # build and publish even if unchanged
# gh workflow run "nightly build" -f dry_run=true # build and sign with a throwaway key, publish nothing
on:
schedule:
- cron: '17 20 * * *' # 20:17 UTC: after the day's waves, before the 03:07 Paris parity audit takes the Mac mini
workflow_dispatch:
inputs:
force:
description: Build and publish even if tonight's pick is the commit already published
type: boolean
default: false
dry_run:
description: Build and sign with a throwaway key, publish nothing
type: boolean
default: false
issue:
description: The issue or pull request whose `/nightly` comment asked for this build (answered there)
type: string
default: ''
request:
description: The command's id, in the run's name so it can link the run
type: string
default: ''
prs:
description: A dry run's own set, `2640@<head> 2642@<head>` (the saved set stays)
type: string
default: ''
base:
description: The commit of main to build on (default main's newest with green checks and parity-main)
type: string
default: ''
urgent:
description: Past the day's cap of 12 builds (the train, for a release-now pull request or a forced run)
type: boolean
default: false
permissions:
contents: read
concurrency:
# One build at a time. A newer request replaces one still waiting: it builds the set as it is by then, so no pick
# is lost.
group: nightly-build
cancel-in-progress: false
jobs:
pick:
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: write # nightly/build-<date>-<n> and the nightly-state branch (main is protected)
issues: write # says in the thread when the picked pull requests conflict
pull-requests: write
actions: read # main's newest green checks and parity-main runs (the base)
outputs:
build: ${{ steps.pick.outputs.build }}
sha: ${{ steps.pick.outputs.sha }}
short: ${{ steps.pick.outputs.short }}
source: ${{ steps.pick.outputs.source }}
date: ${{ steps.pick.outputs.date }}
version: ${{ steps.pick.outputs.version }}
nightly: ${{ steps.pick.outputs.nightly }}
source_tag: ${{ steps.pick.outputs.source_tag }}
dry_run: ${{ steps.mode.outputs.dry_run }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
persist-credentials: false
- id: mode
# A dry run never publishes; a hand-started run off main is always one.
run: echo "dry_run=${{ github.event_name == 'workflow_dispatch' && (inputs.dry_run || github.ref != 'refs/heads/main') }}" >> "$GITHUB_OUTPUT"
- id: pick
env:
GH_TOKEN: ${{ github.token }}
FORCE: ${{ (inputs.force || steps.mode.outputs.dry_run == 'true') && 'yes' || '' }}
DRY_RUN: ${{ steps.mode.outputs.dry_run }}
NIGHTLY_ISSUE: ${{ inputs.issue }}
NIGHTLY_PRS: ${{ inputs.prs }}
NIGHTLY_BASE: ${{ inputs.base }}
NIGHTLY_URGENT: ${{ inputs.urgent }}
run: |
python3 tools/ci/nightly.py pick ${FORCE:+--force} | tee -a "$GITHUB_STEP_SUMMARY"
{ echo; cat nightly-notes.md; } >> "$GITHUB_STEP_SUMMARY"
- name: Old nightly/build-* branches deleted (scheduled runs only)
# Ludovic, 2026-10-27: older than 7 days, but never the newest build or one a published nightly points to;
# nothing but nightly/build-* (nightly.py stale_builds, tests/test_nightly.py).
if: github.event_name == 'schedule'
env:
GH_TOKEN: ${{ github.token }}
run: |
{ echo "### nightly/build-* branches"; python3 tools/ci/nightly.py prune-builds --days 7; } | tee -a "$GITHUB_STEP_SUMMARY"
- name: The source of the build (horizon-source.tar.gz)
if: steps.pick.outputs.build == 'true'
env:
SHA: ${{ steps.pick.outputs.sha }}
run: git archive --format=tar.gz --prefix="horizon-${SHA:0:7}/" -o horizon-source.tar.gz "$SHA"
- uses: actions/upload-artifact@v4
if: steps.pick.outputs.build == 'true'
with:
name: horizon-source
path: horizon-source.tar.gz
if-no-files-found: error
retention-days: 3
- uses: actions/upload-artifact@v4
with:
name: nightly-notes
path: nightly-notes.md
retention-days: 3
build:
needs: pick
if: needs.pick.outputs.build == 'true'
uses: ./.github/workflows/game-build.yml
with:
ref: ${{ needs.pick.outputs.sha }}
version: ${{ needs.pick.outputs.version }}
nightly: ${{ needs.pick.outputs.nightly }}
developer_id: false
trusted: false # it can hold unreviewed pull requests: their own cache key, never saved, not the release runner
sign:
needs: [pick, build]
runs-on: ubuntu-latest
timeout-minutes: 10
# The key's only job: main's sign_release.sh hashes the archives and signs with the runner's OpenSSL.
environment: ${{ needs.pick.outputs.dry_run != 'true' && 'release' || '' }}
steps:
- uses: actions/checkout@v4
with:
sparse-checkout: tools/ci/sign_release.sh
sparse-checkout-cone-mode: false
persist-credentials: false
- uses: actions/download-artifact@v4
with:
path: dist
pattern: horizon-*
merge-multiple: true
- name: Sign the checksums (SHA256SUMS, SHA256SUMS.sig) for the nightly channel
env:
RELEASE_SIGNING_KEY: ${{ secrets.RELEASE_SIGNING_KEY }}
RELEASE_SIGNING_KEY_NEXT: ${{ secrets.RELEASE_SIGNING_KEY_NEXT }}
EPHEMERAL: ${{ needs.pick.outputs.dry_run == 'true' && 'yes' || '' }}
run: |
bash tools/ci/sign_release.sh nightly dist/horizon-linux-x86_64.tar.gz dist/horizon-windows-x64.zip \
dist/horizon-macos-arm64.zip dist/horizon-server-linux-x86_64.tar.gz dist/horizon-source.tar.gz
- uses: actions/upload-artifact@v4
with:
name: signature
path: dist/SHA256SUMS*
if-no-files-found: error
retention-days: 3
- uses: actions/upload-artifact@v4
if: needs.pick.outputs.dry_run == 'true'
with:
name: ephemeral.pub
path: dist/ephemeral.pub
retention-days: 3
publish:
needs: [pick, build, sign]
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: write
issues: read # the open release-blocker issues (gate)
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/download-artifact@v4
with:
path: dist
merge-multiple: true
- name: The signature, checked as launchers check it
env:
EPHEMERAL: ${{ needs.pick.outputs.dry_run == 'true' && 'yes' || '' }}
run: |
key=(); [[ -n "$EPHEMERAL" ]] && key=(--key "$(cat dist/ephemeral.pub)")
cargo run --locked --quiet -p horizon-release-sign -- verify --tag nightly --publishing "${key[@]}" dist
- name: Ready to publish (the smoke test drew a world, no release-blocker is open)
# Right before publishing, as for a release: an issue labelled while the builds ran still holds it. A dry run
# only reports what a real run would do (that is how a blocker's fix is tried).
env:
GH_TOKEN: ${{ github.token }}
DRY_RUN: ${{ needs.pick.outputs.dry_run }}
run: |
if ! python3 tools/release_train.py gate --kind game --smoke dist/smoke-test.json --blockers; then
[[ "$DRY_RUN" == true ]] || { echo "::error::Nothing published"; exit 1; }
echo "::warning::A real run would publish nothing"
fi
- name: Publish as the `nightly` pre-release
if: needs.pick.outputs.dry_run != 'true'
env:
GH_TOKEN: ${{ github.token }}
SHA: ${{ needs.pick.outputs.sha }}
DATE: ${{ needs.pick.outputs.date }}
SOURCE_TAG: ${{ needs.pick.outputs.source_tag }}
run: |
set -euo pipefail
python3 tools/ci/nightly.py rotate
signature=(); [[ -f dist/SHA256SUMS.sig ]] && signature=(dist/SHA256SUMS.sig)
# A pre-release, never GitHub's latest: the latest stays the last vX.Y.Z, which launchers and servers from
# before the Nightly-only switch keep (they don't read Nightlies).
gh release create nightly --prerelease --latest=false --target "$SHA" \
--title "Nightly $DATE · ${SHA:0:7}" --notes-file dist/nightly-notes.md \
"dist/horizon-linux-x86_64.tar.gz#Nightly game files: Linux (horizon-linux-x86_64.tar.gz)" \
"dist/horizon-windows-x64.zip#Nightly game files: Windows (horizon-windows-x64.zip)" \
"dist/horizon-macos-arm64.zip#Nightly game files: macOS (horizon-macos-arm64.zip)" \
"dist/horizon-server-linux-x86_64.tar.gz#Nightly dedicated server for Linux (horizon-server-linux-x86_64.tar.gz)" \
"dist/horizon-source.tar.gz#Source of this Nightly, commit ${SHA:0:7} (horizon-source.tar.gz; kept for good as the tag $SOURCE_TAG)" \
dist/SHA256SUMS "${signature[@]}" dist/smoke-test.json
python3 tools/ci/nightly.py keep-source "$SHA" "$DATE"
python3 tools/ci/nightly.py prune --days 7
- name: Dry run, the nightly that would be published
if: needs.pick.outputs.dry_run == 'true'
run: |
echo "Would publish nightly at ${{ needs.pick.outputs.sha }} (${{ needs.pick.outputs.source }})"
ls -l dist
answer:
# Tells the thread that asked for this build how it ended. A conflict `pick` already explained there.
needs: [pick, build, sign, publish]
if: always() && inputs.issue != '' && needs.pick.result == 'success'
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
issues: write
pull-requests: write
steps:
- env:
GH_TOKEN: ${{ github.token }}
ISSUE: ${{ inputs.issue }}
BUILD: ${{ needs.pick.outputs.build }}
PUBLISHED: ${{ needs.publish.result }}
DRY_RUN: ${{ needs.pick.outputs.dry_run }}
NAME: Nightly ${{ needs.pick.outputs.date }} · ${{ needs.pick.outputs.short }}
RUN: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
if [[ "$BUILD" != true ]]; then
text="nothing new: the published Nightly is already this build."
elif [[ "$PUBLISHED" == success && "$DRY_RUN" == true ]]; then
text="the dry run built and signed $NAME; nothing was published. $RUN"
elif [[ "$PUBLISHED" == success ]]; then
text="published $NAME: https://github.com/$GITHUB_REPOSITORY/releases/tag/nightly. Launchers install it at their next check."
else
text="the build failed, nothing was published: $RUN"
fi
gh api "repos/$GITHUB_REPOSITORY/issues/$ISSUE/comments" -f body="🤖 Nightly: $text"
+46
View File
@@ -0,0 +1,46 @@
name: nightly command
# `/nightly …` comments: the trusted reviewers in .github/trusted-reviewers.txt (and Ludovic) pick the pull requests
# the Nightly holds (docs/release-flow.md "Nightly channel", tools/ci/nightly_set.py). This job only checks who asked,
# saves the set on the nightly-state branch, starts `nightly build` and answers in the thread.
#
# Why no pull request's code runs here, though the token can write:
# - `issue_comment` always runs this file from main, whoever comments and wherever; a pull request's own copy of it
# is never read.
# - It checks out main and runs main's nightly_set.py. The comment's text and author reach it as env vars, never in
# a shell line.
# - It never checks out, fetches or merges a pull request: it reads their file lists through the API. The merge
# happens in nightly-build.yml's `pick`, with git objects only; their code runs in its `build`, without secrets.
on:
issue_comment:
types: [created]
permissions:
contents: read
jobs:
command:
# Every other comment skips here, before a runner starts.
if: startsWith(github.event.comment.body, '/nightly') && github.event.comment.user.type != 'Bot'
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: write # the nightly-state branch (main is protected)
actions: write # starts `nightly build`
issues: write # answers in the thread
pull-requests: write
steps:
- uses: actions/checkout@v4
with:
ref: main
persist-credentials: false
sparse-checkout: |
.github/CODEOWNERS
.github/trusted-reviewers.txt
tools/ci/nightly_set.py
sparse-checkout-cone-mode: false
- name: Run the command
env:
GH_TOKEN: ${{ github.token }}
BODY: ${{ github.event.comment.body }}
AUTHOR: ${{ github.event.comment.user.login }}
ISSUE: ${{ github.event.issue.number }}
COMMENT_URL: ${{ github.event.comment.html_url }}
run: python3 tools/ci/nightly_set.py command
+41
View File
@@ -0,0 +1,41 @@
name: overlap note
# One comment on a pull request naming the other open pull requests that close the same issue or change the same
# functions, edited on every push (tools/ci/overlap_note.py, issue #3859).
#
# Why no pull request's code runs here, though `pull_request_target` gives a token that can write:
# - `pull_request_target` runs this file and the script from main, never the pull request's copy.
# - The checkout is main's (`ref: main`), blobless, with the script alone on disk. The pull requests' heads are fetched
# as git objects and only diffed, never checked out; git runs with hooks, fsmonitor, external diff and textconv off.
# - The rest comes from the API. The token writes the comment only.
on:
pull_request_target:
types: [opened, reopened, synchronize, ready_for_review]
permissions:
contents: read
jobs:
note:
if: github.event.pull_request.base.ref == 'main'
# A newer push replaces the note being computed.
concurrency:
group: overlap-note-${{ github.event.pull_request.number }}
cancel-in-progress: true
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v4
with:
ref: main
fetch-depth: 0
filter: blob:none
persist-credentials: false
sparse-checkout: |
tools/ci/overlap_note.py
sparse-checkout-cone-mode: false
- name: Note
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
run: python3 tools/ci/overlap_note.py event
+36
View File
@@ -0,0 +1,36 @@
name: pr labels
# A pull request's kind label (the box ticked in the template) and area labels (the paths it changes):
# tools/ci/pr_labels.py, CONTRIBUTING.md "Pull requests".
#
# Why no pull request's code runs here, though `pull_request_target` gives a token that can write to fork PRs:
# - `pull_request_target` runs this file and the script from main, never the pull request's copy.
# - It checks out main's script only (`ref: main`), never the pull request's head or merge commit.
# - The script reads the event file (the description is data in JSON, never in a shell line) and the API's list of
# changed file names; it runs, builds and imports nothing of theirs. The token writes labels only.
on:
pull_request_target:
types: [opened, reopened, synchronize, edited, ready_for_review]
permissions:
contents: read
jobs:
label:
# Title-only edits change nothing: skip them before a runner starts.
if: github.event.action != 'edited' || github.event.changes.body != null
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v4
with:
ref: main
persist-credentials: false
sparse-checkout: |
tools/ci/pr_labels.py
sparse-checkout-cone-mode: false
- name: Label
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
run: python3 tools/ci/pr_labels.py event
+31 -377
View File
@@ -1,22 +1,15 @@
name: release
# Automatic game releases (docs/release-flow.md). After every green `checks` run on main, every 10 minutes (so a
# release waiting for merges to settle goes out without another merge) and by hand, tools/release_train.py decides,
# for a commit whose checks and parity-main (the vanilla harnesses on main) both passed:
# a release when 5 pull requests merged since the last one changed what players run and the newest merged 5 minutes
# ago, or when the oldest has waited 6 hours, or at once for a pull request labelled release-now.
# RELEASES_PAUSED=1 (repository variable) stops them; so does an open issue labelled release-blocker, forced runs too.
# Builds Linux (an older Ubuntu, so the binary runs on any recent distribution and on NixOS through nix-ld) and
# Windows on GitHub, macOS on the Mac mini's runner (MAC_RUNNER=macos-latest sends it to GitHub), then
# publishes vX.Y.Z on the release line in .github/release-line.txt (docs/versioning.md; without it, the dated
# vYYYY.MM.DD[.N] from before 0.2) with notes from the merged pull requests, as the release and GitHub's latest: one
# channel, which launchers, `horizon-server update` and install.sh follow at once. So it is published only after the
# checks that used to gate a promotion pass in this run: the Mac mini's smoke test drew a world, and no release-blocker
# is open (tools/release_train.py gate); otherwise nothing is published. macOS: with the repository variable
# MACOS_SIGN_IDENTITY set, the programs are signed with
# Developer ID and the hardened runtime and the zip is notarized, from the runner's signing keychain
# (packaging/macos/README.md); unset (forks), ad hoc.
# gh workflow run release --ref <branch> # dry run: builds and notes, publishes nothing
# gh workflow run release -f dry_run=false -f force=true # on main: release now, whatever the rules say
# Automatic game releases (docs/release-flow.md). Horizon ships only the Nightly since 2026-10-28 (Ludovic): this
# workflow is the batch train that decides when a new Nightly goes out, and `nightly build` (nightly-build.yml) builds,
# signs and publishes it. After every green `checks` run on main, every 10 minutes (so a batch waiting for merges to
# settle goes out without another merge) and by hand, tools/release_train.py decides, for a commit whose checks and
# parity-main (the vanilla harnesses on main) both passed: a Nightly when 5 pull requests merged since the published
# Nightly changed what players run and the newest merged 5 minutes ago, or when the oldest has waited 6 hours, or at
# once for a pull request labelled release-now. RELEASES_PAUSED=1 (repository variable) stops it; so does an open issue
# labelled release-blocker, forced runs too (nightly-build.yml checks again before publishing). One Nightly build at a
# time, at most 12 a day (release-now and force=true pass the cap). No `vX.Y.Z` game release is made any more.
# gh workflow run release --ref <branch> # dry run: decides, starts nothing
# gh workflow run release -f dry_run=false -f force=true # on main: a Nightly now, whatever the rules say
on:
workflow_run:
# A game release ships a commit both passed on (parity-main runs on batches: the newest commit it passed on).
@@ -28,19 +21,24 @@ on:
workflow_dispatch:
inputs:
dry_run:
description: Build everything and write the notes, but publish nothing
description: Decide only; start no Nightly build
type: boolean
default: true
force:
description: Release even without player-facing changes, before merges settle or while paused
description: A Nightly even without player-facing changes, before merges settle, while paused or past the day's cap
type: boolean
default: false
permissions:
contents: read
# The open release-blocker issues (decide).
issues: read
# The checks, parity-main and nightly build runs (decide).
actions: read
concurrency:
group: game-release
# One release at a time. A hand-started dry run publishes nothing, so it can't race a real release: it gets a
# group of its own. In the shared group, a release queued behind it replaced it as the pending run and GitHub
# cancelled it (#2779's dry run, 2026-10-27), which happens all day while releases go out in batches.
group: game-release${{ github.event_name == 'workflow_dispatch' && inputs.dry_run && '-dry-run' || '' }}
cancel-in-progress: false
jobs:
decide:
@@ -51,9 +49,8 @@ jobs:
timeout-minutes: 10
outputs:
release: ${{ steps.decide.outputs.release }}
tag: ${{ steps.decide.outputs.tag }}
version: ${{ steps.decide.outputs.version }}
head: ${{ steps.decide.outputs.head }}
urgent: ${{ steps.decide.outputs.urgent }}
dry_run: ${{ steps.mode.outputs.dry_run }}
steps:
- uses: actions/checkout@v4
@@ -73,364 +70,21 @@ jobs:
run: |
python3 tools/release_train.py decide --kind game ${HEAD_SHA:+--head "$HEAD_SHA"} ${FORCE:+--force} | tee decision.json
echo "### $(jq -r '.reason' decision.json)" >> "$GITHUB_STEP_SUMMARY"
# The version the programs are built with (docs/versioning.md): the tag without its v.
tag=$(jq -r '.tag' decision.json)
echo "version=${tag#v}" >> "$GITHUB_OUTPUT"
# A dry run builds whatever the decision, to test the builds.
if [[ "${{ steps.mode.outputs.dry_run }}" == true ]]; then echo "release=true" >> "$GITHUB_OUTPUT"; fi
exporters:
nightly:
# The Nightly is the release: nightly-build.yml builds the decided commit plus the reviewers' picks, signs it for
# the Nightly channel and publishes it once its smoke test passed and no release-blocker is open.
needs: decide
if: needs.decide.outputs.release == 'true'
# This runner holds the maintainer's own pinned Minecraft jars. Only our classes leave it.
runs-on: ${{ vars.MAC_RUNNER || 'ludovics-mac-mini' }}
# Three full preparations (two isolated, one batched) besides the compile.
timeout-minutes: 60
steps:
- uses: actions/checkout@v4
with:
ref: ${{ needs.decide.outputs.head }}
persist-credentials: false
clean: true
- uses: dtolnay/rust-toolchain@stable
- run: bash tools/ci/runner-env.sh
- run: bash tools/ci/vanilla-data.sh
- name: Compile and validate original exporters against the pinned jars
shell: bash
run: |
set -euo pipefail
# target/ is gone after the clean checkout, and vanilla-data.sh builds the preparer only when it
# prepares afresh.
cargo build --release --locked -p horizon-prepare
# Compiled here, never taken from the runner's shared preparations: pull requests' parity jobs
# write those (vanilla-data.sh links .local/exporters to them).
rm -rf .local/exporters
bundle=$(target/release/horizon-prepare compile-exporters --root "$PWD" | tail -1)
mkdir -p exporters
cp -R "$bundle/server" "$bundle/client" "$bundle/manifest.json" exporters/
# Only our classes and their manifest leave this runner.
stray=$(find exporters -type f ! -path exporters/manifest.json \
! \( -name '*.class' \( -path 'exporters/server/dev/horizonlab/parity/*' -o -path 'exporters/client/dev/horizonlab/parity/*' \) \))
[ -z "$stray" ] || { echo "::error::not one of our classes: $stray"; exit 1; }
python3 tools/check_prepare_exporters.py --binary target/release/horizon-prepare
- uses: actions/upload-artifact@v4
with:
name: original-exporters
path: exporters/
if-no-files-found: error
retention-days: 3
build:
needs: [decide, exporters]
if: needs.decide.outputs.release == 'true'
name: build ${{ matrix.name }}
strategy:
fail-fast: false
matrix:
include:
- name: linux
os: ubuntu-22.04
dir: horizon-linux-x86_64
asset: horizon-linux-x86_64.tar.gz
# The server build too: packaging/server/install.sh installs it on Linux.
server_dir: horizon-server-linux-x86_64
server_asset: horizon-server-linux-x86_64.tar.gz
- name: windows
os: windows-latest
dir: horizon-windows-x64
asset: horizon-windows-x64.zip
exe: .exe
# The C runtime linked in, so the game runs without the Visual C++ redistributable.
rustflags: -C target-feature=+crt-static
- name: macos
mac: true
dir: horizon-macos-arm64
asset: horizon-macos-arm64.zip
runs-on: ${{ matrix.mac && (vars.MAC_RUNNER || 'ludovics-mac-mini') || matrix.os }}
# Notarizing the zip can take a few minutes.
timeout-minutes: ${{ matrix.mac && 60 || 40 }}
env:
RUSTFLAGS: ${{ matrix.rustflags }}
# The release's version, built in (horizon_core::release): --version, user agents, crash reports and
# telemetry say it. No version-bump commits.
HORIZON_VERSION: ${{ needs.decide.outputs.version }}
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v4
with:
ref: ${{ needs.decide.outputs.head }}
persist-credentials: false
# Always from a clean checkout, the Mac mini's runner too: its work folder is shared with pull requests'
# parity jobs, and nothing they leave (target/, an untracked .cargo/config.toml, a build script's output)
# may end up in a release. target/ is rebuilt from scratch; .local/ is relinked by vanilla-data.sh, which
# keeps its preparations outside the checkout.
clean: true
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
if: runner.environment == 'github-hosted'
with:
shared-key: release-${{ matrix.name }}
- name: ALSA and udev headers
if: runner.os == 'Linux'
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends libasound2-dev libudev-dev
- name: Build the client, horizon-ctl, horizon-prepare and horizon-server
# `horizon-client/mods`: every released game runs code mods (the feature is off in dev builds).
run: cargo build --release --locked --features horizon-client/mods -p horizon-client --bin horizon-client --bin horizon-ctl -p horizon-prepare --bin horizon-prepare -p horizon-server --bin horizon-server
- name: Every program the release folder takes was built
run: |
for binary in horizon-client horizon-ctl horizon-prepare horizon-server; do
test -f "target/release/$binary${{ matrix.exe }}" || { echo "::error::target/release/$binary${{ matrix.exe }} is missing"; exit 1; }
done
- name: The signing keychain, unlocked
if: runner.os == 'macOS' && vars.MACOS_SIGN_IDENTITY != ''
env:
MACOS_SIGN_IDENTITY: ${{ vars.MACOS_SIGN_IDENTITY }}
run: packaging/macos/keychain.sh unlock
- name: Signed with Developer ID and the hardened runtime
# Before the checks below, so they run the programs as players will.
if: runner.os == 'macOS' && vars.MACOS_SIGN_IDENTITY != ''
env:
MACOS_SIGN_IDENTITY: ${{ vars.MACOS_SIGN_IDENTITY }}
run: |
for binary in horizon-client horizon-ctl horizon-prepare horizon-server; do
codesign --force --options runtime --timestamp --keychain "$MACOS_SIGN_KEYCHAIN" \
--identifier "dev.horizon.${binary#horizon-}" --sign "$MACOS_SIGN_IDENTITY" "target/release/$binary"
codesign --verify --strict --verbose=2 "target/release/$binary"
done
- name: Only system libraries
# Windows: no non-system DLL (a "DLL not found" dialog on some players' machines). Linux: the standard
# loader and libraries every desktop distribution has. macOS: only the system's own libraries.
run: |
for binary in horizon-client horizon-ctl horizon-prepare horizon-server; do
file="target/release/$binary${{ matrix.exe }}"
if [[ "$RUNNER_OS" == macOS ]]; then
others=$(otool -L "$file" | tail -n +2 | awk '{print $1}' | grep -v -E '^(/usr/lib/|/System/Library/)' || true)
[[ -z "$others" ]] || { echo "::error::$binary links $others"; exit 1; }
else
${{ runner.os == 'Windows' && 'python' || 'python3' }} tools/check_imports.py "$file"
fi
done
- name: Every program starts
run: |
set -euo pipefail
bin=target/release
"$bin/horizon-client${{ matrix.exe }}" --help | grep -q -- '--resources'
"$bin/horizon-ctl${{ matrix.exe }}" --help > /dev/null
"$bin/horizon-prepare${{ matrix.exe }}" --help | grep -q -- '--minecraft-dir'
# The version is the release's.
for binary in horizon-prepare horizon-server; do
out=$("$bin/$binary${{ matrix.exe }}" --version | tr -d '\r')
echo "$out"
[[ "$out" == "$binary $HORIZON_VERSION" ]] || { echo "::error::expected $binary $HORIZON_VERSION"; exit 1; }
done
- name: Tools from the runner
if: runner.environment == 'self-hosted'
run: bash tools/ci/runner-env.sh
- name: Resources prepared from the runner's own Minecraft
if: runner.environment == 'self-hosted'
run: bash tools/ci/vanilla-data.sh
- name: The game runs (offscreen, a new world, 1500 frames)
if: runner.environment == 'self-hosted'
run: |
set -euo pipefail
# From a scratch folder: the game keeps its saves and options in the folder it starts in.
home="$RUNNER_TEMP/horizon-smoke"
rm -rf "$home" && mkdir -p "$home"
version=$(sed -n 's/.*"version": *"\([^"]*\)".*/\1/p' research-lock.json | head -1)
client="$PWD/target/release/horizon-client"
resources="$PWD/.local/horizon-client/$version"
(cd "$home" && HORIZON_HOME="$home" HORIZON_NO_AUDIO=1 "$client" --resources "$resources" --offscreen \
--world smoke --seed 1 --render-distance 4 --world-frames 1500 --timeout-seconds 180 \
--smoke-report "$home/smoke.json" > "$home/client.log" 2>&1) || { tail -40 "$home/client.log"; exit 1; }
python3 - "$home/smoke.json" <<'EOF'
import json, sys
report = json.load(open(sys.argv[1]))
print(json.dumps(report))
if report.get("error") is not None or report.get("frames", 0) < 1500 or not report.get("chunks") \
or not report.get("meshed_sections") or not report.get("player"):
sys.exit("the smoke run did not draw a world")
EOF
# Published with the release; the publish job checks it again before it publishes anything.
cp "$home/smoke.json" "$RUNNER_TEMP/smoke-test.json"
- uses: actions/upload-artifact@v4
if: runner.environment == 'self-hosted'
with:
name: smoke-test.json
path: ${{ runner.temp }}/smoke-test.json
if-no-files-found: error
retention-days: 3
- uses: actions/download-artifact@v4
with:
name: original-exporters
path: exporters
- name: Package
run: |
set -euo pipefail
cargo run --locked -p xtask -- package "target/release/horizon-client${{ matrix.exe }}" "dist/${{ matrix.dir }}"
cd dist
rm -f "${{ matrix.asset }}"
case "${{ matrix.asset }}" in
*.tar.gz) tar czf "${{ matrix.asset }}" "${{ matrix.dir }}" ;;
*) if [[ "$RUNNER_OS" == Windows ]]; then 7z a -tzip "${{ matrix.asset }}" "${{ matrix.dir }}" > /dev/null
else
# Without resource forks and extended attributes: plain ditto adds an
# AppleDouble `._` file beside every file that has any.
ditto -c -k --norsrc --noextattr --noqtn --keepParent "${{ matrix.dir }}" "${{ matrix.asset }}"
if unzip -Z1 "${{ matrix.asset }}" | grep -E '(^|/)(\._|__MACOSX/)'; then
echo "::error::macOS metadata files in ${{ matrix.asset }}"; exit 1
fi
fi ;;
esac
ls -l "${{ matrix.asset }}"
- name: Notarized
# A zip of programs can't be stapled: Gatekeeper asks Apple the first time each one runs.
if: runner.os == 'macOS' && vars.MACOS_SIGN_IDENTITY != ''
run: |
for binary in horizon-client horizon-ctl horizon-prepare horizon-server; do
codesign --verify --strict "dist/${{ matrix.dir }}/$binary"
done
packaging/macos/notarize.sh "dist/${{ matrix.asset }}"
- uses: actions/upload-artifact@v4
with:
name: ${{ matrix.asset }}
path: dist/${{ matrix.asset }}
if-no-files-found: error
retention-days: 3
- name: The server build (the game without sound or controllers, as horizon-engine)
if: matrix.server_asset
run: |
set -euo pipefail
# After the client is packaged: target/release/horizon-client is rebuilt without the audio and
# controllers features, so it needs neither libasound nor libudev (minimal server images lack them),
# and with code mods like the client.
cargo build --release --locked -p horizon-client --no-default-features --features mods --bin horizon-client
cargo run --locked -p xtask -- package-server target/release/horizon-client "dist/${{ matrix.server_dir }}"
folder="dist/${{ matrix.server_dir }}"
python3 tools/check_imports.py --server "$folder/horizon-engine" "$folder/horizon-server" "$folder/horizon-prepare"
"$folder/horizon-engine" --help | grep -q -- '--resources'
[[ "$("$folder/horizon-server" --version)" == "horizon-server $HORIZON_VERSION" ]]
cd dist
rm -f "${{ matrix.server_asset }}"
tar czf "${{ matrix.server_asset }}" "${{ matrix.server_dir }}"
ls -l "${{ matrix.server_asset }}"
- uses: actions/upload-artifact@v4
if: matrix.server_asset
with:
name: ${{ matrix.server_asset }}
path: dist/${{ matrix.server_asset }}
if-no-files-found: error
retention-days: 3
- name: The signing keychain, locked again
if: always() && runner.os == 'macOS' && vars.MACOS_SIGN_IDENTITY != ''
run: packaging/macos/keychain.sh lock
sign:
needs: [decide, build]
if: needs.decide.outputs.release == 'true' && needs.decide.outputs.dry_run != 'true'
runs-on: ubuntu-latest
timeout-minutes: 10
# The only job that holds the release signing key, a secret of the `release` environment, which only main's real
# releases enter (dry runs sign with a throwaway key). It runs no code from the repository but
# tools/ci/sign_release.sh, which calls only the runner's sha256sum and OpenSSL (docs/release-flow.md).
environment: ${{ needs.decide.outputs.dry_run != 'true' && 'release' || '' }}
steps:
- uses: actions/checkout@v4
with:
sparse-checkout: tools/ci/sign_release.sh
sparse-checkout-cone-mode: false
persist-credentials: false
- uses: actions/download-artifact@v4
with:
path: dist
pattern: horizon-*
merge-multiple: true
- name: Sign the checksums (SHA256SUMS, SHA256SUMS.sig)
env:
RELEASE_SIGNING_KEY: ${{ secrets.RELEASE_SIGNING_KEY }}
RELEASE_SIGNING_KEY_NEXT: ${{ secrets.RELEASE_SIGNING_KEY_NEXT }}
EPHEMERAL: ${{ needs.decide.outputs.dry_run == 'true' && 'yes' || '' }}
TAG: ${{ needs.decide.outputs.tag }}
run: |
bash tools/ci/sign_release.sh "$TAG" dist/horizon-linux-x86_64.tar.gz dist/horizon-windows-x64.zip \
dist/horizon-macos-arm64.zip dist/horizon-server-linux-x86_64.tar.gz
- uses: actions/upload-artifact@v4
with:
name: signature
path: dist/SHA256SUMS*
if-no-files-found: error
retention-days: 3
- uses: actions/upload-artifact@v4
if: needs.decide.outputs.dry_run == 'true'
with:
name: ephemeral.pub
path: dist/ephemeral.pub
retention-days: 3
publish:
needs: [decide, build, sign]
runs-on: ubuntu-latest
timeout-minutes: 15
timeout-minutes: 5
permissions:
contents: write
issues: read
actions: write # starts `nightly build`
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Notes
env:
- env:
GH_TOKEN: ${{ github.token }}
HEAD_SHA: ${{ needs.decide.outputs.head }}
URGENT: ${{ needs.decide.outputs.urgent }}
run: |
python3 tools/release_train.py notes --kind game --tag "${{ needs.decide.outputs.tag }}" \
--head "${{ needs.decide.outputs.head }}" --out notes > /dev/null
{ echo "## $(cat notes/title.txt)"; echo; cat notes/body.md; } >> "$GITHUB_STEP_SUMMARY"
- uses: actions/download-artifact@v4
with:
path: dist
merge-multiple: true
- name: The signature, checked as launchers check it
env:
TAG: ${{ needs.decide.outputs.tag }}
EPHEMERAL: ${{ needs.decide.outputs.dry_run == 'true' && 'yes' || '' }}
run: |
key=(); [[ -n "$EPHEMERAL" ]] && key=(--key "$(cat dist/ephemeral.pub)")
cargo run --locked --quiet -p horizon-release-sign -- verify --tag "$TAG" --publishing "${key[@]}" dist
- name: Ready to publish (the smoke test passed, no release-blocker is open)
# Right before publishing: an issue labelled while the builds ran still holds this release. A dry run only
# reports what a real run would do.
env:
GH_TOKEN: ${{ github.token }}
DRY_RUN: ${{ needs.decide.outputs.dry_run }}
run: |
if ! python3 tools/release_train.py gate --kind game --smoke dist/smoke-test.json --blockers; then
[[ "$DRY_RUN" == true ]] || { echo "::error::Nothing published"; exit 1; }
echo "::warning::A real run would publish nothing"
fi
- name: Publish ${{ needs.decide.outputs.tag }}
if: needs.decide.outputs.dry_run != 'true'
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ needs.decide.outputs.tag }}
run: |
signature=(); [[ -f dist/SHA256SUMS.sig ]] && signature=(dist/SHA256SUMS.sig)
# The release and the latest at once: there is one channel (docs/release-flow.md). Each archive gets a
# label (file#label), which GitHub's page shows instead of the file name, so nobody takes the game's
# files for the download (#1647). Launchers, servers and install.sh match the name, which stays.
gh release create "$TAG" --target "${{ needs.decide.outputs.head }}" --title "$(cat notes/title.txt)" \
--notes-file notes/body.md --latest \
"dist/horizon-linux-x86_64.tar.gz#Game files the launcher installs: Linux (horizon-linux-x86_64.tar.gz)" \
"dist/horizon-windows-x64.zip#Game files the launcher installs: Windows (horizon-windows-x64.zip)" \
"dist/horizon-macos-arm64.zip#Game files the launcher installs: macOS (horizon-macos-arm64.zip)" \
"dist/horizon-server-linux-x86_64.tar.gz#Dedicated server for Linux (horizon-server-linux-x86_64.tar.gz)" \
dist/SHA256SUMS "${signature[@]}" dist/smoke-test.json
- name: Dry run, the release that would be published
if: needs.decide.outputs.dry_run == 'true'
run: |
echo "Would publish ${{ needs.decide.outputs.tag }} at ${{ needs.decide.outputs.head }} as the release and latest: $(cat notes/title.txt)"
ls -l dist
- uses: actions/upload-artifact@v4
with:
name: release-notes
path: notes/
retention-days: 3
gh workflow run nightly-build.yml --repo "$GITHUB_REPOSITORY" --ref main -f base="$HEAD_SHA" -f urgent="$URGENT"
echo "### Started \`nightly build\` on ${HEAD_SHA:0:9}" >> "$GITHUB_STEP_SUMMARY"
+6 -2
View File
@@ -5,11 +5,15 @@ name: run-parity
# skipped `verify`, and GitHub counts a skipped required check as passed.
on:
pull_request:
types: [labeled]
# A push starts a run only to cancel the old head's (see concurrency): the job runs on the label alone.
types: [labeled, synchronize]
permissions:
contents: read
concurrency:
group: run-parity-${{ github.event.pull_request.number }}
# The run-parity label and pushes share one group per pull request, so the newest run cancels the one before: a
# pushed pull request's run tested a head the merge would no longer land. Any other label gets a group of its own:
# in the shared one, adding needs-proof cancelled the parity run queued seconds before (run 40766180150).
group: run-parity-${{ github.event.pull_request.number }}${{ github.event.action == 'labeled' && github.event.label.name != 'run-parity' && format('-{0}', github.run_id) || '' }}
cancel-in-progress: true
env:
# Test runs never reach the telemetry server: no live-count ping, no uploads (docs/telemetry.md).
+48
View File
@@ -0,0 +1,48 @@
name: triage command
# `/close`, `/reopen`, `/label`, `/unlabel` comments: the trusted reviewers in .github/trusted-reviewers.txt (and
# Ludovic) triage issues and pull requests without any repository role (docs/release-flow.md "Trusted reviewers",
# tools/ci/triage_command.py).
#
# Why no pull request's code runs here, though the token can write:
# - `issue_comment` always runs this file from main, whoever comments and wherever.
# - It checks out main's script and list only, never a pull request; it reads threads through the API.
# - The comment's text and author reach the script as env vars, never in a shell line. The token stays in the step.
on:
issue_comment:
types: [created]
permissions:
contents: read
jobs:
command:
# Every other comment skips here, before a runner starts.
if: >-
github.event.comment.user.type != 'Bot' && (
startsWith(github.event.comment.body, '/close') ||
startsWith(github.event.comment.body, '/reopen') ||
startsWith(github.event.comment.body, '/label') ||
startsWith(github.event.comment.body, '/unlabel'))
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
issues: write
pull-requests: write
steps:
- uses: actions/checkout@v4
with:
ref: main
persist-credentials: false
sparse-checkout: |
.github/trusted-reviewers.txt
tools/ci/nightly_set.py
tools/ci/triage_command.py
sparse-checkout-cone-mode: false
- name: Run the command
env:
GH_TOKEN: ${{ github.token }}
BODY: ${{ github.event.comment.body }}
AUTHOR: ${{ github.event.comment.user.login }}
ISSUE: ${{ github.event.issue.number }}
REPO: ${{ github.repository }}
COMMENT_URL: ${{ github.event.comment.html_url }}
run: python3 tools/ci/triage_command.py command
+31 -19
View File
@@ -13,13 +13,15 @@ Feature → files → harness for the most-changed areas: [docs/architecture.md]
`XDG_CONFIG_HOME`, plus `HORIZON_LAUNCHER_NO_SELF_UPDATE=1` and `HORIZON_NO_AUDIO=1`, and, for the
launcher, `HORIZON_LAUNCHER_TRASH`, `HORIZON_LAUNCHER_CLIPBOARD` and `HORIZON_LAUNCHER_DESKTOP` pointing into the scratch home. Never write to
`~/.local/share/{applications,horizon,horizon-launcher}` or `~/.local/bin`.
- **Mark what an agent posts.** A PR body starts with a line naming whose agent opened it and with what.
Ludovic's agents write `> 🤖 Opened by Ludovic's AI agent (Claude), not by Ludovic himself.`, and a
contributor's agent names its own person and tool (issues: "Filed by", reviews: "Review by"). Every
comment starts with `🤖 Agent:`. Never write as Ludovic or as anyone else.
- **Mark what an agent posts.** A PR body starts with a line naming the person whose agent opened it and
the tool: `> 🤖 Opened by <login>'s AI agent (<tool, model>), not by <login> personally.`, where `<login>`
is the GitHub account the PR is opened from (`gh api user --jq .login`). Issues say "Filed by", reviews
"Review by". Every comment starts with `🤖 Agent:`. Write only as the person you work for: the lines on
this page that name Ludovic are for his own agents (account `ludovic111`), never for anyone else's.
- **Attribution.** Commits end with a `Co-Authored-By:` line for the agent that wrote them, and PR bodies
with that agent's own "Generated with" line. Ludovic's agents use
`Co-Authored-By: Claude <model> <[email protected]>` and
with that agent's own "Generated with" line. Example, Ludovic's agents (Claude Code on `ludovic111`):
first line `> 🤖 Opened by Ludovic's AI agent (Claude), not by Ludovic himself.`, commits
`Co-Authored-By: Claude <model> <[email protected]>`, last line
`🤖 Generated with [Claude Code](https://claude.com/claude-code)`.
- Everything players run is Rust. Python/Java only for dev tools and harness drivers in `tools/`.
- Horizon-only features follow [docs/design-system.md](docs/design-system.md) and are off by default where
@@ -28,6 +30,8 @@ Feature → files → harness for the most-changed areas: [docs/architecture.md]
change), none for a fix. Agents never add `release-now`: the maintainer decides it. Merges ship in batches of 5, or after
6 hours ([docs/release-flow.md](docs/release-flow.md)).
- Contributors first: `gh pr list --state open --search <area>` before you start; never duplicate theirs.
- **Claim before you start**: comment `/claim` on the issue (no issue yet: open one, then claim it). An assigned
issue is taken: pick another. `/unclaim` when you stop; a claim with no open PR linking it is released after 5 days.
## Crates
@@ -35,9 +39,12 @@ Game workspace (root `Cargo.toml`); `launcher/` and `crates/horizon-telemetry-se
| Crate | Owns |
|---|---|
| `horizon-client` | The game: a lib (`src/lib.rs`, ~180 flat modules) and the bin (`src/main.rs` → `game.rs` + `game/`), plus `horizon-ctl` (`src/bin/horizon-ctl/`). Also the dedicated server (`--dedicated`) |
| `horizon-client` | The game: a lib (`src/lib.rs`, ~100 flat modules: gameplay, networking, title screens) and the bin (`src/main.rs` → `game.rs` + `game/`), plus `horizon-ctl` (`src/bin/horizon-ctl/`). Also the dedicated server (`--dedicated`) |
| `horizon-items` | Blocks and items below gameplay: `blocks.rs` (block facts), `items.rs`, `item_data.rs` (components), `enchant.rs`, `cooldowns.rs`, `bundle.rs`, `item_nbt.rs`. Re-exported as `horizon_client::items`... |
| `horizon-command` | Command parsing (Brigadier): the vanilla tree, argument types, selectors, suggestions, the tree's network form. Re-exported as `horizon_client::command` |
| `horizon-client-core` | The client lib's bottom layer: `text.rs`, `lang.rs`, `snbt.rs`, `nbt_path.rs`, `component.rs`, `scoreboard.rs`, `bossbar.rs`, `gamerules.rs`, `block_entity_nbt.rs`, registry/particle/sound tables. Re-exported under the old paths (`horizon_client::text`...) |
| `horizon-client-input` | Key bindings and key names, toggle keys, mouse options, controller settings and glyphs, the clipboard, window options (`keybinds.rs`, `gamepad.rs`, `mouse.rs`, `window.rs`...). Re-exported under the old paths |
| `horizon-client-render` | The client's world data, rendering and screens (chunks, meshing, light, streaming, `gfx`, HUD, menus, settings): ~80 modules that horizon-client re-exports under the same paths. Nothing in it reaches gameplay |
| `horizon-world` | Vanilla world data with no gameplay loop: NBT, regions, chunks, `datafix/`, `worldgen/`, `level/` (server-side block behaviour, ticks, redstone), entity physics, items, effects, food, damage |
| `horizon-core` | Simulation primitives: Java `Random`s, `jmath` (HotSpot-exact `Math`), positions, noise, density, paletted storage, `version.rs` (the pinned version) |
| `horizon-protocol` | Network protocol 777: codecs, framing, encryption, `packets/{handshake,status,login,configuration,play}.rs`, `entity_data.rs` |
@@ -57,7 +64,7 @@ Game workspace (root `Cargo.toml`); `launcher/` and `crates/horizon-telemetry-se
Other top-level folders: `tools/` (harnesses and dev scripts), `tests/` (Python unit tests: `python3 -m
unittest discover -s tests`), `mods/horizon/src/test/java/dev/horizonlab/parity/` (Java oracles),
`docs/`, `.github/workflows/`. The website (playhorizon.dev) is the private ludovic111/horizon-site: its build
`docs/`, `.github/workflows/`. The website (playhorizon.dev) is the private horizon-rust/horizon-site: its build
reads `README.md`, `launcher/links.json`, `docs/api/`, `roadmap/roadmap.json` and `crates/horizon-theme/tokens.css` from main.
## Build and test fast
@@ -71,7 +78,7 @@ cargo build --profile fast -p horizon-client # optimized, quick to link:
python3 tools/parity.py --changed origin/main # the harnesses your branch can affect (CI's parity job)
python3 tools/parity.py --changed-files paths.txt --list # which harnesses cover these files, without running them
python3 tools/parity.py mob_ai sculk # named harnesses
python3 tools/lan_e2e.py --check chest # two real games over LAN (server-thread host); --in-frame compares the fallback
python3 tools/lan_e2e.py --check chest # two real games over LAN (server-thread host)
python3 tools/agent_control_e2e.py # horizon-ctl / MCP end to end
```
@@ -97,6 +104,9 @@ python3 tools/agent_control_e2e.py # horizon-ctl / MCP end to end
## Vanilla reference
- Start parity work from the port ledger, not grep: `python3 tools/port_ledger.py issue N` (or `where
Class.method`, `what file:line`), and `check --base origin/main` before the PR. How:
[.claude/skills/vanilla-parity/SKILL.md](.claude/skills/vanilla-parity/SKILL.md), [docs/port-ledger.md](docs/port-ledger.md).
- Decompiled sources: `.local/minecraft/26.3/sources/{client,server}/` (`python3 tools/lab.py decompile`).
- Bytecode queries (callers through lambdas, every overload):
`python3 tools/rea_vanilla.py method net.minecraft.world.level.block.DispenserBlock dispenseFrom`
@@ -108,19 +118,20 @@ python3 tools/agent_control_e2e.py # horizon-ctl / MCP end to end
## horizon-client: where things live
`L:` = lib (`crates/horizon-client/src/`), `G:` = bin (`crates/horizon-client/src/game/`). The lib can't see
`L:` = lib (`crates/horizon-client/src/`, or `crates/horizon-client-render/src/` for the modules that crate holds, under
the same `horizon_client::<module>` paths), `G:` = bin (`crates/horizon-client/src/game/`). The lib can't see
`Game`; bin code reaches the lib as `horizon_client::<module>`.
| Subsystem | Files |
|---|---|
| Game loop, tick, frame | `game.rs`: `fn run`, `impl ApplicationHandler for Game` (`window_event`, `about_to_wait`), `fn tick`, `fn tick_world`, `fn render`; L: `clock.rs`, `window.rs`, `spans.rs` |
| 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/`, `ticket_levels.rs`; G: `world_loading.rs`, `postprocess.rs`; game.rs `fn update_world` |
| World generation | `crates/horizon-world/src/worldgen/` (`generator*.rs`, `feature_graph.rs`, `structure/`, `carver/`) |
| Block behaviour | `crates/horizon-world/src/level/` (`mod.rs`, `tick.rs`, `hooks.rs`, `redstone.rs`, `place.rs`, `interact.rs`); game.rs `fn tick_blocks`, `fn process_level_events` |
| Mobs | L: `mobs.rs` (types), `mobs/` (`ai.rs`, `goals.rs`, `brain.rs`, `*_brain.rs`, `nav.rs`, `path.rs`, `species.rs`, `spawning.rs`, `render.rs`, `anim.rs`); G: `creatures.rs` (ticking, spawning, mob events), one file per mob family (`dragon.rs`, `wither.rs`, `villagers.rs`, `piglins.rs`, `golems.rs`...), `natural_spawning.rs` |
| Other entities | L: `entities.rs`, `projectiles.rs`, `arrows.rs`; G: `boats.rs`, `minecarts.rs`, `mounts.rs`, `armor_stands.rs`, `paintings.rs`, `frames.rs`, `fireworks.rs`, `tnt.rs`, `explosion.rs` |
| Player actions | game.rs (`fn use_hand`, `fn use_item`, `fn attack`, `fn tick_mining`, `fn break_block`); G: `hand.rs`, `combat.rs`, `consume.rs`, `potions.rs`, `shield.rs`, `crossbow.rs`, `trident.rs`, `fishing.rs`; L: `motion.rs` + `motion/` (movement), `collisions.rs`, `items.rs`, `item_data.rs` |
| 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` |
| 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` |
@@ -129,7 +140,7 @@ python3 tools/agent_control_e2e.py # horizon-ctl / MCP end to end
| Integrated server | G: `integrated.rs` (the server thread), `levels.rs` (one level per dimension, `fn tick_server`), `server_travel.rs` |
| Dedicated server | G: `dedicated.rs`, `dedicated/status.rs`; crates `horizon-server-files`, `horizon-server` |
| Save and load | L: `save.rs` (level.dat, regions, playerdata), `entity_nbt.rs`, `options_file.rs`; `horizon-client-core/src/block_entity_nbt.rs`; G: `saving.rs`, `persist.rs` (entities); `crates/horizon-world/src/{region.rs,nbt.rs,datafix/}` |
| Input | game.rs `fn window_event`, `fn key_pressed`, `fn play_mouse`; L: `keybinds.rs`, `keymapping.rs`, `key_names.rs`, `mouse.rs`, `input.rs`, `interaction_input.rs`, `gamepad.rs`, `osk.rs`, `clipboard.rs`; G: `pad.rs`, `pad_menu.rs` |
| Input | game.rs `fn window_event`, `fn key_pressed`, `fn play_mouse`; crate `horizon-client-input` (`keybinds.rs`, `keymapping.rs`, `key_names.rs`, `mouse.rs`, `input.rs`, `interaction_input.rs`, `gamepad.rs`, `clipboard.rs`); L: `osk.rs`; G: `pad.rs`, `pad_menu.rs` |
| Audio | L: `audio.rs` (mixer), `sound_engine.rs`, `sound_events.rs`, `music.rs`, `ambience.rs`; G: `music.rs`, `subtitle_hud.rs` |
| Resource packs | L: `packs.rs` + `packs/`, `pack_screen.rs`, `atlas_sources.rs`, `sprites.rs`, `mipmap.rs`, `model_library.rs`, `font.rs`; `horizon-client-core/src/lang.rs`; crate `horizon-assets`; G: `packs.rs` |
| Agent control | L: `control.rs` (socket), `bin/horizon-ctl/`; G: `control.rs`, `agent.rs`, `events.rs`, `ui_control.rs` ([docs/agent-control.md](docs/agent-control.md)) |
@@ -160,17 +171,18 @@ python3 tools/agent_control_e2e.py # horizon-ctl / MCP end to end
"Server thread" with one level per dimension; the window joins it as a client through memory (`remote`),
exactly as a LAN guest or a dedicated server's player does. This is what players run. Open
`[server-thread]` issues list the gaps left.
2. **In-frame (`--in-frame`, `--no-server-thread` or `HORIZON_SERVER_THREAD=0`):** the window's `Game` ticks
the world itself, one level that follows the player (`fn enter_dimension`). Deterministic rule fixtures still use this path; player-facing integration uses the default server thread.
2. **In-frame (`--in-frame`, `--no-server-thread` or `HORIZON_SERVER_THREAD=0`): DEPRECATED, never use it, it
will be removed soon.** The window's `Game` ticks the world itself, one level that follows the player
(`fn enter_dimension`). Compatibility does not matter: don't add features or fixes for it, don't write new
tests against it, and move code off it when you touch it. The deterministic rule fixtures still run on it
until they are ported.
LAN guests and dedicated servers always use the server code, so a fix there serves both. Test both paths
when you touch player-facing gameplay:
LAN guests and dedicated servers always use the server code, so a fix there serves both. Test the server
thread when you touch player-facing gameplay:
```sh
cargo run --profile fast -p horizon-client -- --resources .local/horizon-client/26.3 --world T # server thread
cargo run --profile fast -p horizon-client -- --resources .local/horizon-client/26.3 --world T --in-frame # in-frame
python3 tools/lan_e2e.py --check <name> # host on its server thread, guest is a server client
python3 tools/lan_e2e.py --in-frame --check <name> # comparison (portals, levels, distance, lava need server-thread mode)
python3 tools/parity.py dedicated_server server_entities server_player_damage
```
+24 -4
View File
@@ -74,6 +74,19 @@ including examples. The dependency scope includes optional, platform, build and
Tests run library, binary, integration and doc tests without building unrelated example executables. Shared Cargo
configuration and toolchain changes select the whole workspace; CI also keeps its workspace-wide checks.
## Claim before you start
Work is tracked in GitHub issues, and an issue that is assigned is taken (so is one with the `in progress` label:
claimed on Discord with Kira's `/claim`): pick another, or ask its holder in the thread. Before you start, comment
**`/claim`** on the issue; you are assigned at once, no role needed. No issue for your work yet? Open one (a line is
enough), then claim it. `/unclaim` gives it back.
A claim with no open pull request linking the issue (`Fixes #123` in the description, or a draft) is released after
5 days, with a comment: open a draft early. You can hold 5 open issues at a time. Agents claim the same way.
When your pull request changes the same functions as another open one, or closes the same issue, a bot comment names
it (edited on every push, never repeated): talk to its author in the thread before you both finish the same work.
## Pull requests
- **Small.** One change per pull request, under about 500 lines of code where you can; split bigger work into
@@ -82,10 +95,17 @@ configuration and toolchain changes select the whole workspace; CI also keeps it
can see.
- **Tick "Allow edits by maintainers".** When something small is left (a lint, a rebase, a nit) we push the fixup
ourselves instead of sending it back to you; only design questions come back.
- **Labels** (we set them if you can't): `feature` when players get something new (it appears under "New" in the
release notes), `skip-release` when players see no change (tooling, docs, tests), nothing for a fix.
- **Tick its kind** in the template: `parity` (Horizon now does what vanilla does), `bug` (a crash or breakage),
`perf`, `feature` (new for players), `extra` (Horizon-only, off by default), `tooling` or `docs`. A workflow sets
that label, and area labels (`launcher`, `mobs`, `worldgen`, `rendering`...) from the paths you change.
`label:parity` is the list of real progress toward vanilla.
- **Release labels** (we set them if you can't): `feature` also puts it under "New" in the release notes,
`skip-release` when players see no change (tooling, docs, tests), nothing for a fix.
`release-now` is for crash fixes that shouldn't wait. pr_ready suggests one.
Who reviews and merges: the Horizon Team, on CI for ordinary pull requests; risky ones (saves, network, world
generation, the launcher's updates) wait for a local proof first ([merging](docs/release-flow.md#merging)).
Every merge to main is released automatically ([release flow](docs/release-flow.md)), so main always stays playable.
How agents, checks, releases and signed updates keep this safe: [security](docs/security.md).
@@ -98,5 +118,5 @@ new pushes, we add it again.
## Talk to us
[Discord](https://discord.gg/HcTeMZxe5E) for questions and ideas, [issues](https://github.com/ludovic111/horizon/issues)
for bugs. If you want to take something on, say so in its issue so nobody else starts the same work.
[Discord](https://discord.gg/HcTeMZxe5E) for questions and ideas, [issues](https://github.com/horizon-rust/horizon/issues)
for bugs. To take something on, `/claim` its issue ([above](#claim-before-you-start)).
Generated
+105 -810
View File
File diff suppressed because it is too large. Load diff
+7 -15
View File
@@ -8,7 +8,10 @@ members = [
"crates/horizon-world",
"crates/horizon-client",
"crates/horizon-client-core",
"crates/horizon-client-input",
"crates/horizon-client-render",
"crates/horizon-command",
"crates/horizon-items",
"crates/horizon-server-files",
"crates/horizon-server",
"crates/horizon-bots",
@@ -20,26 +23,13 @@ members = [
"crates/horizon-bench",
"crates/horizon-friends",
"crates/horizon-relay",
"crates/horizon-mod-sdk",
"crates/horizon-mods",
"crates/horizon-mods/tests/guest",
"crates/horizon-mods/tests/ui_guest",
"crates/cargo-horizon-mod",
"crates/horizon-port-pack",
"crates/horizon-shaders",
"crates/horizon-shader-api",
"crates/horizon-release-sign",
"crates/horizon-mod-sdk/examples/coords-hud",
"crates/horizon-mod-sdk/examples/predator-missile",
"crates/horizon-mod-sdk/examples/welcome",
"crates/horizon-mod-sdk/examples/beacons",
"mods/official/fps-overlay",
"mods/official/keystrokes",
"mods/official/far-view",
"mods/official/minimap",
"mods/official/zoom",
"xtask",
"mods/official/recipes",
]
resolver = "2"
@@ -59,8 +49,10 @@ large_enum_variant = "allow"
# wgpu-hal 30.0.1 with gfx-rs/wgpu#10124: wgpu 30 creates a Vulkan descriptor
# pool with no pool sizes while it creates the device, which Vulkan allows only
# since 1.3.215, and Intel's Windows driver for 6th-10th gen iGPUs (HD 520/530,
# UHD 620/630) crashes on it with an access violation (#746). Drop this and the
# folder once a wgpu release has the fix.
# UHD 620/630) crashes on it with an access violation (#746). It also fixes the
# Metal backend's lookup of the CoreGraphics HDR colour space names, which handed
# CoreGraphics a symbol's address as the string and crashed HDR output on macOS.
# Drop this and the folder once a wgpu release has both fixes.
[patch.crates-io]
wgpu-hal = { path = "third_party/wgpu-hal" }
+1 -1
View File
@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2026 Ludovic
Copyright (c) 2026 Ludovic Marie and the Horizon contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+17 -12
View File
@@ -1,9 +1,9 @@
<p align="center">
<img src="docs/media/banner.webp" alt="Horizon: Minecraft: Java Edition 26.3, rewritten in Rust and checked against vanilla by 281 differential harnesses" width="100%">
<img src="docs/media/banner.webp" alt="Horizon: Minecraft: Java Edition 26.3, rewritten in Rust and checked against vanilla by more than 300 differential harnesses" width="100%">
</p>
<p align="center">
<a href="https://github.com/ludovic111/horizon/actions/workflows/ci.yml"><img src="https://github.com/ludovic111/horizon/actions/workflows/ci.yml/badge.svg?branch=main" alt="CI"></a>
<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%">
@@ -15,7 +15,7 @@
<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/ludovic111/horizon/releases"><b>Release notes</b></a> ·
<a href="https://github.com/horizon-rust/horizon/releases"><b>Release notes</b></a> ·
<a href="ROADMAP.md"><b>Roadmap</b></a> ·
<a href="#project-status"><b>Project status</b></a> ·
<a href="#contributing"><b>Contributing</b></a>
@@ -23,15 +23,15 @@
# Horizon
Horizon is Minecraft: Java Edition 26.3 rewritten in Rust: a native game that reads the same worlds, follows the same rules and generates the same terrain as the Java game, and is checked against it by 281 differential harnesses. You play it through a small launcher, on the files from your own copy of Minecraft. It is free, open source and unofficial.
Horizon is Minecraft: Java Edition 26.3 rewritten in Rust: a native game that reads the same worlds, follows the same rules and generates the same terrain as the Java game, and is checked against it by more than 300 differential harnesses. You play it through a small launcher, on the files from your own copy of Minecraft. It is free, open source and unofficial.
> Not an official Minecraft product. Not approved by or associated with Mojang or Microsoft. Horizon contains no Minecraft code, assets or data: you need your own legally obtained copy of Minecraft: Java Edition 26.3.
## Download
Horizon is installed through **the Horizon launcher**. Get it from [playhorizon.dev](https://playhorizon.dev) (the Download button picks your system) or from [the launcher releases](https://github.com/ludovic111/horizon/releases?q=launcher-v&expanded=true), for Linux x86-64, macOS on Apple Silicon or Windows x64. The launcher installs and updates the game, prepares its files from your own copy of Minecraft, and updates itself.
Horizon is installed through **the Horizon launcher**. Get it from [playhorizon.dev](https://playhorizon.dev) (the Download button picks your system) or from [the launcher releases](https://github.com/horizon-rust/horizon/releases?q=launcher-v&expanded=true), for Linux x86-64, macOS on Apple Silicon or Windows x64. The launcher installs the game and keeps it up to date with each new build (Horizon ships on one channel, the Nightly), prepares its files from your copy of Minecraft (your own install, or downloaded from Mojang for your account), and updates itself.
**You need** Minecraft: Java Edition 26.3, played once (the official launcher, Prism, MultiMC and other launchers all work); a Java 25 runtime, for the one-time preparation; and a GPU with Vulkan on Linux and Windows, or any Apple Silicon Mac. Step by step, with macOS's first-open prompt and the first launch: [Install and first launch](https://playhorizon.dev/docs/install).
**You need** a Microsoft account that owns Minecraft: Java Edition (the launcher signs in with it, as other launchers do; see [Microsoft account](docs/microsoft-account.md)) and a GPU with Vulkan or OpenGL 3.3 on Linux, Vulkan or DirectX 12 on Windows, or any Apple Silicon Mac. Nothing else: with no Minecraft on your computer, Play downloads 26.3 and the Java it needs from Mojang for your account, as the official launcher does, then prepares it once. An install you already have (the official launcher, Prism, MultiMC and other launchers all work) is used instead. Step by step, with macOS's first-open prompt and the first launch: [Install and first launch](https://playhorizon.dev/docs/install).
<table>
<tr>
@@ -46,9 +46,9 @@ The [documentation](https://playhorizon.dev/docs/) covers each of these.
- **The game:** singleplayer with vanilla's world generation, mobs, villagers, raids, redstone, every command, the HUD and the recipe book, on saves in vanilla's format. Worlds from 1.8 to 26.2 are upgraded as vanilla does. [Status table](#project-status).
- **Horizon's own extras:** new menus (vanilla's are one setting away), graphics presets, distant terrain up to 1024 chunks, controller support, optional zoom, minimap and more, all off by default where they would change the vanilla look. See [Settings](https://playhorizon.dev/docs/settings).
- **Resource packs and data packs:** resource packs work, and data packs load functions, predicates and item modifiers.
- **Playing with others:** invite friends from the pause menu's Friends button with a code, Open to LAN, and joining offline-mode servers. Horizon cannot sign in to a Microsoft account yet, so it cannot join servers that check accounts. See [Multiplayer](https://playhorizon.dev/docs/multiplayer).
- **Mods:** Horizon has its own mod system (Rust compiled to WebAssembly). Java mod jars do not run. See [Mods and data packs](https://playhorizon.dev/docs/mods).
- **Resource packs and data packs:** resource packs work, and data packs load functions, predicates, item modifiers and world generation (vanilla's heights and biomes for now).
- **Playing with others:** invite friends from the pause menu's Friends button with a code, Open to LAN, and joining servers, online-mode ones with the Microsoft account the launcher signs in with. See [Multiplayer](https://playhorizon.dev/docs/multiplayer).
- **Mods:** Horizon has its own mods, installed from the launcher. The official ones (Zoom, FPS Counter, Keystrokes and Minimap) are native Rust that the launcher builds into your game, with no Rust to install; older WebAssembly mods still run while they move. Java mod jars do not run in Horizon. See [Mods and data packs](https://playhorizon.dev/docs/mods).
Screenshots, captured in the Rust client itself with vanilla assets and no mods or shaders:
@@ -93,13 +93,13 @@ Commands block until the action is done and reply with what happened; errors are
For developers. Requirements: Rust 1.90 or newer, Python 3.10 or newer, JDK 25, and on Linux the ALSA and udev development headers (`libasound2-dev libudev-dev` on Debian and Ubuntu). Development and the vanilla harnesses run on Linux.
Horizon contains no game files and never downloads them. It builds its resources from **your own copy** of Minecraft: Java Edition 26.3, verified against the checksums in `research-lock.json`:
Horizon contains no game files, and `horizon-prepare` never downloads them (the launcher downloads them from Mojang only for a signed-in account that owns the game). It builds its resources from **your own copy** of Minecraft: Java Edition 26.3, verified against the checksums in `research-lock.json`:
1. **Client:** install Java Edition with the official launcher and play 26.3 once. Horizon reads `versions/26.3/`, `libraries/` and `assets/` from the launcher's game directory (`~/.minecraft` on Linux). If yours lives elsewhere, pass `--minecraft-dir` or set `HORIZON_MINECRAFT_DIR`.
2. **Server jar:** the 26.3 server jar, from the `downloads.server.url` in your launcher's `versions/26.3/26.3.json` (or [minecraft.net/download/server](https://www.minecraft.net/en-us/download/server) while 26.3 is the current release). Pass it with `--server-jar` or set `HORIZON_SERVER_JAR`. It is only needed the first time.
```sh
git clone https://github.com/ludovic111/horizon.git
git clone https://github.com/horizon-rust/horizon.git
cd horizon
cargo run --release -p horizon-prepare -- --prepare --server-jar ~/Downloads/server.jar # needs JDK 25
cargo run --release -p horizon-prepare -- sounds # optional: sounds from your launcher install
@@ -139,6 +139,11 @@ average; `tests/test_parity.py` keeps it in sync with this table. Last audited
rerun whenever one of its inputs changed, and the Mac mini now runs every one of
them too (Rust uses Java's own float maths on each platform).
The harnesses measure behaviour. A second measure is method by method: the [port ledger](tools/port_ledger.py)
lists every method of vanilla 26.3 next to the Rust functions that port it, with a verdict for each one
(ported, folded into another function, missing, or needing no port), and the untested ports. Run
`python3 tools/port_ledger.py build`, then `coverage`, with the vanilla data in `.local/`.
| Subsystem | Score | Main gaps |
| --- | ---: | --- |
| Window, input and game loop | 1 | None: the game loop, options, camera, auto-jump, toggle sprint/sneak, local input and the frame limiter match vanilla |
@@ -158,7 +163,7 @@ them too (Rust uses Java's own float maths on each platform).
### Known issues
Horizon can't sign in to a Microsoft account yet, so it can't join online-mode servers (it joins offline-mode ones); skins served over https show the default skin unless already cached; pack icons show the unknown-pack texture in game; the creative hotbar keys (C/X) aren't wired to the hotbar loader; item tooltips lack attribute lines.
Skins served over https show the default skin unless already cached; pack icons show the unknown-pack texture in game; the creative hotbar keys (C/X) aren't wired to the hotbar loader; item tooltips lack attribute lines.
## Licence
+394 -458
View File
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,773 @@
{
"format": "horizon-benchmark-report",
"version": 1,
"kind": "game",
"created": "2026-10-29T01:13:01Z",
"app": {
"name": "horizon-client",
"version": "v0.2.97",
"commit": null
},
"hardware": {
"os": "windows",
"arch": "x86_64",
"cpu_threads": 16,
"os_version": "Microsoft Windows [Version 10.0.26200.8037]",
"cpu": "AMD Ryzen 7 9800X3D 8-Core Processor",
"ram_mb": 95943,
"gpu": {
"name": "NVIDIA GeForce RTX 5080",
"vendor": "nvidia",
"driver": "NVIDIA 617.42",
"backend": "vulkan",
"kind": "discrete"
}
},
"conditions": {
"scenario": "perf_bench:village,forest,ocean,mountain",
"seed": "1234",
"render_distance": 16,
"simulation_distance": 12,
"vsync": false,
"offscreen": false,
"repeats": 3,
"warmup_s": 2.0,
"window": [
1280,
720
]
},
"metrics": {
"startup.title_ms": {
"n": 3,
"mean": 3158.208,
"stdev": 215.77,
"min": 3023.046,
"max": 3407.049,
"ci95": 536.046
},
"startup.world_ms": {
"n": 3,
"mean": 7170.164,
"stdev": 557.958,
"min": 6703.242,
"max": 7788.08,
"ci95": 1386.157
},
"chunks.village.load_ms": {
"n": 3,
"mean": 1694.647,
"stdev": 41.285,
"min": 1653.122,
"max": 1735.687,
"ci95": 102.566
},
"chunks.village.meshed_count": {
"n": 3,
"mean": 23544.0,
"stdev": 47.127,
"min": 23495.0,
"max": 23589.0,
"ci95": 117.081
},
"fps.village.avg_fps": {
"n": 3,
"mean": 1711.652,
"stdev": 77.412,
"min": 1623.403,
"max": 1768.093,
"ci95": 192.317
},
"fps.village.low_1pct_fps": {
"n": 3,
"mean": 593.407,
"stdev": 32.279,
"min": 571.485,
"max": 630.473,
"ci95": 80.191
},
"fps.village.low_01pct_fps": {
"n": 3,
"mean": 358.473,
"stdev": 27.832,
"min": 326.35,
"max": 375.353,
"ci95": 69.143
},
"memory.village.gpu_mb": {
"n": 3,
"mean": 631.813,
"stdev": 8.381,
"min": 626.803,
"max": 641.489,
"ci95": 20.822
},
"chunks.forest.load_ms": {
"n": 3,
"mean": 2521.363,
"stdev": 291.624,
"min": 2336.881,
"max": 2857.57,
"ci95": 724.493
},
"chunks.forest.meshed_count": {
"n": 3,
"mean": 27346.667,
"stdev": 57.934,
"min": 27306.0,
"max": 27413.0,
"ci95": 143.927
},
"fps.forest.avg_fps": {
"n": 3,
"mean": 2423.783,
"stdev": 55.746,
"min": 2386.353,
"max": 2487.851,
"ci95": 138.493
},
"fps.forest.low_1pct_fps": {
"n": 3,
"mean": 709.165,
"stdev": 66.309,
"min": 638.666,
"max": 770.286,
"ci95": 164.734
},
"fps.forest.low_01pct_fps": {
"n": 3,
"mean": 401.039,
"stdev": 40.413,
"min": 354.472,
"max": 426.929,
"ci95": 100.399
},
"memory.forest.gpu_mb": {
"n": 3,
"mean": 705.008,
"stdev": 6.062,
"min": 698.466,
"max": 710.435,
"ci95": 15.06
},
"chunks.ocean.load_ms": {
"n": 3,
"mean": 2094.909,
"stdev": 521.225,
"min": 1758.024,
"max": 2695.274,
"ci95": 1294.9
},
"chunks.ocean.meshed_count": {
"n": 3,
"mean": 24312.667,
"stdev": 261.934,
"min": 24116.0,
"max": 24610.0,
"ci95": 650.732
},
"fps.ocean.avg_fps": {
"n": 3,
"mean": 2201.125,
"stdev": 73.595,
"min": 2153.6,
"max": 2285.897,
"ci95": 182.834
},
"fps.ocean.low_1pct_fps": {
"n": 3,
"mean": 553.968,
"stdev": 100.955,
"min": 445.599,
"max": 645.355,
"ci95": 250.806
},
"fps.ocean.low_01pct_fps": {
"n": 3,
"mean": 325.078,
"stdev": 131.154,
"min": 177.4,
"max": 427.981,
"ci95": 325.831
},
"memory.ocean.gpu_mb": {
"n": 3,
"mean": 652.944,
"stdev": 5.177,
"min": 647.51,
"max": 657.818,
"ci95": 12.862
},
"chunks.mountain.load_ms": {
"n": 3,
"mean": 3304.58,
"stdev": 134.041,
"min": 3155.996,
"max": 3416.409,
"ci95": 333.003
},
"chunks.mountain.meshed_count": {
"n": 3,
"mean": 26591.0,
"stdev": 57.472,
"min": 26525.0,
"max": 26630.0,
"ci95": 142.779
},
"fps.mountain.avg_fps": {
"n": 3,
"mean": 1961.086,
"stdev": 39.725,
"min": 1921.241,
"max": 2000.69,
"ci95": 98.69
},
"fps.mountain.low_1pct_fps": {
"n": 3,
"mean": 536.973,
"stdev": 38.828,
"min": 495.61,
"max": 572.635,
"ci95": 96.461
},
"fps.mountain.low_01pct_fps": {
"n": 3,
"mean": 296.586,
"stdev": 30.236,
"min": 275.859,
"max": 331.281,
"ci95": 75.118
},
"memory.mountain.gpu_mb": {
"n": 3,
"mean": 896.941,
"stdev": 5.177,
"min": 891.506,
"max": 901.814,
"ci95": 12.862
},
"memory.rss_peak_mb": {
"n": 3,
"mean": 1458.578,
"stdev": 3.229,
"min": 1455.32,
"max": 1461.777,
"ci95": 8.022
},
"memory.rss_steady_mb": {
"n": 3,
"mean": 1236.28,
"stdev": 4.869,
"min": 1230.701,
"max": 1239.672,
"ci95": 12.096
}
},
"frames": [
{
"phase": "village",
"repeat": 0,
"frames": 21218,
"seconds": 12.0,
"avg_fps": 1768.093,
"low_1pct_fps": 630.473,
"low_01pct_fps": 375.353,
"p50_ms": 0.537,
"p95_ms": 0.846,
"p99_ms": 1.222,
"p999_ms": 2.081,
"max_ms": 6.256,
"cpu_p50_ms": 0.38,
"gpu_p50_ms": 0.104,
"histogram": [
20695,
499,
22,
1,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 1,
"frames": 19482,
"seconds": 12.001,
"avg_fps": 1623.403,
"low_1pct_fps": 578.262,
"low_01pct_fps": 373.717,
"p50_ms": 0.586,
"p95_ms": 0.95,
"p99_ms": 1.357,
"p999_ms": 2.158,
"max_ms": 7.35,
"cpu_p50_ms": 0.445,
"gpu_p50_ms": 0.104,
"histogram": [
18729,
713,
38,
1,
0,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 2,
"frames": 20922,
"seconds": 12.0,
"avg_fps": 1743.46,
"low_1pct_fps": 571.485,
"low_01pct_fps": 326.35,
"p50_ms": 0.526,
"p95_ms": 0.883,
"p99_ms": 1.313,
"p999_ms": 2.14,
"max_ms": 12.027,
"cpu_p50_ms": 0.379,
"gpu_p50_ms": 0.104,
"histogram": [
20308,
582,
28,
2,
1,
0,
0,
0,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 0,
"frames": 29855,
"seconds": 12.0,
"avg_fps": 2487.851,
"low_1pct_fps": 770.286,
"low_01pct_fps": 421.717,
"p50_ms": 0.365,
"p95_ms": 0.619,
"p99_ms": 0.948,
"p999_ms": 1.717,
"max_ms": 9.446,
"cpu_p50_ms": 0.232,
"gpu_p50_ms": 0.157,
"histogram": [
29607,
233,
13,
0,
0,
1,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 1,
"frames": 28766,
"seconds": 12.0,
"avg_fps": 2397.145,
"low_1pct_fps": 718.542,
"low_01pct_fps": 426.929,
"p50_ms": 0.374,
"p95_ms": 0.662,
"p99_ms": 1.016,
"p999_ms": 1.858,
"max_ms": 6.379,
"cpu_p50_ms": 0.239,
"gpu_p50_ms": 0.157,
"histogram": [
28457,
293,
14,
0,
0,
1,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 2,
"frames": 28637,
"seconds": 12.0,
"avg_fps": 2386.353,
"low_1pct_fps": 638.666,
"low_01pct_fps": 354.472,
"p50_ms": 0.373,
"p95_ms": 0.704,
"p99_ms": 1.159,
"p999_ms": 1.952,
"max_ms": 14.388,
"cpu_p50_ms": 0.233,
"gpu_p50_ms": 0.157,
"histogram": [
28198,
415,
18,
5,
0,
0,
0,
0,
0,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 0,
"frames": 25968,
"seconds": 12.001,
"avg_fps": 2163.877,
"low_1pct_fps": 570.949,
"low_01pct_fps": 369.854,
"p50_ms": 0.408,
"p95_ms": 0.767,
"p99_ms": 1.339,
"p999_ms": 2.38,
"max_ms": 3.329,
"cpu_p50_ms": 0.271,
"gpu_p50_ms": 0.141,
"histogram": [
25383,
534,
47,
4,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 1,
"frames": 25843,
"seconds": 12.0,
"avg_fps": 2153.6,
"low_1pct_fps": 445.599,
"low_01pct_fps": 177.4,
"p50_ms": 0.413,
"p95_ms": 0.725,
"p99_ms": 1.309,
"p999_ms": 3.451,
"max_ms": 12.071,
"cpu_p50_ms": 0.275,
"gpu_p50_ms": 0.142,
"histogram": [
25375,
373,
56,
22,
1,
7,
3,
5,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 2,
"frames": 27431,
"seconds": 12.0,
"avg_fps": 2285.897,
"low_1pct_fps": 645.355,
"low_01pct_fps": 427.981,
"p50_ms": 0.388,
"p95_ms": 0.693,
"p99_ms": 1.163,
"p999_ms": 2.041,
"max_ms": 3.068,
"cpu_p50_ms": 0.254,
"gpu_p50_ms": 0.141,
"histogram": [
27036,
363,
31,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 0,
"frames": 23536,
"seconds": 12.0,
"avg_fps": 1961.326,
"low_1pct_fps": 542.675,
"low_01pct_fps": 331.281,
"p50_ms": 0.436,
"p95_ms": 0.856,
"p99_ms": 1.425,
"p999_ms": 2.22,
"max_ms": 9.458,
"cpu_p50_ms": 0.296,
"gpu_p50_ms": 0.14,
"histogram": [
22819,
671,
40,
5,
0,
0,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 1,
"frames": 23056,
"seconds": 12.001,
"avg_fps": 1921.241,
"low_1pct_fps": 495.61,
"low_01pct_fps": 282.617,
"p50_ms": 0.44,
"p95_ms": 0.911,
"p99_ms": 1.516,
"p999_ms": 2.543,
"max_ms": 14.342,
"cpu_p50_ms": 0.299,
"gpu_p50_ms": 0.141,
"histogram": [
22198,
783,
65,
7,
0,
2,
0,
0,
0,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 2,
"frames": 24009,
"seconds": 12.0,
"avg_fps": 2000.69,
"low_1pct_fps": 572.635,
"low_01pct_fps": 275.859,
"p50_ms": 0.436,
"p95_ms": 0.819,
"p99_ms": 1.274,
"p999_ms": 2.085,
"max_ms": 14.003,
"cpu_p50_ms": 0.297,
"gpu_p50_ms": 0.14,
"histogram": [
23421,
560,
22,
0,
1,
0,
2,
2,
0,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
}
],
"label": "horizon-win-9800x3d-5080"
}
@@ -0,0 +1,773 @@
{
"format": "horizon-benchmark-report",
"version": 1,
"kind": "game",
"created": "2026-10-29T18:55:59Z",
"app": {
"name": "horizon-client",
"version": "v0.2.97",
"commit": null
},
"hardware": {
"os": "windows",
"arch": "x86_64",
"cpu_threads": 16,
"os_version": "Microsoft Windows [Version 10.0.26200.8037]",
"cpu": "AMD Ryzen 7 9800X3D 8-Core Processor",
"ram_mb": 95943,
"gpu": {
"name": "NVIDIA GeForce RTX 5080",
"vendor": "nvidia",
"driver": "NVIDIA 617.42",
"backend": "vulkan",
"kind": "discrete"
}
},
"conditions": {
"scenario": "perf_bench:village,forest,ocean,mountain",
"seed": "1234",
"render_distance": 32,
"simulation_distance": 12,
"vsync": false,
"offscreen": false,
"repeats": 3,
"warmup_s": 2.0,
"window": [
1280,
720
]
},
"metrics": {
"startup.title_ms": {
"n": 3,
"mean": 2897.812,
"stdev": 82.455,
"min": 2844.677,
"max": 2992.8,
"ci95": 204.846
},
"startup.world_ms": {
"n": 3,
"mean": 10503.454,
"stdev": 493.592,
"min": 9934.491,
"max": 10816.992,
"ci95": 1226.251
},
"chunks.village.load_ms": {
"n": 3,
"mean": 3717.47,
"stdev": 1330.649,
"min": 2942.872,
"max": 5253.954,
"ci95": 3305.783
},
"chunks.village.meshed_count": {
"n": 3,
"mean": 47525.333,
"stdev": 1369.122,
"min": 46133.0,
"max": 48870.0,
"ci95": 3401.363
},
"fps.village.avg_fps": {
"n": 3,
"mean": 1163.541,
"stdev": 3.485,
"min": 1159.52,
"max": 1165.693,
"ci95": 8.658
},
"fps.village.low_1pct_fps": {
"n": 3,
"mean": 415.992,
"stdev": 15.742,
"min": 399.062,
"max": 430.188,
"ci95": 39.108
},
"fps.village.low_01pct_fps": {
"n": 3,
"mean": 215.549,
"stdev": 34.678,
"min": 177.857,
"max": 246.101,
"ci95": 86.151
},
"memory.village.gpu_mb": {
"n": 3,
"mean": 1833.432,
"stdev": 4.415,
"min": 1828.334,
"max": 1835.997,
"ci95": 10.969
},
"chunks.forest.load_ms": {
"n": 3,
"mean": 5455.049,
"stdev": 593.897,
"min": 4973.659,
"max": 6118.723,
"ci95": 1475.44
},
"chunks.forest.meshed_count": {
"n": 3,
"mean": 82758.0,
"stdev": 413.547,
"min": 82487.0,
"max": 83234.0,
"ci95": 1027.39
},
"fps.forest.avg_fps": {
"n": 3,
"mean": 1389.46,
"stdev": 28.128,
"min": 1371.096,
"max": 1421.843,
"ci95": 69.88
},
"fps.forest.low_1pct_fps": {
"n": 3,
"mean": 504.377,
"stdev": 13.251,
"min": 492.444,
"max": 518.637,
"ci95": 32.919
},
"fps.forest.low_01pct_fps": {
"n": 3,
"mean": 329.682,
"stdev": 47.451,
"min": 294.872,
"max": 383.732,
"ci95": 117.885
},
"memory.forest.gpu_mb": {
"n": 3,
"mean": 1985.175,
"stdev": 6.354,
"min": 1978.907,
"max": 1991.612,
"ci95": 15.786
},
"chunks.ocean.load_ms": {
"n": 3,
"mean": 4504.145,
"stdev": 745.393,
"min": 3650.589,
"max": 5026.8,
"ci95": 1851.809
},
"chunks.ocean.meshed_count": {
"n": 3,
"mean": 74927.333,
"stdev": 399.375,
"min": 74476.0,
"max": 75235.0,
"ci95": 992.182
},
"fps.ocean.avg_fps": {
"n": 3,
"mean": 1188.639,
"stdev": 10.807,
"min": 1177.005,
"max": 1198.365,
"ci95": 26.849
},
"fps.ocean.low_1pct_fps": {
"n": 3,
"mean": 154.773,
"stdev": 3.712,
"min": 151.38,
"max": 158.738,
"ci95": 9.223
},
"fps.ocean.low_01pct_fps": {
"n": 3,
"mean": 67.809,
"stdev": 3.032,
"min": 65.903,
"max": 71.305,
"ci95": 7.532
},
"memory.ocean.gpu_mb": {
"n": 3,
"mean": 2084.845,
"stdev": 9.674,
"min": 2074.009,
"max": 2092.615,
"ci95": 24.034
},
"chunks.mountain.load_ms": {
"n": 3,
"mean": 9048.431,
"stdev": 344.33,
"min": 8821.41,
"max": 9444.624,
"ci95": 855.432
},
"chunks.mountain.meshed_count": {
"n": 3,
"mean": 89659.667,
"stdev": 87.123,
"min": 89561.0,
"max": 89726.0,
"ci95": 216.442
},
"fps.mountain.avg_fps": {
"n": 3,
"mean": 1085.847,
"stdev": 13.819,
"min": 1070.378,
"max": 1096.974,
"ci95": 34.332
},
"fps.mountain.low_1pct_fps": {
"n": 3,
"mean": 257.7,
"stdev": 31.437,
"min": 224.366,
"max": 286.814,
"ci95": 78.101
},
"fps.mountain.low_01pct_fps": {
"n": 3,
"mean": 107.783,
"stdev": 14.883,
"min": 91.541,
"max": 120.768,
"ci95": 36.975
},
"memory.mountain.gpu_mb": {
"n": 3,
"mean": 2708.833,
"stdev": 9.674,
"min": 2697.997,
"max": 2716.603,
"ci95": 24.034
},
"memory.rss_peak_mb": {
"n": 3,
"mean": 2044.895,
"stdev": 10.562,
"min": 2032.875,
"max": 2052.691,
"ci95": 26.238
},
"memory.rss_steady_mb": {
"n": 3,
"mean": 1730.107,
"stdev": 14.764,
"min": 1717.52,
"max": 1746.357,
"ci95": 36.678
}
},
"frames": [
{
"phase": "village",
"repeat": 0,
"frames": 13986,
"seconds": 12.001,
"avg_fps": 1165.41,
"low_1pct_fps": 399.062,
"low_01pct_fps": 177.857,
"p50_ms": 0.822,
"p95_ms": 1.21,
"p99_ms": 1.691,
"p999_ms": 3.499,
"max_ms": 13.089,
"cpu_p50_ms": 0.665,
"gpu_p50_ms": 0.193,
"histogram": [
12380,
1527,
58,
13,
2,
2,
1,
2,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 1,
"frames": 13915,
"seconds": 12.001,
"avg_fps": 1159.52,
"low_1pct_fps": 430.188,
"low_01pct_fps": 246.101,
"p50_ms": 0.809,
"p95_ms": 1.212,
"p99_ms": 1.702,
"p999_ms": 3.291,
"max_ms": 6.894,
"cpu_p50_ms": 0.648,
"gpu_p50_ms": 0.192,
"histogram": [
11996,
1839,
59,
17,
2,
1,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 2,
"frames": 13989,
"seconds": 12.001,
"avg_fps": 1165.693,
"low_1pct_fps": 418.725,
"low_01pct_fps": 222.688,
"p50_ms": 0.816,
"p95_ms": 1.22,
"p99_ms": 1.692,
"p999_ms": 3.264,
"max_ms": 7.781,
"cpu_p50_ms": 0.669,
"gpu_p50_ms": 0.193,
"histogram": [
12321,
1578,
72,
11,
2,
4,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 0,
"frames": 17063,
"seconds": 12.001,
"avg_fps": 1421.843,
"low_1pct_fps": 502.049,
"low_01pct_fps": 294.872,
"p50_ms": 0.666,
"p95_ms": 0.935,
"p99_ms": 1.549,
"p999_ms": 2.692,
"max_ms": 6.831,
"cpu_p50_ms": 0.533,
"gpu_p50_ms": 0.362,
"histogram": [
16417,
596,
40,
7,
2,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 1,
"frames": 16454,
"seconds": 12.001,
"avg_fps": 1371.096,
"low_1pct_fps": 492.444,
"low_01pct_fps": 310.443,
"p50_ms": 0.688,
"p95_ms": 0.979,
"p99_ms": 1.556,
"p999_ms": 2.693,
"max_ms": 4.596,
"cpu_p50_ms": 0.553,
"gpu_p50_ms": 0.36,
"histogram": [
15692,
697,
57,
7,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 2,
"frames": 16506,
"seconds": 12.001,
"avg_fps": 1375.442,
"low_1pct_fps": 518.637,
"low_01pct_fps": 383.732,
"p50_ms": 0.675,
"p95_ms": 1.017,
"p99_ms": 1.605,
"p999_ms": 2.369,
"max_ms": 3.201,
"cpu_p50_ms": 0.54,
"gpu_p50_ms": 0.364,
"histogram": [
15628,
833,
44,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 0,
"frames": 14381,
"seconds": 12.001,
"avg_fps": 1198.365,
"low_1pct_fps": 154.2,
"low_01pct_fps": 66.219,
"p50_ms": 0.731,
"p95_ms": 1.023,
"p99_ms": 3.003,
"p999_ms": 12.478,
"max_ms": 18.863,
"cpu_p50_ms": 0.596,
"gpu_p50_ms": 0.315,
"histogram": [
13586,
546,
105,
60,
13,
13,
8,
16,
8,
11,
5,
7,
3,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 1,
"frames": 14287,
"seconds": 12.0,
"avg_fps": 1190.548,
"low_1pct_fps": 158.738,
"low_01pct_fps": 71.305,
"p50_ms": 0.741,
"p95_ms": 1.041,
"p99_ms": 2.9,
"p999_ms": 12.0,
"max_ms": 17.23,
"cpu_p50_ms": 0.602,
"gpu_p50_ms": 0.315,
"histogram": [
13450,
591,
109,
57,
13,
8,
10,
11,
14,
10,
7,
6,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 2,
"frames": 14124,
"seconds": 12.0,
"avg_fps": 1177.005,
"low_1pct_fps": 151.38,
"low_01pct_fps": 65.903,
"p50_ms": 0.739,
"p95_ms": 1.096,
"p99_ms": 2.928,
"p999_ms": 11.884,
"max_ms": 19.375,
"cpu_p50_ms": 0.604,
"gpu_p50_ms": 0.314,
"histogram": [
13052,
822,
114,
54,
16,
8,
4,
14,
11,
15,
3,
8,
3,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 0,
"frames": 12845,
"seconds": 12.0,
"avg_fps": 1070.378,
"low_1pct_fps": 224.366,
"low_01pct_fps": 91.541,
"p50_ms": 0.825,
"p95_ms": 1.362,
"p99_ms": 2.325,
"p999_ms": 9.621,
"max_ms": 13.309,
"cpu_p50_ms": 0.689,
"gpu_p50_ms": 0.307,
"histogram": [
10937,
1699,
143,
25,
5,
6,
4,
8,
8,
8,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 1,
"frames": 13083,
"seconds": 12.001,
"avg_fps": 1090.188,
"low_1pct_fps": 261.921,
"low_01pct_fps": 111.039,
"p50_ms": 0.816,
"p95_ms": 1.335,
"p99_ms": 2.14,
"p999_ms": 8.222,
"max_ms": 11.95,
"cpu_p50_ms": 0.681,
"gpu_p50_ms": 0.306,
"histogram": [
11105,
1812,
112,
21,
5,
5,
5,
6,
11,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 2,
"frames": 13164,
"seconds": 12.0,
"avg_fps": 1096.974,
"low_1pct_fps": 286.814,
"low_01pct_fps": 120.768,
"p50_ms": 0.822,
"p95_ms": 1.307,
"p99_ms": 1.989,
"p999_ms": 7.701,
"max_ms": 9.429,
"cpu_p50_ms": 0.687,
"gpu_p50_ms": 0.307,
"histogram": [
11280,
1758,
82,
13,
5,
5,
6,
8,
7,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
}
],
"label": "horizon-win-9800x3d-5080"
}
@@ -0,0 +1,773 @@
{
"format": "horizon-benchmark-report",
"version": 1,
"kind": "game",
"created": "2026-10-29T18:37:03Z",
"app": {
"name": "horizon-client",
"version": "v0.2.97",
"commit": null
},
"hardware": {
"os": "windows",
"arch": "x86_64",
"cpu_threads": 16,
"os_version": "Microsoft Windows [Version 10.0.26200.8037]",
"cpu": "AMD Ryzen 7 9800X3D 8-Core Processor",
"ram_mb": 95943,
"gpu": {
"name": "NVIDIA GeForce RTX 5080",
"vendor": "nvidia",
"driver": "NVIDIA 617.42",
"backend": "vulkan",
"kind": "discrete"
}
},
"conditions": {
"scenario": "perf_bench:village,forest,ocean,mountain",
"seed": "1234",
"render_distance": 8,
"simulation_distance": 12,
"vsync": false,
"offscreen": false,
"repeats": 3,
"warmup_s": 2.0,
"window": [
1280,
720
]
},
"metrics": {
"startup.title_ms": {
"n": 3,
"mean": 2950.343,
"stdev": 162.732,
"min": 2822.925,
"max": 3133.657,
"ci95": 404.282
},
"startup.world_ms": {
"n": 3,
"mean": 6135.1,
"stdev": 863.831,
"min": 5547.861,
"max": 7126.98,
"ci95": 2146.05
},
"chunks.village.load_ms": {
"n": 3,
"mean": 985.519,
"stdev": 28.184,
"min": 956.244,
"max": 1012.469,
"ci95": 70.019
},
"chunks.village.meshed_count": {
"n": 3,
"mean": 6908.0,
"stdev": 9.849,
"min": 6897.0,
"max": 6916.0,
"ci95": 24.468
},
"fps.village.avg_fps": {
"n": 3,
"mean": 2941.608,
"stdev": 79.283,
"min": 2850.416,
"max": 2994.188,
"ci95": 196.965
},
"fps.village.low_1pct_fps": {
"n": 3,
"mean": 1271.318,
"stdev": 58.797,
"min": 1203.758,
"max": 1310.917,
"ci95": 146.072
},
"fps.village.low_01pct_fps": {
"n": 3,
"mean": 839.261,
"stdev": 37.982,
"min": 795.415,
"max": 862.067,
"ci95": 94.36
},
"memory.village.gpu_mb": {
"n": 3,
"mean": 363.984,
"stdev": 0.926,
"min": 363.449,
"max": 365.053,
"ci95": 2.299
},
"chunks.forest.load_ms": {
"n": 3,
"mean": 1350.752,
"stdev": 293.128,
"min": 1106.764,
"max": 1675.913,
"ci95": 728.23
},
"chunks.forest.meshed_count": {
"n": 3,
"mean": 8066.0,
"stdev": 108.885,
"min": 7986.0,
"max": 8190.0,
"ci95": 270.508
},
"fps.forest.avg_fps": {
"n": 3,
"mean": 3494.693,
"stdev": 110.997,
"min": 3389.894,
"max": 3610.993,
"ci95": 275.755
},
"fps.forest.low_1pct_fps": {
"n": 3,
"mean": 1295.14,
"stdev": 159.638,
"min": 1148.468,
"max": 1465.171,
"ci95": 396.596
},
"fps.forest.low_01pct_fps": {
"n": 3,
"mean": 859.326,
"stdev": 119.845,
"min": 721.994,
"max": 942.743,
"ci95": 297.734
},
"memory.forest.gpu_mb": {
"n": 3,
"mean": 381.189,
"stdev": 5.222,
"min": 376.423,
"max": 386.77,
"ci95": 12.972
},
"chunks.ocean.load_ms": {
"n": 3,
"mean": 1123.386,
"stdev": 604.175,
"min": 734.302,
"max": 1819.414,
"ci95": 1500.976
},
"chunks.ocean.meshed_count": {
"n": 3,
"mean": 6613.0,
"stdev": 135.632,
"min": 6523.0,
"max": 6769.0,
"ci95": 336.955
},
"fps.ocean.avg_fps": {
"n": 3,
"mean": 3346.003,
"stdev": 59.312,
"min": 3303.535,
"max": 3413.77,
"ci95": 147.352
},
"fps.ocean.low_1pct_fps": {
"n": 3,
"mean": 1357.991,
"stdev": 95.364,
"min": 1248.887,
"max": 1425.451,
"ci95": 236.917
},
"fps.ocean.low_01pct_fps": {
"n": 3,
"mean": 911.822,
"stdev": 94.717,
"min": 802.528,
"max": 969.989,
"ci95": 235.308
},
"memory.ocean.gpu_mb": {
"n": 3,
"mean": 391.636,
"stdev": 5.435,
"min": 387.424,
"max": 397.771,
"ci95": 13.502
},
"chunks.mountain.load_ms": {
"n": 3,
"mean": 1588.954,
"stdev": 458.924,
"min": 1309.548,
"max": 2118.606,
"ci95": 1140.123
},
"chunks.mountain.meshed_count": {
"n": 3,
"mean": 7887.667,
"stdev": 41.041,
"min": 7862.0,
"max": 7935.0,
"ci95": 101.959
},
"fps.mountain.avg_fps": {
"n": 3,
"mean": 3375.623,
"stdev": 26.82,
"min": 3352.309,
"max": 3404.933,
"ci95": 66.629
},
"fps.mountain.low_1pct_fps": {
"n": 3,
"mean": 1383.745,
"stdev": 67.628,
"min": 1324.071,
"max": 1457.203,
"ci95": 168.01
},
"fps.mountain.low_01pct_fps": {
"n": 3,
"mean": 850.213,
"stdev": 103.149,
"min": 790.305,
"max": 969.318,
"ci95": 256.256
},
"memory.mountain.gpu_mb": {
"n": 3,
"mean": 448.64,
"stdev": 5.435,
"min": 444.428,
"max": 454.775,
"ci95": 13.502
},
"memory.rss_peak_mb": {
"n": 3,
"mean": 1171.629,
"stdev": 0.337,
"min": 1171.398,
"max": 1172.016,
"ci95": 0.837
},
"memory.rss_steady_mb": {
"n": 3,
"mean": 1082.078,
"stdev": 12.494,
"min": 1069.117,
"max": 1094.047,
"ci95": 31.04
}
},
"frames": [
{
"phase": "village",
"repeat": 0,
"frames": 35764,
"seconds": 12.0,
"avg_fps": 2980.22,
"low_1pct_fps": 1310.917,
"low_01pct_fps": 860.301,
"p50_ms": 0.317,
"p95_ms": 0.433,
"p99_ms": 0.633,
"p999_ms": 0.937,
"max_ms": 2.807,
"cpu_p50_ms": 0.192,
"gpu_p50_ms": 0.063,
"histogram": [
35741,
22,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 1,
"frames": 35931,
"seconds": 12.0,
"avg_fps": 2994.188,
"low_1pct_fps": 1299.278,
"low_01pct_fps": 795.415,
"p50_ms": 0.318,
"p95_ms": 0.428,
"p99_ms": 0.62,
"p999_ms": 0.943,
"max_ms": 5.797,
"cpu_p50_ms": 0.191,
"gpu_p50_ms": 0.063,
"histogram": [
35906,
23,
1,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 2,
"frames": 34206,
"seconds": 12.0,
"avg_fps": 2850.416,
"low_1pct_fps": 1203.758,
"low_01pct_fps": 862.067,
"p50_ms": 0.326,
"p95_ms": 0.508,
"p99_ms": 0.705,
"p999_ms": 1.025,
"max_ms": 1.398,
"cpu_p50_ms": 0.196,
"gpu_p50_ms": 0.063,
"histogram": [
34167,
39,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 0,
"frames": 41800,
"seconds": 12.0,
"avg_fps": 3483.193,
"low_1pct_fps": 1271.782,
"low_01pct_fps": 913.242,
"p50_ms": 0.264,
"p95_ms": 0.415,
"p99_ms": 0.657,
"p999_ms": 0.935,
"max_ms": 1.482,
"cpu_p50_ms": 0.144,
"gpu_p50_ms": 0.075,
"histogram": [
41768,
32,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 1,
"frames": 43333,
"seconds": 12.0,
"avg_fps": 3610.993,
"low_1pct_fps": 1465.171,
"low_01pct_fps": 942.743,
"p50_ms": 0.263,
"p95_ms": 0.355,
"p99_ms": 0.561,
"p999_ms": 0.859,
"max_ms": 1.694,
"cpu_p50_ms": 0.143,
"gpu_p50_ms": 0.074,
"histogram": [
43310,
23,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 2,
"frames": 40679,
"seconds": 12.0,
"avg_fps": 3389.894,
"low_1pct_fps": 1148.468,
"low_01pct_fps": 721.994,
"p50_ms": 0.267,
"p95_ms": 0.446,
"p99_ms": 0.67,
"p999_ms": 1.183,
"max_ms": 2.434,
"cpu_p50_ms": 0.146,
"gpu_p50_ms": 0.076,
"histogram": [
40590,
87,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 0,
"frames": 39849,
"seconds": 12.0,
"avg_fps": 3320.704,
"low_1pct_fps": 1399.634,
"low_01pct_fps": 969.989,
"p50_ms": 0.281,
"p95_ms": 0.404,
"p99_ms": 0.601,
"p999_ms": 0.875,
"max_ms": 1.494,
"cpu_p50_ms": 0.159,
"gpu_p50_ms": 0.069,
"histogram": [
39831,
18,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 1,
"frames": 40966,
"seconds": 12.0,
"avg_fps": 3413.77,
"low_1pct_fps": 1425.451,
"low_01pct_fps": 962.948,
"p50_ms": 0.278,
"p95_ms": 0.372,
"p99_ms": 0.581,
"p999_ms": 0.857,
"max_ms": 1.576,
"cpu_p50_ms": 0.157,
"gpu_p50_ms": 0.069,
"histogram": [
40947,
19,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 2,
"frames": 39643,
"seconds": 12.0,
"avg_fps": 3303.535,
"low_1pct_fps": 1248.887,
"low_01pct_fps": 802.528,
"p50_ms": 0.28,
"p95_ms": 0.413,
"p99_ms": 0.642,
"p999_ms": 1.087,
"max_ms": 2.215,
"cpu_p50_ms": 0.158,
"gpu_p50_ms": 0.069,
"histogram": [
39582,
60,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 0,
"frames": 40436,
"seconds": 12.0,
"avg_fps": 3369.626,
"low_1pct_fps": 1369.962,
"low_01pct_fps": 790.305,
"p50_ms": 0.279,
"p95_ms": 0.403,
"p99_ms": 0.604,
"p999_ms": 0.812,
"max_ms": 10.313,
"cpu_p50_ms": 0.159,
"gpu_p50_ms": 0.069,
"histogram": [
40425,
8,
1,
1,
0,
0,
0,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 1,
"frames": 40860,
"seconds": 12.0,
"avg_fps": 3404.933,
"low_1pct_fps": 1457.203,
"low_01pct_fps": 969.318,
"p50_ms": 0.278,
"p95_ms": 0.389,
"p99_ms": 0.574,
"p999_ms": 0.814,
"max_ms": 2.714,
"cpu_p50_ms": 0.158,
"gpu_p50_ms": 0.069,
"histogram": [
40847,
11,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 2,
"frames": 40229,
"seconds": 12.0,
"avg_fps": 3352.309,
"low_1pct_fps": 1324.071,
"low_01pct_fps": 791.016,
"p50_ms": 0.28,
"p95_ms": 0.402,
"p99_ms": 0.617,
"p999_ms": 0.968,
"max_ms": 3.746,
"cpu_p50_ms": 0.159,
"gpu_p50_ms": 0.069,
"histogram": [
40192,
35,
1,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
}
],
"label": "horizon-win-9800x3d-5080"
}
@@ -0,0 +1,706 @@
{
"format": "horizon-benchmark-report",
"version": 1,
"kind": "game",
"created": "2026-10-29T01:25:53Z",
"app": {
"name": "minecraft",
"version": "26.3",
"mods": [
{
"name": "fabric-loader",
"version": "0.19.5"
},
{
"name": "sodium",
"version": "mc26.3-0.9.2-fabric"
},
{
"name": "lithium",
"version": "mc26.3-0.26.2-fabric"
},
{
"name": "ferrite-core",
"version": "9.0.0-fabric"
},
{
"name": "immediatelyfast",
"version": "1.17.1+26.3-fabric"
}
]
},
"hardware": {
"os": "windows",
"arch": "x86_64",
"cpu_threads": 16,
"os_version": "Microsoft Windows [Version 10.0.26200.8037]",
"cpu": "AMD Ryzen 7 9800X3D 8-Core Processor",
"ram_mb": 95943,
"gpu": {
"name": "NVIDIA GeForce RTX 5080/PCIe/SSE2",
"vendor": "NVIDIA Corporation",
"driver": "3.3.0 NVIDIA 617.42",
"backend": "opengl",
"kind": ""
}
},
"conditions": {
"scenario": "perf_bench:village,forest,ocean,mountain",
"seed": "1234",
"render_distance": 16,
"simulation_distance": 12,
"vsync": false,
"offscreen": false,
"repeats": 3,
"warmup_s": 2.0,
"window": [
1280,
720
]
},
"metrics": {
"startup.title_ms": {
"n": 3,
"mean": 10777.756,
"stdev": 129.67,
"min": 10636.92,
"max": 10892.2,
"ci95": 322.144
},
"startup.world_ms": {
"n": 3,
"mean": 23477.423,
"stdev": 346.614,
"min": 23130.92,
"max": 23824.148,
"ci95": 861.106
},
"chunks.village.load_ms": {
"n": 3,
"mean": 6007.033,
"stdev": 104.227,
"min": 5891.431,
"max": 6093.825,
"ci95": 258.936
},
"fps.village.avg_fps": {
"n": 3,
"mean": 1282.11,
"stdev": 65.207,
"min": 1228.13,
"max": 1354.56,
"ci95": 161.997
},
"fps.village.low_1pct_fps": {
"n": 3,
"mean": 343.465,
"stdev": 9.96,
"min": 335.223,
"max": 354.532,
"ci95": 24.744
},
"fps.village.low_01pct_fps": {
"n": 3,
"mean": 131.669,
"stdev": 4.692,
"min": 126.427,
"max": 135.476,
"ci95": 11.656
},
"chunks.forest.load_ms": {
"n": 3,
"mean": 8669.311,
"stdev": 434.39,
"min": 8280.854,
"max": 9138.35,
"ci95": 1079.173
},
"fps.forest.avg_fps": {
"n": 3,
"mean": 1745.757,
"stdev": 13.014,
"min": 1734.41,
"max": 1759.963,
"ci95": 32.332
},
"fps.forest.low_1pct_fps": {
"n": 3,
"mean": 460.958,
"stdev": 10.859,
"min": 449.051,
"max": 470.316,
"ci95": 26.978
},
"fps.forest.low_01pct_fps": {
"n": 3,
"mean": 178.955,
"stdev": 3.777,
"min": 175.213,
"max": 182.766,
"ci95": 9.383
},
"chunks.ocean.load_ms": {
"n": 3,
"mean": 7056.368,
"stdev": 117.329,
"min": 6986.912,
"max": 7191.833,
"ci95": 291.485
},
"fps.ocean.avg_fps": {
"n": 3,
"mean": 1815.056,
"stdev": 47.875,
"min": 1759.788,
"max": 1843.745,
"ci95": 118.937
},
"fps.ocean.low_1pct_fps": {
"n": 3,
"mean": 514.215,
"stdev": 57.205,
"min": 452.445,
"max": 565.368,
"ci95": 142.117
},
"fps.ocean.low_01pct_fps": {
"n": 3,
"mean": 191.842,
"stdev": 23.217,
"min": 175.546,
"max": 218.426,
"ci95": 57.68
},
"chunks.mountain.load_ms": {
"n": 3,
"mean": 14094.582,
"stdev": 399.624,
"min": 13641.534,
"max": 14397.0,
"ci95": 992.801
},
"fps.mountain.avg_fps": {
"n": 3,
"mean": 1499.768,
"stdev": 22.197,
"min": 1477.269,
"max": 1521.651,
"ci95": 55.146
},
"fps.mountain.low_1pct_fps": {
"n": 3,
"mean": 408.466,
"stdev": 16.114,
"min": 395.097,
"max": 426.358,
"ci95": 40.032
},
"fps.mountain.low_01pct_fps": {
"n": 3,
"mean": 139.743,
"stdev": 3.969,
"min": 136.465,
"max": 144.156,
"ci95": 9.86
},
"memory.rss_peak_mb": {
"n": 3,
"mean": 2495.607,
"stdev": 75.339,
"min": 2412.738,
"max": 2559.965,
"ci95": 187.166
},
"memory.rss_steady_mb": {
"n": 3,
"mean": 2326.611,
"stdev": 15.702,
"min": 2314.98,
"max": 2344.473,
"ci95": 39.01
}
},
"frames": [
{
"phase": "village",
"repeat": 0,
"frames": 14738,
"seconds": 12.0,
"avg_fps": 1228.13,
"low_1pct_fps": 335.223,
"low_01pct_fps": 133.103,
"p50_ms": 0.722,
"p95_ms": 1.384,
"p99_ms": 2.043,
"p999_ms": 5.224,
"max_ms": 11.459,
"histogram": [
12159,
2421,
128,
14,
1,
5,
2,
3,
4,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 1,
"frames": 16255,
"seconds": 12.0,
"avg_fps": 1354.56,
"low_1pct_fps": 354.532,
"low_01pct_fps": 135.476,
"p50_ms": 0.672,
"p95_ms": 1.136,
"p99_ms": 1.841,
"p999_ms": 4.913,
"max_ms": 16.94,
"histogram": [
14841,
1298,
86,
13,
2,
5,
4,
1,
3,
1,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 2,
"frames": 15164,
"seconds": 12.0,
"avg_fps": 1263.64,
"low_1pct_fps": 340.639,
"low_01pct_fps": 126.427,
"p50_ms": 0.708,
"p95_ms": 1.299,
"p99_ms": 1.895,
"p999_ms": 5.173,
"max_ms": 16.748,
"histogram": [
13026,
2014,
94,
13,
1,
3,
3,
6,
2,
1,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 0,
"frames": 20915,
"seconds": 12.0,
"avg_fps": 1742.897,
"low_1pct_fps": 470.316,
"low_01pct_fps": 182.766,
"p50_ms": 0.5,
"p95_ms": 0.936,
"p99_ms": 1.437,
"p999_ms": 2.77,
"max_ms": 13.414,
"histogram": [
20067,
788,
44,
4,
0,
4,
3,
3,
1,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 1,
"frames": 21120,
"seconds": 12.0,
"avg_fps": 1759.963,
"low_1pct_fps": 449.051,
"low_01pct_fps": 178.887,
"p50_ms": 0.49,
"p95_ms": 0.901,
"p99_ms": 1.531,
"p999_ms": 3.005,
"max_ms": 12.907,
"histogram": [
20414,
644,
40,
10,
1,
3,
2,
2,
2,
1,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 2,
"frames": 20813,
"seconds": 12.0,
"avg_fps": 1734.41,
"low_1pct_fps": 463.506,
"low_01pct_fps": 175.213,
"p50_ms": 0.496,
"p95_ms": 0.924,
"p99_ms": 1.414,
"p999_ms": 2.733,
"max_ms": 16.832,
"histogram": [
20049,
707,
41,
5,
0,
1,
5,
2,
2,
0,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 0,
"frames": 22100,
"seconds": 12.0,
"avg_fps": 1841.634,
"low_1pct_fps": 565.368,
"low_01pct_fps": 218.426,
"p50_ms": 0.483,
"p95_ms": 0.835,
"p99_ms": 1.209,
"p999_ms": 2.082,
"max_ms": 14.627,
"histogram": [
21540,
532,
18,
1,
1,
1,
2,
2,
1,
1,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 1,
"frames": 21118,
"seconds": 12.0,
"avg_fps": 1759.788,
"low_1pct_fps": 452.445,
"low_01pct_fps": 175.546,
"p50_ms": 0.489,
"p95_ms": 0.915,
"p99_ms": 1.532,
"p999_ms": 2.811,
"max_ms": 11.846,
"histogram": [
20308,
755,
36,
9,
0,
2,
0,
3,
2,
3,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 2,
"frames": 22125,
"seconds": 12.0,
"avg_fps": 1843.745,
"low_1pct_fps": 524.832,
"low_01pct_fps": 181.555,
"p50_ms": 0.477,
"p95_ms": 0.855,
"p99_ms": 1.241,
"p999_ms": 2.236,
"max_ms": 12.478,
"histogram": [
21487,
608,
18,
1,
1,
1,
2,
2,
1,
2,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 0,
"frames": 18005,
"seconds": 12.0,
"avg_fps": 1500.383,
"low_1pct_fps": 403.944,
"low_01pct_fps": 136.465,
"p50_ms": 0.591,
"p95_ms": 1.058,
"p99_ms": 1.592,
"p999_ms": 3.186,
"max_ms": 12.608,
"histogram": [
16864,
1084,
32,
8,
0,
5,
3,
5,
1,
2,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 1,
"frames": 17728,
"seconds": 12.001,
"avg_fps": 1477.269,
"low_1pct_fps": 395.097,
"low_01pct_fps": 138.609,
"p50_ms": 0.598,
"p95_ms": 1.042,
"p99_ms": 1.561,
"p999_ms": 5.033,
"max_ms": 11.462,
"histogram": [
16612,
1051,
39,
7,
1,
6,
5,
3,
1,
3,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 2,
"frames": 18260,
"seconds": 12.0,
"avg_fps": 1521.651,
"low_1pct_fps": 426.358,
"low_01pct_fps": 144.156,
"p50_ms": 0.585,
"p95_ms": 1.015,
"p99_ms": 1.458,
"p999_ms": 3.609,
"max_ms": 11.945,
"histogram": [
17267,
943,
26,
6,
5,
2,
3,
3,
2,
3,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
}
],
"label": "vanilla-mods-win-9800x3d-5080"
}
@@ -0,0 +1,706 @@
{
"format": "horizon-benchmark-report",
"version": 1,
"kind": "game",
"created": "2026-10-29T19:27:56Z",
"app": {
"name": "minecraft",
"version": "26.3",
"mods": [
{
"name": "fabric-loader",
"version": "0.19.5"
},
{
"name": "sodium",
"version": "mc26.3-0.9.2-fabric"
},
{
"name": "lithium",
"version": "mc26.3-0.26.2-fabric"
},
{
"name": "ferrite-core",
"version": "9.0.0-fabric"
},
{
"name": "immediatelyfast",
"version": "1.17.1+26.3-fabric"
}
]
},
"hardware": {
"os": "windows",
"arch": "x86_64",
"cpu_threads": 16,
"os_version": "Microsoft Windows [Version 10.0.26200.8037]",
"cpu": "AMD Ryzen 7 9800X3D 8-Core Processor",
"ram_mb": 95943,
"gpu": {
"name": "NVIDIA GeForce RTX 5080/PCIe/SSE2",
"vendor": "NVIDIA Corporation",
"driver": "3.3.0 NVIDIA 617.42",
"backend": "opengl",
"kind": ""
}
},
"conditions": {
"scenario": "perf_bench:village,forest,ocean,mountain",
"seed": "1234",
"render_distance": 32,
"simulation_distance": 12,
"vsync": false,
"offscreen": false,
"repeats": 3,
"warmup_s": 2.0,
"window": [
1280,
720
]
},
"metrics": {
"startup.title_ms": {
"n": 3,
"mean": 11038.922,
"stdev": 118.058,
"min": 10935.424,
"max": 11167.506,
"ci95": 293.295
},
"startup.world_ms": {
"n": 3,
"mean": 39248.588,
"stdev": 479.755,
"min": 38964.834,
"max": 39802.506,
"ci95": 1191.875
},
"chunks.village.load_ms": {
"n": 3,
"mean": 11176.953,
"stdev": 356.144,
"min": 10836.65,
"max": 11547.064,
"ci95": 884.781
},
"fps.village.avg_fps": {
"n": 3,
"mean": 925.977,
"stdev": 6.727,
"min": 918.832,
"max": 932.189,
"ci95": 16.713
},
"fps.village.low_1pct_fps": {
"n": 3,
"mean": 250.092,
"stdev": 1.868,
"min": 248.431,
"max": 252.114,
"ci95": 4.64
},
"fps.village.low_01pct_fps": {
"n": 3,
"mean": 92.692,
"stdev": 4.466,
"min": 88.423,
"max": 97.332,
"ci95": 11.095
},
"chunks.forest.load_ms": {
"n": 3,
"mean": 17335.638,
"stdev": 687.588,
"min": 16544.168,
"max": 17785.772,
"ci95": 1708.201
},
"fps.forest.avg_fps": {
"n": 3,
"mean": 929.034,
"stdev": 3.11,
"min": 926.32,
"max": 932.428,
"ci95": 7.727
},
"fps.forest.low_1pct_fps": {
"n": 3,
"mean": 312.69,
"stdev": 16.386,
"min": 297.667,
"max": 330.163,
"ci95": 40.708
},
"fps.forest.low_01pct_fps": {
"n": 3,
"mean": 129.331,
"stdev": 12.786,
"min": 115.486,
"max": 140.694,
"ci95": 31.765
},
"chunks.ocean.load_ms": {
"n": 3,
"mean": 12601.063,
"stdev": 148.042,
"min": 12430.182,
"max": 12690.506,
"ci95": 367.785
},
"fps.ocean.avg_fps": {
"n": 3,
"mean": 1016.598,
"stdev": 5.481,
"min": 1010.482,
"max": 1021.068,
"ci95": 13.618
},
"fps.ocean.low_1pct_fps": {
"n": 3,
"mean": 325.361,
"stdev": 15.112,
"min": 315.682,
"max": 342.774,
"ci95": 37.543
},
"fps.ocean.low_01pct_fps": {
"n": 3,
"mean": 117.042,
"stdev": 11.095,
"min": 107.807,
"max": 129.349,
"ci95": 27.563
},
"chunks.mountain.load_ms": {
"n": 3,
"mean": 38513.008,
"stdev": 958.119,
"min": 37447.333,
"max": 39303.226,
"ci95": 2380.292
},
"fps.mountain.avg_fps": {
"n": 3,
"mean": 644.782,
"stdev": 3.94,
"min": 640.866,
"max": 648.745,
"ci95": 9.788
},
"fps.mountain.low_1pct_fps": {
"n": 3,
"mean": 166.029,
"stdev": 4.758,
"min": 162.326,
"max": 171.395,
"ci95": 11.819
},
"fps.mountain.low_01pct_fps": {
"n": 3,
"mean": 86.231,
"stdev": 8.587,
"min": 76.632,
"max": 93.184,
"ci95": 21.334
},
"memory.rss_peak_mb": {
"n": 3,
"mean": 2964.911,
"stdev": 8.27,
"min": 2959.813,
"max": 2974.453,
"ci95": 20.545
},
"memory.rss_steady_mb": {
"n": 3,
"mean": 2894.541,
"stdev": 12.847,
"min": 2879.928,
"max": 2904.059,
"ci95": 31.917
}
},
"frames": [
{
"phase": "village",
"repeat": 0,
"frames": 11123,
"seconds": 12.0,
"avg_fps": 926.911,
"low_1pct_fps": 249.732,
"low_01pct_fps": 92.322,
"p50_ms": 0.989,
"p95_ms": 1.492,
"p99_ms": 2.33,
"p999_ms": 7.409,
"max_ms": 15.258,
"histogram": [
6224,
4701,
147,
28,
3,
2,
5,
3,
3,
3,
1,
3,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 1,
"frames": 11187,
"seconds": 12.001,
"avg_fps": 932.189,
"low_1pct_fps": 252.114,
"low_01pct_fps": 88.423,
"p50_ms": 0.984,
"p95_ms": 1.496,
"p99_ms": 2.328,
"p999_ms": 7.858,
"max_ms": 16.843,
"histogram": [
6518,
4486,
138,
20,
9,
1,
1,
6,
1,
2,
3,
1,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 2,
"frames": 11026,
"seconds": 12.0,
"avg_fps": 918.832,
"low_1pct_fps": 248.431,
"low_01pct_fps": 97.332,
"p50_ms": 0.989,
"p95_ms": 1.524,
"p99_ms": 2.37,
"p999_ms": 7.446,
"max_ms": 16.573,
"histogram": [
6075,
4740,
162,
23,
5,
2,
4,
7,
4,
1,
1,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 0,
"frames": 11117,
"seconds": 12.001,
"avg_fps": 926.32,
"low_1pct_fps": 310.239,
"low_01pct_fps": 131.814,
"p50_ms": 1.011,
"p95_ms": 1.34,
"p99_ms": 2.045,
"p999_ms": 4.711,
"max_ms": 14.077,
"histogram": [
3849,
7150,
78,
26,
5,
1,
3,
2,
1,
0,
1,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 1,
"frames": 11190,
"seconds": 12.001,
"avg_fps": 932.428,
"low_1pct_fps": 330.163,
"low_01pct_fps": 140.694,
"p50_ms": 1.008,
"p95_ms": 1.307,
"p99_ms": 1.869,
"p999_ms": 4.341,
"max_ms": 13.489,
"histogram": [
4273,
6822,
60,
20,
7,
0,
3,
3,
0,
1,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 2,
"frames": 11141,
"seconds": 12.001,
"avg_fps": 928.354,
"low_1pct_fps": 297.667,
"low_01pct_fps": 115.486,
"p50_ms": 1.005,
"p95_ms": 1.352,
"p99_ms": 2.054,
"p999_ms": 5.292,
"max_ms": 14.339,
"histogram": [
4901,
6118,
83,
23,
3,
2,
4,
3,
0,
1,
2,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 0,
"frames": 12126,
"seconds": 12.0,
"avg_fps": 1010.482,
"low_1pct_fps": 317.626,
"low_01pct_fps": 113.971,
"p50_ms": 0.933,
"p95_ms": 1.23,
"p99_ms": 1.935,
"p999_ms": 4.393,
"max_ms": 14.824,
"histogram": [
9183,
2830,
82,
17,
3,
1,
2,
3,
0,
1,
3,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 1,
"frames": 12219,
"seconds": 12.0,
"avg_fps": 1018.243,
"low_1pct_fps": 342.774,
"low_01pct_fps": 129.349,
"p50_ms": 0.929,
"p95_ms": 1.222,
"p99_ms": 1.867,
"p999_ms": 4.329,
"max_ms": 10.33,
"histogram": [
9421,
2710,
60,
15,
3,
0,
0,
5,
3,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 2,
"frames": 12253,
"seconds": 12.0,
"avg_fps": 1021.068,
"low_1pct_fps": 315.682,
"low_01pct_fps": 107.807,
"p50_ms": 0.923,
"p95_ms": 1.225,
"p99_ms": 1.969,
"p999_ms": 5.235,
"max_ms": 19.524,
"histogram": [
9231,
2904,
92,
10,
2,
5,
0,
4,
1,
2,
0,
1,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 0,
"frames": 7691,
"seconds": 12.001,
"avg_fps": 640.866,
"low_1pct_fps": 164.367,
"low_01pct_fps": 93.184,
"p50_ms": 1.454,
"p95_ms": 1.95,
"p99_ms": 3.4,
"p999_ms": 9.754,
"max_ms": 14.995,
"histogram": [
36,
7331,
233,
26,
17,
13,
20,
3,
6,
5,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 1,
"frames": 7737,
"seconds": 12.0,
"avg_fps": 644.736,
"low_1pct_fps": 162.326,
"low_01pct_fps": 76.632,
"p50_ms": 1.454,
"p95_ms": 1.905,
"p99_ms": 3.676,
"p999_ms": 9.926,
"max_ms": 18.459,
"histogram": [
46,
7399,
199,
24,
21,
24,
10,
6,
1,
1,
4,
1,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 2,
"frames": 7785,
"seconds": 12.0,
"avg_fps": 648.745,
"low_1pct_fps": 171.395,
"low_01pct_fps": 88.876,
"p50_ms": 1.439,
"p95_ms": 1.886,
"p99_ms": 3.562,
"p999_ms": 10.342,
"max_ms": 12.927,
"histogram": [
57,
7432,
186,
44,
23,
20,
9,
3,
3,
7,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
}
],
"label": "vanilla-mods-win-9800x3d-5080"
}
@@ -0,0 +1,706 @@
{
"format": "horizon-benchmark-report",
"version": 1,
"kind": "game",
"created": "2026-10-29T18:49:40Z",
"app": {
"name": "minecraft",
"version": "26.3",
"mods": [
{
"name": "fabric-loader",
"version": "0.19.5"
},
{
"name": "sodium",
"version": "mc26.3-0.9.2-fabric"
},
{
"name": "lithium",
"version": "mc26.3-0.26.2-fabric"
},
{
"name": "ferrite-core",
"version": "9.0.0-fabric"
},
{
"name": "immediatelyfast",
"version": "1.17.1+26.3-fabric"
}
]
},
"hardware": {
"os": "windows",
"arch": "x86_64",
"cpu_threads": 16,
"os_version": "Microsoft Windows [Version 10.0.26200.8037]",
"cpu": "AMD Ryzen 7 9800X3D 8-Core Processor",
"ram_mb": 95943,
"gpu": {
"name": "NVIDIA GeForce RTX 5080/PCIe/SSE2",
"vendor": "NVIDIA Corporation",
"driver": "3.3.0 NVIDIA 617.42",
"backend": "opengl",
"kind": ""
}
},
"conditions": {
"scenario": "perf_bench:village,forest,ocean,mountain",
"seed": "1234",
"render_distance": 8,
"simulation_distance": 12,
"vsync": false,
"offscreen": false,
"repeats": 3,
"warmup_s": 2.0,
"window": [
1280,
720
]
},
"metrics": {
"startup.title_ms": {
"n": 3,
"mean": 11089.831,
"stdev": 46.336,
"min": 11036.36,
"max": 11118.192,
"ci95": 115.114
},
"startup.world_ms": {
"n": 3,
"mean": 17643.831,
"stdev": 47.672,
"min": 17589.192,
"max": 17676.942,
"ci95": 118.433
},
"chunks.village.load_ms": {
"n": 3,
"mean": 3603.773,
"stdev": 161.342,
"min": 3488.106,
"max": 3788.086,
"ci95": 400.828
},
"fps.village.avg_fps": {
"n": 3,
"mean": 2103.54,
"stdev": 40.465,
"min": 2078.464,
"max": 2150.222,
"ci95": 100.529
},
"fps.village.low_1pct_fps": {
"n": 3,
"mean": 577.326,
"stdev": 33.693,
"min": 557.613,
"max": 616.23,
"ci95": 83.704
},
"fps.village.low_01pct_fps": {
"n": 3,
"mean": 195.53,
"stdev": 18.899,
"min": 181.495,
"max": 217.019,
"ci95": 46.951
},
"chunks.forest.load_ms": {
"n": 3,
"mean": 3282.612,
"stdev": 48.569,
"min": 3233.16,
"max": 3330.246,
"ci95": 120.661
},
"fps.forest.avg_fps": {
"n": 3,
"mean": 2424.739,
"stdev": 26.074,
"min": 2394.789,
"max": 2442.383,
"ci95": 64.777
},
"fps.forest.low_1pct_fps": {
"n": 3,
"mean": 732.987,
"stdev": 11.926,
"min": 724.144,
"max": 746.55,
"ci95": 29.627
},
"fps.forest.low_01pct_fps": {
"n": 3,
"mean": 249.926,
"stdev": 8.826,
"min": 241.293,
"max": 258.934,
"ci95": 21.928
},
"chunks.ocean.load_ms": {
"n": 3,
"mean": 2705.954,
"stdev": 173.487,
"min": 2538.486,
"max": 2884.891,
"ci95": 431.0
},
"fps.ocean.avg_fps": {
"n": 3,
"mean": 2431.99,
"stdev": 33.071,
"min": 2394.02,
"max": 2454.493,
"ci95": 82.159
},
"fps.ocean.low_1pct_fps": {
"n": 3,
"mean": 764.065,
"stdev": 14.782,
"min": 747.287,
"max": 775.168,
"ci95": 36.723
},
"fps.ocean.low_01pct_fps": {
"n": 3,
"mean": 261.954,
"stdev": 14.724,
"min": 252.457,
"max": 278.916,
"ci95": 36.58
},
"chunks.mountain.load_ms": {
"n": 3,
"mean": 4982.865,
"stdev": 115.523,
"min": 4849.837,
"max": 5057.932,
"ci95": 286.997
},
"fps.mountain.avg_fps": {
"n": 3,
"mean": 2538.05,
"stdev": 67.405,
"min": 2462.815,
"max": 2592.938,
"ci95": 167.458
},
"fps.mountain.low_1pct_fps": {
"n": 3,
"mean": 760.16,
"stdev": 13.503,
"min": 746.001,
"max": 772.895,
"ci95": 33.547
},
"fps.mountain.low_01pct_fps": {
"n": 3,
"mean": 240.897,
"stdev": 17.872,
"min": 224.391,
"max": 259.878,
"ci95": 44.401
},
"memory.rss_peak_mb": {
"n": 3,
"mean": 2052.982,
"stdev": 57.756,
"min": 1992.574,
"max": 2107.66,
"ci95": 143.487
},
"memory.rss_steady_mb": {
"n": 3,
"mean": 1903.921,
"stdev": 62.254,
"min": 1838.307,
"max": 1962.156,
"ci95": 154.659
}
},
"frames": [
{
"phase": "village",
"repeat": 0,
"frames": 25803,
"seconds": 12.0,
"avg_fps": 2150.222,
"low_1pct_fps": 616.23,
"low_01pct_fps": 217.019,
"p50_ms": 0.433,
"p95_ms": 0.642,
"p99_ms": 1.04,
"p999_ms": 2.307,
"max_ms": 7.466,
"histogram": [
25503,
269,
8,
6,
7,
6,
2,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 1,
"frames": 24942,
"seconds": 12.0,
"avg_fps": 2078.464,
"low_1pct_fps": 557.613,
"low_01pct_fps": 181.495,
"p50_ms": 0.45,
"p95_ms": 0.665,
"p99_ms": 1.1,
"p999_ms": 2.305,
"max_ms": 9.819,
"histogram": [
24615,
288,
17,
2,
4,
9,
2,
3,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 2,
"frames": 24984,
"seconds": 12.0,
"avg_fps": 2081.934,
"low_1pct_fps": 558.135,
"low_01pct_fps": 188.076,
"p50_ms": 0.444,
"p95_ms": 0.682,
"p99_ms": 1.101,
"p999_ms": 2.412,
"max_ms": 11.567,
"histogram": [
24645,
308,
9,
3,
5,
5,
7,
1,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 0,
"frames": 29245,
"seconds": 12.0,
"avg_fps": 2437.044,
"low_1pct_fps": 728.266,
"low_01pct_fps": 249.55,
"p50_ms": 0.38,
"p95_ms": 0.555,
"p99_ms": 0.892,
"p999_ms": 1.851,
"max_ms": 6.973,
"histogram": [
29066,
154,
4,
6,
7,
6,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 1,
"frames": 28738,
"seconds": 12.0,
"avg_fps": 2394.789,
"low_1pct_fps": 724.144,
"low_01pct_fps": 241.293,
"p50_ms": 0.384,
"p95_ms": 0.571,
"p99_ms": 0.907,
"p999_ms": 1.833,
"max_ms": 8.35,
"histogram": [
28573,
140,
6,
3,
6,
5,
3,
1,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 2,
"frames": 29309,
"seconds": 12.0,
"avg_fps": 2442.383,
"low_1pct_fps": 746.55,
"low_01pct_fps": 258.934,
"p50_ms": 0.378,
"p95_ms": 0.566,
"p99_ms": 0.876,
"p999_ms": 1.744,
"max_ms": 10.291,
"histogram": [
29138,
148,
5,
6,
5,
2,
4,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 0,
"frames": 29454,
"seconds": 12.0,
"avg_fps": 2454.493,
"low_1pct_fps": 769.741,
"low_01pct_fps": 252.457,
"p50_ms": 0.378,
"p95_ms": 0.55,
"p99_ms": 0.853,
"p999_ms": 1.471,
"max_ms": 6.629,
"histogram": [
29323,
110,
2,
2,
7,
7,
3,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 1,
"frames": 28729,
"seconds": 12.0,
"avg_fps": 2394.02,
"low_1pct_fps": 747.287,
"low_01pct_fps": 254.49,
"p50_ms": 0.383,
"p95_ms": 0.579,
"p99_ms": 0.896,
"p999_ms": 1.56,
"max_ms": 7.367,
"histogram": [
28567,
141,
3,
5,
4,
4,
4,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 2,
"frames": 29370,
"seconds": 12.0,
"avg_fps": 2447.458,
"low_1pct_fps": 775.168,
"low_01pct_fps": 278.916,
"p50_ms": 0.376,
"p95_ms": 0.559,
"p99_ms": 0.887,
"p999_ms": 1.567,
"max_ms": 9.65,
"histogram": [
29209,
141,
3,
9,
0,
5,
1,
1,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 0,
"frames": 31116,
"seconds": 12.0,
"avg_fps": 2592.938,
"low_1pct_fps": 761.584,
"low_01pct_fps": 224.391,
"p50_ms": 0.356,
"p95_ms": 0.511,
"p99_ms": 0.767,
"p999_ms": 1.843,
"max_ms": 8.478,
"histogram": [
31011,
76,
4,
3,
13,
4,
3,
1,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 1,
"frames": 29554,
"seconds": 12.0,
"avg_fps": 2462.815,
"low_1pct_fps": 746.001,
"low_01pct_fps": 238.423,
"p50_ms": 0.366,
"p95_ms": 0.579,
"p99_ms": 0.849,
"p999_ms": 1.75,
"max_ms": 7.607,
"histogram": [
29430,
101,
2,
3,
9,
4,
3,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 2,
"frames": 30701,
"seconds": 12.0,
"avg_fps": 2558.396,
"low_1pct_fps": 772.895,
"low_01pct_fps": 259.878,
"p50_ms": 0.357,
"p95_ms": 0.537,
"p99_ms": 0.81,
"p999_ms": 1.702,
"max_ms": 6.682,
"histogram": [
30568,
112,
1,
6,
5,
3,
6,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
}
],
"label": "vanilla-mods-win-9800x3d-5080"
}
@@ -0,0 +1,690 @@
{
"format": "horizon-benchmark-report",
"version": 1,
"kind": "game",
"created": "2026-10-29T01:19:23Z",
"app": {
"name": "minecraft",
"version": "26.3",
"mods": [
{
"name": "fabric-loader",
"version": "0.19.5"
}
]
},
"hardware": {
"os": "windows",
"arch": "x86_64",
"cpu_threads": 16,
"os_version": "Microsoft Windows [Version 10.0.26200.8037]",
"cpu": "AMD Ryzen 7 9800X3D 8-Core Processor",
"ram_mb": 95943,
"gpu": {
"name": "NVIDIA GeForce RTX 5080/PCIe/SSE2",
"vendor": "NVIDIA Corporation",
"driver": "3.3.0 NVIDIA 617.42",
"backend": "opengl",
"kind": ""
}
},
"conditions": {
"scenario": "perf_bench:village,forest,ocean,mountain",
"seed": "1234",
"render_distance": 16,
"simulation_distance": 12,
"vsync": false,
"offscreen": false,
"repeats": 3,
"warmup_s": 2.0,
"window": [
1280,
720
]
},
"metrics": {
"startup.title_ms": {
"n": 3,
"mean": 7681.834,
"stdev": 189.48,
"min": 7538.777,
"max": 7896.728,
"ci95": 470.732
},
"startup.world_ms": {
"n": 3,
"mean": 19898.834,
"stdev": 192.981,
"min": 19695.728,
"max": 20079.777,
"ci95": 479.431
},
"chunks.village.load_ms": {
"n": 3,
"mean": 6127.947,
"stdev": 26.756,
"min": 6097.19,
"max": 6145.854,
"ci95": 66.471
},
"fps.village.avg_fps": {
"n": 3,
"mean": 1218.486,
"stdev": 26.337,
"min": 1188.13,
"max": 1235.248,
"ci95": 65.43
},
"fps.village.low_1pct_fps": {
"n": 3,
"mean": 318.468,
"stdev": 6.292,
"min": 313.2,
"max": 325.435,
"ci95": 15.632
},
"fps.village.low_01pct_fps": {
"n": 3,
"mean": 126.59,
"stdev": 1.844,
"min": 124.744,
"max": 128.431,
"ci95": 4.58
},
"chunks.forest.load_ms": {
"n": 3,
"mean": 8548.814,
"stdev": 152.302,
"min": 8380.912,
"max": 8678.071,
"ci95": 378.37
},
"fps.forest.avg_fps": {
"n": 3,
"mean": 1912.359,
"stdev": 130.238,
"min": 1791.386,
"max": 2050.215,
"ci95": 323.555
},
"fps.forest.low_1pct_fps": {
"n": 3,
"mean": 401.907,
"stdev": 35.911,
"min": 379.849,
"max": 443.345,
"ci95": 89.216
},
"fps.forest.low_01pct_fps": {
"n": 3,
"mean": 149.26,
"stdev": 9.983,
"min": 139.809,
"max": 159.701,
"ci95": 24.801
},
"chunks.ocean.load_ms": {
"n": 3,
"mean": 6953.478,
"stdev": 276.636,
"min": 6635.649,
"max": 7140.072,
"ci95": 687.258
},
"fps.ocean.avg_fps": {
"n": 3,
"mean": 1860.512,
"stdev": 116.512,
"min": 1726.149,
"max": 1933.597,
"ci95": 289.454
},
"fps.ocean.low_1pct_fps": {
"n": 3,
"mean": 446.336,
"stdev": 56.993,
"min": 382.26,
"max": 491.369,
"ci95": 141.589
},
"fps.ocean.low_01pct_fps": {
"n": 3,
"mean": 164.934,
"stdev": 11.839,
"min": 152.493,
"max": 176.062,
"ci95": 29.413
},
"chunks.mountain.load_ms": {
"n": 3,
"mean": 15211.386,
"stdev": 256.348,
"min": 14938.008,
"max": 15446.375,
"ci95": 636.856
},
"fps.mountain.avg_fps": {
"n": 3,
"mean": 1653.223,
"stdev": 61.705,
"min": 1584.652,
"max": 1704.273,
"ci95": 153.297
},
"fps.mountain.low_1pct_fps": {
"n": 3,
"mean": 331.929,
"stdev": 10.858,
"min": 319.394,
"max": 338.396,
"ci95": 26.974
},
"fps.mountain.low_01pct_fps": {
"n": 3,
"mean": 113.436,
"stdev": 3.255,
"min": 110.869,
"max": 117.097,
"ci95": 8.086
},
"memory.rss_peak_mb": {
"n": 3,
"mean": 2540.129,
"stdev": 152.057,
"min": 2373.152,
"max": 2670.633,
"ci95": 377.761
},
"memory.rss_steady_mb": {
"n": 3,
"mean": 2299.514,
"stdev": 81.451,
"min": 2207.129,
"max": 2360.973,
"ci95": 202.352
}
},
"frames": [
{
"phase": "village",
"repeat": 0,
"frames": 14785,
"seconds": 12.0,
"avg_fps": 1232.081,
"low_1pct_fps": 325.435,
"low_01pct_fps": 128.431,
"p50_ms": 0.742,
"p95_ms": 1.265,
"p99_ms": 2.032,
"p999_ms": 5.653,
"max_ms": 12.791,
"histogram": [
12845,
1784,
127,
7,
1,
8,
4,
3,
5,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 1,
"frames": 14258,
"seconds": 12.0,
"avg_fps": 1188.13,
"low_1pct_fps": 313.2,
"low_01pct_fps": 126.595,
"p50_ms": 0.757,
"p95_ms": 1.349,
"p99_ms": 2.12,
"p999_ms": 5.541,
"max_ms": 13.998,
"histogram": [
11994,
2076,
157,
9,
2,
8,
2,
4,
5,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 2,
"frames": 14823,
"seconds": 12.0,
"avg_fps": 1235.248,
"low_1pct_fps": 316.768,
"low_01pct_fps": 124.744,
"p50_ms": 0.744,
"p95_ms": 1.199,
"p99_ms": 2.047,
"p999_ms": 5.628,
"max_ms": 15.519,
"histogram": [
13214,
1453,
118,
16,
2,
8,
6,
3,
0,
1,
0,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 0,
"frames": 21497,
"seconds": 12.0,
"avg_fps": 1791.386,
"low_1pct_fps": 382.527,
"low_01pct_fps": 139.809,
"p50_ms": 0.474,
"p95_ms": 0.931,
"p99_ms": 1.646,
"p999_ms": 5.274,
"max_ms": 13.459,
"histogram": [
20598,
799,
66,
9,
3,
8,
8,
2,
2,
0,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 1,
"frames": 24603,
"seconds": 12.0,
"avg_fps": 2050.215,
"low_1pct_fps": 443.345,
"low_01pct_fps": 159.701,
"p50_ms": 0.426,
"p95_ms": 0.752,
"p99_ms": 1.442,
"p999_ms": 3.134,
"max_ms": 13.282,
"histogram": [
24041,
487,
48,
6,
5,
6,
2,
3,
3,
1,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 2,
"frames": 22746,
"seconds": 12.0,
"avg_fps": 1895.476,
"low_1pct_fps": 379.849,
"low_01pct_fps": 148.269,
"p50_ms": 0.456,
"p95_ms": 0.829,
"p99_ms": 1.561,
"p999_ms": 4.176,
"max_ms": 10.671,
"histogram": [
22092,
523,
91,
17,
4,
7,
3,
4,
4,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 0,
"frames": 23062,
"seconds": 12.0,
"avg_fps": 1921.791,
"low_1pct_fps": 465.379,
"low_01pct_fps": 166.247,
"p50_ms": 0.463,
"p95_ms": 0.809,
"p99_ms": 1.45,
"p999_ms": 2.398,
"max_ms": 11.808,
"histogram": [
22454,
561,
27,
3,
2,
6,
0,
4,
3,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 1,
"frames": 23204,
"seconds": 12.0,
"avg_fps": 1933.597,
"low_1pct_fps": 491.369,
"low_01pct_fps": 176.062,
"p50_ms": 0.466,
"p95_ms": 0.766,
"p99_ms": 1.406,
"p999_ms": 2.229,
"max_ms": 15.102,
"histogram": [
22699,
469,
19,
4,
1,
1,
4,
2,
2,
2,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 2,
"frames": 20714,
"seconds": 12.0,
"avg_fps": 1726.149,
"low_1pct_fps": 382.26,
"low_01pct_fps": 152.493,
"p50_ms": 0.5,
"p95_ms": 0.956,
"p99_ms": 1.742,
"p999_ms": 3.852,
"max_ms": 12.888,
"histogram": [
19814,
787,
83,
11,
2,
7,
3,
5,
0,
1,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 0,
"frames": 20049,
"seconds": 12.0,
"avg_fps": 1670.745,
"low_1pct_fps": 338.396,
"low_01pct_fps": 117.097,
"p50_ms": 0.502,
"p95_ms": 0.949,
"p99_ms": 1.673,
"p999_ms": 6.456,
"max_ms": 16.791,
"histogram": [
19187,
753,
70,
8,
1,
6,
12,
4,
3,
4,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 1,
"frames": 19016,
"seconds": 12.0,
"avg_fps": 1584.652,
"low_1pct_fps": 337.998,
"low_01pct_fps": 112.343,
"p50_ms": 0.531,
"p95_ms": 1.082,
"p99_ms": 1.717,
"p999_ms": 5.939,
"max_ms": 12.958,
"histogram": [
17849,
1069,
63,
7,
3,
6,
2,
7,
4,
5,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 2,
"frames": 20452,
"seconds": 12.0,
"avg_fps": 1704.273,
"low_1pct_fps": 319.394,
"low_01pct_fps": 110.869,
"p50_ms": 0.496,
"p95_ms": 0.911,
"p99_ms": 1.706,
"p999_ms": 5.938,
"max_ms": 24.863,
"histogram": [
19694,
626,
82,
14,
3,
14,
5,
4,
5,
3,
1,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
}
],
"label": "vanilla-win-9800x3d-5080"
}
@@ -0,0 +1,690 @@
{
"format": "horizon-benchmark-report",
"version": 1,
"kind": "game",
"created": "2026-10-29T19:06:49Z",
"app": {
"name": "minecraft",
"version": "26.3",
"mods": [
{
"name": "fabric-loader",
"version": "0.19.5"
}
]
},
"hardware": {
"os": "windows",
"arch": "x86_64",
"cpu_threads": 16,
"os_version": "Microsoft Windows [Version 10.0.26200.8037]",
"cpu": "AMD Ryzen 7 9800X3D 8-Core Processor",
"ram_mb": 95943,
"gpu": {
"name": "NVIDIA GeForce RTX 5080/PCIe/SSE2",
"vendor": "NVIDIA Corporation",
"driver": "3.3.0 NVIDIA 617.42",
"backend": "opengl",
"kind": ""
}
},
"conditions": {
"scenario": "perf_bench:village,forest,ocean,mountain",
"seed": "1234",
"render_distance": 32,
"simulation_distance": 12,
"vsync": false,
"offscreen": false,
"repeats": 3,
"warmup_s": 2.0,
"window": [
1280,
720
]
},
"metrics": {
"startup.title_ms": {
"n": 3,
"mean": 7518.874,
"stdev": 42.968,
"min": 7469.532,
"max": 7548.049,
"ci95": 106.748
},
"startup.world_ms": {
"n": 3,
"mean": 35424.207,
"stdev": 144.711,
"min": 35318.049,
"max": 35589.041,
"ci95": 359.511
},
"chunks.village.load_ms": {
"n": 3,
"mean": 11146.828,
"stdev": 49.683,
"min": 11094.798,
"max": 11193.773,
"ci95": 123.43
},
"fps.village.avg_fps": {
"n": 3,
"mean": 882.053,
"stdev": 19.433,
"min": 866.126,
"max": 903.706,
"ci95": 48.279
},
"fps.village.low_1pct_fps": {
"n": 3,
"mean": 183.995,
"stdev": 4.924,
"min": 178.512,
"max": 188.041,
"ci95": 12.234
},
"fps.village.low_01pct_fps": {
"n": 3,
"mean": 83.193,
"stdev": 6.797,
"min": 77.734,
"max": 90.806,
"ci95": 16.886
},
"chunks.forest.load_ms": {
"n": 3,
"mean": 18184.002,
"stdev": 266.879,
"min": 17880.667,
"max": 18382.735,
"ci95": 663.017
},
"fps.forest.avg_fps": {
"n": 3,
"mean": 1202.907,
"stdev": 20.426,
"min": 1179.9,
"max": 1218.906,
"ci95": 50.744
},
"fps.forest.low_1pct_fps": {
"n": 3,
"mean": 228.139,
"stdev": 5.166,
"min": 222.394,
"max": 232.401,
"ci95": 12.833
},
"fps.forest.low_01pct_fps": {
"n": 3,
"mean": 107.956,
"stdev": 4.675,
"min": 105.185,
"max": 113.354,
"ci95": 11.614
},
"chunks.ocean.load_ms": {
"n": 3,
"mean": 13146.632,
"stdev": 507.943,
"min": 12572.118,
"max": 13536.145,
"ci95": 1261.902
},
"fps.ocean.avg_fps": {
"n": 3,
"mean": 1223.169,
"stdev": 13.11,
"min": 1208.837,
"max": 1234.556,
"ci95": 32.569
},
"fps.ocean.low_1pct_fps": {
"n": 3,
"mean": 236.27,
"stdev": 6.77,
"min": 228.686,
"max": 241.702,
"ci95": 16.818
},
"fps.ocean.low_01pct_fps": {
"n": 3,
"mean": 99.089,
"stdev": 8.354,
"min": 91.897,
"max": 108.253,
"ci95": 20.755
},
"chunks.mountain.load_ms": {
"n": 3,
"mean": 41846.641,
"stdev": 661.649,
"min": 41435.666,
"max": 42609.895,
"ci95": 1643.76
},
"fps.mountain.avg_fps": {
"n": 3,
"mean": 815.008,
"stdev": 24.644,
"min": 797.565,
"max": 843.201,
"ci95": 61.224
},
"fps.mountain.low_1pct_fps": {
"n": 3,
"mean": 55.371,
"stdev": 5.128,
"min": 50.389,
"max": 60.633,
"ci95": 12.739
},
"fps.mountain.low_01pct_fps": {
"n": 3,
"mean": 7.966,
"stdev": 1.103,
"min": 6.848,
"max": 9.053,
"ci95": 2.74
},
"memory.rss_peak_mb": {
"n": 3,
"mean": 2892.492,
"stdev": 29.54,
"min": 2859.574,
"max": 2916.691,
"ci95": 73.387
},
"memory.rss_steady_mb": {
"n": 3,
"mean": 2799.159,
"stdev": 12.256,
"min": 2786.234,
"max": 2810.613,
"ci95": 30.447
}
},
"frames": [
{
"phase": "village",
"repeat": 0,
"frames": 10845,
"seconds": 12.001,
"avg_fps": 903.706,
"low_1pct_fps": 185.432,
"low_01pct_fps": 81.039,
"p50_ms": 0.98,
"p95_ms": 1.708,
"p99_ms": 3.082,
"p999_ms": 9.304,
"max_ms": 15.487,
"histogram": [
6030,
4403,
297,
49,
25,
16,
8,
2,
6,
2,
4,
3,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 1,
"frames": 10516,
"seconds": 12.0,
"avg_fps": 876.328,
"low_1pct_fps": 188.041,
"low_01pct_fps": 90.806,
"p50_ms": 1.012,
"p95_ms": 1.806,
"p99_ms": 3.354,
"p999_ms": 8.714,
"max_ms": 14.39,
"histogram": [
4927,
5158,
292,
70,
33,
13,
6,
5,
6,
2,
3,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 2,
"frames": 10394,
"seconds": 12.001,
"avg_fps": 866.126,
"low_1pct_fps": 178.512,
"low_01pct_fps": 77.734,
"p50_ms": 1.026,
"p95_ms": 1.789,
"p99_ms": 3.399,
"p999_ms": 10.6,
"max_ms": 15.389,
"histogram": [
4437,
5552,
273,
65,
18,
28,
8,
1,
1,
3,
5,
3,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 0,
"frames": 14519,
"seconds": 12.0,
"avg_fps": 1209.916,
"low_1pct_fps": 232.401,
"low_01pct_fps": 105.185,
"p50_ms": 0.726,
"p95_ms": 1.306,
"p99_ms": 2.25,
"p999_ms": 7.033,
"max_ms": 13.303,
"histogram": [
13121,
1197,
103,
36,
24,
19,
4,
3,
8,
2,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 1,
"frames": 14159,
"seconds": 12.0,
"avg_fps": 1179.9,
"low_1pct_fps": 222.394,
"low_01pct_fps": 105.33,
"p50_ms": 0.732,
"p95_ms": 1.366,
"p99_ms": 2.499,
"p999_ms": 8.061,
"max_ms": 11.797,
"histogram": [
12501,
1356,
195,
45,
25,
14,
6,
3,
9,
5,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 2,
"frames": 14627,
"seconds": 12.0,
"avg_fps": 1218.906,
"low_1pct_fps": 229.622,
"low_01pct_fps": 113.354,
"p50_ms": 0.713,
"p95_ms": 1.292,
"p99_ms": 2.317,
"p999_ms": 7.136,
"max_ms": 12.041,
"histogram": [
13194,
1224,
97,
47,
27,
21,
2,
6,
6,
2,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 0,
"frames": 14714,
"seconds": 12.001,
"avg_fps": 1226.113,
"low_1pct_fps": 238.423,
"low_01pct_fps": 97.117,
"p50_ms": 0.722,
"p95_ms": 1.223,
"p99_ms": 2.243,
"p999_ms": 7.032,
"max_ms": 13.21,
"histogram": [
13490,
1027,
115,
29,
18,
13,
7,
3,
3,
5,
4,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 1,
"frames": 14507,
"seconds": 12.001,
"avg_fps": 1208.837,
"low_1pct_fps": 228.686,
"low_01pct_fps": 91.897,
"p50_ms": 0.719,
"p95_ms": 1.256,
"p99_ms": 2.354,
"p999_ms": 7.473,
"max_ms": 15.432,
"histogram": [
13124,
1112,
180,
35,
23,
9,
8,
4,
2,
6,
2,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 2,
"frames": 14815,
"seconds": 12.0,
"avg_fps": 1234.556,
"low_1pct_fps": 241.702,
"low_01pct_fps": 108.253,
"p50_ms": 0.721,
"p95_ms": 1.2,
"p99_ms": 2.197,
"p999_ms": 6.689,
"max_ms": 12.791,
"histogram": [
13565,
1055,
107,
29,
23,
15,
8,
3,
4,
5,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 0,
"frames": 10119,
"seconds": 12.001,
"avg_fps": 843.201,
"low_1pct_fps": 60.633,
"low_01pct_fps": 9.053,
"p50_ms": 0.891,
"p95_ms": 1.845,
"p99_ms": 3.91,
"p999_ms": 8.209,
"max_ms": 316.409,
"histogram": [
6956,
2747,
240,
83,
45,
22,
8,
8,
5,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
4,
0,
0
]
},
{
"phase": "mountain",
"repeat": 1,
"frames": 9693,
"seconds": 12.052,
"avg_fps": 804.258,
"low_1pct_fps": 50.389,
"low_01pct_fps": 6.848,
"p50_ms": 0.906,
"p95_ms": 1.924,
"p99_ms": 4.157,
"p999_ms": 8.578,
"max_ms": 297.695,
"histogram": [
6578,
2678,
248,
94,
45,
18,
16,
5,
4,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
5,
0,
0
]
},
{
"phase": "mountain",
"repeat": 2,
"frames": 9571,
"seconds": 12.0,
"avg_fps": 797.565,
"low_1pct_fps": 55.09,
"low_01pct_fps": 7.997,
"p50_ms": 0.929,
"p95_ms": 1.944,
"p99_ms": 4.495,
"p999_ms": 7.764,
"max_ms": 321.994,
"histogram": [
5954,
3172,
230,
103,
39,
34,
19,
11,
4,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
4,
0,
0
]
}
],
"label": "vanilla-win-9800x3d-5080"
}
@@ -0,0 +1,690 @@
{
"format": "horizon-benchmark-report",
"version": 1,
"kind": "game",
"created": "2026-10-29T18:43:17Z",
"app": {
"name": "minecraft",
"version": "26.3",
"mods": [
{
"name": "fabric-loader",
"version": "0.19.5"
}
]
},
"hardware": {
"os": "windows",
"arch": "x86_64",
"cpu_threads": 16,
"os_version": "Microsoft Windows [Version 10.0.26200.8037]",
"cpu": "AMD Ryzen 7 9800X3D 8-Core Processor",
"ram_mb": 95943,
"gpu": {
"name": "NVIDIA GeForce RTX 5080/PCIe/SSE2",
"vendor": "NVIDIA Corporation",
"driver": "3.3.0 NVIDIA 617.42",
"backend": "opengl",
"kind": ""
}
},
"conditions": {
"scenario": "perf_bench:village,forest,ocean,mountain",
"seed": "1234",
"render_distance": 8,
"simulation_distance": 12,
"vsync": false,
"offscreen": false,
"repeats": 3,
"warmup_s": 2.0,
"window": [
1280,
720
]
},
"metrics": {
"startup.title_ms": {
"n": 3,
"mean": 7676.526,
"stdev": 305.763,
"min": 7426.024,
"max": 8017.247,
"ci95": 759.617
},
"startup.world_ms": {
"n": 3,
"mean": 13862.526,
"stdev": 284.577,
"min": 13569.024,
"max": 14137.247,
"ci95": 706.985
},
"chunks.village.load_ms": {
"n": 3,
"mean": 3808.014,
"stdev": 61.275,
"min": 3737.989,
"max": 3851.799,
"ci95": 152.227
},
"fps.village.avg_fps": {
"n": 3,
"mean": 2216.576,
"stdev": 45.247,
"min": 2166.285,
"max": 2253.985,
"ci95": 112.409
},
"fps.village.low_1pct_fps": {
"n": 3,
"mean": 545.307,
"stdev": 14.876,
"min": 528.15,
"max": 554.607,
"ci95": 36.957
},
"fps.village.low_01pct_fps": {
"n": 3,
"mean": 215.412,
"stdev": 5.692,
"min": 209.522,
"max": 220.882,
"ci95": 14.14
},
"chunks.forest.load_ms": {
"n": 3,
"mean": 3212.958,
"stdev": 151.722,
"min": 3076.655,
"max": 3376.428,
"ci95": 376.928
},
"fps.forest.avg_fps": {
"n": 3,
"mean": 2456.542,
"stdev": 336.369,
"min": 2068.162,
"max": 2654.549,
"ci95": 835.654
},
"fps.forest.low_1pct_fps": {
"n": 3,
"mean": 624.759,
"stdev": 90.0,
"min": 521.303,
"max": 685.008,
"ci95": 223.59
},
"fps.forest.low_01pct_fps": {
"n": 3,
"mean": 252.469,
"stdev": 21.044,
"min": 229.228,
"max": 270.233,
"ci95": 52.28
},
"chunks.ocean.load_ms": {
"n": 3,
"mean": 2983.236,
"stdev": 168.416,
"min": 2788.777,
"max": 3082.249,
"ci95": 418.403
},
"fps.ocean.avg_fps": {
"n": 3,
"mean": 2469.831,
"stdev": 333.258,
"min": 2085.121,
"max": 2669.9,
"ci95": 827.925
},
"fps.ocean.low_1pct_fps": {
"n": 3,
"mean": 688.684,
"stdev": 96.943,
"min": 576.803,
"max": 747.759,
"ci95": 240.839
},
"fps.ocean.low_01pct_fps": {
"n": 3,
"mean": 286.444,
"stdev": 21.36,
"min": 262.183,
"max": 302.422,
"ci95": 53.066
},
"chunks.mountain.load_ms": {
"n": 3,
"mean": 6079.869,
"stdev": 719.864,
"min": 5542.196,
"max": 6897.69,
"ci95": 1788.385
},
"fps.mountain.avg_fps": {
"n": 3,
"mean": 2605.12,
"stdev": 326.424,
"min": 2228.824,
"max": 2812.075,
"ci95": 810.948
},
"fps.mountain.low_1pct_fps": {
"n": 3,
"mean": 699.63,
"stdev": 87.846,
"min": 599.44,
"max": 763.45,
"ci95": 218.239
},
"fps.mountain.low_01pct_fps": {
"n": 3,
"mean": 264.461,
"stdev": 28.459,
"min": 239.602,
"max": 295.503,
"ci95": 70.701
},
"memory.rss_peak_mb": {
"n": 3,
"mean": 2120.426,
"stdev": 116.406,
"min": 2046.93,
"max": 2254.637,
"ci95": 289.191
},
"memory.rss_steady_mb": {
"n": 3,
"mean": 1912.642,
"stdev": 38.925,
"min": 1889.766,
"max": 1957.586,
"ci95": 96.702
}
},
"frames": [
{
"phase": "village",
"repeat": 0,
"frames": 27048,
"seconds": 12.0,
"avg_fps": 2253.985,
"low_1pct_fps": 553.164,
"low_01pct_fps": 215.833,
"p50_ms": 0.412,
"p95_ms": 0.617,
"p99_ms": 1.187,
"p999_ms": 2.238,
"max_ms": 9.792,
"histogram": [
26716,
296,
15,
4,
7,
5,
2,
2,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 1,
"frames": 26754,
"seconds": 12.0,
"avg_fps": 2229.458,
"low_1pct_fps": 554.607,
"low_01pct_fps": 220.882,
"p50_ms": 0.417,
"p95_ms": 0.626,
"p99_ms": 1.212,
"p999_ms": 2.448,
"max_ms": 8.388,
"histogram": [
26423,
293,
16,
9,
4,
5,
1,
1,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 2,
"frames": 25996,
"seconds": 12.0,
"avg_fps": 2166.285,
"low_1pct_fps": 528.15,
"low_01pct_fps": 209.522,
"p50_ms": 0.43,
"p95_ms": 0.662,
"p99_ms": 1.308,
"p999_ms": 2.477,
"max_ms": 11.033,
"histogram": [
25615,
338,
23,
5,
5,
5,
2,
2,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 0,
"frames": 31855,
"seconds": 12.0,
"avg_fps": 2654.549,
"low_1pct_fps": 667.966,
"low_01pct_fps": 257.945,
"p50_ms": 0.341,
"p95_ms": 0.519,
"p99_ms": 0.932,
"p999_ms": 1.761,
"max_ms": 10.944,
"histogram": [
31558,
275,
6,
2,
7,
0,
4,
2,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 1,
"frames": 31763,
"seconds": 12.0,
"avg_fps": 2646.916,
"low_1pct_fps": 685.008,
"low_01pct_fps": 270.233,
"p50_ms": 0.344,
"p95_ms": 0.52,
"p99_ms": 0.94,
"p999_ms": 1.685,
"max_ms": 11.406,
"histogram": [
31474,
272,
3,
4,
2,
2,
1,
3,
1,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 2,
"frames": 24818,
"seconds": 12.0,
"avg_fps": 2068.162,
"low_1pct_fps": 521.303,
"low_01pct_fps": 229.228,
"p50_ms": 0.439,
"p95_ms": 0.767,
"p99_ms": 1.284,
"p999_ms": 2.351,
"max_ms": 11.468,
"histogram": [
24356,
410,
36,
4,
4,
3,
2,
2,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 0,
"frames": 32039,
"seconds": 12.0,
"avg_fps": 2669.9,
"low_1pct_fps": 741.491,
"low_01pct_fps": 294.727,
"p50_ms": 0.345,
"p95_ms": 0.501,
"p99_ms": 0.86,
"p999_ms": 1.431,
"max_ms": 9.257,
"histogram": [
31771,
250,
3,
2,
4,
6,
1,
1,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 1,
"frames": 31854,
"seconds": 12.0,
"avg_fps": 2654.471,
"low_1pct_fps": 747.759,
"low_01pct_fps": 302.422,
"p50_ms": 0.347,
"p95_ms": 0.503,
"p99_ms": 0.853,
"p999_ms": 1.404,
"max_ms": 13.925,
"histogram": [
31580,
261,
3,
2,
0,
1,
3,
3,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 2,
"frames": 25022,
"seconds": 12.0,
"avg_fps": 2085.121,
"low_1pct_fps": 576.803,
"low_01pct_fps": 262.183,
"p50_ms": 0.428,
"p95_ms": 0.758,
"p99_ms": 1.214,
"p999_ms": 2.109,
"max_ms": 9.206,
"histogram": [
24600,
390,
20,
3,
2,
3,
2,
1,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 0,
"frames": 33745,
"seconds": 12.0,
"avg_fps": 2812.075,
"low_1pct_fps": 736.0,
"low_01pct_fps": 258.279,
"p50_ms": 0.327,
"p95_ms": 0.471,
"p99_ms": 0.808,
"p999_ms": 1.689,
"max_ms": 10.07,
"histogram": [
33516,
205,
7,
0,
7,
4,
3,
1,
1,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 1,
"frames": 33294,
"seconds": 12.0,
"avg_fps": 2774.461,
"low_1pct_fps": 763.45,
"low_01pct_fps": 295.503,
"p50_ms": 0.334,
"p95_ms": 0.471,
"p99_ms": 0.813,
"p999_ms": 1.63,
"max_ms": 8.512,
"histogram": [
33056,
221,
3,
5,
1,
2,
3,
2,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 2,
"frames": 26747,
"seconds": 12.0,
"avg_fps": 2228.824,
"low_1pct_fps": 599.44,
"low_01pct_fps": 239.602,
"p50_ms": 0.403,
"p95_ms": 0.678,
"p99_ms": 1.107,
"p999_ms": 2.054,
"max_ms": 9.178,
"histogram": [
26418,
298,
13,
6,
2,
8,
0,
1,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
}
],
"label": "vanilla-win-9800x3d-5080"
}
@@ -0,0 +1,106 @@
# Horizon v0.2.97 vs vanilla 26.3 on Windows 11: Ryzen 7 9800X3D + RTX 5080 (2026-10-29, RD 8 and 32 rerun 2026-10-30)
The first `tools/perf_bench.py` run on Windows ([docs/benchmark.md](../../docs/benchmark.md)): Horizon, vanilla 26.3 and
vanilla with performance mods, render distances 8, 16 and 32, 1280x720, three games each, every game from a fresh
world. The reports are the `*-win-9800x3d-5080-rd*.json` files next to this page.
```sh
python tools/perf_bench.py --table horizon-win-9800x3d-5080 vanilla-win-9800x3d-5080 vanilla-mods-win-9800x3d-5080
```
## Setup
- **Machine:** AMD Ryzen 7 9800X3D (8 cores, 16 threads), 94 GB RAM (DDR5, EXPO off), NVIDIA GeForce RTX 5080
(driver 617.42), Windows 11 Pro 26200, Balanced power plan, desktop. One 1920x1080 120 Hz display. The game windows
were on the desktop of an unlocked session.
- **Horizon:** release v0.2.97 as the launcher installs it (`--build %APPDATA%\horizon-launcher\versions\v0.2.97`,
`--resources` its `.local\horizon-client\26.3`), Vulkan.
- **Vanilla:** the 26.3 client jar with Fabric Loader 0.19.5 and the driver mod, Java 25.0.5 (Temurin), `-Xmx2G`
with the launcher's G1 flags and the manifest's `-XX:StackShadowPages=32`, OpenGL 3.3.
- **Vanilla + mods:** the same with Sodium mc26.3-0.9.2, Lithium mc26.3-0.26.2, FerriteCore 9.0.0 and
ImmediatelyFast 1.17.1 (`tools/benchmark-deps/mods.lock.json`).
- **Commands:** `python tools/perf_bench.py --game <game> --label <game>-win-9800x3d-5080 --distances <d>
--sizes 1280x720 --repeats 3` (plus `--build` and `--resources` for Horizon), with the games taking turns at
each distance.
- **Memory** is the working set on Windows (what Task Manager shows), sampled from outside the process. Horizon's
GPU figure is its own Vulkan allocator total. `nvidia-smi` gives no per-process figure under WDDM, so vanilla has
none.
## Load
Windows has no load average, so the reports have no `load_avg_1m`. Every game ran on an otherwise idle machine:
- **RD 8 and RD 32** (rerun on 2026-10-30, after the first pass had builds running alongside): a script started each
game only once no `rustc`, `cargo`, `link` (or other compiler) process and no other game (`horizon-client`, `java`,
`javaw`, Minecraft) was running, and the machine's CPU load had stayed under 15 % in every 5-second sample for
30 seconds. During each game it sampled every 2 seconds the machine's CPU load and the CPU of everything outside the
benchmark's own processes. A game during which a compiler or another game ran, or other processes used over 15 % of
the machine for 10 seconds, was discarded and run again: two of the 20 games were (vanilla + mods at RD 32, a
build started during one, background processes during the other).
- **RD 16** (2026-10-29): no `rustc`, `cargo` or `link` process at any point, the load logged every 5 seconds.
| Game | RD | Mean / max CPU load (machine, the game included) | Other processes' CPU, mean / max | Compiler processes |
|---|---|---|---|---|
| Horizon | 8 | 18 % / 49 % | 6 % / 12 % | 0 |
| Vanilla | 8 | 28 % / 75 % | 7 % / 20 % | 0 |
| Vanilla + mods | 8 | 30 % / 81 % | 7 % / 20 % | 0 |
| Horizon | 16 | 29 % / 80 % | – | 0 |
| Vanilla | 16 | 41 % / 91 % | – | 0 |
| Vanilla + mods | 16 | 51 % / 90 % | – | 0 |
| Horizon | 32 | 36 % / 100 % | 5 % / 15 % | 0 |
| Vanilla | 32 | 39 % / 73 % | 6 % / 18 % | 0 |
| Vanilla + mods | 32 | 42 % / 88 % | 6 % / 21 % | 0 |
"Other processes" is the machine's load minus the benchmark's own processes (the game and `perf_bench.py`), in
2-second samples. Most of it is the system's share of the game's own drawing (the graphics driver and the
compositor), which no process of the benchmark is charged for; the desktop apps left open (a browser, chat apps)
briefly reached 5 % at most. RD 16's load was logged from outside every 5 seconds, so it has no such split, and its mean includes the time between
games. Compared with the first pass, the idle RD 8 runs give Horizon about 10 % higher averages and about 50 % higher
1% lows, and vanilla up to 10 % higher averages.
## Results
Mean of 3 games ± 95% confidence interval. FPS over the 12-second flight at 16 blocks/s (average / 1% low /
0.1% low):
| RD | scene | Horizon v0.2.97 | Vanilla 26.3 | Vanilla + mods |
| --- | --- | --- | --- | --- |
| 8 | forest | 3495 ±276 / 1295 ±397 / 859 ±298 | 2457 ±836 / 625 ±224 / 252 ±52 | 2425 ±65 / 733 ±30 / 250 ±22 |
| 8 | mountain | 3376 ±67 / 1384 ±168 / 850 ±256 | 2605 ±811 / 700 ±218 / 264 ±71 | 2538 ±167 / 760 ±34 / 241 ±44 |
| 8 | ocean | 3346 ±147 / 1358 ±237 / 912 ±235 | 2470 ±828 / 689 ±241 / 286 ±53 | 2432 ±82 / 764 ±37 / 262 ±37 |
| 8 | village | 2942 ±197 / 1271 ±146 / 839 ±94 | 2217 ±112 / 545 ±37 / 215 ±14 | 2104 ±101 / 577 ±84 / 196 ±47 |
| 16 | forest | 2424 ±138 / 709 ±165 / 401 ±100 | 1912 ±324 / 402 ±89 / 149 ±25 | 1746 ±32 / 461 ±27 / 179 ±9 |
| 16 | mountain | 1961 ±99 / 537 ±96 / 297 ±75 | 1653 ±153 / 332 ±27 / 113 ±8 | 1500 ±55 / 408 ±40 / 140 ±10 |
| 16 | ocean | 2201 ±183 / 554 ±251 / 325 ±326 | 1861 ±289 / 446 ±142 / 165 ±29 | 1815 ±119 / 514 ±142 / 192 ±58 |
| 16 | village | 1712 ±192 / 593 ±80 / 358 ±69 | 1218 ±65 / 318 ±16 / 127 ±5 | 1282 ±162 / 343 ±25 / 132 ±12 |
| 32 | forest | 1389 ±70 / 504 ±33 / 330 ±118 | 1203 ±51 / 228 ±13 / 108 ±12 | 929 ±8 / 313 ±41 / 129 ±32 |
| 32 | mountain | 1086 ±34 / 258 ±78 / 108 ±37 | 815 ±61 / 55 ±13 / 8 ±3 | 645 ±10 / 166 ±12 / 86 ±21 |
| 32 | ocean | 1189 ±27 / 155 ±9 / 68 ±8 | 1223 ±33 / 236 ±17 / 99 ±21 | 1017 ±14 / 325 ±38 / 117 ±28 |
| 32 | village | 1164 ±9 / 416 ±39 / 216 ±86 | 882 ±48 / 184 ±12 / 83 ±17 | 926 ±17 / 250 ±5 / 93 ±11 |
Startup, loading and memory (means of 3 games):
| RD | | Horizon v0.2.97 | Vanilla 26.3 | Vanilla + mods |
| --- | --- | --- | --- | --- |
| 8 | start to title / in-world | 3.0 s / 6.1 s | 7.7 s / 13.9 s | 11.1 s / 17.6 s |
| 8 | mean scene load | 1.3 s | 4.0 s | 3.6 s |
| 8 | working set, steady / peak | 1082 / 1172 MB | 1913 / 2120 MB | 1904 / 2053 MB |
| 16 | start to title / in-world | 3.2 s / 7.2 s | 7.7 s / 19.9 s | 10.8 s / 23.5 s |
| 16 | mean scene load | 2.4 s | 9.2 s | 9.0 s |
| 16 | working set, steady / peak | 1236 / 1459 MB | 2300 / 2540 MB | 2327 / 2496 MB |
| 32 | start to title / in-world | 2.9 s / 10.5 s | 7.5 s / 35.4 s | 11.0 s / 39.2 s |
| 32 | mean scene load | 5.7 s | 21.1 s | 19.9 s |
| 32 | working set, steady / peak | 1730 / 2045 MB | 2799 / 2892 MB | 2895 / 2965 MB |
## What it shows
- At RD 8 and 16, Horizon's average FPS is 18–42 % above vanilla's in every scene. Its 1% lows are 1.2–2.3 times
vanilla's and its 0.1% lows 2.0–3.9 times. On this CPU and GPU the performance mods leave vanilla's averages
level or slightly lower, and raise its 1% lows by 6–23 %.
- At RD 32, Horizon's average is 15–33 % above vanilla's over the forest, the mountain and the village, with 1% lows
2.2–4.7 times vanilla's. Over the ocean its average is level with vanilla's, but its lows are lower than vanilla's
and the modded client's (155 vs 236 and 325 FPS 1% low, 68 vs 99 and 117 FPS 0.1% low), on an idle machine too.
That scene is worth a closer look.
- Horizon reaches the title screen 2.4–2.6 times faster than vanilla (3.4–3.8 times faster than the modded client)
and loads a scene's chunks 3–4 times faster. Its steady working set is 38–46 % smaller.
- Each game uses its own graphics API: Horizon Vulkan, vanilla OpenGL. That difference is part of what's compared.
@@ -119,7 +119,7 @@ machines during the runs.
## Reproduce
curl -fsSL https://raw.githubusercontent.com/ludovic111/horizon/main/packaging/server/install.sh | sh
curl -fsSL https://raw.githubusercontent.com/horizon-rust/horizon/main/packaging/server/install.sh | sh
horizon-prepare --prepare --server-jar /path/to/server.jar # your own copy
cargo build --release -p horizon-bots # on the machine that runs the bots
# in a checkout whose .local/minecraft holds the pinned vanilla jars (tools/lab.py prepare):
@@ -148,7 +148,7 @@ early, wrote no report. Its lines come from its output.
## Reproduce
curl -fsSL https://raw.githubusercontent.com/ludovic111/horizon/main/packaging/server/install.sh | sh
curl -fsSL https://raw.githubusercontent.com/horizon-rust/horizon/main/packaging/server/install.sh | sh
horizon-prepare --prepare --server-jar /path/to/server.jar # your own copy
cargo build --release -p horizon-bots # on the machine that runs the bots
# in a checkout whose .local/minecraft holds the pinned vanilla jars (tools/lab.py prepare):
+1 -1
View File
@@ -103,7 +103,7 @@ the Nix store, so the run set `LD_LIBRARY_PATH` to them (see #898).
## Reproduce
curl -fsSL https://raw.githubusercontent.com/ludovic111/horizon/main/packaging/server/install.sh | sh
curl -fsSL https://raw.githubusercontent.com/horizon-rust/horizon/main/packaging/server/install.sh | sh
horizon-prepare --prepare --server-jar /path/to/server.jar # your own copy
cargo build --release -p horizon-bots # on the machine that runs the bots
# in a checkout whose .local/minecraft holds the pinned vanilla jars (tools/lab.py prepare):
+2 -2
View File
@@ -4,10 +4,10 @@ version = "0.1.0"
edition.workspace = true
license.workspace = true
rust-version = "1.86"
description = "cargo horizon-mod new/build/run/check: make Horizon mods in Rust (docs/mods.md)"
description = "cargo horizon-mod new/build/run/check: make Horizon content mods (docs/mods.md)"
[dependencies]
horizon-mods = { path = "../horizon-mods", default-features = false, features = ["pack", "runtime", "index"] }
horizon-mods = { path = "../horizon-mods", default-features = false, features = ["index"] }
serde_json = "1.0.151"
[lints]
+22 -167
View File
@@ -1,35 +1,19 @@
//! `cargo horizon-mod check <file>`: what the registry checks of a submission,
//! run anywhere: its kind's own checks (docs/api/mod-index.md), its signature
//! when given a key, and for a mod a trial run in the sandbox: it must start,
//! survive ticks, frames, a chat line and each of its commands (a server mod:
//! a player joining, chatting, typing them and leaving), and import no more
//! than its capabilities.
//! when given a key, and for a `.hmod` that it is content only.
use horizon_mods::{
Clock, GameMode, Mods, Output, Package, Player, ServerPlayer, Snapshot, Status,
Package,
index::{self, Facts, Kind},
manifest::component_imports,
};
use serde_json::{Value, json};
use std::path::Path;
/// How long the trial run plays: two seconds of game.
const TICKS: u64 = 40;
const FRAMES_PER_TICK: u64 = 3;
pub struct Report {
pub kind: Kind,
pub facts: Facts,
pub sha256: String,
pub size: u64,
pub signed: bool,
/// A mod has code to run (a content mod may have none).
pub code: bool,
/// A mod's component imports.
pub imports: Vec<String>,
/// A mod's commands, each run once.
pub commands: Vec<String>,
/// What the mod showed during the trial run.
pub output: Vec<String>,
}
impl Report {
@@ -53,10 +37,6 @@ impl Report {
"sha256": self.sha256,
"size": self.size,
"signed": self.signed,
"code": self.code,
"imports": self.imports,
"commands": self.commands,
"output": self.output,
})
}
@@ -97,18 +77,11 @@ impl Report {
for r in &f.requires {
out += &format!(" needs {} {} ({})\n", r.id, r.version, &r.sha256[..12]);
}
if self.kind == Kind::Mod && !self.code {
if self.kind == Kind::NativeMod {
out += " native source: unsandboxed; not executed by the registry check. Compile and test against the exact pinned Horizon and Mixins revisions before review.\n";
}
if self.kind == Kind::Mod {
out += " no code: content only, nothing to run\n";
} else if self.kind == Kind::Mod {
let commands = if self.commands.is_empty() {
String::new()
} else {
format!(", /{}", self.commands.join(", /"))
};
out += &format!(
" trial run: started, {TICKS} ticks, {} frames, a chat line{commands}: ok\n",
TICKS * FRAMES_PER_TICK
);
}
out
}
@@ -118,6 +91,7 @@ impl Report {
pub fn kind_of(path: &Path) -> Option<Kind> {
match path.extension()?.to_str()? {
"hmod" => Some(Kind::Mod),
"hnmod" => Some(Kind::NativeMod),
"hpack" => Some(Kind::Modpack),
"zip" => Some(Kind::ResourcePack),
"hshader" => Some(Kind::Shader),
@@ -149,145 +123,26 @@ pub fn check(
(None, None) => false,
_ => return Err("--key and --signature go together".into()),
};
let mut report = Report {
if kind == Kind::Mod {
content_only(path)?;
}
Ok(Report {
kind,
facts,
sha256: index::sha256_file(path)?,
size,
signed,
code: false,
imports: Vec::new(),
commands: Vec::new(),
output: Vec::new(),
};
if report.kind == Kind::Mod {
trial_run(path, &mut report)?;
}
Ok(report)
})
}
/// Runs a mod in the sandbox, as the game does, from a scratch copy so its
/// storage lands in a folder removed afterwards. A content mod has no code to
/// run: `inspect` already read its content as the game does.
fn trial_run(path: &Path, report: &mut Report) -> Result<(), String> {
let package = Package::read(path)?;
let Some(wasm) = package.wasm.as_deref() else {
return Ok(());
};
report.code = true;
report.imports = component_imports(wasm)?;
package
.manifest
.check_imports(report.imports.iter().map(String::as_str))?;
let dir = std::env::temp_dir().join(format!("horizon-mod-check-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?;
let copy = dir.join("mod.hmod");
let result = std::fs::copy(path, &copy)
.map_err(|e| e.to_string())
.and_then(|_| play(&copy, report));
let _ = std::fs::remove_dir_all(&dir);
result
}
fn play(path: &Path, report: &mut Report) -> Result<(), String> {
let mut mods = Mods::load(&[path.to_path_buf()], |_| true, |_| false);
let failed = |mods: &Mods| match &mods.entries()[0].status {
Status::Failed(reason) => Some(reason.clone()),
_ => None,
};
if let Some(reason) = failed(&mods) {
return Err(format!("it didn't start: {reason}"));
}
let client: Vec<String> = mods.commands().map(str::to_owned).collect();
let server: Vec<String> = mods.server_commands().map(|c| c.name.clone()).collect();
report.commands = [client.clone(), server.clone()].concat();
// A server mod plays with one player on, who joins, chats and leaves.
let steve = ServerPlayer {
id: "00000000-0000-4000-8000-000000000001".into(),
name: "Steve".into(),
x: 0.5,
y: 64.0,
z: 0.5,
dimension: "minecraft:overworld".into(),
game_mode: GameMode::Survival,
health: 20.0,
};
let snapshot = |t: u64| Snapshot {
player: Some(Player {
x: 0.5 + t as f64 * 0.1,
y: 64.0,
z: 0.5,
yaw: 90.0,
pitch: 0.0,
health: 20.0,
food: 20,
dimension: "minecraft:overworld".into(),
}),
clock: Some(Clock {
game_time: t,
day_time: 1000 + t,
}),
players: vec![steve.clone()],
// The GUI of a 1280 x 720 window at scale 3.
gui_size: (427.0, 240.0),
..Snapshot::default()
};
mods.set_snapshot(snapshot(0));
mods.player_joined(&steve);
for t in 0..TICKS {
mods.set_snapshot(snapshot(t));
mods.tick(t);
mods.server_tick(t);
for _ in 0..FRAMES_PER_TICK {
mods.frame(1.0 / 60.0);
}
}
mods.chat("<Steve> hello");
mods.player_chat(&steve, "hello");
for name in &client {
mods.command(name);
}
for name in &server {
mods.server_command(&steve, name);
}
mods.set_snapshot(Snapshot::default());
mods.player_left(&steve);
for out in mods.drain() {
report.output.push(match out {
Output::Chat { text, .. } => format!("chat: {text}"),
Output::Tell { text, .. } => format!("tell: {text}"),
Output::Broadcast { text, .. } => format!("broadcast: {text}"),
Output::Feedback { text, error } | Output::Reply { text, error, .. } => {
format!("{}: {text}", if error { "error" } else { "feedback" })
}
Output::Command { line } => format!("command: /{line}"),
Output::QuietCommands { lines, .. } => {
format!("{} quiet command(s)", lines.len())
}
Output::Stopped { reason, .. } => return Err(format!("it stopped: {reason}")),
Output::SetBlocks {
dimension, blocks, ..
} => {
format!("set {} block(s) in {dimension}", blocks.len())
}
Output::OpenScreen { .. } => "opened a screen".to_string(),
Output::CloseScreen { .. } => "closed its screen".to_string(),
Output::PlaceRecipe { book_id, all } => {
format!("placed recipe {book_id}{}", if all { " (all)" } else { "" })
}
Output::Give { item, count, .. } => format!("gave {count} {item}"),
Output::Shake { strength, seconds } => {
format!("shook the camera: {strength:.2} for {seconds:.1} s")
}
});
}
let shapes = mods.hud_shapes().count();
if shapes > 0 {
report.output.push(format!("drew {shapes} HUD shape(s)"));
}
match failed(&mods) {
Some(reason) => Err(format!("it stopped: {reason}")),
/// A mod is content only: WebAssembly code mods don't run any more, code mods are native
/// (`.hnmod`, docs/native-mods.md). `inspect` already read its content as the game does.
fn content_only(path: &Path) -> Result<(), String> {
match Package::read(path)?.wasm {
Some(_) => Err(
"it has WebAssembly code, which Horizon doesn't run any more: make it a native mod (docs/native-mods.md)"
.into(),
),
None => Ok(()),
}
}
@@ -312,7 +167,7 @@ mod tests {
.text()
.contains("cozy 1.0.0 (modpack, format 1, for 26.3)")
);
// The fixture mod's code is a stand-in: its manifest reads, its run fails.
// A mod with WebAssembly code is refused: code mods are native.
let hmod = fixture("coords_hud-0.1.0.hmod");
let key = std::fs::read_to_string(fixture("test.pub")).unwrap();
let sig = std::fs::read_to_string(fixture("coords_hud-0.1.0.hmod.minisig")).unwrap();
@@ -320,7 +175,7 @@ mod tests {
let e = check(&hmod, Kind::Mod, Some(key), Some(&sig))
.err()
.unwrap();
assert!(e.contains("didn't start"), "{e}");
assert!(e.contains("native mod"), "{e}");
let e = check(
&hmod,
Kind::Mod,
+38 -196
View File
@@ -1,32 +1,23 @@
//! `cargo horizon-mod`: make Horizon mods in Rust (`docs/mods.md`).
//! `cargo horizon-mod`: make Horizon content mods (`docs/modpacks.md`): blocks,
//! items, data and assets, no code. Code mods are native (docs/native-mods.md).
//!
//! - `new <path> [--template code|content] [--name "Name"] [--sdk-path <dir>]`:
//! a mod from a template: a Rust crate, or a content mod (no code).
//! - `build [--debug]`: the `.hmod` in `target/horizon-mod/`, with the mod's
//! `assets/` and `data/`; a folder without `Cargo.toml` is content only.
//! - `new <path> [--name "Name"]`: a content mod from the template.
//! - `build`: the `.hmod` in `target/horizon-mod/`, with the mod's `assets/`
//! and `data/`.
//! - `run [--game <horizon-client>] [-- <game args>]`: build, then play with
//! the mod; run `build` again and the game reloads it.
//! - `test [--debug] [--hmod <file>] [<name>]`: build, then run the mod's
//! `tests/*.toml` scripts in the sandbox, with no game.
//! the mod.
//! - `check <file> [--json]`: what the registry checks of a submission
//! (`.hmod`, `.hpack`, resource pack `.zip`), with a trial run for a mod.
//! (`.hmod`, `.hnmod`, `.hpack`, resource pack `.zip`, `.hshader`).
//! - `registry build|review|check|list`: the registry repository's CI.
use horizon_mods::{
Capability, Content, Manifest, Package,
manifest::{MANIFEST, component_imports, componentize},
testing::{Script, TestGame},
};
use horizon_mods::{Capability, Content, Manifest, Package, manifest::MANIFEST};
mod check;
mod registry;
use std::{
path::{Path, PathBuf},
process::{Command, ExitCode, Stdio},
process::{Command, ExitCode},
};
const TARGET: &str = "wasm32-unknown-unknown";
const SDK_GIT: &str = "{ git = \"https://github.com/ludovic111/horizon\" }";
fn main() -> ExitCode {
// Cargo runs `cargo-horizon-mod horizon-mod <args>`.
let mut args: Vec<String> = std::env::args().skip(1).collect();
@@ -35,14 +26,13 @@ fn main() -> ExitCode {
}
let result = match args.first().map(String::as_str) {
Some("new") => new(&args[1..]),
Some("build") => build(!args.iter().any(|a| a == "--debug")).map(|p| {
Some("build") => build().map(|p| {
println!("Built {}", p.display());
}),
Some("run") => run(&args[1..]),
Some("test") => test(&args[1..]),
Some("check") => check(&args[1..]),
Some("registry") => registry::main(&args[1..]),
_ => Err("usage: cargo horizon-mod new <path> [--template code|content] | build [--debug] | run [--game <horizon-client>] [-- <game args>] | test [--debug] [--hmod <file>] [<name>] | check <file> [--kind <kind>] [--key <public key> --signature <file.minisig>] [--json] | registry build|review|check|list".into()),
_ => Err("usage: cargo horizon-mod new <path> [--name \"Name\"] | build | run [--game <horizon-client>] [-- <game args>] | check <file> [--kind <kind>] [--key <public key> --signature <file.minisig>] [--json] | registry build|review|check|list".into()),
};
match result {
Ok(()) => ExitCode::SUCCESS,
@@ -83,31 +73,9 @@ fn new(args: &[String]) -> Result<(), String> {
})
.collect();
let name = flag(args, "--name").map_or_else(|| base.replace(['-', '_'], " "), str::to_string);
let sdk = match flag(args, "--sdk-path") {
Some(p) => {
let p = std::fs::canonicalize(p).map_err(|e| format!("--sdk-path {p}: {e}"))?;
format!("{{ path = {:?} }}", p.display().to_string())
}
None => SDK_GIT.to_string(),
};
let fill = |t: &str| {
t.replace("{crate}", &id.replace('_', "-"))
.replace("{id}", &id)
.replace("{name}", &name)
.replace("{sdk}", &sdk)
};
let code = [
("Cargo.toml", include_str!("../template/Cargo.toml.in")),
(MANIFEST, include_str!("../template/horizon-mod.toml")),
("src/lib.rs", include_str!("../template/src/lib.rs")),
(".gitignore", include_str!("../template/gitignore")),
(
"tests/hello.toml",
include_str!("../template/tests/hello.toml"),
),
];
let fill = |t: &str| t.replace("{id}", &id).replace("{name}", &name);
// A block copying a vanilla one, with its model, name, loot, recipe and tag.
let content = [
let files = [
(
MANIFEST,
include_str!("../template/content/horizon-mod.toml"),
@@ -146,12 +114,12 @@ fn new(args: &[String]) -> Result<(), String> {
),
(".gitignore", include_str!("../template/gitignore")),
];
let files: &[(&str, &str)] = match flag(args, "--template").unwrap_or("code") {
"code" => &code,
"content" => &content,
other => return Err(format!("no template {other}: code or content")),
};
for &(file, text) in files {
if flag(args, "--template").is_some_and(|t| t != "content") {
return Err(
"only content mods are made here: code mods are native (docs/native-mods.md)".into(),
);
}
for (file, text) in files {
let file = fill(file);
let file = file.as_str();
let to = dir.join(file);
@@ -169,29 +137,24 @@ fn new(args: &[String]) -> Result<(), String> {
Ok(())
}
/// Builds the mod in the current folder and packs `target/horizon-mod/<id>.hmod`:
/// the manifest, the crate's component (none without a `Cargo.toml`: a
/// content mod) and the files in `assets/` and `data/`.
fn build(release: bool) -> Result<PathBuf, String> {
build_in(Path::new("."), release)
/// Packs the content mod in the current folder into `target/horizon-mod/<id>.hmod`: the
/// manifest and the files in `assets/` and `data/`.
fn build() -> Result<PathBuf, String> {
build_in(Path::new("."))
}
fn build_in(root: &Path, release: bool) -> Result<PathBuf, String> {
fn build_in(root: &Path) -> Result<PathBuf, String> {
let manifest_text = std::fs::read_to_string(root.join(MANIFEST))
.map_err(|e| format!("{MANIFEST}: {e} (run this in a mod's folder)"))?;
let manifest = Manifest::parse(&manifest_text)?;
let (wasm, target_dir) = if root.join("Cargo.toml").exists() {
let (wasm, target) = compile(root, release)?;
// The game refuses a component importing capabilities the manifest lacks.
manifest.check_imports(component_imports(&wasm)?.iter().map(String::as_str))?;
(Some(wasm), target)
} else if manifest.capabilities.contains(&Capability::Registries) {
(None, root.join("target"))
} else {
if root.join("Cargo.toml").exists() {
return Err("a Cargo.toml: code mods are native now (docs/native-mods.md)".into());
}
if !manifest.capabilities.contains(&Capability::Registries) {
return Err(format!(
"no Cargo.toml: only a content mod (capability registries in {MANIFEST}) has no code"
"a content mod has capability registries in {MANIFEST}"
));
};
}
// The same checks as the game's, and the files in the order it lists them.
let content = Content::read(root, &manifest)?;
let mut reader = content.reader()?;
@@ -200,53 +163,17 @@ fn build_in(root: &Path, release: bool) -> Result<PathBuf, String> {
files.push((name.as_str(), reader.read(name)?));
}
let files: Vec<(&str, &[u8])> = files.iter().map(|(n, b)| (*n, b.as_slice())).collect();
let dir = target_dir.join("horizon-mod");
let dir = root.join("target/horizon-mod");
std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?;
let path = dir.join(format!("{}.hmod", manifest.id));
// Write then rename, so a running game never reads half a file.
let partial = path.with_extension("hmod.partial");
let bytes = Package::write_files(&manifest_text, wasm.as_deref(), &files)?;
let bytes = Package::write_files(&manifest_text, None, &files)?;
std::fs::write(&partial, bytes).map_err(|e| e.to_string())?;
std::fs::rename(&partial, &path).map_err(|e| e.to_string())?;
Ok(path)
}
/// Builds the crate for WebAssembly: its component, and Cargo's target folder.
fn compile(root: &Path, release: bool) -> Result<(Vec<u8>, PathBuf), String> {
let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".into());
let mut cmd = Command::new(cargo);
cmd.current_dir(root).args([
"build",
"--lib",
"--target",
TARGET,
"--message-format=json-render-diagnostics",
]);
if release {
cmd.arg("--release");
}
let out = cmd
.stderr(Stdio::inherit())
.output()
.map_err(|e| e.to_string())?;
if !out.status.success() {
return Err(format!(
"cargo build failed (is the target installed? rustup target add {TARGET})"
));
}
let core = String::from_utf8_lossy(&out.stdout)
.lines()
.filter_map(|l| serde_json::from_str::<serde_json::Value>(l).ok())
.filter(|m| m["reason"] == "compiler-artifact")
.flat_map(|m| m["filenames"].as_array().cloned().unwrap_or_default())
.filter_map(|f| f.as_str().map(PathBuf::from))
.rfind(|f| f.extension().is_some_and(|e| e == "wasm"))
.ok_or("the build made no .wasm: is the crate a cdylib?")?;
let wasm = componentize(&std::fs::read(&core).map_err(|e| e.to_string())?)?;
let target = core.ancestors().nth(3).ok_or("unexpected target folder")?;
Ok((wasm, target.to_path_buf()))
}
/// `check <file>`: the registry's checks of a submission, printed; fails
/// when one does.
fn check(args: &[String]) -> Result<(), String> {
@@ -278,74 +205,14 @@ fn check(args: &[String]) -> Result<(), String> {
Ok(())
}
/// `test`: every `tests/*.toml` script (those whose name holds `<name>`)
/// against the built mod, or the `.hmod` given with `--hmod`.
fn test(args: &[String]) -> Result<(), String> {
let hmod = match flag(args, "--hmod") {
Some(p) => PathBuf::from(p),
None => build(!args.iter().any(|a| a == "--debug"))?,
};
let filter = args
.iter()
.enumerate()
.find(|(i, a)| !a.starts_with("--") && (*i == 0 || args[i - 1] != "--hmod"))
.map(|(_, a)| a.as_str());
let mut scripts: Vec<PathBuf> = std::fs::read_dir("tests")
.map_err(|e| {
format!(
"tests/: {e} (a mod's tests are tests/<name>.toml: docs/mods.md, \"Testing a mod\")"
)
})?
.filter_map(|e| Some(e.ok()?.path()))
.filter(|p| p.extension().is_some_and(|e| e == "toml"))
.filter(|p| filter.is_none_or(|f| p.to_string_lossy().contains(f)))
.collect();
scripts.sort();
if scripts.is_empty() {
return Err("no tests/*.toml to run".into());
}
println!(
"running {} scripts against {}",
scripts.len(),
hmod.display()
);
let mut failed = 0;
for path in &scripts {
let result = std::fs::read_to_string(path)
.map_err(|e| e.to_string())
.and_then(|text| Script::parse(&text))
.and_then(|script| {
let mut game = TestGame::load(&hmod)?;
script.run(&mut game)?;
Ok(script.steps.len())
});
match result {
Ok(steps) => println!("test {} ... ok ({steps} steps)", path.display()),
Err(e) => {
failed += 1;
println!("test {} ... FAILED\n {e}", path.display());
}
}
}
let passed = scripts.len() - failed;
if failed > 0 {
return Err(format!("{failed} failed, {passed} passed"));
}
println!("test result: ok. {passed} passed");
Ok(())
}
fn run(args: &[String]) -> Result<(), String> {
let path = build(true)?;
let path = build()?;
let game = flag(args, "--game")
.map(str::to_string)
.or_else(|| std::env::var("HORIZON_CLIENT").ok())
.unwrap_or_else(|| "horizon-client".into());
let extra = args.iter().skip_while(|a| *a != "--").skip(1);
println!(
"Starting {game} with {} (rebuild with `cargo horizon-mod build` to reload it)",
path.display()
);
println!("Starting {game} with {}", path.display());
let status = Command::new(&game)
.arg("--dev-mod")
.arg(&path)
@@ -364,28 +231,6 @@ fn run(args: &[String]) -> Result<(), String> {
mod tests {
use super::*;
#[test]
fn the_template_is_a_valid_mod() {
let dir = std::env::temp_dir().join(format!("horizon-mod-new-{}", std::process::id()));
let path = dir.join("My-Mod");
new(&[path.display().to_string(), "--name".into(), "My Mod".into()]).unwrap();
let manifest =
Manifest::parse(&std::fs::read_to_string(path.join(MANIFEST)).unwrap()).unwrap();
assert_eq!(
(manifest.id.as_str(), manifest.name.as_str()),
("my_mod", "My Mod")
);
let cargo = std::fs::read_to_string(path.join("Cargo.toml")).unwrap();
assert!(cargo.contains("name = \"my-mod\"") && cargo.contains(SDK_GIT));
let script = std::fs::read_to_string(path.join("tests/hello.toml")).unwrap();
assert_eq!(Script::parse(&script).unwrap().steps.len(), 3);
assert!(
new(&[path.display().to_string()]).is_err(),
"never overwrites"
);
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn the_content_template_builds_and_checks_with_no_code() {
let dir = std::env::temp_dir().join(format!("horizon-mod-content-{}", std::process::id()));
@@ -397,7 +242,7 @@ mod tests {
];
new(&new_args).unwrap();
assert!(!path.join("Cargo.toml").exists());
let hmod = build_in(&path, true).unwrap();
let hmod = build_in(&path).unwrap();
assert_eq!(hmod, path.join("target/horizon-mod/gems.hmod"));
let package = Package::read(&hmod).unwrap();
assert!(package.wasm.is_none());
@@ -409,17 +254,14 @@ mod tests {
);
assert_eq!(content.files.len(), 8, "{:?}", content.files);
let report = check::check(&hmod, check::kind_of(&hmod).unwrap(), None, None).unwrap();
assert!(!report.code);
assert!(
report.text().contains("no code: content only"),
"{}",
report.text()
);
// Without registries, a mod needs code.
let manifest = std::fs::read_to_string(path.join(MANIFEST)).unwrap();
let manifest = manifest.replace("[\"registries\"]", "[]");
std::fs::write(path.join(MANIFEST), manifest).unwrap();
assert!(build_in(&path, true).unwrap_err().contains("no Cargo.toml"));
// Code is native now.
std::fs::write(path.join("Cargo.toml"), "").unwrap();
assert!(build_in(&path).unwrap_err().contains("native"));
assert!(
new(&[
dir.join("x").display().to_string(),
+2 -2
View File
@@ -1,5 +1,5 @@
//! `cargo horizon-mod registry build|review|check|list`: the registry repository's CI
//! (`ludovic111/horizon-mods`, docs/api/mod-index.md). Network-free: the
//! (`horizon-rust/horizon-mods`, docs/api/mod-index.md). Network-free: the
//! workflow downloads the files and publishes what this writes.
use horizon_mods::index::{
FILE_HOST,
@@ -8,7 +8,7 @@ use horizon_mods::index::{
use std::path::Path;
/// Where the registry's site is published (its icons).
const SITE: &str = "https://ludovic111.github.io/horizon-mods/";
const SITE: &str = "https://horizon-rust.github.io/horizon-mods/";
const USAGE: &str = "usage: cargo horizon-mod registry build <checkout> --files <dir> --out <index.json> [--release <url>] [--site <url>] [--generated <time>]
cargo horizon-mod registry review <base checkout> <head checkout> --author <login> [--json]
@@ -1,14 +0,0 @@
[package]
name = "{crate}"
version = "0.1.0"
edition = "2024"
[lib]
crate-type = ["cdylib"]
[dependencies]
horizon-mod-sdk = {sdk}
[profile.release]
opt-level = "s"
lto = true
@@ -1,12 +0,0 @@
id = "{id}"
name = "{name}"
version = "0.1.0"
description = ""
authors = []
# The Horizon mod API the SDK targets.
api = "0.1"
# client: runs in the player's game.
side = "client"
# What the mod may do, shown to players before they turn it on:
# game-read (position, health, time), chat (show messages), hud (draw text).
capabilities = ["chat"]
@@ -1,19 +0,0 @@
use horizon_mod_sdk::{Mod, Registration, chat};
struct MyMod {
greeted: u32,
}
impl Mod for MyMod {
fn init() -> Result<(Self, Registration), String> {
Ok((MyMod { greeted: 0 }, Registration::new().command("hello")))
}
fn command(&mut self, _name: &str, args: &str) -> Result<Option<String>, String> {
self.greeted += 1;
chat::show(&format!("Hello {args}! ({} so far)", self.greeted));
Ok(None)
}
}
horizon_mod_sdk::export_mod!(MyMod);
@@ -1,14 +0,0 @@
# `cargo horizon-mod test` runs every tests/*.toml against the built mod, with no game:
# steps happen in order, and `expect` checks what the mod showed during the step.
# Every key: docs/mods.md, "Testing a mod".
[[step]]
expect.commands = ["hello"]
[[step]]
command = "hello world"
expect.chat = ["Hello world! (1 so far)"]
[[step]]
command = "hello again"
expect.chat = ["Hello again! (2 so far)"]
@@ -61,6 +61,16 @@ fn f32_tag(v: f32) -> Tag {
Tag::Float(v.to_bits())
}
/// `TheEndGatewayBlockEntity.loadAdditional`: BlockPos.CODEC and Level.isInSpawnableBounds.
pub fn gateway_exit(tag: &Tag) -> Option<[i32; 3]> {
let Tag::IntArray(v) = tag else { return None };
let p: [i32; 3] = v.as_slice().try_into().ok()?;
((-30_000_000..30_000_000).contains(&p[0])
&& (-20_000_000..20_000_000).contains(&p[1])
&& (-30_000_000..30_000_000).contains(&p[2]))
.then_some(p)
}
/// What a block entity reads and writes, in `saveAdditional` order.
struct Fields<'a> {
input: &'a Tag,
@@ -424,10 +434,7 @@ pub fn canonical(tag: &Tag) -> Option<Tag> {
}
"minecraft:end_gateway" => {
f.long("Age", 0);
f.keep("exit_portal", |t| {
is_pos(t)
&& matches!(t, Tag::IntArray(v) if v[1].abs() <= 30_000_000 && v[0].abs() <= 30_000_000 && v[2].abs() <= 30_000_000)
});
f.keep("exit_portal", |t| gateway_exit(t).is_some());
if flag(tag.get("ExactTeleport")) == Some(true) {
f.put("ExactTeleport", boolean(true));
}
@@ -487,7 +494,11 @@ pub fn canonical(tag: &Tag) -> Option<Tag> {
_ => string("minecraft:air"),
},
);
f.put("facing", Tag::Int(int(tag.get("facing")).unwrap_or(0)));
// Direction.LEGACY_ID_CODEC: the 3D data value as a byte.
f.put(
"facing",
Tag::Byte(int(tag.get("facing")).unwrap_or(0) as i8),
);
f.float("progress", 0.0);
f.flag("extending", false);
f.flag("source", false);
@@ -866,6 +877,23 @@ mod tests {
);
}
/// `PistonMovingBlockEntity.saveAdditional`: `facing` through
/// `Direction.LEGACY_ID_CODEC` (a byte), `progress` a float.
#[test]
fn moving_piston_fields_match_vanilla() {
let out = canon(
"{id:\"minecraft:piston\",x:1,y:2,z:3,blockState:{Name:\"minecraft:stone\"},\
facing:5,progress:0.5f,extending:1b,source:0b}",
);
assert_eq!(out.get("facing"), Some(&Tag::Byte(5)));
assert_eq!(out.get("progress"), Some(&f32_tag(0.5)));
assert_eq!(out.get("extending"), Some(&Tag::Byte(1)));
assert_eq!(out.get("source"), Some(&Tag::Byte(0)));
let fresh = canon("{id:\"minecraft:piston\",x:1,y:2,z:3}");
assert_eq!(fresh.get("facing"), Some(&Tag::Byte(0)));
assert_eq!(fresh.get("blockState"), Some(&string("minecraft:air")));
}
#[test]
fn loot_table_replaces_items_and_zero_seed_is_omitted() {
let out = canon(
+297 -59
View File
@@ -1,6 +1,7 @@
//! Text components from commands: `ComponentArgument` reads an SNBT (or JSON)
//! value, `ComponentSerialization` turns it into styled text.
use crate::text::{Style, Text, named_color};
use crate::text::{InlineObject, Style, Text, named_color};
use horizon_world::nbt::Tag;
use serde_json::{Map, Value};
/// `SnbtGrammar`, enough for components: compounds, lists, quoted and
@@ -10,13 +11,18 @@ pub fn parse_snbt(input: &str) -> Result<(Value, usize), String> {
let mut p = Parser {
s: input.as_bytes(),
i: 0,
depth: 0,
};
let v = p.value()?;
Ok((v, p.i))
}
/// Nesting deeper than NBT's own limit (`NbtAccounter`'s 512) is refused
/// rather than overflowing the stack.
const SNBT_MAX_DEPTH: usize = 512;
struct Parser<'a> {
s: &'a [u8],
i: usize,
depth: usize,
}
impl Parser<'_> {
fn ws(&mut self) {
@@ -40,57 +46,22 @@ impl Parser<'_> {
self.ws();
match self.peek() {
Some(b'{') => {
self.i += 1;
let mut map = Map::new();
self.ws();
if self.peek() == Some(b'}') {
self.i += 1;
return Ok(Value::Object(map));
}
loop {
self.ws();
let key = self.string()?;
self.expect(b':')?;
map.insert(key, self.value()?);
self.ws();
match self.peek() {
Some(b',') => self.i += 1,
Some(b'}') => {
self.i += 1;
return Ok(Value::Object(map));
}
_ => return Err("expected ',' or '}'".into()),
}
if self.depth >= SNBT_MAX_DEPTH {
return Err(format!("tag nesting too deep at {}", self.i));
}
self.depth += 1;
let v = self.object();
self.depth -= 1;
v
}
Some(b'[') => {
self.i += 1;
let mut list = Vec::new();
self.ws();
// Typed arrays [B;...], [I;...], [L;...].
if matches!(
self.s.get(self.i..self.i + 2),
Some([b'B' | b'I' | b'L', b';'])
) {
self.i += 2;
}
self.ws();
if self.peek() == Some(b']') {
self.i += 1;
return Ok(Value::Array(list));
}
loop {
list.push(self.value()?);
self.ws();
match self.peek() {
Some(b',') => self.i += 1,
Some(b']') => {
self.i += 1;
return Ok(Value::Array(list));
}
_ => return Err("expected ',' or ']'".into()),
}
if self.depth >= SNBT_MAX_DEPTH {
return Err(format!("tag nesting too deep at {}", self.i));
}
self.depth += 1;
let v = self.array();
self.depth -= 1;
v
}
Some(b'"' | b'\'') => Ok(Value::String(self.string()?)),
Some(_) => {
@@ -104,10 +75,20 @@ impl Parser<'_> {
_ => {
let digits = word
.trim_end_matches(['b', 'B', 's', 'S', 'l', 'L', 'f', 'F', 'd', 'D']);
match (digits.parse::<i64>(), digits.parse::<f64>()) {
(Ok(n), _) if digits.len() + 1 >= word.len() => Value::from(n),
(_, Ok(f)) if digits.len() + 1 >= word.len() => Value::from(f),
_ => Value::String(word),
// Only digits (and an exponent) make a number: `nan`
// or `Infinity`, which Rust's parse would take, stays
// a string.
if digits
.bytes()
.any(|c| c.is_ascii_alphabetic() && c != b'e' && c != b'E')
{
Value::String(word)
} else {
match (digits.parse::<i64>(), digits.parse::<f64>()) {
(Ok(n), _) if digits.len() + 1 >= word.len() => Value::from(n),
(_, Ok(f)) if digits.len() + 1 >= word.len() => Value::from(f),
_ => Value::String(word),
}
}
}
})
@@ -115,6 +96,59 @@ impl Parser<'_> {
None => Err("expected a value".into()),
}
}
fn object(&mut self) -> Result<Value, String> {
self.i += 1;
let mut map = Map::new();
self.ws();
if self.peek() == Some(b'}') {
self.i += 1;
return Ok(Value::Object(map));
}
loop {
self.ws();
let key = self.string()?;
self.expect(b':')?;
map.insert(key, self.value()?);
self.ws();
match self.peek() {
Some(b',') => self.i += 1,
Some(b'}') => {
self.i += 1;
return Ok(Value::Object(map));
}
_ => return Err("expected ',' or '}'".into()),
}
}
}
fn array(&mut self) -> Result<Value, String> {
self.i += 1;
let mut list = Vec::new();
self.ws();
// Typed arrays [B;...], [I;...], [L;...].
if matches!(
self.s.get(self.i..self.i + 2),
Some([b'B' | b'I' | b'L', b';'])
) {
self.i += 2;
}
self.ws();
if self.peek() == Some(b']') {
self.i += 1;
return Ok(Value::Array(list));
}
loop {
list.push(self.value()?);
self.ws();
match self.peek() {
Some(b',') => self.i += 1,
Some(b']') => {
self.i += 1;
return Ok(Value::Array(list));
}
_ => return Err("expected ',' or ']'".into()),
}
}
}
fn unquoted(&mut self) -> String {
let start = self.i;
while let Some(c) = self.peek() {
@@ -161,6 +195,7 @@ fn object_style(o: &serde_json::Map<String, Value>) -> Style {
.and_then(|h| u32::from_str_radix(h, 16).ok())
});
}
style.shadow_color = o.get("shadow_color").and_then(shadow_color);
let flag = |k: &str| o.get(k).and_then(Value::as_bool);
style.bold = flag("bold");
style.italic = flag("italic");
@@ -173,22 +208,77 @@ fn object_style(o: &serde_json::Map<String, Value>) -> Style {
if let Some(e) = o.get("click_event").and_then(Value::as_object) {
let text = |k: &str| e.get(k).and_then(Value::as_str).map(str::to_owned);
style.click = match e.get("action").and_then(Value::as_str) {
// ClickEvent.OpenUrl.CODEC: ExtraCodecs.UNTRUSTED_URI allows http and
// https only, the scheme in any case (Util.parseAndValidateUntrustedUri).
Some("open_url") => text("url")
.filter(|u| {
u.split_once(':').is_some_and(|(scheme, _)| {
scheme.eq_ignore_ascii_case("http") || scheme.eq_ignore_ascii_case("https")
})
})
.map(crate::text::ClickEvent::OpenUrl),
// ClickEvent.ChangePage.CODEC: ExtraCodecs.POSITIVE_INT.
Some("change_page") => e
.get("page")
.and_then(Value::as_i64)
.and_then(|p| i32::try_from(p).ok())
.filter(|p| *p >= 1)
.map(crate::text::ClickEvent::ChangePage),
Some("suggest_command") => text("command").map(crate::text::ClickEvent::SuggestCommand),
Some("run_command") => text("command").map(crate::text::ClickEvent::RunCommand),
Some("copy_to_clipboard") => {
text("value").map(crate::text::ClickEvent::CopyToClipboard)
}
// ClickEvent.ShowDialog.CODEC: a `minecraft:dialog` registry id or
// the dialog itself; the server may send it (isAllowedFromServer).
Some("show_dialog") => e.get("dialog").map(|dialog| {
crate::text::ClickEvent::ShowDialog(crate::scoreboard::json_to_tag(dialog))
}),
_ => None,
};
}
// HoverEvent.ShowText.
if let Some(e) = o.get("hover_event").and_then(Value::as_object)
&& e.get("action").and_then(Value::as_str) == Some("show_text")
&& let Some(value) = e.get("value")
{
style.hover = Some(Box::new(to_text(value, &mut |s| Text::literal(s))));
// HoverEvent.ShowText and ShowItem.
if let Some(e) = o.get("hover_event").and_then(Value::as_object) {
match e.get("action").and_then(Value::as_str) {
Some("show_text") => {
if let Some(value) = e.get("value") {
style.hover = Some(Box::new(to_text(value, &mut |s| Text::literal(s))));
}
}
Some("show_item") if e.get("id").is_some_and(Value::is_string) => {
style.hover_item = Some(Box::new(Value::Object(e.clone())));
}
_ => {}
}
}
style.insertion = o
.get("insertion")
.and_then(Value::as_str)
.map(str::to_owned);
style
}
/// `Style.shadow_color`: a packed ARGB int or normalized RGBA array.
pub(crate) fn shadow_color(value: &Value) -> Option<u32> {
if let Some(packed) = value.as_i64() {
return i32::try_from(packed).ok().map(|n| n as u32);
}
let rgba = value.as_array()?;
if rgba.len() != 4 {
return None;
}
let channels = rgba
.iter()
.map(|channel| {
// ExtraCodecs.ARGB_COLOR_CODEC reads floats; ARGB.as8BitChannel
// floors each normalized channel multiplied by 255.
let value = channel.as_f64()? as f32;
(value.is_finite() && (0.0..=1.0).contains(&value))
.then_some((value * 255.0_f32).floor() as u32)
})
.collect::<Option<Vec<_>>>()?;
Some(channels[3] << 24 | channels[0] << 16 | channels[1] << 8 | channels[2])
}
/// `ComponentSerialization.CODEC` decoding into text. `selector` components
/// are resolved by `resolve` (ComponentUtils.updateForEntity).
pub fn to_text(v: &Value, resolve: &mut dyn FnMut(&str) -> Text) -> Text {
@@ -229,6 +319,10 @@ pub fn to_text_with(
let style = object_style(o);
let content = if let Some(t) = contents(o) {
t
} else if let Some(object) = inline_object(o, resolve, contents) {
let mut text = Text::literal("\u{fffc}");
text.spans[0].style.inline_object = Some(object);
text
} else if let Some(t) = o.get("text") {
match t {
Value::String(s) => Text::literal(s.clone()),
@@ -265,16 +359,142 @@ pub fn to_text_with(
{
out.push(to_text_with(extra, resolve, contents).with_style(&style));
}
out
if o.contains_key("object") || o.contains_key("player") || o.contains_key("sprite") {
out.with_component(Value::Object(o.clone()))
} else {
out
}
}
Value::Null => Text::empty(),
}
}
fn inline_object(
object: &serde_json::Map<String, Value>,
resolve: &mut dyn FnMut(&str) -> Text,
contents: &mut dyn FnMut(&serde_json::Map<String, Value>) -> Option<Text>,
) -> Option<InlineObject> {
let kind = object.get("object").and_then(Value::as_str);
let fallback = object
.get("fallback")
.map(|value| to_text_with(value, resolve, contents).plain());
match kind {
Some("player") | None if object.contains_key("player") => {
let profile = object.get("player")?.clone();
// PlayerSprite.defaultFallback.
let fallback = fallback.unwrap_or_else(|| {
match profile
.as_str()
.or_else(|| profile.get("name").and_then(Value::as_str))
{
Some(name) => format!("[{name} head]"),
None => "[unknown player head]".to_owned(),
}
});
Some(InlineObject::Player {
profile,
hat: object.get("hat").and_then(Value::as_bool).unwrap_or(true),
fallback,
})
}
Some("atlas") | None if object.contains_key("sprite") => {
let atlas = object
.get("atlas")
.and_then(Value::as_str)
.unwrap_or("minecraft:blocks")
.to_owned();
let sprite = object.get("sprite")?.as_str()?.to_owned();
// AtlasSprite.defaultFallback: short names, the atlas unless blocks.
let fallback = fallback.unwrap_or_else(|| {
let short = |id: &str| match id.split_once(':') {
Some(("minecraft", path)) => path.to_owned(),
Some(_) => id.to_owned(),
None => id.to_owned(),
};
if short(&atlas) == "blocks" {
format!("[{}]", short(&sprite))
} else {
format!("[{}@{}]", short(&sprite), short(&atlas))
}
});
Some(InlineObject::Atlas {
atlas,
sprite,
fallback,
})
}
_ => None,
}
}
/// `CustomName` as text: a string keeps its legacy formatting codes, anything
/// else is a component (`ComponentSerialization`).
pub fn name_text(tag: &Tag) -> Text {
match tag {
Tag::String(s) => Text::legacy(&s.to_string_lossy()),
t => crate::scoreboard::comp_text(&crate::scoreboard::tag_to_json(t)),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn shadow_color_accepts_packed_and_rgba_styles() {
let packed = serde_json::json!({"text": "A", "shadow_color": -16711936});
let text = to_text(&packed, &mut |_| Text::empty());
assert_eq!(text.spans[0].style.shadow_color, Some(0xff00ff00));
assert_eq!(text.component()["shadow_color"], -16711936);
let rgba = serde_json::json!({"text": "A", "shadow_color": [1, 0, 0.5, 1]});
let text = to_text(&rgba, &mut |_| Text::empty());
assert_eq!(text.spans[0].style.shadow_color, Some(0xffff007f));
let translucent = serde_json::json!({"text": "A", "shadow_color": [1, 0, 0.5, 0.5]});
let text = to_text(&translucent, &mut |_| Text::empty());
assert_eq!(text.spans[0].style.shadow_color, Some(0x7fff007f));
let no_shadow = serde_json::json!({"text": "A", "shadow_color": 0});
let text = to_text(&no_shadow, &mut |_| Text::empty());
assert_eq!(text.spans[0].style.shadow_color, Some(0));
}
#[test]
fn player_object_survives_text_parsing_and_keeps_its_siblings() {
let component = serde_json::json!({
"text": "<",
"extra": [
{"object": "player", "player": {"name": "elyapluggin"}, "hat": false,
"fallback": "[head]"},
{"text": " >", "color": "gold"}
]
});
let text = to_text(&component, &mut |_| Text::empty());
assert_eq!(text.plain(), "<[head] >");
assert!(matches!(
&text.spans[1].style.inline_object,
Some(InlineObject::Player { hat: false, .. })
));
assert!(text.spans[2].style.inline_object.is_none());
assert_eq!(text.component(), component);
let sprite = serde_json::json!({"object": "atlas", "sprite": "minecraft:item/porkchop"});
let text = to_text(&sprite, &mut |_| Text::empty());
assert!(matches!(
&text.spans[0].style.inline_object,
Some(InlineObject::Atlas { sprite, .. }) if sprite == "minecraft:item/porkchop"
));
// ObjectContents.visit without a fallback: the sprite's defaultFallback.
assert_eq!(text.plain(), "[item/porkchop]");
let head = serde_json::json!({"object": "player", "player": {"name": "Steve"}});
assert_eq!(
to_text(&head, &mut |_| Text::empty()).plain(),
"[Steve head]"
);
let gui = serde_json::json!({"atlas": "gui", "sprite": "hud/heart/full"});
assert_eq!(
to_text(&gui, &mut |_| Text::empty()).plain(),
"[hud/heart/full@gui]"
);
}
#[test]
fn snbt_and_json_components() {
let input = "{text:'Hi ',color:red,extra:[{text:\"there\",bold:1b}]} rest";
@@ -308,4 +528,22 @@ mod tests {
let t = to_text(&v, &mut |_| Text::empty());
assert_eq!(t.spans[0].style.font, None);
}
#[test]
fn snbt_nesting_is_bounded() {
let nest = |n: usize| format!("{}1{}", "[".repeat(n), "]".repeat(n));
assert!(parse_snbt(&nest(512)).is_ok());
assert!(parse_snbt(&nest(513)).is_err());
assert!(parse_snbt(&nest(100_000)).is_err());
}
#[test]
fn bare_words_that_look_numeric_stay_strings() {
let (v, _) = parse_snbt("{text:nan}").unwrap();
assert_eq!(v["text"], Value::String("nan".into()));
let (v, _) = parse_snbt("{text:Infinity}").unwrap();
assert_eq!(v["text"], Value::String("Infinity".into()));
let (v, _) = parse_snbt("{n:1.5}").unwrap();
assert_eq!(v["n"], Value::from(1.5));
}
}
@@ -16,3 +16,20 @@ pub fn parse_identifier(raw: &str) -> Option<String> {
});
(ns_ok && path_ok).then(|| format!("{ns}:{path}"))
}
/// `Identifier.CODEC` parsing an entity id: lowercase namespace and path
/// characters, `minecraft` when the namespace is left out.
pub fn normalize_id(s: &str) -> Option<String> {
let (ns, path) = match s.split_once(':') {
Some((ns, path)) => (if ns.is_empty() { "minecraft" } else { ns }, path),
None => ("minecraft", s),
};
let ok = |c: char, slash: bool| {
c.is_ascii_lowercase()
|| c.is_ascii_digit()
|| matches!(c, '_' | '-' | '.')
|| slash && c == '/'
};
(ns.chars().all(|c| ok(c, false)) && path.chars().all(|c| ok(c, true)))
.then(|| format!("{ns}:{path}"))
}
+51 -1
View File
@@ -40,6 +40,10 @@ static BIDIRECTIONAL: AtomicBool = AtomicBool::new(false);
/// A source of resource files: the prepared root plus packs, highest priority last.
pub trait ResourceSource {
fn read(&self, path: &str) -> Option<Vec<u8>>;
/// Whether `path` is there, without reading it where the source can tell.
fn exists(&self, path: &str) -> bool {
self.read(path).is_some()
}
/// The namespaces under `assets/` (resource packs may add their own).
fn namespaces(&self) -> Vec<String> {
vec!["minecraft".into()]
@@ -56,6 +60,12 @@ impl ResourceSource for Bundled {
"assets/horizon/lang/ru_ru.json" => {
include_str!("../../horizon-client/assets/horizon/lang/ru_ru.json")
}
"assets/horizon/lang/pl_pl.json" => {
include_str!("../../horizon-client/assets/horizon/lang/pl_pl.json")
}
"assets/horizon/lang/uk_ua.json" => {
include_str!("../../horizon-client/assets/horizon/lang/uk_ua.json")
}
_ => return None,
};
Some(body.as_bytes().to_vec())
@@ -68,6 +78,9 @@ impl ResourceSource for std::path::PathBuf {
fn read(&self, path: &str) -> Option<Vec<u8>> {
std::fs::read(self.join(path)).ok()
}
fn exists(&self, path: &str) -> bool {
self.join(path).is_file()
}
fn namespaces(&self) -> Vec<String> {
std::fs::read_dir(self.join("assets"))
.into_iter()
@@ -190,7 +203,7 @@ pub fn available(sources: &[&dyn ResourceSource]) -> Vec<LanguageInfo> {
for (code, info) in value["language"].as_object().into_iter().flatten() {
// Only languages whose file exists somewhere are selectable.
let file = format!("assets/minecraft/lang/{code}.json");
if code != DEFAULT && !sources.iter().any(|s| s.read(&file).is_some()) {
if code != DEFAULT && !sources.iter().any(|s| s.exists(&file)) {
continue;
}
found.insert(
@@ -259,6 +272,21 @@ mod tests {
assert_eq!(en.get("horizon.options.controller"), Some("Controller"));
let ru = Language::load(&[&empty], "ru_ru");
assert_eq!(ru.get("horizon.options.controller"), Some("Геймпад"));
// Every bundled string carries both languages: a key added to one file
// and forgotten in the other shows as a key name on screen.
for key in en.map().keys() {
assert!(ru.get(key).is_some(), "{key} has no ru_ru value");
}
assert_eq!(en.get("horizon.pages.mods"), Some("Mods"));
assert_eq!(ru.get("horizon.pages.mods"), Some("Моды"));
let pl = Language::load(&[&empty], "pl_pl");
assert_eq!(pl.get("horizon.options.controller"), Some("Kontroler"));
assert_eq!(pl.get("horizon.options.vibration"), Some("Wibracje"));
let uk = Language::load(&[&empty], "uk_ua");
assert_eq!(
uk.get("horizon.options.on_screen_keyboard"),
Some("Екранна клавіатура")
);
}
#[test]
@@ -284,4 +312,26 @@ mod tests {
);
assert_eq!(langs[1].label(), "Français (France)");
}
#[test]
fn languages_are_listed_without_reading_their_files() {
struct NoLangReads;
impl ResourceSource for NoLangReads {
fn read(&self, path: &str) -> Option<Vec<u8>> {
assert!(!path.contains("/lang/"), "read {path}");
(path == "assets/pack.mcmeta").then(|| {
br#"{"language":{"fr_fr":{"name":"French"},"de_de":{"name":"German"}}}"#
.to_vec()
})
}
fn exists(&self, path: &str) -> bool {
path == "assets/minecraft/lang/fr_fr.json"
}
}
let langs = available(&[&NoLangReads]);
assert_eq!(
langs.iter().map(|l| l.code.as_str()).collect::<Vec<_>>(),
["en_us", "fr_fr"]
);
}
}
+7 -2
View File
@@ -1,8 +1,9 @@
//! The client's lowest layer, below commands and gameplay: chat text and
//! translations, SNBT and NBT paths, text components, the scoreboard, boss
//! bars, game rules, block entity NBT tables, and the registry, particle and
//! sound source tables commands name. horizon-client re-exports each module
//! under its old path (`horizon_client::text`, ...).
//! sound source tables commands name, and the `pack.mcmeta` filter regex.
//! horizon-client re-exports each module under its old path
//! (`horizon_client::text`, ...).
#![forbid(unsafe_code)]
pub mod block_entity_nbt;
pub mod bossbar;
@@ -13,11 +14,15 @@ pub mod identifier;
pub mod key_display;
pub mod lang;
pub mod nbt_path;
pub mod nbt_value;
pub mod particle_type;
pub mod pattern;
pub mod potions;
pub mod registries;
pub mod scoreboard;
pub mod snbt;
pub mod sound_source;
pub mod text;
pub mod uuid;
pub mod who;
pub type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
@@ -0,0 +1,32 @@
//! Reading and writing single NBT values the way vanilla's codecs accept them.
use horizon_world::nbt::Tag;
pub fn string(s: &str) -> Tag {
Tag::String(s.into())
}
pub fn text(tag: Option<&Tag>) -> Option<String> {
match tag {
Some(Tag::String(s)) => Some(s.to_string_lossy()),
_ => None,
}
}
/// Any numeric tag as i64 (vanilla's `getIntOr` etc. accept every number type).
pub fn number(tag: Option<&Tag>) -> Option<i64> {
Some(match tag? {
Tag::Byte(v) => i64::from(*v),
Tag::Short(v) => i64::from(*v),
Tag::Int(v) => i64::from(*v),
Tag::Long(v) => *v,
Tag::Float(b) => f32::from_bits(*b) as i64,
Tag::Double(b) => f64::from_bits(*b) as i64,
_ => return None,
})
}
/// Any numeric tag as f64, keeping a float's or double's fraction.
pub fn float(tag: Option<&Tag>) -> Option<f64> {
Some(match tag? {
Tag::Float(b) => f64::from(f32::from_bits(*b)),
Tag::Double(b) => f64::from_bits(*b),
other => number(Some(other))? as f64,
})
}
+250
View File
@@ -0,0 +1,250 @@
//! The `MobEffects` and `Potions` registries (code in vanilla, ported as
//! tables): each effect's colour and category, each potion's effects, and the
//! potion colour mix. horizon-client re-exports them from `brewing`.
use std::{collections::HashMap, sync::OnceLock};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Category {
Beneficial,
Harmful,
Neutral,
}
/// A `MobEffect`: id (without namespace), colour, category, instantaneous.
#[derive(Clone, Copy, Debug)]
pub struct EffectDef {
pub id: &'static str,
pub color: u32,
pub category: Category,
pub instant: bool,
}
const fn fx(id: &'static str, color: u32, category: Category) -> EffectDef {
EffectDef {
id,
color,
category,
instant: false,
}
}
use Category::{Beneficial as B, Harmful as H, Neutral as N};
/// `MobEffects` in registration order.
pub const EFFECTS: &[EffectDef] = &[
fx("speed", 3402751, B),
fx("slowness", 9154528, H),
fx("haste", 14270531, B),
fx("mining_fatigue", 4866583, H),
fx("strength", 16762624, B),
EffectDef {
id: "instant_health",
color: 16262179,
category: B,
instant: true,
},
EffectDef {
id: "instant_damage",
color: 11101546,
category: H,
instant: true,
},
fx("jump_boost", 16646020, B),
fx("nausea", 5578058, H),
fx("regeneration", 13458603, B),
fx("resistance", 9520880, B),
fx("fire_resistance", 16750848, B),
fx("water_breathing", 10017472, B),
fx("invisibility", 16185078, B),
fx("blindness", 2039587, H),
fx("night_vision", 12779366, B),
fx("hunger", 5797459, H),
fx("weakness", 4738376, H),
fx("poison", 8889187, H),
fx("wither", 7561558, H),
fx("health_boost", 16284963, B),
fx("absorption", 2445989, B),
EffectDef {
id: "saturation",
color: 16262179,
category: B,
instant: true,
},
fx("glowing", 9740385, N),
fx("levitation", 13565951, H),
fx("luck", 5882118, B),
fx("unluck", 12624973, H),
fx("slow_falling", 15978425, B),
fx("conduit_power", 1950417, B),
fx("dolphins_grace", 8954814, B),
fx("bad_omen", 745784, N),
fx("hero_of_the_village", 4521796, B),
fx("darkness", 2696993, H),
fx("trial_omen", 1484454, N),
fx("raid_omen", 14565464, N),
fx("wind_charged", 12438015, H),
fx("weaving", 7891290, H),
fx("oozing", 10092451, H),
fx("infested", 9214860, H),
fx("breath_of_the_nautilus", 65518, B),
];
pub fn effect(id: &str) -> Option<&'static EffectDef> {
let id = id.trim_start_matches("minecraft:");
EFFECTS.iter().find(|e| e.id == id)
}
/// Namespaced effect ids for mob effect storage.
pub fn namespaced(id: &'static str) -> &'static str {
static IDS: OnceLock<HashMap<&'static str, &'static str>> = OnceLock::new();
let key = id.strip_prefix("minecraft:").unwrap_or(id);
IDS.get_or_init(|| {
EFFECTS
.iter()
.map(|e| {
(
e.id,
&*Box::leak(format!("minecraft:{}", e.id).into_boxed_str()),
)
})
.collect()
})
.get(key)
.copied()
.unwrap_or(id)
}
/// A `Potion`: id, name suffix (`item.minecraft.potion.effect.<name>`) and
/// effects as (effect, duration ticks, amplifier).
#[derive(Clone, Copy, Debug)]
pub struct PotionDef {
pub id: &'static str,
pub name: &'static str,
pub effects: &'static [(&'static str, i32, i32)],
}
const fn p(
id: &'static str,
name: &'static str,
effects: &'static [(&'static str, i32, i32)],
) -> PotionDef {
PotionDef { id, name, effects }
}
/// `Potions` in registration order.
pub const POTIONS: &[PotionDef] = &[
p("water", "water", &[]),
p("mundane", "mundane", &[]),
p("thick", "thick", &[]),
p("awkward", "awkward", &[]),
p("night_vision", "night_vision", &[("night_vision", 3600, 0)]),
p(
"long_night_vision",
"night_vision",
&[("night_vision", 9600, 0)],
),
p("invisibility", "invisibility", &[("invisibility", 3600, 0)]),
p(
"long_invisibility",
"invisibility",
&[("invisibility", 9600, 0)],
),
p("leaping", "leaping", &[("jump_boost", 3600, 0)]),
p("long_leaping", "leaping", &[("jump_boost", 9600, 0)]),
p("strong_leaping", "leaping", &[("jump_boost", 1800, 1)]),
p(
"fire_resistance",
"fire_resistance",
&[("fire_resistance", 3600, 0)],
),
p(
"long_fire_resistance",
"fire_resistance",
&[("fire_resistance", 9600, 0)],
),
p("swiftness", "swiftness", &[("speed", 3600, 0)]),
p("long_swiftness", "swiftness", &[("speed", 9600, 0)]),
p("strong_swiftness", "swiftness", &[("speed", 1800, 1)]),
p("slowness", "slowness", &[("slowness", 1800, 0)]),
p("long_slowness", "slowness", &[("slowness", 4800, 0)]),
p("strong_slowness", "slowness", &[("slowness", 400, 3)]),
p(
"turtle_master",
"turtle_master",
&[("slowness", 400, 3), ("resistance", 400, 2)],
),
p(
"long_turtle_master",
"turtle_master",
&[("slowness", 800, 3), ("resistance", 800, 2)],
),
p(
"strong_turtle_master",
"turtle_master",
&[("slowness", 400, 5), ("resistance", 400, 3)],
),
p(
"water_breathing",
"water_breathing",
&[("water_breathing", 3600, 0)],
),
p(
"long_water_breathing",
"water_breathing",
&[("water_breathing", 9600, 0)],
),
p("healing", "healing", &[("instant_health", 1, 0)]),
p("strong_healing", "healing", &[("instant_health", 1, 1)]),
p("harming", "harming", &[("instant_damage", 1, 0)]),
p("strong_harming", "harming", &[("instant_damage", 1, 1)]),
p("poison", "poison", &[("poison", 900, 0)]),
p("long_poison", "poison", &[("poison", 1800, 0)]),
p("strong_poison", "poison", &[("poison", 432, 1)]),
p("regeneration", "regeneration", &[("regeneration", 900, 0)]),
p(
"long_regeneration",
"regeneration",
&[("regeneration", 1800, 0)],
),
p(
"strong_regeneration",
"regeneration",
&[("regeneration", 450, 1)],
),
p("strength", "strength", &[("strength", 3600, 0)]),
p("long_strength", "strength", &[("strength", 9600, 0)]),
p("strong_strength", "strength", &[("strength", 1800, 1)]),
p("weakness", "weakness", &[("weakness", 1800, 0)]),
p("long_weakness", "weakness", &[("weakness", 4800, 0)]),
p("luck", "luck", &[("luck", 6000, 0)]),
p("slow_falling", "slow_falling", &[("slow_falling", 1800, 0)]),
p(
"long_slow_falling",
"slow_falling",
&[("slow_falling", 4800, 0)],
),
p("wind_charged", "wind_charged", &[("wind_charged", 3600, 0)]),
p("weaving", "weaving", &[("weaving", 3600, 0)]),
p("oozing", "oozing", &[("oozing", 3600, 0)]),
p("infested", "infested", &[("infested", 3600, 0)]),
];
pub fn potion(id: &str) -> Option<&'static PotionDef> {
let id = id.trim_start_matches("minecraft:");
POTIONS.iter().find(|p| p.id == id)
}
/// `PotionContents.BASE_POTION_COLOR` (ARGB -13083194) as RGB.
pub const BASE_POTION_COLOR: u32 = 0x385DC6;
/// `PotionContents.getColor`: amplifier-weighted mix of visible effect colours.
pub fn potion_color(id: Option<&str>) -> u32 {
let Some(def) = id.and_then(potion) else {
return BASE_POTION_COLOR;
};
let (mut r, mut g, mut b, mut total) = (0u32, 0u32, 0u32, 0u32);
for (e, _, amp) in def.effects {
let Some(e) = effect(e) else { continue };
let w = *amp as u32 + 1;
r += w * (e.color >> 16 & 255);
g += w * (e.color >> 8 & 255);
b += w * (e.color & 255);
total += w;
}
match (
r.checked_div(total),
g.checked_div(total),
b.checked_div(total),
) {
(Some(r), Some(g), Some(b)) => (r << 16) | (g << 8) | b,
_ => BASE_POTION_COLOR,
}
}
@@ -54,6 +54,12 @@ pub struct Registries {
pub function_tags: Vec<String>,
/// The block the source is looking at (relevant coordinates).
pub target_block: Option<[i32; 3]>,
/// `ClientSuggestionProvider.getSelectedEntities`: the UUID of the
/// entity under the crosshair (`Entity.getStringUUID`), if any.
pub selected_entities: Vec<String>,
/// `ClientSuggestionProvider.getAbsoluteCoordinates`: the exact point of
/// that block hit, each coordinate as `%.2f` (`Locale.ROOT`).
pub target_point: Option<[String; 3]>,
/// Every registry's element and tag ids (`minecraft:item` -> ...), as
/// the server's registry access and reloadable registries hold them.
pub by_registry: BTreeMap<String, RegistryIds>,
+63 -4
View File
@@ -112,6 +112,7 @@ pub fn style_from_json(v: &Value) -> Style {
.and_then(|h| u32::from_str_radix(h, 16).ok())
});
}
s.shadow_color = o.get("shadow_color").and_then(component::shadow_color);
let flag = |k: &str| o.get(k).and_then(Value::as_bool);
s.bold = flag("bold");
s.italic = flag("italic");
@@ -662,10 +663,22 @@ pub fn name_shown(own: Option<&Team>, viewer: Option<&Team>, v: NameView) -> boo
}
!v.hud_hidden && !v.is_camera && visible && !v.is_vehicle
}
/// `ServerPlayer.die`: whether a player's death message reaches the player
/// themselves (the team broadcasts leave out the dying player).
pub fn own_death_message_shown(team: Option<&Team>) -> bool {
team.is_none_or(|t| t.death_message_visibility == Visibility::Always)
/// `ServerPlayer.die`: whether `other` hears the death message of `dying`
/// (`broadcastSystemMessage`, `broadcastSystemToTeam` or
/// `broadcastSystemToAllExceptTeam`, by the team's death message visibility).
pub fn death_message_reaches(board: &Scoreboard, dying: &str, other: &str) -> bool {
let Some(team) = board.players_team(dying) else {
return true;
};
let same_team = board
.players_team(other)
.is_some_and(|t| t.name == team.name);
match team.death_message_visibility {
Visibility::Always => true,
Visibility::Never => false,
Visibility::HideForOtherTeams => same_team && other != dying,
Visibility::HideForOwnTeam => !same_team,
}
}
// ---- objectives and scores ---------------------------------------------
@@ -1551,3 +1564,49 @@ mod tests {
);
}
}
/// `DamageSource.getLocalizedDeathMessage`: the cause's item with a custom
/// name gets the `<id>.item` message, with the item's name as a third
/// argument.
pub fn death_message_args(
key: &str,
victim: &Text,
cause: &Text,
item: Option<&Text>,
) -> (String, Vec<Text>) {
match item {
Some(item) => (
format!("{key}.item"),
vec![victim.clone(), cause.clone(), item.clone()],
),
None => (key.to_owned(), vec![victim.clone(), cause.clone()]),
}
}
#[cfg(test)]
mod death_message_tests {
use super::*;
#[test]
fn the_death_message_names_the_causes_item_when_it_has_a_custom_name() {
let victim = Text::literal("Steve");
let cause = Text::literal("Zombie");
// DamageSource.getLocalizedDeathMessage: the item variant, with the
// item's name as the third argument.
let (key, args) = death_message_args(
"death.attack.arrow",
&victim,
&cause,
Some(&Text::literal("My Sword")),
);
assert_eq!(key, "death.attack.arrow.item");
assert_eq!(args.len(), 3);
assert_eq!(args[0].plain(), "Steve");
assert_eq!(args[1].plain(), "Zombie");
assert_eq!(args[2].plain(), "My Sword");
// Without a custom name the message keeps its two arguments.
let (key, args) = death_message_args("death.attack.arrow", &victim, &cause, None);
assert_eq!(key, "death.attack.arrow");
assert_eq!(args.len(), 2);
}
}
+106 -27
View File
@@ -42,17 +42,50 @@ pub fn named_color(name: &str) -> Option<u32> {
/// Click actions carried by locally constructed text components.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ClickEvent {
/// `ClickEvent.OpenUrl`: an http or https link.
OpenUrl(String),
/// `ClickEvent.ChangePage`: a book's page, counted from 1.
ChangePage(i32),
SuggestCommand(String),
/// `ClickEvent.RunCommand`: the command, with or without its `/`.
RunCommand(String),
CopyToClipboard(String),
/// `ClickEvent.ShowDialog`: a `minecraft:dialog` registry id, or the
/// dialog itself as the component carried it.
ShowDialog(horizon_world::nbt::Tag),
/// A local file to open with the system handler.
/// Only allow this in locally constructed messages, never server/LAN text.
OpenFile(String),
}
/// A non-character `minecraft:object` embedded in a text component.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum InlineObject {
Player {
profile: Value,
hat: bool,
fallback: String,
},
Atlas {
atlas: String,
sprite: String,
fallback: String,
},
}
impl InlineObject {
pub fn fallback(&self) -> &str {
match self {
Self::Player { fallback, .. } | Self::Atlas { fallback, .. } => fallback,
}
}
}
/// Text style; `None` fields inherit from the enclosing style.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Style {
pub color: Option<u32>,
/// Text component `shadow_color` as packed ARGB. Zero suppresses shadows.
pub shadow_color: Option<u32>,
pub bold: Option<bool>,
pub italic: Option<bool>,
pub underlined: Option<bool>,
@@ -61,9 +94,15 @@ pub struct Style {
pub click: Option<ClickEvent>,
/// Hover text.
pub hover: Option<Box<Text>>,
/// `HoverEvent.ShowItem`: the event's `{id, count?, components?}` object.
pub hover_item: Option<Box<Value>>,
/// `Style.insertion`: the text a Shift-click puts in the chat box.
pub insertion: Option<String>,
/// The font (`FontDescription.Resource`), a full id like
/// `minecraft:alt`; the default font when unset.
pub font: Option<String>,
/// Content metadata for a single U+FFFC object span; never inherited.
pub inline_object: Option<InlineObject>,
}
/// A style's `font` field: an identifier (`alt` is `minecraft:alt`).
pub fn font_id(v: &Value) -> Option<String> {
@@ -79,6 +118,7 @@ impl Style {
/// Fill unset fields from `base` (Style.applyTo).
pub fn inherit(&mut self, base: &Style) {
self.color = self.color.or(base.color);
self.shadow_color = self.shadow_color.or(base.shadow_color);
self.bold = self.bold.or(base.bold);
self.italic = self.italic.or(base.italic);
self.underlined = self.underlined.or(base.underlined);
@@ -90,6 +130,12 @@ impl Style {
if self.hover.is_none() {
self.hover.clone_from(&base.hover);
}
if self.hover_item.is_none() {
self.hover_item.clone_from(&base.hover_item);
}
if self.insertion.is_none() {
self.insertion.clone_from(&base.insertion);
}
if self.font.is_none() {
self.font.clone_from(&base.font);
}
@@ -318,7 +364,13 @@ impl Text {
}
/// Component.getString: the plain characters.
pub fn plain(&self) -> String {
self.spans.iter().map(|s| s.text.as_str()).collect()
self.spans
.iter()
.map(|s| match &s.style.inline_object {
Some(object) if s.text == "\u{fffc}" => object.fallback(),
_ => s.text.as_str(),
})
.collect()
}
pub fn is_empty(&self) -> bool {
self.spans.iter().all(|s| s.text.is_empty())
@@ -384,31 +436,7 @@ impl Text {
continue;
}
let Some(code) = chars.next() else { break };
let code = code.to_ascii_lowercase();
let next = if let Some(c) = COLORS.iter().find(|c| c.0 == code) {
// Style.applyLegacyFormat: a colour clears the decorations.
Style {
color: Some(c.2),
bold: Some(false),
italic: Some(false),
underlined: Some(false),
strikethrough: Some(false),
obfuscated: Some(false),
..style.clone()
}
} else {
let mut s = style.clone();
match code {
'k' => s.obfuscated = Some(true),
'l' => s.bold = Some(true),
'm' => s.strikethrough = Some(true),
'n' => s.underlined = Some(true),
'o' => s.italic = Some(true),
'r' => s = span.style.clone(),
_ => {}
}
s
};
let next = legacy_format(&style, code, &span.style);
if next != style {
if !current.is_empty() {
out.spans.push(Span {
@@ -495,8 +523,9 @@ impl Text {
}
/// The style fields of a component's JSON.
const STYLE_KEYS: [&str; 9] = [
const STYLE_KEYS: [&str; 11] = [
"color",
"shadow_color",
"bold",
"italic",
"underlined",
@@ -504,6 +533,7 @@ const STYLE_KEYS: [&str; 9] = [
"obfuscated",
"click_event",
"hover_event",
"insertion",
"font",
];
/// Style.Serializer: the fields a style sets.
@@ -516,6 +546,9 @@ fn style_json(style: &Style) -> Vec<(String, Value)> {
.map_or_else(|| format!("#{c:06X}"), |n| n.1.to_owned());
out.push(("color".to_owned(), name.into()));
}
if let Some(shadow) = style.shadow_color {
out.push(("shadow_color".to_owned(), Value::from(shadow as i32)));
}
for (key, flag) in [
("bold", style.bold),
("italic", style.italic),
@@ -529,12 +562,22 @@ fn style_json(style: &Style) -> Vec<(String, Value)> {
}
if let Some(click) = &style.click {
let event = match click {
ClickEvent::OpenUrl(url) => json!({ "action": "open_url", "url": url }),
ClickEvent::ChangePage(page) => json!({ "action": "change_page", "page": page }),
ClickEvent::SuggestCommand(command) => {
json!({ "action": "suggest_command", "command": command })
}
ClickEvent::CopyToClipboard(value) => {
json!({ "action": "copy_to_clipboard", "value": value })
}
ClickEvent::RunCommand(command) => {
json!({ "action": "run_command", "command": command })
}
// ClickEvent.ShowDialog.CODEC: the dialog the server sent it with.
ClickEvent::ShowDialog(dialog) => json!({
"action": "show_dialog",
"dialog": crate::scoreboard::tag_to_json(dialog),
}),
ClickEvent::OpenFile(path) => json!({ "action": "open_file", "path": path }),
};
out.push(("click_event".to_owned(), event));
@@ -544,6 +587,13 @@ fn style_json(style: &Style) -> Vec<(String, Value)> {
"hover_event".to_owned(),
json!({ "action": "show_text", "value": hover.component() }),
));
} else if let Some(Value::Object(item)) = style.hover_item.as_deref() {
let mut event = item.clone();
event.insert("action".into(), "show_item".into());
out.push(("hover_event".to_owned(), Value::Object(event)));
}
if let Some(insertion) = &style.insertion {
out.push(("insertion".to_owned(), insertion.clone().into()));
}
if let Some(font) = &style.font {
out.push(("font".to_owned(), font.clone().into()));
@@ -551,6 +601,35 @@ fn style_json(style: &Style) -> Vec<(String, Value)> {
out
}
/// `Style.applyLegacyFormat` for the code after a `§`, `§r` going back to
/// `reset` (`StringDecomposer.iterateFormatted`); an unknown code changes nothing.
pub fn legacy_format(style: &Style, code: char, reset: &Style) -> Style {
let code = code.to_ascii_lowercase();
if let Some(c) = COLORS.iter().find(|c| c.0 == code) {
// A colour clears the decorations.
return Style {
color: Some(c.2),
bold: Some(false),
italic: Some(false),
underlined: Some(false),
strikethrough: Some(false),
obfuscated: Some(false),
..style.clone()
};
}
let mut s = style.clone();
match code {
'k' => s.obfuscated = Some(true),
'l' => s.bold = Some(true),
'm' => s.strikethrough = Some(true),
'n' => s.underlined = Some(true),
'o' => s.italic = Some(true),
'r' => s = reset.clone(),
_ => {}
}
s
}
/// TranslatableContents.decomposeTemplate: `%s` takes the next argument,
/// `%n$s` a numbered one and `%%` is a percent sign. Malformed templates are
/// shown verbatim.
+8
View File
@@ -0,0 +1,8 @@
//! Who an action came from or is aimed at: the player or a mob by id.
/// Who caused damage or is being targeted.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Who {
Player,
Mob(u32),
}
+23
View File
@@ -0,0 +1,23 @@
[package]
name = "horizon-client-input"
version = "0.1.0"
edition.workspace = true
license.workspace = true
rust-version = "1.90"
description = "The client's input layer: key bindings and their names, mouse, controllers, clipboard and window options"
[dependencies]
horizon-client-core = { path = "../horizon-client-core" }
winit = "0.30.13"
# The window icon.
horizon-theme = { path = "../horizon-theme" }
png = "0.18"
arboard = { version = "3.6.1", default-features = false, features = ["wayland-data-control"] }
# The DualSense's trigger effects (the `controllers` feature).
horizon-dualsense = { path = "../horizon-dualsense", optional = true }
[features]
controllers = ["dep:horizon-dualsense"]
[lints]
workspace = true
@@ -60,6 +60,19 @@ pub fn edit_shortcut(
})
}
/// Windows reports AltGr as Ctrl plus Alt: the system presses a left Control
/// in front of the right Alt.
pub const ALTGR_IS_CTRL_ALT: bool = cfg!(windows);
/// Whether a key's text reaches the screen (`KeyboardHandler.textInput`, then
/// `charTyped` per code point). The edit modifier's shortcuts type nothing,
/// but `textInput` takes no modifiers: with AltGr held (Ctrl plus Alt where
/// `altgr_is_ctrl_alt`) the layout's third-level characters are text, as `/`
/// on ABNT2, `@`, `{` and `[` on German or the Polish letters.
pub fn types_text(edit_modifier: bool, alt: bool, altgr_is_ctrl_alt: bool) -> bool {
!edit_modifier || (altgr_is_ctrl_alt && alt)
}
/// `StringUtil.isAllowedChatCharacter`.
pub fn is_allowed_chat_character(c: char) -> bool {
c != '§' && c >= ' ' && c != '\u{7f}'
@@ -303,6 +316,22 @@ pub fn append_limited(value: &mut String, text: &str, max_chars: usize) -> bool
mod tests {
use super::*;
#[test]
fn altgr_types_text_where_it_is_ctrl_alt() {
// Plain keys and Shift type.
assert!(types_text(false, false, true));
assert!(types_text(false, false, false));
// Ctrl (Cmd) shortcuts do not.
assert!(!types_text(true, false, true));
assert!(!types_text(true, false, false));
// AltGr on Windows is Ctrl+Alt: ABNT2 `/`, German `@` type.
assert!(types_text(true, true, true));
// Where AltGr is its own modifier, Ctrl+Alt is a shortcut.
assert!(!types_text(true, true, false));
// Alt alone (macOS Option, Linux AltGr) types.
assert!(types_text(false, true, false));
}
#[test]
fn shortcuts_need_the_edit_modifier_alone() {
let c = Some('c');
File renamed without changes.
@@ -145,6 +145,22 @@ const KEYS: &[(&str, KeyCode)] = &[
("key.keyboard.period", KeyCode::Period),
("key.keyboard.slash", KeyCode::Slash),
("key.keyboard.caps.lock", KeyCode::CapsLock),
("key.keyboard.world.1", KeyCode::IntlBackslash),
("key.keyboard.international1", KeyCode::IntlRo),
("key.keyboard.international3", KeyCode::IntlYen),
("key.keyboard.lang1", KeyCode::Lang1),
("key.keyboard.lang2", KeyCode::Lang2),
("key.keyboard.lang3", KeyCode::Lang3),
("key.keyboard.lang4", KeyCode::Lang4),
("key.keyboard.lang5", KeyCode::Lang5),
("key.keyboard.help", KeyCode::Help),
("key.keyboard.select", KeyCode::Select),
("key.keyboard.again", KeyCode::Again),
("key.keyboard.undo", KeyCode::Undo),
("key.keyboard.cut", KeyCode::Cut),
("key.keyboard.copy", KeyCode::Copy),
("key.keyboard.paste", KeyCode::Paste),
("key.keyboard.find", KeyCode::Find),
("key.keyboard.f1", KeyCode::F1),
("key.keyboard.f2", KeyCode::F2),
("key.keyboard.f3", KeyCode::F3),
@@ -157,6 +173,18 @@ const KEYS: &[(&str, KeyCode)] = &[
("key.keyboard.f10", KeyCode::F10),
("key.keyboard.f11", KeyCode::F11),
("key.keyboard.f12", KeyCode::F12),
("key.keyboard.f13", KeyCode::F13),
("key.keyboard.f14", KeyCode::F14),
("key.keyboard.f15", KeyCode::F15),
("key.keyboard.f16", KeyCode::F16),
("key.keyboard.f17", KeyCode::F17),
("key.keyboard.f18", KeyCode::F18),
("key.keyboard.f19", KeyCode::F19),
("key.keyboard.f20", KeyCode::F20),
("key.keyboard.f21", KeyCode::F21),
("key.keyboard.f22", KeyCode::F22),
("key.keyboard.f23", KeyCode::F23),
("key.keyboard.f24", KeyCode::F24),
("key.keyboard.print.screen", KeyCode::PrintScreen),
("key.keyboard.scroll.lock", KeyCode::ScrollLock),
("key.keyboard.pause", KeyCode::Pause),
@@ -188,6 +216,36 @@ const KEYS: &[(&str, KeyCode)] = &[
("key.keyboard.keypad.0", KeyCode::Numpad0),
("key.keyboard.keypad.period", KeyCode::NumpadDecimal),
("key.keyboard.keypad.equal", KeyCode::NumpadEqual),
("key.keyboard.keypad.comma", KeyCode::NumpadComma),
("key.keyboard.keypad.backspace", KeyCode::NumpadBackspace),
(
"key.keyboard.keypad.left.parenthesis",
KeyCode::NumpadParenLeft,
),
(
"key.keyboard.keypad.right.parenthesis",
KeyCode::NumpadParenRight,
),
("key.keyboard.keypad.hash", KeyCode::NumpadHash),
(
"key.keyboard.keypad.memory.store",
KeyCode::NumpadMemoryStore,
),
(
"key.keyboard.keypad.memory.recall",
KeyCode::NumpadMemoryRecall,
),
(
"key.keyboard.keypad.memory.clear",
KeyCode::NumpadMemoryClear,
),
("key.keyboard.keypad.memory.add", KeyCode::NumpadMemoryAdd),
(
"key.keyboard.keypad.memory.subtract",
KeyCode::NumpadMemorySubtract,
),
("key.keyboard.keypad.clear", KeyCode::NumpadClear),
("key.keyboard.keypad.clear.entry", KeyCode::NumpadClearEntry),
("key.keyboard.menu", KeyCode::ContextMenu),
("key.keyboard.left.control", KeyCode::ControlLeft),
("key.keyboard.left.shift", KeyCode::ShiftLeft),
@@ -242,9 +300,8 @@ impl Default for KeyBinds {
}
}
/// Each default key once, in order: two mappings that share a default key
/// (Advancements and the debug profiler both on L) are one press of it, not
/// two (the second would close the screen the first opened). Vanilla clicks
/// both mappings, but the profiler one only acts with F3 held.
/// (Pick Block and the spectator hotbar both on the middle button) are one
/// press of it, not two.
fn distinct<T: PartialEq>(keys: Vec<T>) -> Vec<T> {
let mut out = Vec::with_capacity(keys.len());
for k in keys {
@@ -310,6 +367,25 @@ impl KeyBinds {
.map(|m| m.0)
.filter(move |n| self.get(n) == key)
}
/// The mappings bound to `key` that [`KeyBinds::translate_all`] follows:
/// all but the debug keys. Those act only with F3 held
/// (`KeyboardHandler.handleDebugKeys`, `debug_keys.rs` reads the physical
/// key and the binding itself); their defaults (A, S, D, T, L, 1 to 4...)
/// would otherwise turn the key back into its old gameplay meaning.
fn translated_mappings<'a>(&'a self, key: &'a str) -> impl Iterator<Item = &'static str> + 'a {
MAPPINGS
.iter()
.filter(|m| m.2 != "debug")
.map(|m| m.0)
.filter(move |n| self.get(n) == key)
}
/// Whether a mapping (other than a debug key) whose default is `key` was
/// bound elsewhere.
fn default_moved(&self, key: &str) -> bool {
MAPPINGS
.iter()
.any(|(n, d, c)| *d == key && *c != "debug" && !self.is_default(n))
}
/// Translate a physical key into the default key of the mapping it is
/// bound to, so game code written against vanilla defaults follows the
/// player's bindings. `None` means the key now does nothing; mouse
@@ -326,7 +402,7 @@ impl KeyBinds {
return vec![Ok(key)];
};
let bound: Vec<_> = self
.mappings_for(name)
.translated_mappings(name)
.filter_map(|mapping| {
let default = Self::default_of(mapping);
match key_code(default) {
@@ -339,10 +415,7 @@ impl KeyBinds {
return distinct(bound);
}
// An unbound key whose default meaning was moved elsewhere is inert.
if MAPPINGS
.iter()
.any(|(n, d, _)| *d == name && !self.is_default(n))
{
if self.default_moved(name) {
return Vec::new();
}
vec![Ok(key)]
@@ -355,7 +428,7 @@ impl KeyBinds {
pub fn translate_mouse_all(&self, button: MouseButton) -> Vec<Result<KeyCode, MouseButton>> {
let name = mouse_name(button);
let bound: Vec<_> = self
.mappings_for(&name)
.translated_mappings(&name)
.filter_map(|mapping| {
let default = Self::default_of(mapping);
match mouse_button(default) {
@@ -367,10 +440,7 @@ impl KeyBinds {
if !bound.is_empty() {
return distinct(bound);
}
if MAPPINGS
.iter()
.any(|(n, d, _)| *d == name && !self.is_default(n))
{
if self.default_moved(&name) {
return Vec::new();
}
vec![Err(button)]
@@ -450,6 +520,29 @@ mod tests {
assert_eq!(k.conflicts("key.forward"), ["key.jump"]);
}
#[test]
fn a_moved_key_keeps_no_default_action() {
// #3158: the debug keys (F3+A, F3+1...) share these keys' defaults,
// and must not hand them back their gameplay meaning.
let mut k = KeyBinds::default();
k.set("key.hotbar.1", "key.keyboard.q");
assert_eq!(k.translate_all(KeyCode::Digit1), []);
assert_eq!(
k.translate_all(KeyCode::KeyQ),
[Ok(KeyCode::KeyQ), Ok(KeyCode::Digit1)]
);
k.set("key.inventory", "key.keyboard.a");
k.set("key.left", "key.keyboard.e");
assert_eq!(k.translate_all(KeyCode::KeyA), [Ok(KeyCode::KeyE)]);
assert_eq!(k.translate_all(KeyCode::KeyE), [Ok(KeyCode::KeyA)]);
k.set("key.back", "key.keyboard.down");
assert_eq!(k.translate_all(KeyCode::KeyS), []);
// Keys left alone still act, and a moved debug key changes nothing.
k.set("key.debug.fpsCharts", "key.keyboard.9");
assert_eq!(k.translate_all(KeyCode::Digit2), [Ok(KeyCode::Digit2)]);
assert_eq!(k.translate_all(KeyCode::Digit9), [Ok(KeyCode::Digit9)]);
assert_eq!(k.translate_all(KeyCode::F3), [Ok(KeyCode::F3)]);
}
#[test]
fn display_names() {
assert_eq!(display_name("key.keyboard.w").plain(), "W");
assert_eq!(display_name("key.keyboard.slash").plain(), "/");
+14
View File
@@ -0,0 +1,14 @@
//! The client's input layer, below gameplay and screens: key bindings and their
//! names, toggle keys, mouse settings, controllers, the clipboard and window
//! options. horizon-client re-exports each module under its old path
//! (`horizon_client::keybinds`, ...).
#![forbid(unsafe_code)]
pub mod clipboard;
pub mod gamepad;
pub mod input;
pub mod interaction_input;
pub mod key_names;
pub mod keybinds;
pub mod keymapping;
pub mod mouse;
pub mod window;
@@ -324,6 +324,24 @@ pub fn ease_in_out(t: f64) -> f64 {
0.5 - 0.5 * (std::f64::consts::PI * t).cos()
}
/// `MouseHandler.grabMouse` / `releaseMouse`: the mouse position is the
/// window's centre whenever the pointer's grab state changes, so a screen
/// opened from gameplay starts with the cursor in the middle. A release of
/// an already free pointer (or a grab of an already grabbed one) leaves the
/// position where it is, as vanilla does.
pub fn mouse_position_after_grab_change(
grab: bool,
was_grabbed: bool,
size: [u32; 2],
scale: f32,
) -> Option<(f32, f32)> {
if grab == was_grabbed {
None
} else {
Some((size[0] as f32 / 2.0 / scale, size[1] as f32 / 2.0 / scale))
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -485,4 +503,27 @@ mod tests {
assert_eq!(look.on_scroll(0.0, 0.4, false, 1.0), None);
assert_eq!(look.on_scroll(0.0, 0.7, false, 1.0), Some((1, (0, 1))));
}
#[test]
fn the_mouse_position_is_the_centre_when_the_grab_changes() {
// A grab of a free pointer and a release of a grabbed one both put
// the position at the window's centre (`MouseHandler.grabMouse`,
// `MouseHandler.releaseMouse`).
assert_eq!(
mouse_position_after_grab_change(true, false, [1920, 1080], 1.0),
Some((960.0, 540.0))
);
assert_eq!(
mouse_position_after_grab_change(false, true, [1920, 1080], 2.0),
Some((480.0, 270.0))
);
// Without a change of the grab state the position stays where it is.
assert_eq!(
mouse_position_after_grab_change(false, false, [1920, 1080], 1.0),
None
);
assert_eq!(
mouse_position_after_grab_change(true, true, [1920, 1080], 1.0),
None
);
}
}
+67
View File
@@ -0,0 +1,67 @@
[package]
name = "horizon-client-render"
version = "0.1.0"
edition.workspace = true
license.workspace = true
rust-version = "1.90"
description = "The client's world data, rendering and screens: chunks, meshing, light, streaming, the renderer, the HUD, menus and settings; no gameplay"
[dependencies]
horizon-assets = { path = "../horizon-assets" }
horizon-bench = { path = "../horizon-bench" }
horizon-client-core = { path = "../horizon-client-core" }
horizon-client-input = { path = "../horizon-client-input" }
horizon-command = { path = "../horizon-command" }
horizon-core = { path = "../horizon-core" }
horizon-hdr = { path = "../horizon-hdr" }
horizon-items = { path = "../horizon-items" }
horizon-protocol = { path = "../horizon-protocol" }
horizon-shader-api = { path = "../horizon-shader-api" }
horizon-shaders = { path = "../horizon-shaders" }
horizon-telemetry = { path = "../horizon-telemetry", default-features = false, features = ["live"] }
horizon-theme = { path = "../horizon-theme" }
horizon-threads = { path = "../horizon-threads" }
horizon-world = { path = "../horizon-world" }
ab_glyph = "0.2.32"
bytemuck = { version = "1.24", features = ["derive"] }
# Sound (ALSA on Linux); the `audio` feature.
cpal = { version = "0.16", optional = true }
chrono = { version = "0.4.42", default-features = false, features = ["clock", "std"] }
flate2 = { version = "1.1.10", default-features = false, features = ["zlib-rs", "runtime_detection"] }
glam = "0.30"
log = "0.4"
miniz_oxide = "0.8"
naga = { version = "30.0.1", features = ["wgsl-in", "hlsl-out"] }
png = "0.18"
pollster = "0.4"
serde = { version = "1", features = ["derive"] }
serde_json = { version = "1.0.151", features = ["raw_value"] }
sha2 = "0.10.9"
symphonia = { version = "0.5", default-features = false, features = ["ogg", "vorbis"] }
unicode-bidi = "0.3.18"
ureq = { version = "2", default-features = false, features = ["tls"] }
wgpu = { version = "30.0.1", default-features = false, features = ["std", "parking_lot", "vulkan", "metal", "wgsl", "naga-ir"] }
winit = "0.30.13"
zip = { version = "2", default-features = false, features = ["deflate"] }
[target.'cfg(unix)'.dependencies]
rustix = { version = "1", default-features = false, features = ["std", "time"] }
[target.'cfg(windows)'.dependencies]
wgpu = { version = "30.0.1", default-features = false, features = ["dx12"] }
[target.'cfg(not(target_os = "macos"))'.dependencies]
wgpu = { version = "30.0.1", default-features = false, features = ["gles"] }
[features]
# Sound output: horizon-client's `audio` feature.
audio = ["dep:cpal"]
# Helpers horizon-client's tests build their fixtures with.
test-support = []
[lints]
workspace = true
[dev-dependencies]
horizon-items = { path = "../horizon-items", features = ["test-support"] }
naga = { version = "30.0.1", features = ["wgsl-in", "hlsl-out", "msl-out", "spv-out"] }
@@ -0,0 +1,151 @@
//! Two offscreen frames presented in one window, without game resources or player data.
//! `cargo run --profile fast -p horizon-client-render --example compositor -- --smoke`
use horizon_client_render::gfx::compositor::Compositor;
use std::sync::Arc;
use winit::{
application::ApplicationHandler,
event::WindowEvent,
event_loop::{ActiveEventLoop, EventLoop},
window::{Window, WindowId},
};
struct Frames {
window: Arc<Window>,
compositor: Compositor,
textures: [wgpu::Texture; 2],
}
impl Frames {
fn new(event_loop: &ActiveEventLoop) -> horizon_client_render::Result<Self> {
let window = Arc::new(
event_loop.create_window(
Window::default_attributes()
.with_title("Compositor: red and blue views")
.with_inner_size(winit::dpi::PhysicalSize::new(640, 360)),
)?,
);
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_with_display_handle(
Box::new(window.clone()),
));
let surface = instance.create_surface(window.clone())?;
let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
compatible_surface: Some(&surface),
..Default::default()
}))?;
let (device, queue) = pollster::block_on(adapter.request_device(&Default::default()))?;
let textures = [[0, 0, 255, 255], [255, 0, 0, 255]].map(|pixel| {
let size = wgpu::Extent3d {
width: 320,
height: 360,
depth_or_array_layers: 1,
};
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("Offscreen view"),
size,
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Bgra8Unorm,
usage: wgpu::TextureUsages::COPY_SRC | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
queue.write_texture(
texture.as_image_copy(),
&pixel.repeat(320 * 360),
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(320 * 4),
rows_per_image: Some(360),
},
size,
);
texture
});
let compositor = Compositor::new(
surface,
&adapter,
&device,
&queue,
window.inner_size().into(),
)?;
Ok(Self {
window,
compositor,
textures,
})
}
}
#[derive(Default)]
struct Demo {
frames: Option<Frames>,
error: Option<String>,
presented: usize,
smoke: bool,
}
impl ApplicationHandler for Demo {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
if self.frames.is_none() {
match Frames::new(event_loop) {
Ok(frames) => self.frames = Some(frames),
Err(error) => {
self.error = Some(error.to_string());
event_loop.exit();
}
}
}
}
fn window_event(&mut self, event_loop: &ActiveEventLoop, _: WindowId, event: WindowEvent) {
let Some(f) = &mut self.frames else { return };
match event {
WindowEvent::CloseRequested => event_loop.exit(),
WindowEvent::Resized(size) => f.compositor.resize(size.width, size.height),
// Uncovered pixels are black; resize smaller to exercise clipping.
WindowEvent::RedrawRequested
if f.compositor
.present(&[(&f.textures[0], [0, 0]), (&f.textures[1], [320, 0])]) =>
{
self.presented += 1;
if self.smoke && self.presented == 10 {
f.compositor.set_vsync(false);
f.compositor.resize(0, 0);
assert!(!f.compositor.present(&[]));
f.compositor.resize(u32::MAX, u32::MAX);
assert!(!f.compositor.present(&[]));
f.compositor.resize(400, 240);
let _ = f
.window
.request_inner_size(winit::dpi::PhysicalSize::new(400, 240));
}
if self.smoke && self.presented == 20 {
println!("compositor: 20 frames, resize, suspension and clipping passed");
event_loop.exit();
}
}
_ => {}
}
}
fn about_to_wait(&mut self, _: &ActiveEventLoop) {
if let Some(f) = &self.frames {
f.window.request_redraw();
}
}
}
fn main() -> horizon_client_render::Result<()> {
let mut demo = Demo {
smoke: std::env::args().any(|a| a == "--smoke"),
..Default::default()
};
EventLoop::new()?.run_app(&mut demo)?;
if let Some(error) = demo.error {
return Err(error.into());
}
if demo.smoke && demo.presented < 20 {
return Err("smoke test closed early".into());
}
Ok(())
}
@@ -42,11 +42,11 @@ impl HeightmapState for StateFacts {
}
#[derive(Clone)]
pub struct Properties(Arc<Vec<StateFacts>>);
#[cfg(test)]
#[cfg(any(test, feature = "test-support"))]
impl Properties {
/// Heightmap facts for tests, per state: (air, has a fluid, blocks
/// motion, blocks motion but is not leaves). State 0 is minecraft:air.
pub(crate) fn for_tests(states: &[(bool, bool, bool, bool)]) -> Self {
pub fn for_tests(states: &[(bool, bool, bool, bool)]) -> Self {
Self(Arc::new(
states
.iter()
@@ -196,7 +196,7 @@ impl Assets {
impl RenderAssets {
pub fn load(root: &Path, assets: &Assets) -> Result<Self> {
let registry = assets.registry;
let mut sprites = crate::sprites::Sprites::default();
let mut sprites = crate::sprites::Sprites::with_disk_cache(root);
for name in MATERIALS {
sprites.insert(root, &format!("minecraft:block/{name}"))?;
}
@@ -217,7 +217,10 @@ impl RenderAssets {
for stage in 0..10 {
sprites.insert(root, &format!("minecraft:block/destroy_stage_{stage}"))?;
}
sprites.save_disk_cache();
sprites.finish();
// `finish` grows the buffer by doubling: keep only the bytes in use.
sprites.pixels.shrink_to_fit();
Ok(Self {
sprite_size: sprites.size,
sprite_minimum_size: sprites.minimum_size,
@@ -234,3 +237,81 @@ impl RenderAssets {
})
}
}
#[cfg(test)]
mod tests {
use crate::items::ConsumeEffect;
#[test]
fn always_edible_and_consume_effects_loaded() {
let root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../.local/horizon-client/26.3");
if !root.join("items.json").exists() {
return;
}
let assets = crate::assets::Assets::load(&root).unwrap();
let facts = crate::blocks::BlockFacts::load(&root, assets.registry).unwrap();
let items = crate::items::Items::load(&root, assets.registry, &facts).unwrap();
let apple = items.get("minecraft:golden_apple").expect("golden apple");
assert!(apple.can_always_eat);
assert!(apple.food.is_some());
assert!(!apple.consumable.as_ref().unwrap().effects.is_empty());
let enchanted = items
.get("minecraft:enchanted_golden_apple")
.expect("enchanted golden apple");
assert!(enchanted.can_always_eat);
assert!(enchanted.food.is_some());
assert!(!enchanted.consumable.as_ref().unwrap().effects.is_empty());
let beef = items.get("minecraft:cooked_beef").expect("cooked beef");
assert!(!beef.can_always_eat);
assert!(beef.food.is_some());
// Consumables without food, their style and their remainders.
use horizon_world::consumable::UseAnimation;
let milk = items.get("minecraft:milk_bucket").expect("milk bucket");
let drink = milk.consumable.as_ref().unwrap();
assert!(milk.food.is_none());
assert_eq!(drink.effects, [ConsumeEffect::ClearAllEffects]);
assert_eq!((drink.ticks(), drink.animation), (32, UseAnimation::Drink));
assert_eq!(drink.sound, "minecraft:entity.generic.drink");
assert!(!drink.particles);
assert_eq!(milk.use_remainder.as_deref(), Some("minecraft:bucket"));
let honey = items.get("minecraft:honey_bottle").expect("honey bottle");
let sip = honey.consumable.as_ref().unwrap();
assert_eq!(sip.ticks(), 40);
assert_eq!(sip.sound, "minecraft:item.honey_bottle.drink");
assert_eq!(
sip.effects,
[ConsumeEffect::RemoveEffects(vec![
"minecraft:poison".into()
])]
);
assert_eq!(
honey.use_remainder.as_deref(),
Some("minecraft:glass_bottle")
);
let kelp = items.get("minecraft:dried_kelp").unwrap();
let bite = kelp.consumable.as_ref().unwrap();
assert_eq!((bite.ticks(), bite.animation), (16, UseAnimation::Eat));
assert_eq!(bite.sound, "minecraft:entity.generic.eat");
assert!(bite.particles && kelp.use_remainder.is_none());
let chorus = items.get("minecraft:chorus_fruit").unwrap();
assert_eq!(
chorus.consumable.as_ref().unwrap().effects,
[ConsumeEffect::TeleportRandomly(
horizon_world::teleport_consume::TeleportConsume {
diameter: 16.0,
directional_particles: true,
}
)]
);
let chicken = items.get("minecraft:chicken").unwrap();
assert!(matches!(
chicken.consumable.as_ref().unwrap().effects[..],
[ConsumeEffect::ApplyEffects { probability, .. }] if probability == 0.3
));
}
}
@@ -3,8 +3,8 @@
//! `filter`, `unstitch` and `paletted_permutations` sources run in order
//! against the resource tree; a later source replaces an earlier sprite of
//! the same id and a filter removes the matching ones.
use crate::packs::pattern::Pattern;
use horizon_assets::palette::apply_palette;
use horizon_client_core::pattern::Pattern;
use serde_json::Value;
use std::{
collections::BTreeMap,
@@ -187,7 +187,14 @@ pub fn list_textures(root: &Path) -> Vec<String> {
};
for e in entries.flatten() {
let p = e.path();
if p.is_dir() {
// `file_type` comes with the listing; `Path::is_dir` is a stat per
// entry, kept for links (which it follows).
let dir = match e.file_type() {
Ok(t) if t.is_symlink() => p.is_dir(),
Ok(t) => t.is_dir(),
Err(_) => p.is_dir(),
};
if dir {
stack.push(p);
} else if p.extension().is_some_and(|x| x == "png")
&& let Ok(rel) = p.strip_prefix(&base)
@@ -144,6 +144,7 @@ type Stream = cpal::Stream;
type Stream = std::convert::Infallible;
pub struct Audio {
_stream: Option<Stream>,
device_name: String,
pub engine: SoundEngine,
backend: MixerBackend,
/// Record `Heard` sounds (the subtitle overlay is listening).
@@ -543,15 +544,48 @@ fn build_stream<T: cpal::SizedSample + cpal::FromSample<f32>>(
)?;
Ok(stream)
}
/// Available audio output device names.
#[cfg(feature = "audio")]
pub fn output_devices() -> Vec<String> {
if output_disabled(|name| std::env::var_os(name)).is_some() {
return Vec::new();
}
let host = cpal::default_host();
let mut names = Vec::new();
if let Ok(devices) = host.output_devices() {
for device in devices {
if let Ok(name) = device.name()
&& !names.contains(&name)
{
names.push(name);
}
}
}
names
}
#[cfg(not(feature = "audio"))]
pub fn output_devices() -> Vec<String> {
Vec::new()
}
impl Audio {
pub fn new(root: &Path) -> Self {
Self::with_output(root, true)
Self::with_device(root, "")
}
pub fn with_device(root: &Path, device_name: &str) -> Self {
Self::with_output(root, true, device_name)
}
/// No output device: the dedicated server.
pub fn silent(root: &Path) -> Self {
Self::with_output(root, false)
Self::with_output(root, false, "")
}
fn with_output(root: &Path, output: bool) -> Self {
/// The output device asked for (`soundDevice`), empty for the system's default; the
/// default plays when that one can't be opened, as in vanilla.
pub fn device(&self) -> &str {
&self.device_name
}
fn with_output(root: &Path, output: bool, target_device: &str) -> Self {
let registry = SoundRegistry::load(root);
if output && registry.is_empty() {
// The game then plays nothing on a working output (#1035, #1076).
@@ -564,7 +598,7 @@ impl Audio {
}
let mixer = Arc::new(Mutex::new(Mixer::default()));
let opened = if output {
Self::open(mixer.clone())
Self::open_named(mixer.clone(), target_device)
} else {
Err("no output on a dedicated server".into())
};
@@ -581,6 +615,7 @@ impl Audio {
engine.loaded = live;
Self {
_stream: stream,
device_name: target_device.to_owned(),
engine,
backend: MixerBackend {
mixer,
@@ -602,10 +637,61 @@ impl Audio {
}
}
#[cfg(not(feature = "audio"))]
fn open(_: Arc<Mutex<Mixer>>) -> Result<Stream> {
fn open_named(_: Arc<Mutex<Mixer>>, _: &str) -> Result<Stream> {
Err("this build has no sound output (built without the `audio` feature)".into())
}
#[cfg(feature = "audio")]
fn open_named(mixer: Arc<Mutex<Mixer>>, target: &str) -> Result<Stream> {
if let Some(reason) = output_disabled(|name| std::env::var_os(name)) {
return Err(reason.into());
}
let host = cpal::default_host();
if !target.is_empty()
&& let Ok(devices) = host.output_devices()
{
for device in devices {
let name = device.name().unwrap_or_else(|_| "unnamed device".into());
if name == target {
match open_device(&device, mixer.clone()) {
Ok((stream, config)) => {
log::info!(
"Horizon: audio output {name} ({} Hz, {} channels, {})",
config.sample_rate().0,
config.channels(),
config.sample_format()
);
return Ok(stream);
}
Err(e) => {
log::warn!(
"Horizon: could not open audio device '{target}': {e}; falling back to default"
);
}
}
break;
}
}
}
Self::open(mixer)
}
#[cfg(feature = "audio")]
pub fn set_device(&mut self, name: &str) -> Result<()> {
if !self.backend.live && self._stream.is_none() && !self.engine.loaded {
self.device_name = name.to_owned();
return Ok(());
}
self._stream = Some(Self::open_named(self.backend.mixer.clone(), name)?);
self.device_name = name.to_owned();
self.backend.live = true;
self.engine.loaded = true;
Ok(())
}
#[cfg(not(feature = "audio"))]
pub fn set_device(&mut self, name: &str) -> Result<()> {
self.device_name = name.to_owned();
Ok(())
}
#[cfg(feature = "audio")]
fn open(mixer: Arc<Mutex<Mixer>>) -> Result<Stream> {
if let Some(reason) = output_disabled(|name| std::env::var_os(name)) {
return Err(reason.into());
@@ -179,7 +179,7 @@ fn agent() -> ureq::Agent {
}
/// The discovery document, fetched once per game (a failure is retried next time).
fn discovery() -> Result<Arc<serde_json::Value>, JoinError> {
pub(crate) fn discovery() -> Result<Arc<serde_json::Value>, JoinError> {
static CACHE: Mutex<Option<Arc<serde_json::Value>>> = Mutex::new(None);
if let Some(d) = CACHE.lock().unwrap().clone() {
return Ok(d);
File renamed without changes.
@@ -20,12 +20,68 @@ pub struct SpawnEntry {
/// A `ConstantInt` count (sampling draws nothing).
pub constant: bool,
}
/// A `natural_mob_spawns` / `spawn_overrides` entry list.
pub fn parse_entries(list: &Value) -> Vec<SpawnEntry> {
list.as_array()
.into_iter()
.flatten()
.filter_map(|e| {
let count = &e["count"];
let (min, max, constant) = match count {
Value::Number(n) => {
let n = n.as_u64().unwrap_or(1) as u32;
(n, n, true)
}
c if c["type"].as_str() == Some("minecraft:constant") => {
let n = c["value"].as_u64().unwrap_or(1) as u32;
(n, n, true)
}
c => (
c["min_inclusive"].as_u64().unwrap_or(1) as u32,
c["max_inclusive"].as_u64().unwrap_or(1) as u32,
false,
),
};
Some(SpawnEntry {
entity: e["type"].as_str()?.to_owned(),
weight: e["weight"].as_u64().unwrap_or(1) as u32,
min,
max,
constant,
})
})
.collect()
}
/// `MobSpawnSettings.MobSpawnCost`.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct SpawnCost {
pub energy_budget: f64,
pub charge: f64,
}
/// The `spawn_costs` of a `natural_mob_spawns` value.
pub fn parse_costs(costs: &Value) -> HashMap<String, SpawnCost> {
costs
.as_object()
.into_iter()
.flatten()
.map(|(k, c)| {
(
k.clone(),
SpawnCost {
energy_budget: c["energy_budget"].as_f64().unwrap_or(0.0),
charge: c["charge"].as_f64().unwrap_or(0.0),
},
)
})
.collect()
}
#[derive(Clone, Debug)]
pub struct BiomeInfo {
/// `natural_mob_spawns` by category (creature, monster, ...).
pub spawns: HashMap<String, Vec<SpawnEntry>>,
/// `natural_mob_spawns` spawn costs by entity type.
pub spawn_costs: HashMap<String, crate::mobs::spawn_selection::SpawnCost>,
pub spawn_costs: HashMap<String, SpawnCost>,
/// The `gameplay/creature_world_gen_spawn_probability` attribute.
pub creature_probability: f32,
/// The `gameplay/increased_fire_burnout` attribute (jungles, swamps, peaks,
@@ -66,6 +122,13 @@ impl BiomeInfo {
}
.height_adjusted(pos, sea_level)
}
/// `Biome.getPrecipitationAt` in the Overworld (sea level 63): None
/// without precipitation, else whether it falls as snow
/// (`getHeightAdjustedTemperature` below 0.15).
pub fn snows_at(&self, x: i32, y: i32, z: i32) -> Option<bool> {
self.precipitation
.then(|| self.temperature_at(BlockPos::new(x, y, z), 63) < 0.15)
}
}
pub struct Biomes {
pub list: Vec<BiomeInfo>,
@@ -143,12 +206,12 @@ impl Biomes {
};
let categories = &natural["spawns_by_category"];
for (category, entries) in categories.as_object().into_iter().flatten() {
let list = crate::mobs::spawn_selection::parse_entries(entries);
let list = parse_entries(entries);
spawns.insert(category.clone(), list);
}
list.push(BiomeInfo {
spawns,
spawn_costs: crate::mobs::spawn_selection::parse_costs(&natural["spawn_costs"]),
spawn_costs: parse_costs(&natural["spawn_costs"]),
creature_probability:
attributes["minecraft:gameplay/creature_world_gen_spawn_probability"]
.as_f64()
@@ -193,6 +256,10 @@ impl Biomes {
.copied()
.unwrap_or(0)
}
/// Whether the game has the biome `name`.
pub fn contains(&self, name: &str) -> bool {
self.index.contains_key(name)
}
pub fn get(&self, id: u16) -> &BiomeInfo {
&self.list[usize::from(id).min(self.list.len() - 1)]
}
@@ -23,9 +23,10 @@
//! Which states tick is a table by state id, built by the game from the block
//! registry; whether a slot is made is decided when the slot is promoted at
//! the start of the next tick, from the block state then.
use crate::containers::BlockEntity;
use crate::chunk::block_entity::BlockEntity;
use crate::fast_hash::{FxHashMap, FxHashSet};
use std::collections::HashMap;
use std::sync::Arc;
pub type Pos = (i32, i32, i32);
@@ -46,7 +47,7 @@ pub enum Ticking {
pub struct BlockEntities {
map: HashMap<Pos, BlockEntity>,
/// `Level.blockEntityTickers`: positions and the slot each was given.
tickers: Vec<(Pos, u32)>,
tickers: Arc<Vec<(Pos, u32)>>,
/// `LevelChunk.tickersInLevel`: the live slot of each ticking position.
live: FxHashMap<Pos, u32>,
/// Positions that may need a slot, in the order vanilla would have made
@@ -63,6 +64,10 @@ pub struct BlockEntities {
/// Positions whose block entity `take` took and `put` has not brought
/// back.
out: FxHashSet<Pos>,
/// Positions a loaded chunk gave a new block entity over an old one: at
/// the next tick their slot stays only if their block state ticks
/// (`updateBlockEntityTicker`: rebind, or remove without a ticker).
replaced: FxHashSet<Pos>,
/// `Ticking` by block state id; empty until the game builds it.
states: Vec<Ticking>,
}
@@ -233,20 +238,25 @@ impl BlockEntities {
}
pub fn clear(&mut self) {
self.map.clear();
self.tickers.clear();
self.tickers = Arc::default();
self.live.clear();
self.added.clear();
self.pending.clear();
self.out.clear();
self.replaced.clear();
self.freed = false;
}
/// A loaded or generated chunk's block entities, in the order the chunk
/// lists them; they tick in its block entity map's order
/// (`registerAllBlockEntitiesAfterLevelLoad`).
/// (`registerAllBlockEntitiesAfterLevelLoad`). One replacing an entity
/// already at its position (`LevelChunk.setBlockEntity`) drops that one;
/// its slot is decided by the block state at the next tick.
pub fn load_chunk(&mut self, entities: Vec<(Pos, BlockEntity)>) {
let order = chunk_map_order(entities.iter().map(|(p, _)| *p));
for (p, e) in entities {
self.map.insert(p, e);
if self.map.insert(p, e).is_some() {
self.replaced.insert(p);
}
}
for p in order {
self.add(p);
@@ -274,7 +284,7 @@ impl BlockEntities {
/// and freed ones leave it. `state` reads the block state at a position.
/// Returns the slots to tick, in order; check each with `is_live` before
/// ticking it, as the ticks before it can free it.
pub fn tickers(&mut self, state: impl Fn(Pos) -> u32) -> Vec<(Pos, u32)> {
pub fn tickers(&mut self, state: impl Fn(Pos) -> u32) -> Arc<Vec<(Pos, u32)>> {
for (pos, token) in std::mem::take(&mut self.added) {
if self.pending.get(&pos) != Some(&token) {
continue;
@@ -288,21 +298,26 @@ impl BlockEntities {
}
let slot = self.next();
self.live.insert(pos, slot);
self.tickers.push((pos, slot));
Arc::make_mut(&mut self.tickers).push((pos, slot));
}
for pos in std::mem::take(&mut self.replaced) {
if self.live.contains_key(&pos) && self.ticking(state(pos)) != Ticking::Yes {
self.cancel(&pos);
}
}
if !std::mem::take(&mut self.freed) && self.out.is_empty() {
return self.tickers.clone();
return Arc::clone(&self.tickers);
}
self.out.clear();
let (live, map) = (&mut self.live, &self.map);
self.tickers.retain(|(p, slot)| {
Arc::make_mut(&mut self.tickers).retain(|(p, slot)| {
let mine = live.get(p) == Some(slot);
if mine && !map.contains_key(p) {
live.remove(p);
}
mine && map.contains_key(p)
});
self.tickers.clone()
Arc::clone(&self.tickers)
}
/// Whether a slot from `tickers` still ticks.
pub fn is_live(&self, (pos, slot): (Pos, u32)) -> bool {
@@ -329,8 +344,8 @@ mod tests {
}
fn order(b: &mut BlockEntities, states: &HashMap<Pos, u32>) -> Vec<Pos> {
b.tickers(|p| states.get(&p).copied().unwrap_or(0))
.into_iter()
.map(|(p, _)| p)
.iter()
.map(|(p, _)| *p)
.collect()
}
@@ -389,4 +404,33 @@ mod tests {
let order = chunk_map_order(many.iter().copied().chain(many.iter().copied()));
assert_eq!(order.len(), 100);
}
/// A chunk loaded over a block entity already there: the new one replaces
/// it, and keeps the old slot only while its block state ticks.
#[test]
fn loaded_chunk_replaces_block_entity_by_state() {
let mut b = level();
let mut s = HashMap::new();
let (p, q) = ((0, 0, 0), (1, 0, 0));
for pos in [p, q] {
s.insert(pos, 1);
}
// Two chunks, so the order is the order they load in.
b.load_chunk(vec![(p, hopper())]);
b.load_chunk(vec![(q, hopper())]);
assert_eq!(order(&mut b, &s), [p, q]);
// Replaced where the state ticks: the slot stays in its place.
b.load_chunk(vec![(p, hopper())]);
assert_eq!(order(&mut b, &s), [p, q]);
// Replaced where the state has no ticker: the slot is freed, and the
// new block entity is the one in the map.
s.insert(p, 2);
b.load_chunk(vec![(p, BlockEntity::Chest(vec![None; 27]))]);
assert_eq!(order(&mut b, &s), [q]);
assert!(matches!(b.get(&p), Some(BlockEntity::Chest(items)) if items.len() == 27));
// Ticking again later appends it, as any new ticker does.
s.insert(p, 1);
b.state_changed(p, 2, 1);
assert_eq!(order(&mut b, &s), [q, p]);
}
}
@@ -435,7 +435,8 @@ pub struct BellTick {
pub ended: Option<(Vec<([f64; 3], i32)>, Vec<u64>)>,
}
fn closer_to_center_than(pos: [i32; 3], p: [f64; 3], distance: f64) -> bool {
/// `Vec3i.closerToCenterThan`: `p` is within `distance` of the block's centre.
pub fn closer_to_center_than(pos: [i32; 3], p: [f64; 3], distance: f64) -> bool {
let dx = f64::from(pos[0]) + 0.5 - p[0];
let dy = f64::from(pos[1]) + 0.5 - p[1];
let dz = f64::from(pos[2]) + 0.5 - p[2];
@@ -5,7 +5,11 @@
//! (`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.
use crate::{hud::Rect, mesher::Vertex, world::World};
use crate::{
hud::Rect,
mesher::{CULL_BACK, Vertex},
world::World,
};
use glam::{DVec3, Mat4, Quat, Vec3};
use horizon_core::geometry::{
Direction,
@@ -93,6 +97,16 @@ pub fn diffuse(normal: Vec3, lights: (Vec3, Vec3)) -> f32 {
pub fn vertex_light(packed: u8) -> u32 {
(u32::from(packed >> 4) * 16) | ((u32::from(packed & 15) * 16) << 8)
}
/// `ChestRenderer.xmasTextures`: `SpecialDates.isExtendedChristmas` (24 to 26
/// December in the local time zone), read once when the renderer is created.
fn extended_christmas() -> bool {
use chrono::Datelike;
static XMAS: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
*XMAS.get_or_init(|| {
let now = chrono::Local::now();
now.month() == 12 && (24..=26).contains(&now.day())
})
}
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)
@@ -149,6 +163,8 @@ pub fn draw_part(
layer: 0,
color,
light,
glint_uv: [0.0, 0.0],
});
}
}
@@ -581,6 +597,18 @@ fn first_line(line: &str, max_width: f32, advance: &dyn Fn(char) -> f32) -> Stri
line.to_owned()
}
/// `AbstractSignRenderer.isOutlineVisible`: the camera within 16 blocks of
/// the sign's centre.
fn sign_text_near(pos: [i32; 3], camera: DVec3) -> bool {
DVec3::new(
f64::from(pos[0]) + 0.5,
f64::from(pos[1]) + 0.5,
f64::from(pos[2]) + 0.5,
)
.distance_squared(camera)
< 256.0
}
/// `AbstractSignRenderer.submitSignText`: four centred lines, glowing text
/// full-bright with an 8x outline in the dark colour.
fn sign_text_calls(
@@ -626,10 +654,20 @@ fn sign_text_calls(
.collect()
}
/// How far the text of an outlined call sits in front of its outline, in
/// text units (`Font.DisplayMode.POLYGON_OFFSET`, see `draw_text`).
const TEXT_OVER_OUTLINE: f32 = 0.05;
/// Draws a text call with the font atlas: 8x8 glyph cells, the outline as
/// eight one-pixel offsets under the text.
///
/// `TextFeatureRenderer.renderText` draws the outline glyphs NORMAL and the
/// text over them POLYGON_OFFSET, so the coplanar text always wins the depth
/// test. Here both share one mesh, so the text is lifted a hair towards the
/// reader instead (a twentieth of a font pixel, invisible) rather than
/// z-fighting with the outline under it.
fn draw_text(scene: &Scene, call: &TextCall, out: &mut Vec<Vertex>) {
let mut emit = |dx: f32, dy: f32, argb: u32| {
let mut emit = |dx: f32, dy: f32, z: f32, argb: u32| {
let mut x = call.x + dx;
let y0 = call.y + dy;
for ch in call.text.chars() {
@@ -644,12 +682,14 @@ fn draw_text(scene: &Scene, call: &TextCall, out: &mut Vec<Vertex>) {
normal: [0; 4],
pos: call
.matrix
.transform_point3(Vec3::new(cx, cy, 0.0))
.transform_point3(Vec3::new(cx, cy, z))
.to_array(),
uv,
layer: 0,
color: c,
light: call.light,
glint_uv: [0.0, 0.0],
});
}
}
@@ -667,10 +707,15 @@ fn draw_text(scene: &Scene, call: &TextCall, out: &mut Vec<Vertex>) {
(0.0, 1.0),
(1.0, 1.0),
] {
emit(dx, dy, call.outline);
emit(dx, dy, 0.0, call.outline);
}
}
emit(0.0, 0.0, call.color);
let z = if call.outline != 0 {
TEXT_OVER_OUTLINE
} else {
0.0
};
emit(0.0, 0.0, z, call.color);
}
/// Block-entity fields the level ticks, at one position (vanilla's own field
@@ -736,9 +781,12 @@ pub struct Scene<'a> {
/// The skin sprite a `player_head` shows at a position (`SkullBlockRenderer`
/// profile → skin): a downloaded `skins/<sha1>` slot or the profile's
/// `DefaultPlayerSkin` fallback; None keeps `SKIN_BY_TYPE` (`DefaultPlayerSkin.getDefaultTexture`, slim steve).
pub skull: &'a dyn Fn([i32; 3]) -> Option<&'a str>,
pub skull: &'a dyn Fn([i32; 3]) -> Option<String>,
/// Font glyph cell (none for blank glyphs such as the space) and advance.
pub glyph: &'a dyn Fn(char) -> (Option<Rect>, f32),
/// Changes whenever `glyph` may answer differently (`Font::epoch`): sign
/// text laid out under another value is laid out again.
pub glyphs: u64,
}
pub struct BlockEntityRenderer {
@@ -750,7 +798,30 @@ pub struct BlockEntityRenderer {
pot_patterns: HashMap<String, String>,
/// `Block.shouldRenderFace` for End gateway faces (its occlusion pair cache needs a lock).
visibility: Option<Mutex<FaceVisibility>>,
sign_texts: Mutex<SignTexts>,
}
/// Laid-out sign text by position. `AbstractSignRenderer` lays the text out
/// again each frame, glyph by glyph; its vertices only change with what is
/// in the key, so they are kept relative to the block and moved to the camera.
#[derive(Default)]
struct SignTexts {
signs: crate::fast_hash::FxHashMap<[i32; 3], SignTextEntry>,
/// Game time of the last sweep of signs out of view.
swept: i64,
}
struct SignTextEntry {
state: u32,
data: crate::signs::SignData,
packed: u8,
/// Within the glowing text outline's distance (only kept for glowing text).
near: bool,
glyphs: u64,
/// Game time it was last drawn.
used: i64,
vertices: Vec<Vertex>,
}
/// Game ticks a sign's text is kept after it was last drawn.
const SIGN_TEXT_KEEP: i64 = 100;
const MODELS: [(&str, &str); 27] = [
("chest", "minecraft:chest#main"),
("double_chest_left", "minecraft:double_chest_left#main"),
@@ -880,6 +951,7 @@ impl BlockEntityRenderer {
closed_chests,
pot_patterns: HashMap::new(),
visibility: None,
sign_texts: Mutex::default(),
}
}
fn kind(&self, state: u32) -> &Kind {
@@ -914,11 +986,81 @@ impl BlockEntityRenderer {
let origin = (DVec3::new(f64::from(pos[0]), f64::from(pos[1]), f64::from(pos[2]))
- scene.camera)
.as_vec3();
let base = Mat4::from_translation(origin);
let packed = scene.world.light(pos[0], pos[1], pos[2]);
if matches!(kind, Kind::Sign { .. }) && scene.texts.is_none() {
self.sign_text(scene, *pos, *state, origin, packed, out);
continue;
}
let base = Mat4::from_translation(origin);
self.render_one(scene, *pos, kind, base, packed, lights, false, true, out);
}
}
/// A sign's text from the cache, laid out again when its data, light,
/// state, outline or font changed.
fn sign_text(
&self,
scene: &Scene,
pos: [i32; 3],
state: u32,
origin: Vec3,
packed: u8,
out: &mut Vec<Vertex>,
) {
let Some(data) = (scene.sign)(pos) else {
return;
};
let near = (data.front.glowing || data.back.glowing) && sign_text_near(pos, scene.camera);
let Ok(mut cache) = self.sign_texts.lock() else {
return;
};
let now = scene.game_time;
if (now - cache.swept).abs() >= SIGN_TEXT_KEEP {
cache.swept = now;
cache
.signs
.retain(|_, e| (now - SIGN_TEXT_KEEP..=now).contains(&e.used));
}
let glyphs = scene.glyphs;
let signs = &mut cache.signs;
if !signs.get(&pos).is_some_and(|e| {
(e.state, e.packed, e.near, e.glyphs) == (state, packed, near, glyphs)
&& e.data == *data
}) {
let mut vertices = signs.remove(&pos).map_or_else(Vec::new, |e| e.vertices);
vertices.clear();
self.render_one(
scene,
pos,
self.kind(state),
Mat4::IDENTITY,
packed,
diffuse_lights(scene.nether),
false,
true,
&mut vertices,
);
signs.insert(
pos,
SignTextEntry {
state,
data: data.clone(),
packed,
near,
glyphs,
used: now,
vertices,
},
);
}
let Some(entry) = signs.get_mut(&pos) else {
return;
};
entry.used = now;
out.extend(entry.vertices.iter().map(|v| Vertex {
pos: (Vec3::from_array(v.pos) + origin).to_array(),
..*v
}));
}
/// One block entity at `pos` drawn under `base` with the given packed light
/// (the parity fixture's entry point).
pub fn render_block(
@@ -934,6 +1076,30 @@ impl BlockEntityRenderer {
let lights = diffuse_lights(scene.nether);
self.render_one(scene, pos, kind, base, packed, lights, false, false, out);
}
/// `TheEndGatewayBlockEntity.shouldRenderFace` for each face of the gateway
/// at `pos`: the faces its portal is drawn on.
pub fn gateway_faces(
&self,
world: &World,
state: BlockStateId,
pos: [i32; 3],
) -> Vec<Direction> {
Direction::ALL
.into_iter()
.filter(|&d| {
let [dx, dy, dz] = d.normal();
let neighbor = world.block(pos[0] + dx, pos[1] + dy, pos[2] + dz);
self.visibility.as_ref().is_none_or(|v| {
v.lock()
.map(|mut v| {
v.should_render_face(state, neighbor, d, true)
.unwrap_or(true)
})
.unwrap_or(true)
})
})
.collect()
}
/// Whether this block state has a block-entity renderer here.
pub fn renders(&self, state: u32) -> bool {
!matches!(self.kind(state), Kind::None)
@@ -1126,13 +1292,7 @@ impl BlockEntityRenderer {
m *= Mat4::from_translation(Vec3::new(0.0, 1.0 / 3.0, 0.046_666_667))
* Mat4::from_scale(s * 0.010_416_667);
}
let near = DVec3::new(
f64::from(pos[0]) + 0.5,
f64::from(pos[1]) + 0.5,
f64::from(pos[2]) + 0.5,
)
.distance_squared(scene.camera)
< 256.0;
let near = sign_text_near(pos, scene.camera);
let calls = sign_text_calls(scene, data, text, m, packed, near);
match scene.texts {
Some(sink) => sink.borrow_mut().extend(calls),
@@ -1141,11 +1301,25 @@ impl BlockEntityRenderer {
}
}
Kind::Chest {
block,
texture,
part,
y_rot,
..
} => {
// ChestRenderer.getChestMaterial: normal and trapped chests use the christmas
// sprites on 24-26 December. Copper and ender chests keep theirs.
let texture = if matches!(block.as_str(), "chest" | "trapped_chest")
&& extended_christmas()
{
match part {
ChestPart::Single => "entity/chest/christmas",
ChestPart::Left => "entity/chest/christmas_left",
ChestPart::Right => "entity/chest/christmas_right",
}
} else {
*texture
};
let model = match part {
ChestPart::Single => "chest",
ChestPart::Left => "double_chest_left",
@@ -1288,11 +1462,12 @@ impl BlockEntityRenderer {
};
// SkullBlockRenderer.getRenderType: the profile's skin when it
// resolves, `SKIN_BY_TYPE` (steve) otherwise.
let texture = if *kind == SkullType::Player {
(scene.skull)(pos).unwrap_or(texture)
let skin = if *kind == SkullType::Player {
(scene.skull)(pos)
} else {
texture
None
};
let texture = skin.as_deref().unwrap_or(texture);
let (Some(model), Some(paint)) =
(self.models.get(model), paint(texture, 0xFFFFFF, packed))
else {
@@ -1387,6 +1562,7 @@ impl BlockEntityRenderer {
m *= around(Mat4::from_rotation_y(turn * decay), pivot);
}
}
let first = out.len();
if let Some(paint) =
paint("entity/decorated_pot/decorated_pot_base", 0xFFFFFF, packed)
{
@@ -1404,6 +1580,11 @@ impl BlockEntityRenderer {
draw_part(part, side, m, &none, paint, out);
}
}
// `DecoratedPotRenderer` submits every part with `entitySolid`, which culls
// back faces: the flat `top` and `bottom` planes show one side each.
for vertex in &mut out[first..] {
vertex.normal = CULL_BACK;
}
}
Kind::EndPortal => {
// TheEndPortalRenderer.TRANSFORMATION: y from 6/16 to 12/16; only the
@@ -1434,22 +1615,8 @@ impl BlockEntityRenderer {
out,
);
}
// AbstractEndPortalRenderer.extractRenderState: TheEndGatewayBlockEntity.shouldRenderFace.
let faces: Vec<Direction> = Direction::ALL
.into_iter()
.filter(|&d| {
let [dx, dy, dz] = d.normal();
let neighbor = scene.world.block(pos[0] + dx, pos[1] + dy, pos[2] + dz);
self.visibility.as_ref().is_none_or(|v| {
v.lock()
.map(|mut v| {
v.should_render_face(BlockStateId(*state), neighbor, d, true)
.unwrap_or(true)
})
.unwrap_or(true)
})
})
.collect();
// AbstractEndPortalRenderer.extractRenderState.
let faces = self.gateway_faces(scene.world, BlockStateId(*state), pos);
portal_cube(base, &faces, END_GATEWAY_LAYER, out);
}
Kind::Conduit => {
@@ -1716,6 +1883,8 @@ impl BeamPart {
layer: 6,
color,
light: self.wrap,
glint_uv: [0.0, 0.0],
});
}
}
@@ -1786,6 +1955,7 @@ fn portal_cube(m: Mat4, faces: &[Direction], layer: u32, out: &mut Vec<Vertex>)
layer,
color: 0,
light: 0,
glint_uv: [0.0, 0.0],
});
}
}
@@ -1902,6 +2072,7 @@ mod tests {
banner: &|_| Vec::new(),
skull: &|_| None,
glyph: &|_| (None, 0.0),
glyphs: 0,
};
// Compare the cached world path with the original model renderer,
// including atlas replacement, current light and animated fallback.
@@ -2102,6 +2273,191 @@ mod tests {
assert_eq!(out[2].uv, [0.5, 0.5]);
}
/// TextFeatureRenderer.renderText: an outlined call's text is drawn over
/// its outline (Font.DisplayMode.POLYGON_OFFSET), never level with it,
/// and plain text stays on the sign's plane.
#[test]
fn outlined_sign_text_sits_in_front_of_its_outline() {
let world = World::new(BlockStateId(0), 1);
let sprites = HashMap::new();
let glyph = |_: char| {
let rect = Rect {
u0: 0.0,
v0: 0.0,
u1: 0.0625,
v1: 0.0625,
w: 8,
h: 8,
};
(Some(rect), 6.0)
};
let scene = Scene {
camera: DVec3::ZERO,
orientation: Quat::IDENTITY,
anim: &|_| BlockAnim::default(),
texts: None,
partial: 0.0,
game_time: 0,
max_y: 320,
world: &world,
sprites: &sprites,
nether: false,
lid: &|_| 0.0,
sign: &|_| None,
banner: &|_| Vec::new(),
skull: &|_| None,
glyph: &glyph,
glyphs: 0,
};
let mut call = TextCall {
matrix: Mat4::IDENTITY,
x: 0.0,
y: 0.0,
text: "Hi".into(),
color: 0xFFFF_FF00,
outline: 0xFF33_3300,
light: 240 | 240 << 8,
};
let mut out = Vec::new();
draw_text(&scene, &call, &mut out);
// Eight outline copies, then the text, four corners a glyph.
assert_eq!(out.len(), 9 * 2 * 4);
let (outline, text) = out.split_at(8 * 2 * 4);
let front = outline.iter().map(|v| v.pos[2]).fold(f32::MIN, f32::max);
assert!(
text.iter().all(|v| v.pos[2] > front),
"text z {:?} not in front of the outline at {front}",
text.iter().map(|v| v.pos[2]).collect::<Vec<_>>()
);
call.outline = 0;
out.clear();
draw_text(&scene, &call, &mut out);
assert_eq!(out.len(), 2 * 4);
assert!(out.iter().all(|v| v.pos[2] == 0.0));
}
#[test]
fn cached_sign_text_matches_and_follows_changes() {
let renderer = BlockEntityRenderer {
models: HashMap::new(),
kinds: vec![
Kind::Sign {
hanging: false,
wall: false,
angle: 180.0,
},
Kind::Sign {
hanging: true,
wall: true,
angle: 90.0,
},
],
closed_chests: Default::default(),
pot_patterns: HashMap::new(),
visibility: None,
sign_texts: Mutex::default(),
};
let world = World::new(BlockStateId(0), 1);
let sprites = HashMap::new();
let text = |line: &str, glowing| crate::signs::SignText {
lines: [line.into(), "two".into(), String::new(), "four".into()],
color: 3,
glowing,
};
let first_text = crate::signs::SignData {
front: text("one", false),
..Default::default()
};
let changed = crate::signs::SignData {
front: text("changed", false),
..Default::default()
};
let glowing = crate::signs::SignData {
back: text("glow", true),
..changed.clone()
};
let wide = std::cell::Cell::new(6.0);
let glyph = |c: char| {
let u = f32::from(c as u8 % 16) / 16.0;
let rect = Rect {
u0: u,
v0: 0.0,
u1: u + 0.0625,
v1: 0.0625,
w: 8,
h: 8,
};
((c != ' ').then_some(rect), wide.get())
};
let (first_sign, changed_sign, glowing_sign) = (
|_| Some(&first_text),
|_| Some(&changed),
|_| Some(&glowing),
);
let mut scene = Scene {
camera: DVec3::new(3.25, 70.5, -4.75),
orientation: Quat::IDENTITY,
anim: &|_| BlockAnim::default(),
texts: None,
partial: 0.0,
game_time: 0,
max_y: 320,
world: &world,
sprites: &sprites,
nether: false,
lid: &|_| 0.0,
sign: &first_sign,
banner: &|_| Vec::new(),
skull: &|_| None,
glyph: &glyph,
glyphs: 0,
};
let check = |scene: &Scene, pos: [i32; 3], state: u32| {
let mut cached = Vec::new();
renderer.render(scene, [(pos, state)].iter(), &mut cached);
let corner = DVec3::new(f64::from(pos[0]), f64::from(pos[1]), f64::from(pos[2]));
let mut original = Vec::new();
renderer.render_one(
scene,
pos,
renderer.kind(state),
Mat4::from_translation((corner - scene.camera).as_vec3()),
world.light(pos[0], pos[1], pos[2]),
diffuse_lights(false),
false,
true,
&mut original,
);
assert!(!cached.is_empty());
assert_eq!(cached.len(), original.len());
for (a, b) in cached.iter().zip(&original) {
assert!((Vec3::from_array(a.pos) - Vec3::from_array(b.pos)).length() < 1e-5);
assert_eq!((a.uv, a.color, a.light), (b.uv, b.color, b.light));
}
cached.len()
};
let (pos, other) = ([1, 70, 2], [-3, 64, 9]);
let first = check(&scene, pos, 0);
// The camera moves: the same text, moved.
scene.camera.x += 0.5;
check(&scene, pos, 0);
// New text, glowing text near and far, another state, other glyphs.
scene.sign = &changed_sign;
assert_ne!(check(&scene, pos, 0), first);
scene.sign = &glowing_sign;
check(&scene, pos, 0);
scene.camera.z -= 20.0;
check(&scene, pos, 0);
check(&scene, pos, 1);
check(&scene, other, 1);
wide.set(5.0);
scene.glyphs = 1;
check(&scene, pos, 1);
// Signs out of view for a while are dropped.
scene.game_time = 2 * SIGN_TEXT_KEEP;
check(&scene, pos, 0);
assert_eq!(renderer.sign_texts.lock().unwrap().signs.len(), 1);
}
#[test]
fn first_line_wraps_on_bytes_without_splitting_a_char() {
let advance = |c: char| if c == ' ' { 4.0 } else { 6.0 };
@@ -2110,4 +2466,71 @@ mod tests {
assert_eq!(first_line("\u{6771}", 3.0, &advance), "\u{6771}");
assert_eq!(first_line("ab\ncd", 100.0, &advance), "ab");
}
/// `DecoratedPotRenderer` submits every part with `RenderTypes.entitySolid`,
/// whose pipeline culls back faces. The pot's `top` and `bottom` are flat
/// boxes, so without that cull both sides of each plane are drawn and they
/// z-fight: the pot renders wrong.
#[test]
fn the_decorated_pot_culls_its_back_faces() {
let root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../.local/horizon-client")
.join(horizon_core::VANILLA_VERSION);
if !root.join("entity-models.json").exists() {
return;
}
let assets = crate::assets::Assets::load(&root).unwrap();
let registry = assets.registry;
let renderer = BlockEntityRenderer::load(&root, registry).unwrap();
let state = registry.default_state("minecraft:decorated_pot").unwrap();
let sprites: HashMap<String, Rect> = [
"entity/decorated_pot/decorated_pot_base",
"entity/decorated_pot/decorated_pot_side",
]
.into_iter()
.map(|name| {
(
name.to_owned(),
Rect {
u0: 0.25,
v0: 0.125,
u1: 0.375,
v1: 0.25,
w: 16,
h: 16,
},
)
})
.collect();
let world = World::new(BlockStateId(0), 1);
let scene = Scene {
camera: DVec3::ZERO,
orientation: Quat::IDENTITY,
anim: &|_| BlockAnim::default(),
texts: None,
partial: 0.5,
game_time: 0,
max_y: 320,
world: &world,
sprites: &sprites,
nether: false,
lid: &|_| 0.0,
sign: &|_| None,
banner: &|_| Vec::new(),
skull: &|_| None,
glyph: &|_| (None, 0.0),
glyphs: 0,
};
let mut verts = Vec::new();
renderer.render_block(&scene, [0; 3], state.0, Mat4::IDENTITY, 0xff, &mut verts);
// Vanilla submits 28 quads for a plain pot, and the cull keeps them all.
assert_eq!(
verts.len() / 4,
28,
"the pot submits all of vanilla's quads"
);
assert!(
verts.iter().all(|v| v.normal == CULL_BACK),
"entitySolid culls the pot's back faces"
);
}
}
@@ -49,6 +49,8 @@ pub fn geometry(
color: 0xffff_ffff,
light: stage_light(stage),
normal: [0; 4],
glint_uv: [0.0, 0.0],
});
}
}
@@ -2,270 +2,31 @@
//! in vanilla, ported as tables), data-driven `minecraft:brewing` recipes from
//! `data/minecraft/recipe/brewing`, potion colours and tooltips, and
//! `BrewingStandBlockEntity.serverTick`.
pub use crate::chunk::block_entity::BrewingStand;
use crate::{
containers::Providers,
items::{ItemStack, Items},
providers::Providers,
};
use serde_json::Value;
use std::{
collections::{HashMap, HashSet},
fs,
path::Path,
sync::{Arc, OnceLock},
};
use std::{collections::HashSet, fs, path::Path};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Category {
Beneficial,
Harmful,
Neutral,
}
/// A `MobEffect`: id (without namespace), colour, category, instantaneous.
#[derive(Clone, Copy, Debug)]
pub struct EffectDef {
pub id: &'static str,
pub color: u32,
pub category: Category,
pub instant: bool,
}
const fn fx(id: &'static str, color: u32, category: Category) -> EffectDef {
EffectDef {
id,
color,
category,
instant: false,
}
}
use Category::{Beneficial as B, Harmful as H, Neutral as N};
/// `MobEffects` in registration order.
pub const EFFECTS: &[EffectDef] = &[
fx("speed", 3402751, B),
fx("slowness", 9154528, H),
fx("haste", 14270531, B),
fx("mining_fatigue", 4866583, H),
fx("strength", 16762624, B),
EffectDef {
id: "instant_health",
color: 16262179,
category: B,
instant: true,
},
EffectDef {
id: "instant_damage",
color: 11101546,
category: H,
instant: true,
},
fx("jump_boost", 16646020, B),
fx("nausea", 5578058, H),
fx("regeneration", 13458603, B),
fx("resistance", 9520880, B),
fx("fire_resistance", 16750848, B),
fx("water_breathing", 10017472, B),
fx("invisibility", 16185078, B),
fx("blindness", 2039587, H),
fx("night_vision", 12779366, B),
fx("hunger", 5797459, H),
fx("weakness", 4738376, H),
fx("poison", 8889187, H),
fx("wither", 7561558, H),
fx("health_boost", 16284963, B),
fx("absorption", 2445989, B),
EffectDef {
id: "saturation",
color: 16262179,
category: B,
instant: true,
},
fx("glowing", 9740385, N),
fx("levitation", 13565951, H),
fx("luck", 5882118, B),
fx("unluck", 12624973, H),
fx("slow_falling", 15978425, B),
fx("conduit_power", 1950417, B),
fx("dolphins_grace", 8954814, B),
fx("bad_omen", 745784, N),
fx("hero_of_the_village", 4521796, B),
fx("darkness", 2696993, H),
fx("trial_omen", 1484454, N),
fx("raid_omen", 14565464, N),
fx("wind_charged", 12438015, H),
fx("weaving", 7891290, H),
fx("oozing", 10092451, H),
fx("infested", 9214860, H),
fx("breath_of_the_nautilus", 65518, B),
];
pub fn effect(id: &str) -> Option<&'static EffectDef> {
let id = id.trim_start_matches("minecraft:");
EFFECTS.iter().find(|e| e.id == id)
}
/// Namespaced effect ids for mob effect storage.
pub fn namespaced(id: &'static str) -> &'static str {
static IDS: OnceLock<HashMap<&'static str, &'static str>> = OnceLock::new();
IDS.get_or_init(|| {
EFFECTS
.iter()
.map(|e| {
(
e.id,
&*Box::leak(format!("minecraft:{}", e.id).into_boxed_str()),
)
})
.collect()
})
.get(id)
.copied()
.unwrap_or(id)
}
/// A `Potion`: id, name suffix (`item.minecraft.potion.effect.<name>`) and
/// effects as (effect, duration ticks, amplifier).
#[derive(Clone, Copy, Debug)]
pub struct PotionDef {
pub id: &'static str,
pub name: &'static str,
pub effects: &'static [(&'static str, i32, i32)],
}
const fn p(
id: &'static str,
name: &'static str,
effects: &'static [(&'static str, i32, i32)],
) -> PotionDef {
PotionDef { id, name, effects }
}
/// `Potions` in registration order.
pub const POTIONS: &[PotionDef] = &[
p("water", "water", &[]),
p("mundane", "mundane", &[]),
p("thick", "thick", &[]),
p("awkward", "awkward", &[]),
p("night_vision", "night_vision", &[("night_vision", 3600, 0)]),
p(
"long_night_vision",
"night_vision",
&[("night_vision", 9600, 0)],
),
p("invisibility", "invisibility", &[("invisibility", 3600, 0)]),
p(
"long_invisibility",
"invisibility",
&[("invisibility", 9600, 0)],
),
p("leaping", "leaping", &[("jump_boost", 3600, 0)]),
p("long_leaping", "leaping", &[("jump_boost", 9600, 0)]),
p("strong_leaping", "leaping", &[("jump_boost", 1800, 1)]),
p(
"fire_resistance",
"fire_resistance",
&[("fire_resistance", 3600, 0)],
),
p(
"long_fire_resistance",
"fire_resistance",
&[("fire_resistance", 9600, 0)],
),
p("swiftness", "swiftness", &[("speed", 3600, 0)]),
p("long_swiftness", "swiftness", &[("speed", 9600, 0)]),
p("strong_swiftness", "swiftness", &[("speed", 1800, 1)]),
p("slowness", "slowness", &[("slowness", 1800, 0)]),
p("long_slowness", "slowness", &[("slowness", 4800, 0)]),
p("strong_slowness", "slowness", &[("slowness", 400, 3)]),
p(
"turtle_master",
"turtle_master",
&[("slowness", 400, 3), ("resistance", 400, 2)],
),
p(
"long_turtle_master",
"turtle_master",
&[("slowness", 800, 3), ("resistance", 800, 2)],
),
p(
"strong_turtle_master",
"turtle_master",
&[("slowness", 400, 5), ("resistance", 400, 3)],
),
p(
"water_breathing",
"water_breathing",
&[("water_breathing", 3600, 0)],
),
p(
"long_water_breathing",
"water_breathing",
&[("water_breathing", 9600, 0)],
),
p("healing", "healing", &[("instant_health", 1, 0)]),
p("strong_healing", "healing", &[("instant_health", 1, 1)]),
p("harming", "harming", &[("instant_damage", 1, 0)]),
p("strong_harming", "harming", &[("instant_damage", 1, 1)]),
p("poison", "poison", &[("poison", 900, 0)]),
p("long_poison", "poison", &[("poison", 1800, 0)]),
p("strong_poison", "poison", &[("poison", 432, 1)]),
p("regeneration", "regeneration", &[("regeneration", 900, 0)]),
p(
"long_regeneration",
"regeneration",
&[("regeneration", 1800, 0)],
),
p(
"strong_regeneration",
"regeneration",
&[("regeneration", 450, 1)],
),
p("strength", "strength", &[("strength", 3600, 0)]),
p("long_strength", "strength", &[("strength", 9600, 0)]),
p("strong_strength", "strength", &[("strength", 1800, 1)]),
p("weakness", "weakness", &[("weakness", 1800, 0)]),
p("long_weakness", "weakness", &[("weakness", 4800, 0)]),
p("luck", "luck", &[("luck", 6000, 0)]),
p("slow_falling", "slow_falling", &[("slow_falling", 1800, 0)]),
p(
"long_slow_falling",
"slow_falling",
&[("slow_falling", 4800, 0)],
),
p("wind_charged", "wind_charged", &[("wind_charged", 3600, 0)]),
p("weaving", "weaving", &[("weaving", 3600, 0)]),
p("oozing", "oozing", &[("oozing", 3600, 0)]),
p("infested", "infested", &[("infested", 3600, 0)]),
];
pub fn potion(id: &str) -> Option<&'static PotionDef> {
let id = id.trim_start_matches("minecraft:");
POTIONS.iter().find(|p| p.id == id)
}
/// `PotionContents.BASE_POTION_COLOR` (ARGB -13083194) as RGB.
pub const BASE_POTION_COLOR: u32 = 0x385DC6;
/// `PotionContents.getColor`: amplifier-weighted mix of visible effect colours.
pub fn potion_color(id: Option<&str>) -> u32 {
let Some(def) = id.and_then(potion) else {
return BASE_POTION_COLOR;
};
let (mut r, mut g, mut b, mut total) = (0u32, 0u32, 0u32, 0u32);
for (e, _, amp) in def.effects {
let Some(e) = effect(e) else { continue };
let w = *amp as u32 + 1;
r += w * (e.color >> 16 & 255);
g += w * (e.color >> 8 & 255);
b += w * (e.color & 255);
total += w;
}
match (
r.checked_div(total),
g.checked_div(total),
b.checked_div(total),
) {
(Some(r), Some(g), Some(b)) => (r << 16) | (g << 8) | b,
_ => BASE_POTION_COLOR,
}
}
/// `MobEffectUtil.formatDuration` at 20 TPS: m:ss or h:mm:ss.
pub use horizon_client_core::potions::{
BASE_POTION_COLOR, Category, EFFECTS, EffectDef, POTIONS, PotionDef, effect, namespaced,
potion, potion_color,
};
/// `MobEffectUtil.formatDuration` at 20 TPS.
pub fn format_duration(ticks: i32) -> String {
let secs = ticks / 20;
format_tick_duration(ticks, 20.0)
}
/// `StringUtil.formatTickDuration`: mm:ss, or hh:mm:ss from an hour up, every
/// field two digits.
pub fn format_tick_duration(ticks: i32, tick_rate: f32) -> String {
let secs = (ticks as f32 / tick_rate).floor() as i32;
let (h, m, s) = (secs / 3600, secs / 60 % 60, secs % 60);
if h > 0 {
format!("{h}:{m:02}:{s:02}")
format!("{h:02}:{m:02}:{s:02}")
} else {
format!("{m}:{s:02}")
format!("{m:02}:{s:02}")
}
}
/// `item.minecraft.potion.effect.<name>` style translation key for a potion item.
@@ -289,20 +50,30 @@ pub fn tooltip(
Category::Neutral,
)];
}
def.effects
.iter()
effect_lines(def.effects.iter().copied(), scale, translate)
}
/// `PotionContents.addPotionTooltip`'s line per effect: its name, potency and
/// scaled duration, for (effect id, duration, amplifier) entries.
pub fn effect_lines<'a>(
effects: impl IntoIterator<Item = (&'a str, i32, i32)>,
scale: f32,
translate: &dyn Fn(&str) -> Option<String>,
) -> Vec<(String, Category)> {
effects
.into_iter()
.map(|(e, duration, amp)| {
let e = e.trim_start_matches("minecraft:");
let mut line =
translate(&format!("effect.minecraft.{e}")).unwrap_or_else(|| (*e).to_owned());
if *amp > 0 {
translate(&format!("effect.minecraft.{e}")).unwrap_or_else(|| e.to_owned());
if amp > 0 {
line.push(' ');
line.push_str(
&translate(&format!("potion.potency.{amp}"))
.unwrap_or_else(|| crate::enchant::level_name(*amp as u32 + 1)),
.unwrap_or_else(|| crate::enchant::level_name(amp as u32 + 1)),
);
}
let instant = effect(e).is_some_and(|d| d.instant);
let scaled = (*duration as f32 * scale) as i32;
let scaled = (duration as f32 * scale) as i32;
if !instant && scaled > 20 {
line = format!("{line} ({})", format_duration(scaled));
}
@@ -435,18 +206,6 @@ impl Brewing {
}
}
/// `BrewingStandBlockEntity` state. Slots: 0-2 bottles, 3 ingredient, 4 fuel.
#[derive(Clone, Debug, Default)]
pub struct BrewingStand {
pub items: [Option<ItemStack>; 5],
pub brew_time: i32,
pub total_brew_time: i32,
pub fuel: i32,
pub total_fuel: i32,
pub speed: f32,
pub ingredient: Option<Arc<str>>,
pub last_bottles: Option<[bool; 3]>,
}
/// What a brewing tick asks the world to do.
#[derive(Clone, Debug, Default, PartialEq)]
pub struct BrewTick {
@@ -573,10 +332,14 @@ mod tests {
potion_name_key("minecraft:splash_potion", Some("minecraft:long_swiftness")),
"item.minecraft.splash_potion.effect.swiftness"
);
assert_eq!(format_duration(9600), "8:00");
assert_eq!(format_duration(450), "0:22");
assert_eq!(format_duration(9600), "08:00");
assert_eq!(format_duration(450), "00:22");
assert_eq!(format_duration(140), "00:07");
assert_eq!(format_duration(1800), "01:30");
assert_eq!(format_duration(72000), "01:00:00");
assert_eq!(format_tick_duration(9600, 40.0), "04:00");
let t = tooltip(Some("minecraft:strong_poison"), 1.0, &|_| None);
assert_eq!(t[0].0, "poison II (0:21)");
assert_eq!(t[0].0, "poison II (00:21)");
assert_eq!(POTIONS.len(), 46);
}
@@ -633,4 +396,23 @@ mod tests {
stand.tick(&brewing, &items, &providers);
assert_eq!(stand.brew_time, 0);
}
/// #3312: `PotionContents.getPotionDescription` names the effects with
/// translatable components, so a Russian game reads them from ru_ru.json
/// (the prepared one; skipped without it), as the tooltip's other lines.
#[test]
fn effect_lines_read_the_selected_language() {
let root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../.local/horizon-client")
.join(horizon_core::VANILLA_VERSION);
if !root.join("assets/minecraft/lang/ru_ru.json").exists() {
return;
}
let ru = crate::lang::Language::load(&[&root], "ru_ru");
let translate = |key: &str| ru.get(key).map(str::to_owned);
let lines = tooltip(Some("minecraft:turtle_master"), 0.125, &translate);
assert_eq!(lines.len(), 2);
assert!(lines[0].0.starts_with("Замедление IV ("), "{lines:?}");
assert!(lines[1].0.starts_with("Сопротивление III ("), "{lines:?}");
}
}
@@ -653,18 +653,57 @@ pub fn modify_fov_based_on_death_or_fluid(
fov
}
/// `ClientAvatarState`: the walked distance and view bob of a player.
/// `ClientAvatarState`: the walked distance, view bob and cloak of a player.
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct AvatarBob {
pub walk_dist: f32,
pub walk_dist_o: f32,
pub bob: f32,
pub bob_o: f32,
/// `xCloak`, `yCloak`, `zCloak`: a point trailing the player, which the cape leans from.
pub cloak: [f64; 3],
pub cloak_o: [f64; 3],
}
impl AvatarBob {
/// `ClientAvatarState.tick` (at the start of `AbstractClientPlayer.tick`).
pub fn tick(&mut self) {
/// `ClientAvatarState.tick` (at the start of `AbstractClientPlayer.tick`)
/// with the player's position then: `moveCloak` eases the cloak a quarter
/// of the way there, or puts it there from 10 blocks away.
pub fn tick(&mut self, position: [f64; 3]) {
self.walk_dist_o = self.walk_dist;
self.cloak_o = self.cloak;
for i in 0..3 {
let d = position[i] - self.cloak[i];
if d > 10.0 || d < -10.0 {
self.cloak[i] = position[i];
self.cloak_o[i] = position[i];
} else {
self.cloak[i] += d * 0.25;
}
}
}
/// `AvatarRenderer.extractCapeState`: the cape's `capeFlap`, `capeLean`
/// and `capeLean2` (degrees) for the player at `position` (interpolated)
/// with its body turned `body_yaw` and its `fallFlyingScale`.
pub fn cape(
&self,
partial: f32,
position: [f64; 3],
body_yaw: f32,
fall_flying_scale: f32,
) -> [f32; 3] {
let d: [f64; 3] = std::array::from_fn(|i| {
lerp_d(f64::from(partial), self.cloak_o[i], self.cloak[i]) - position[i]
});
let rad = f64::from(body_yaw * DEG_TO_RAD);
let (sin, cos) = (f64::from(trig::sin(rad)), f64::from(-trig::cos(rad)));
let mut flap = (d[1] as f32 * 10.0).clamp(-6.0, 32.0);
let lean = ((d[0] * sin + d[2] * cos) as f32 * 100.0 * (1.0 - fall_flying_scale))
.clamp(0.0, 150.0);
let lean2 = ((d[0] * cos - d[2] * sin) as f32 * 100.0).clamp(-20.0, 20.0);
let bob = self.interpolated_bob(partial);
let walk = lerp(partial, self.walk_dist_o, self.walk_dist);
flap += trig::sin(f64::from(walk * 6.0)) * 32.0 * bob;
[flap, lean, lean2]
}
/// `LocalPlayer.move`: `addWalkedDistance(Mth.length(dx, dz) * 0.6F)`
/// with the float horizontal displacement.
@@ -772,8 +811,46 @@ pub fn bob_matrix(state: &BobState, damage_tilt_strength: f64, bob_view_option:
pose
}
/// The spinning effect `GameRenderer.renderLevel` multiplies into the level's
/// projection after the bob: a skew about a tilted axis, from a portal's or
/// Nausea's intensity (already scaled by Distortion Effects) and the angle in
/// degrees. The first-person hand keeps its own projection without it.
pub fn spinning_effect_matrix(intensity: f32, angle_degrees: f32) -> glam::Mat4 {
if intensity <= 0.0 {
return glam::Mat4::IDENTITY;
}
let mut skew = 5.0 / (intensity * intensity + 5.0) - intensity * 0.04;
skew *= skew;
let axis = glam::Vec3::new(
0.0,
std::f32::consts::SQRT_2 / 2.0,
std::f32::consts::SQRT_2 / 2.0,
);
let angle = angle_degrees * DEG_TO_RAD;
glam::Mat4::from_axis_angle(axis, angle)
* glam::Mat4::from_scale(glam::Vec3::new(1.0 / skew, 1.0, 1.0))
* glam::Mat4::from_axis_angle(axis, -angle)
}
#[cfg(test)]
mod tests {
/// `moveCloak` trails a quarter of the way, jumps from 10 blocks away; the
/// cape leans back as the player walks forward (south, body yaw 0).
#[test]
fn the_cloak_trails_the_player() {
let mut a = super::AvatarBob::default();
a.tick([0.0, 64.0, 0.0]);
assert_eq!((a.cloak, a.cloak_o), ([0.0, 64.0, 0.0], [0.0, 64.0, 0.0]));
a.tick([0.0, 64.0, 1.0]);
assert_eq!(a.cloak, [0.0, 64.0, 0.25]);
let [flap, lean, lean2] = a.cape(1.0, [0.0, 64.0, 1.0], 0.0, 0.0);
assert_eq!((flap, lean, lean2), (0.0, 75.0, 0.0));
// Falling lifts the flap, at most 32 degrees.
assert_eq!(a.cape(1.0, [0.0, 60.0, 0.25], 0.0, 0.0)[0], 32.0);
// Gliding takes the lean away.
assert_eq!(a.cape(1.0, [0.0, 64.0, 1.0], 0.0, 1.0)[1], 0.0);
}
#[test]
fn depth_far_covers_the_cloud_range() {
// renderDistance 2 with the default Cloud Distance (128 chunks).
@@ -16,6 +16,9 @@ use crate::{
#[path = "chat_tests.rs"]
mod tests;
/// `CommandHistory.PERSISTED_COMMANDS_FILE_NAME` and its size cap.
const COMMAND_HISTORY_FILE: &str = "command_history.txt";
const MAX_PERSISTED_COMMAND_HISTORY: usize = 50;
/// GuiMessageTag.systemSinglePlayer's indicator colour.
pub const SYSTEM_INDICATOR: u32 = 0xD0D0D0;
@@ -80,6 +83,10 @@ impl ChatAbilities {
ChatSource::Client => true,
}
}
/// `ChatAbilities.hasAnyRestrictions`: the Chat option takes something.
pub fn any_restriction(&self) -> bool {
*self != Self::new(ChatVisibility::Full)
}
/// Whether the chat screen may send `input` (`hasAllowedInput`).
pub fn allows(&self, input: &str) -> bool {
if input.starts_with('/') {
@@ -185,6 +192,10 @@ pub struct Chat {
lines: Vec<Line>,
/// Sent messages and commands (ChatComponent.recentChat).
pub recent: Vec<String>,
/// `CommandHistory`: the last commands, kept in `command_history.txt`
/// in the game folder once `load_history` has set it.
commands: Vec<String>,
history_file: Option<std::path::PathBuf>,
scroll: usize,
new_since_scroll: bool,
pub options: ChatOptions,
@@ -310,6 +321,17 @@ impl Chat {
self.lines.clear();
self.scroll = 0;
}
/// `new CommandHistory(gameFolder)`: the commands kept from earlier runs
/// start the chat's history (`ChatComponent`'s constructor).
pub fn load_history(&mut self, game_folder: &std::path::Path) {
let file = game_folder.join(COMMAND_HISTORY_FILE);
if let Ok(text) = std::fs::read_to_string(&file) {
self.commands = text.lines().map(str::to_owned).collect();
self.recent.extend(self.commands.iter().cloned());
}
self.history_file = Some(file);
}
/// `ChatComponent.addRecentChat`: a command also goes to the saved history.
pub fn add_recent(&mut self, message: &str) {
if self.recent.last().map(String::as_str) != Some(message) {
if self.recent.len() >= 100 {
@@ -317,6 +339,21 @@ impl Chat {
}
self.recent.push(message.to_owned());
}
if message.starts_with('/') && self.commands.last().map(String::as_str) != Some(message) {
if self.commands.len() >= MAX_PERSISTED_COMMAND_HISTORY {
self.commands.remove(0);
}
self.commands.push(message.to_owned());
if let Some(file) = &self.history_file {
let mut text = self.commands.join("\n");
text.push('\n');
if let Err(e) = std::fs::write(file, text) {
log::error!(
"Failed to write {COMMAND_HISTORY_FILE}, command history will be missing: {e}"
);
}
}
}
}
pub fn reset_scroll(&mut self) {
self.scroll = 0;
@@ -438,10 +475,71 @@ impl Chat {
out.push(GuiDraw::Pop);
out
}
/// Draws [`Chat::layout`] into the HUD batch.
/// Draws [`Chat::layout`] into the HUD batch, and with the chat screen
/// open the restricted prompt above the lines.
pub fn draw(&self, batch: &mut HudBatch, atlas: &HudAtlas, ticks: u64, focused: bool) {
let draws = self.layout(batch.height as i32, ticks, focused);
hud_layout::paint(batch, atlas, &draws);
if focused && self.abilities.any_restriction() {
let prompt = self.restricted_prompt(&atlas.font, batch.height as i32);
hud_layout::paint(batch, atlas, &prompt);
}
}
/// `ChatComponent.extractRenderState` for `FOREGROUND_RESTRICTED`: a
/// background row and the red, underlined "Chat is restricted" line in
/// the row above the messages, clipped to the chat's width.
fn restricted_prompt(&self, font: &crate::font::Font, height: i32) -> Vec<GuiDraw> {
let o = &self.options;
let scale = o.scale as f32;
let max_width = hud_layout::ceil(o.width_px() as f32 / scale);
let chat_bottom = hud_layout::floor((height - 40) as f32 / scale);
let entry_height = o.line_height();
let bottom_to_text =
(8.0 * (o.line_spacing + 1.0) - 4.0 * o.line_spacing + 0.5).floor() as i32;
let shown = self
.lines
.len()
.saturating_sub(self.scroll)
.min(self.lines_per_page(true));
let top = chat_bottom - (shown as i32 + 1) * entry_height;
let mut prompt = Text::translate("chat_screen.restricted", &[]).with_style(&Style {
color: Some(text::RED),
underlined: Some(true),
..Style::default()
});
// ComponentRenderUtils.clipText.
let mut width = 0.0;
let fits = prompt
.chars()
.take_while(|(c, style)| {
width += font.advance(*c, style);
width <= max_width as f32
})
.count();
if fits < prompt.chars().count() {
prompt = prompt.slice(0, fits);
}
vec![
GuiDraw::Push(vec![
PoseOp::Scale(scale, scale),
PoseOp::Translate(4.0, 0.0),
]),
hud_layout::fill(
-2,
top,
max_width + 4 + 4,
top + entry_height,
hud_layout::as8(o.background_opacity as f32) << 24,
),
GuiDraw::Text {
text: prompt,
x: 0,
y: top + entry_height - bottom_to_text - 1,
color: hud_layout::white(o.opacity as f32 * 0.9 + 0.1),
shadow: true,
},
GuiDraw::Pop,
]
}
/// The click action under a GUI-space cursor while chat is focused.
pub fn click_at(
@@ -458,26 +556,34 @@ impl Chat {
pub fn hover_at(&self, font: &crate::font::Font, height: i32, x: f64, y: f64) -> Option<Text> {
self.style_at(font, height, x, y)?.hover.map(|h| *h)
}
/// ChatComponent.getClickedComponentStyleAt.
fn style_at(
&self,
font: &crate::font::Font,
height: i32,
x: f64,
y: f64,
) -> Option<crate::text::Style> {
/// The wrapped line under a GUI-space cursor while chat is focused, and
/// the cursor's x within it (the text starts at 0).
fn line_at(&self, height: i32, x: f64, y: f64) -> Option<(&Line, f64)> {
let scale = self.options.scale as f32;
let mut x = x / f64::from(scale) - 4.0;
let x = x / f64::from(scale) - 4.0;
let bottom = hud_layout::floor((height - 40) as f32 / scale);
let from_bottom = f64::from(bottom) - y / f64::from(scale);
if x < 0.0 || x >= f64::from(self.max_width()) || from_bottom <= 0.0 {
if x >= f64::from(self.max_width()) || from_bottom <= 0.0 {
return None;
}
let row = (from_bottom / f64::from(self.options.line_height())).ceil() as usize - 1;
if row >= self.lines_per_page(true) {
return None;
}
let line = self.lines.get(self.scroll + row)?;
Some((self.lines.get(self.scroll + row)?, x))
}
/// ChatComponent.getClickedComponentStyleAt.
pub fn style_at(
&self,
font: &crate::font::Font,
height: i32,
x: f64,
y: f64,
) -> Option<crate::text::Style> {
let (line, mut x) = self.line_at(height, x, y)?;
if x < 0.0 {
return None;
}
let content = line.content.resolve_legacy();
for (c, style) in content.chars() {
x -= f64::from(font.advance(c, style));
@@ -487,6 +593,13 @@ impl Chat {
}
None
}
/// Whether the cursor is on a line's indicator bar while chat is focused
/// (`DrawingFocusedGraphicsAccess.handleTag`), whose tooltip is the
/// message tag's text.
pub fn indicator_at(&self, height: i32, x: f64, y: f64) -> bool {
self.line_at(height, x, y)
.is_some_and(|(line, x)| line.indicator.is_some() && (-4.0..-2.0).contains(&x))
}
/// Plain text of the newest messages (for tests and the control socket).
pub fn recent_lines(&self, n: usize) -> Vec<String> {
self.messages
@@ -540,6 +653,42 @@ pub struct Commands<'a> {
pub registries: &'a Registries,
}
/// `CommandSuggestions`' constructor arguments: how a screen's suggestions
/// parse and where they are drawn.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct SuggestLayout {
/// `commandsOnly`: the input is a command without a leading `/`.
pub commands_only: bool,
/// `onlyShowIfCursorPastError`: completions from the command's start,
/// not only past the `/`.
pub only_past_error: bool,
/// `suggestionLineLimit`.
pub line_limit: usize,
/// `anchorToBottom`: the list and usage over a box at the bottom, else
/// under one at the top (y 72).
pub anchor_to_bottom: bool,
/// `fillColor`: behind the list and the usage lines.
pub fill: i32,
}
impl SuggestLayout {
/// ChatScreen.init's `CommandSuggestions`.
pub const CHAT: Self = Self {
commands_only: false,
only_past_error: false,
line_limit: 10,
anchor_to_bottom: true,
fill: FILL,
};
/// AbstractCommandBlockEditScreen.init's `CommandSuggestions`.
pub const COMMAND_BLOCK: Self = Self {
commands_only: true,
only_past_error: true,
line_limit: 7,
anchor_to_bottom: false,
fill: i32::MIN,
};
}
/// The open chat screen: vanilla's `ChatScreen` with its `EditBox` and
/// `CommandSuggestions` (argument colouring, usage lines, parse errors,
/// the suggestion list, Tab completion, history).
@@ -567,6 +716,7 @@ pub struct ChatScreen {
pub auto_suggestions: bool,
/// `CommandSuggestions.setRestrictions`: what the Chat option allows.
pub abilities: ChatAbilities,
pub layout: SuggestLayout,
}
/// Local completions waiting for the server's reply to request `id`.
struct Awaiting {
@@ -637,6 +787,43 @@ impl ChatScreen {
keep_suggestions: false,
auto_suggestions: true,
abilities,
layout: SuggestLayout::CHAT,
};
s.update_command_info(m, c);
s
}
/// `AbstractCommandBlockEditScreen.init`: the command box at the top,
/// focused, holding `command`, with suggestions from the start.
pub fn command_block(
m: &Measure,
c: &Commands,
command: &str,
width: i32,
height: i32,
) -> Self {
let mut input = TextField::new(width / 2 - 150, 50, 300, 20);
input.max_length = 32500;
input.focused = true;
input.set_value(m, command);
let mut s = Self {
input,
width,
height,
history_buffer: String::new(),
history_pos: 0,
parse: None,
pending: None,
awaiting: None,
request: None,
suggestions: None,
usage: Vec::new(),
usage_position: 0,
usage_width: 0,
allow_suggestions: true,
keep_suggestions: false,
auto_suggestions: true,
abilities: ChatAbilities::new(ChatVisibility::Full),
layout: SuggestLayout::COMMAND_BLOCK,
};
s.update_command_info(m, c);
s
@@ -655,7 +842,11 @@ impl ChatScreen {
let value = self.input.value.clone();
let auto = self.auto_suggestions;
let abilities = self.abilities;
*self = Self::new(m, c, &value, self.history_pos, width, height, abilities);
*self = if self.layout == SuggestLayout::CHAT {
Self::new(m, c, &value, self.history_pos, width, height, abilities)
} else {
Self::command_block(m, c, &value, width, height)
};
self.auto_suggestions = auto;
}
}
@@ -701,11 +892,18 @@ impl ChatScreen {
self.usage.clear();
let cursor = self.input.cursor;
let cursor_b = byte_at(&command, cursor);
if command.starts_with('/') {
let slash = command.starts_with('/');
if self.layout.commands_only || slash {
let start = usize::from(slash);
if self.parse.is_none() {
self.parse = Some(c.dispatcher.parse_at(&command, 1, c.registries));
self.parse = Some(c.dispatcher.parse_at(&command, start, c.registries));
}
if cursor >= 1 && (self.suggestions.is_none() || !self.keep_suggestions) {
let from = if self.layout.only_past_error {
start
} else {
1
};
if cursor >= from && (self.suggestions.is_none() || !self.keep_suggestions) {
let parse = self.parse.as_ref().expect("parsed above");
let (range, list, ask) =
c.dispatcher
@@ -889,7 +1087,11 @@ impl ChatScreen {
0,
self.input.screen_x(m, 0) + self.input.inner_width() - width,
);
let y = self.height - 12;
let y = if self.layout.anchor_to_bottom {
self.height - 12
} else {
72
};
// sortSuggestions: those starting with the word at the cursor first.
let partial = &value[..byte_at(&value, self.input.cursor)];
let word = partial[last_word_index(partial)..].to_lowercase();
@@ -898,9 +1100,16 @@ impl ChatScreen {
s.text.starts_with(&word) || s.text.starts_with(&format!("minecraft:{word}"))
});
first.extend(rest);
let n = first.len().min(10) as i32;
// SuggestionsList: beside a bordered box, or over an unbordered one.
let n = first.len().min(self.layout.line_limit) as i32;
let bordered = i32::from(self.input.bordered);
let top = if self.layout.anchor_to_bottom {
y - 3 - n * 12
} else {
y - bordered
};
let mut l = SuggestionList {
rect: [x - 1, y - 3 - n * 12, width + 1, n * 12],
rect: [x - (1 - bordered), top, width + 1, n * 12],
original: value,
list: first,
offset: 0,
@@ -929,11 +1138,12 @@ impl ChatScreen {
}
fn cycle(&mut self, l: &mut SuggestionList, dir: isize) {
self.select(l, l.current as isize + dir);
let max = l.list.len().saturating_sub(10);
let limit = self.layout.line_limit;
let max = l.list.len().saturating_sub(limit);
if l.current < l.offset {
l.offset = l.current.min(max);
} else if l.current > l.offset + 9 {
l.offset = (l.current as isize + 1 - 10).clamp(0, max as isize) as usize;
} else if l.current >= l.offset + limit {
l.offset = (l.current as isize + 1 - limit as isize).clamp(0, max as isize) as usize;
}
}
/// `SuggestionsList.useSuggestion`.
@@ -961,7 +1171,57 @@ impl ChatScreen {
ctrl: bool,
) -> ChatAction {
use winit::keyboard::KeyCode as K;
// CommandSuggestions.keyPressed.
if self.suggestions_key(m, c, key, shift) {
return ChatAction::None;
}
match key {
K::Escape => return ChatAction::Close,
K::Enter | K::NumpadEnter => {
// ChatScreen.keyPressed: without `hasAllowedInput` the input
// stays, unsent, and the screen stays open.
if !self.abilities.allows(&self.input.value) {
return ChatAction::None;
}
return ChatAction::Submit(self.normalized());
}
K::ArrowUp => self.move_in_history(m, c, history, -1),
K::ArrowDown => self.move_in_history(m, c, history, 1),
_ => {
self.field_key(m, c, key, shift, ctrl);
}
}
ChatAction::None
}
/// The input box's own keys (`EditBox.keyPressed`), the suggestions
/// following an edit: whether the box took the key.
pub fn field_key(
&mut self,
m: &Measure,
c: &Commands,
key: winit::keyboard::KeyCode,
shift: bool,
ctrl: bool,
) -> bool {
match self.input.key(m, key, shift, ctrl) {
Some(changed) => {
if changed {
self.edited(m, c);
}
true
}
None => false,
}
}
/// `CommandSuggestions.keyPressed`: the list's arrows, Tab and Escape,
/// and Tab to show it. True when the suggestions took the key.
pub fn suggestions_key(
&mut self,
m: &Measure,
c: &Commands,
key: winit::keyboard::KeyCode,
shift: bool,
) -> bool {
use winit::keyboard::KeyCode as K;
if let Some(mut l) = self.suggestions.take() {
let handled = match key {
K::ArrowUp | K::ArrowDown => {
@@ -978,37 +1238,18 @@ impl ChatScreen {
}
K::Escape => {
self.input.suggestion = None;
return ChatAction::None;
return true;
}
_ => false,
};
self.suggestions = Some(l);
if handled {
return ChatAction::None;
}
handled
} else if key == K::Tab {
self.show_suggestions(m);
return ChatAction::None;
true
} else {
false
}
match key {
K::Escape => return ChatAction::Close,
K::Enter | K::NumpadEnter => {
// ChatScreen.keyPressed: without `hasAllowedInput` the input
// stays, unsent, and the screen stays open.
if !self.abilities.allows(&self.input.value) {
return ChatAction::None;
}
return ChatAction::Submit(self.normalized());
}
K::ArrowUp => self.move_in_history(m, c, history, -1),
K::ArrowDown => self.move_in_history(m, c, history, 1),
_ => {
if self.input.key(m, key, shift, ctrl) == Some(true) {
self.edited(m, c);
}
}
}
ChatAction::None
}
/// `ChatScreen.moveInHistory`.
fn move_in_history(&mut self, m: &Measure, c: &Commands, history: &[String], dir: isize) {
@@ -1062,7 +1303,7 @@ impl ChatScreen {
if !l.contains(mouse.0, mouse.1) {
return false;
}
let max = l.list.len().saturating_sub(10) as i32;
let max = l.list.len().saturating_sub(self.layout.line_limit) as i32;
l.offset = ((l.offset as f64 - amount.clamp(-1.0, 1.0)) as i32).clamp(0, max) as usize;
true
}
@@ -1133,7 +1374,9 @@ impl ChatScreen {
) -> Vec<Draw> {
let (w, h) = (self.width, self.height);
let mut out = Vec::new();
d::fill(&mut out, 2, h - 14, w - 2, h - 2, background);
if self.layout.anchor_to_bottom {
d::fill(&mut out, 2, h - 14, w - 2, h - 2, background);
}
let format = |s: &str, o: usize| self.format(s, o);
self.input.draw(&mut out, m, blink_ms, Some(&format));
if let Some(mut l) = self.suggestions.take() {
@@ -1141,14 +1384,18 @@ impl ChatScreen {
self.suggestions = Some(l);
} else {
for (i, line) in self.usage.iter().enumerate() {
let y = h - 27 - 12 * i as i32;
let y = if self.layout.anchor_to_bottom {
h - 27 - 12 * i as i32
} else {
72 + 12 * i as i32
};
d::fill(
&mut out,
self.usage_position - 1,
y,
self.usage_position + self.usage_width + 1,
y + 12,
FILL,
self.layout.fill,
);
d::text(&mut out, line.clone(), self.usage_position, y + 2, -1, true);
}
@@ -1162,7 +1409,7 @@ impl ChatScreen {
l: &mut SuggestionList,
mouse: (i32, i32),
) {
let limit = l.list.len().min(10);
let limit = l.list.len().min(self.layout.line_limit);
let [rx, ry, rw, rh] = l.rect;
let has_prev = l.offset > 0;
let has_next = l.list.len() > l.offset + limit;
@@ -1171,8 +1418,8 @@ impl ChatScreen {
l.last_mouse = mouse;
}
if has_prev || has_next {
d::fill(out, rx, ry - 1, rx + rw, ry, FILL);
d::fill(out, rx, ry + rh, rx + rw, ry + rh + 1, FILL);
d::fill(out, rx, ry - 1, rx + rw, ry, self.layout.fill);
d::fill(out, rx, ry + rh, rx + rw, ry + rh + 1, self.layout.fill);
if has_prev {
for x in (0..rw).step_by(2) {
d::fill(out, rx + x, ry - 1, rx + x + 1, ry, -1);
@@ -1187,7 +1434,7 @@ impl ChatScreen {
let mut hovered = false;
for i in 0..limit {
let y = ry + 12 * i as i32;
d::fill(out, rx, y, rx + rw, y + 12, FILL);
d::fill(out, rx, y, rx + rw, y + 12, self.layout.fill);
if mouse.0 > rx && mouse.0 < rx + rw && mouse.1 > y && mouse.1 < y + 12 {
if moved {
self.select(l, (i + l.offset) as isize);
@@ -2,9 +2,12 @@
//! threads can mesh and light immutable snapshots while the game thread edits
//! (copy-on-write through `Arc::make_mut`).
use horizon_core::paletted::{PalettedContainer, Strategy};
use horizon_world::nbt::{Compound, Tag};
use horizon_world::registry::BlockStateId;
use std::sync::{Arc, OnceLock};
pub mod block_entity;
pub const MIN_Y: i32 = -64;
pub const HEIGHT: i32 = 384;
pub const SECTIONS: usize = (HEIGHT / 16) as usize;
@@ -148,14 +151,14 @@ pub struct Chunk {
/// Newly generated (not loaded from a save): eligible for generation-time spawns.
pub fresh: bool,
/// Block entities read from a save, moved into the world on arrival.
pub block_entities: Vec<((i32, i32, i32), crate::containers::BlockEntity)>,
pub block_entities: Vec<((i32, i32, i32), block_entity::BlockEntity)>,
/// `CustomName` of the container block entities (shown as the screen title).
pub block_names: Vec<((i32, i32, i32), String)>,
/// Generation postprocessing marks (ProtoChunk.postProcessing) as world
/// positions, applied once by LevelChunk.postProcessGeneration.
pub postprocess: Vec<[i32; 3]>,
/// Saved data Horizon keeps but does not simulate (save.rs).
pub saved: Option<Arc<crate::save::ChunkExtras>>,
pub saved: Option<Arc<ChunkExtras>>,
/// Entities read from a save (vanilla entity NBT), spawned on arrival.
pub entities: Vec<horizon_world::nbt::Tag>,
/// Structure starts reaching into this chunk (`structures.References`):
@@ -309,6 +312,76 @@ impl Chunk {
}
}
/// A proto chunk saved before it became final. Decoration writes into a
/// chunk's neighbours, and decorated chunks wait for theirs, so the chunks
/// past the finished ones hold writes that generating them again would lose
/// (whole structure pieces, trees, ores). Vanilla saves its proto chunks too.
pub struct SavedProto {
/// Blocks, scheduled ticks (in `saved`) and post-processing marks.
pub column: Chunk,
/// Decorated itself (`features`), not only written to by its neighbours
/// (`terrain`).
pub decorated: bool,
/// Block entity data in placement order, with `x`, `y` and `z`.
pub block_entities: Vec<Tag>,
/// Entities placed by structures, with absolute `Pos`.
pub entities: Vec<Tag>,
}
/// A container's loot table and what Horizon rolled from it. Vanilla keeps
/// `LootTable` and `LootTableSeed` until the container is first opened;
/// Horizon rolls when it generates or loads the chunk, and saves the table
/// again while the container still holds exactly the rolled items.
#[derive(Clone, Debug, PartialEq)]
pub struct RolledLoot {
pub pos: (i32, i32, i32),
pub table: String,
/// The saved `LootTableSeed`, written back unless 0 (trySaveLootTable).
pub seed: Option<i64>,
/// The rolled `Items`.
pub items: Tag,
}
impl RolledLoot {
/// Puts the loot table back in place of `Items` while they are still the
/// roll; false once the container holds anything else.
pub fn restore(&self, map: &mut Compound) -> bool {
if map.get(&"Items".into()) != Some(&self.items) {
return false;
}
map.remove(&"Items".into());
map.insert("LootTable".into(), Tag::String(self.table.as_str().into()));
if let Some(seed) = self.seed.filter(|s| *s != 0) {
map.insert("LootTableSeed".into(), Tag::Long(seed));
}
true
}
}
/// Save data loaded with a chunk that Horizon keeps but does not simulate.
#[derive(Clone, Debug, Default)]
pub struct ChunkExtras {
/// Chunk root keys written back as loaded (structures, PostProcessing,
/// InhabitedTime, blending data, ...).
pub root: Compound,
/// Block entities Horizon does not model.
pub block_entities: Vec<Tag>,
/// The NBT of the modelled block entities as loaded, to restore the fields and
/// detail Horizon does not model when it saves them (`block_entity_nbt::overlay`).
pub block_entity_sources: Vec<((i32, i32, i32), Tag)>,
/// Every saved block entity's position in the order of the save's list,
/// the order `SerializableChunkData` puts them in the chunk's maps.
pub block_entity_order: Vec<(i32, i32, i32)>,
/// Scheduled ticks (`block_ticks`/`fluid_ticks`): delays relative to the
/// save time. Registered with the level's tick scheduler on arrival and
/// replaced with the scheduler's pending ticks when saving.
pub block_ticks: Vec<horizon_core::ticks::SavedTick>,
pub fluid_ticks: Vec<horizon_core::ticks::SavedTick>,
/// Game time written as `LastUpdate`.
pub last_update: i64,
/// Containers still holding their loot table's roll.
pub rolled_loot: Vec<RolledLoot>,
}
#[cfg(test)]
mod tests {
use super::*;
@@ -0,0 +1,488 @@
//! Block entity data: what a chunk stores for its block entities (`BlockEntity`
//! and its payloads). Plain data, as vanilla's block entities are world data;
//! their behaviour lives with the features that use them (`containers`,
//! `brewing`, `signs`, ...), which re-export these types under their old paths.
use horizon_client_core::{text::Text, who::Who};
use horizon_items::{banner_layer::BannerLayer, items::ItemStack};
use horizon_world::{
level::sculk::SculkSpreader,
nbt::{Compound, Tag},
};
use std::{collections::HashMap, sync::Arc};
/// An entity's UUID as its four ints (`UUIDUtil.CODEC`).
pub type Uuid = [i32; 4];
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum FurnaceKind {
Furnace,
BlastFurnace,
Smoker,
}
#[derive(Clone, Debug)]
pub struct Furnace {
pub kind: FurnaceKind,
/// input, fuel, result
pub items: [Option<ItemStack>; 3],
pub lit_remaining: i32,
pub lit_total: i32,
pub cooking_timer: i32,
pub cooking_total: i32,
pub speed: f32,
/// `AbstractFurnaceBlockEntity.recipesUsed`: how many of each recipe this furnace has
/// made since a player last took from its output. The experience is owed per recipe
/// rather than per item, and a save keeps it as the `RecipesUsed` compound.
pub recipes_used: HashMap<String, u32>,
}
#[derive(Clone, Debug)]
pub enum BlockEntity {
Chest(Vec<Option<ItemStack>>),
Furnace(Furnace),
Brewing(Box<BrewingStand>),
/// Signs and hanging signs (`SignBlockEntity`).
Sign(SignData),
/// `JukeboxBlockEntity`: the record and its `JukeboxSongPlayer`.
Jukebox(Jukebox),
/// `BeaconBlockEntity`.
Beacon(Box<Beacon>),
/// `LecternBlockEntity`: the book and the open page.
Lectern(Lectern),
/// Sculk sensors, shriekers and catalysts (`crate::sculk`).
Sculk(Box<SculkEntity>),
/// `CreakingHeartBlockEntity`: its protector and the hurt emitter.
CreakingHeart(Box<CreakingHeart>),
/// `BannerBlockEntity`: the pattern layers.
Banner(Banner),
/// `CrafterBlockEntity`.
Crafter(Crafter),
/// `SpawnerBlockEntity` (`crate::mobs::spawner`).
Spawner(Box<Spawner>),
/// `TrialSpawnerBlockEntity` (`crate::mobs::trial_spawner`).
TrialSpawner(Box<TrialSpawner>),
/// `VaultBlockEntity` (`crate::vault`).
Vault(Box<Vault>),
/// `BeehiveBlockEntity` of beehives and bee nests (`crate::beehive`).
Beehive(Box<Beehive>),
/// `CommandBlockEntity` of the three command blocks (`crate::command_block`).
CommandBlock(Box<CommandBlock>),
/// `CampfireBlockEntity` of campfires and soul campfires.
Campfire(Box<Campfire>),
/// `SkullBlockEntity` of every skull and head block.
Skull(Skull),
/// `TheEndGatewayBlockEntity`: its age and configured destination.
EndGateway(EndGateway),
/// `DaylightDetectorBlockEntity`: no data, only the ticker
/// (`DaylightDetectorBlock.tickEntity`).
DaylightDetector,
/// Suspicious sand and gravel: saved loot and the transient brushed face.
Brushable(Box<Brushable>),
/// A shelf holds three displayed item stacks.
Shelf(Box<Shelf>),
/// `ChiseledBookShelfBlockEntity`: a `Container` of six one-book slots.
ChiseledBookshelf(Box<ChiseledBookshelf>),
}
/// `BrushableBlockEntity`: the loot table is resolved on the first brush.
#[derive(Clone, Debug, Default)]
pub struct Brushable {
pub loot_table: Option<horizon_world::nbt::Tag>,
pub loot_seed: Option<horizon_world::nbt::Tag>,
pub item: Option<ItemStack>,
/// Direction's legacy network id; not saved to disk by vanilla.
pub hit_direction: Option<i32>,
}
/// `ShelfBlockEntity`'s displayed slots and alignment flag.
#[derive(Clone, Debug, Default)]
pub struct Shelf {
pub items: [Option<ItemStack>; 3],
pub align_to_bottom: bool,
}
/// `ChiseledBookShelfBlockEntity`: six slots and `lastInteractedSlot`.
#[derive(Clone, Debug, PartialEq)]
pub struct ChiseledBookshelf {
pub items: [Option<ItemStack>; 6],
pub last_slot: i32,
}
impl Default for ChiseledBookshelf {
fn default() -> Self {
Self {
items: Default::default(),
last_slot: -1,
}
}
}
/// `TheEndGatewayBlockEntity`.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct EndGateway {
pub age: i64,
pub exit_portal: Option<[i32; 3]>,
pub exact_teleport: bool,
}
/// `CampfireBlockEntity`: four cooking slots, each with its
/// `cookingProgress` and `cookingTime` (the recipe's).
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Campfire {
pub items: [Option<ItemStack>; 4],
pub progress: [i32; 4],
pub total: [i32; 4],
}
/// `SkullBlockEntity`: the player profile the head shows (kept as loaded:
/// a compound, or a string when it names a player only), the note block
/// sound and `custom_name`.
#[derive(Clone, Debug, Default)]
pub struct Skull {
pub profile: Option<horizon_world::nbt::Tag>,
pub note_block_sound: Option<String>,
pub custom_name: Option<horizon_world::nbt::Tag>,
/// What `profile` draws, read from it once rather than every frame.
pub skin: Option<SkullSkin>,
}
/// `SkullBlockRenderer.getPlayerSkin` of a head's profile: its skin's URL
/// with the `skins/<sha1>` sprite it downloads to, and the
/// `DefaultPlayerSkin` drawn until that sprite is there (steve for a
/// profile with no id).
#[derive(Clone, Debug)]
pub struct SkullSkin {
pub download: Option<(String, String)>,
/// `ResolvableProfile.nameOrId` when the profile is `Dynamic` (a name or
/// an id only): the key `skins::Resolver` resolves it by.
pub dynamic: Option<NameOrId>,
pub fallback: &'static str,
}
/// `Either<String, UUID>`: the key a `Dynamic` `ResolvableProfile` resolves
/// by (`ResolvableProfile.nameOrId`).
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum NameOrId {
Name(String),
Id(u128),
}
/// `CrafterBlockEntity`: nine slots that can be disabled, the powered flag
/// and the ticks left in the crafting animation.
#[derive(Clone, Debug, Default)]
pub struct Crafter {
pub items: [Option<ItemStack>; 9],
pub disabled: [bool; 9],
pub triggered: bool,
pub crafting_ticks: i32,
/// The block state's `triggered` when last ticked (not saved): the block
/// entity flag follows it only when it changes (`CrafterBlock.neighborChanged`).
pub state_triggered: Option<bool>,
}
#[derive(Clone, Debug, Default)]
pub struct Banner {
pub layers: Vec<BannerLayer>,
pub name: Option<String>,
/// Components not consumed by BannerBlockEntity's implicit fields.
pub components: horizon_world::nbt::Compound,
}
/// `CreakingHeartBlockEntity` (the logic is in `game/creaking.rs`).
#[derive(Clone, Debug, Default)]
pub struct CreakingHeart {
/// The protector: its entity id once resolved, and its UUID (the saved form).
pub creaking: Option<(Option<u32>, [i32; 4])>,
pub ticks_existed: i64,
pub ticker: i32,
/// Ticks left of the hurt effect (`emitter`) and where it heads.
pub emitter: i32,
pub emitter_target: Option<[f64; 3]>,
pub output_signal: i32,
}
#[derive(Clone, Debug, Default)]
pub struct Lectern {
pub book: Option<ItemStack>,
pub page: i32,
}
#[derive(Clone, Debug, Default)]
pub struct Beacon {
pub levels: i32,
/// Effect ids (`minecraft:speed`).
pub primary: Option<String>,
pub secondary: Option<String>,
/// Beam sections (RGB colour, height) and the ones being rebuilt.
pub sections: Vec<(u32, i32)>,
pub checking: Vec<(u32, i32)>,
pub last_check_y: i32,
}
#[derive(Clone, Debug, Default)]
pub struct Jukebox {
pub record: Option<ItemStack>,
/// `ticks_since_song_started`, while a song plays.
pub ticks: i64,
pub playing: bool,
}
/// `BrewingStandBlockEntity` state. Slots: 0-2 bottles, 3 ingredient, 4 fuel.
#[derive(Clone, Debug, Default)]
pub struct BrewingStand {
pub items: [Option<ItemStack>; 5],
pub brew_time: i32,
pub total_brew_time: i32,
pub fuel: i32,
pub total_fuel: i32,
pub speed: f32,
pub ingredient: Option<Arc<str>>,
pub last_bottles: Option<[bool; 3]>,
}
/// One sign side (`SignText`).
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SignText {
pub lines: [String; 4],
/// `DyeColor` id.
pub color: u8,
pub glowing: bool,
}
/// `SignBlockEntity` / `HangingSignBlockEntity` data.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct SignData {
pub hanging: bool,
pub front: SignText,
pub back: SignText,
pub waxed: bool,
}
/// `VibrationInfo`.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Info {
pub event: &'static str,
pub distance: f32,
pub pos: [f64; 3],
pub source: Option<Who>,
pub owner: Option<Who>,
}
/// `VibrationSelector`: the closest, then highest-frequency candidate of a tick.
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Selector {
pub current: Option<(Info, i64)>,
}
/// `VibrationSystem.Data`.
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Data {
pub current: Option<Info>,
pub travel: i32,
pub selector: Selector,
}
/// Which sculk block entity this is.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SculkKind {
Sensor,
Calibrated,
Shrieker,
Catalyst,
}
/// `SculkSensorBlockEntity`, `CalibratedSculkSensorBlockEntity`,
/// `SculkShriekerBlockEntity` and `SculkCatalystBlockEntity`.
#[derive(Clone, Debug)]
pub struct SculkEntity {
pub kind: SculkKind,
pub vibration: Data,
/// `last_vibration_frequency` (sensors).
pub last_frequency: i32,
/// `warning_level` (shriekers).
pub warning_level: i32,
/// The catalyst's charge cursors.
pub spreader: SculkSpreader,
}
/// `SpawnData.CustomSpawnRules`: the block and sky light the spawn block
/// must have (inclusive ranges within 0..=15).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct CustomSpawnRules {
pub block: (i32, i32),
pub sky: (i32, i32),
}
/// `SpawnData`: the entity to spawn, optional light rules and an equipment
/// table (kept as loaded).
#[derive(Clone, Debug, Default, PartialEq)]
pub struct SpawnData {
pub entity: Compound,
pub custom_spawn_rules: Option<CustomSpawnRules>,
pub equipment: Option<Tag>,
}
/// Which `SpawnData` is next: one of the potentials (the same object in
/// vanilla, so changing it changes the potential) or its own.
#[derive(Clone, Debug, PartialEq)]
pub enum Next {
Potential(usize),
Own(SpawnData),
}
/// `BaseSpawner`.
#[derive(Clone, Debug, PartialEq)]
pub struct Spawner {
pub spawn_delay: i32,
/// `SpawnPotentials`: data and weight.
pub potentials: Vec<(SpawnData, i32)>,
pub next: Option<Next>,
pub min_spawn_delay: i32,
pub max_spawn_delay: i32,
pub spawn_count: i32,
pub max_nearby_entities: i32,
pub required_player_range: i32,
pub spawn_range: i32,
/// The client copy's delay (`spawnDelay` on the client level), which
/// speeds the spin up as the next spawn nears.
pub client_delay: i32,
pub spin: f64,
pub o_spin: f64,
}
impl Default for Spawner {
fn default() -> Self {
Self {
spawn_delay: 20,
potentials: Vec::new(),
next: None,
min_spawn_delay: 200,
max_spawn_delay: 800,
spawn_count: 4,
max_nearby_entities: 6,
required_player_range: 16,
spawn_range: 4,
client_delay: 20,
spin: 0.0,
o_spin: 0.0,
}
}
}
/// `TrialSpawnerConfig`.
#[derive(Clone, Debug, PartialEq)]
pub struct TrialConfig {
pub spawn_range: i32,
pub total_mobs: f32,
pub simultaneous_mobs: f32,
pub total_mobs_added_per_player: f32,
pub simultaneous_mobs_added_per_player: f32,
pub ticks_between_spawn: i32,
pub spawn_potentials: Vec<(SpawnData, i32)>,
pub loot_tables_to_eject: Vec<(String, i32)>,
pub items_to_drop_when_ominous: String,
}
impl Default for TrialConfig {
/// `TrialSpawnerConfig.Builder`'s defaults.
fn default() -> Self {
Self {
spawn_range: 4,
total_mobs: 6.0,
simultaneous_mobs: 2.0,
total_mobs_added_per_player: 2.0,
simultaneous_mobs_added_per_player: 1.0,
ticks_between_spawn: 40,
spawn_potentials: Vec::new(),
loot_tables_to_eject: vec![
("minecraft:spawners/trial_chamber/consumables".into(), 1),
("minecraft:spawners/trial_chamber/key".into(), 1),
],
items_to_drop_when_ominous:
"minecraft:spawners/trial_chamber/items_to_drop_when_ominous".into(),
}
}
}
/// A `Holder<TrialSpawnerConfig>` as saved: a registry id
/// (`minecraft:trial_chamber/breeze/normal`) or an inline config.
#[derive(Clone, Debug, Default, PartialEq)]
pub struct ConfigSlot {
/// The value as loaded, written back as is; None for the default.
pub saved: Option<Tag>,
pub config: TrialConfig,
}
/// `TrialSpawner` with its `TrialSpawnerStateData`.
#[derive(Clone, Debug, PartialEq)]
pub struct TrialSpawner {
pub normal: ConfigSlot,
pub ominous: ConfigSlot,
pub target_cooldown_length: i32,
pub required_player_range: i32,
pub detected_players: Vec<Uuid>,
pub current_mobs: Vec<Uuid>,
pub cooldown_ends_at: i64,
pub next_mob_spawns_at: i64,
pub total_mobs_spawned: i32,
pub next_spawn_data: Option<SpawnData>,
pub ejecting_loot_table: Option<String>,
/// `isOminous`, set from the block on every server tick.
pub is_ominous: bool,
/// The server's display entity exists (`getOrCreateDisplayEntity`): it is
/// made once per loaded block entity and kept.
pub has_display: bool,
pub spin: f64,
pub o_spin: f64,
/// `getDispensingItems`, kept once rolled (not saved): the ominous items
/// and their weights (the rolled counts).
pub dispensing: Option<Vec<ItemStack>>,
}
impl Default for TrialSpawner {
fn default() -> Self {
Self {
normal: ConfigSlot::default(),
ominous: ConfigSlot::default(),
target_cooldown_length: 36000,
required_player_range: 14,
detected_players: Vec::new(),
current_mobs: Vec::new(),
cooldown_ends_at: 0,
next_mob_spawns_at: 0,
total_mobs_spawned: 0,
next_spawn_data: None,
ejecting_loot_table: None,
is_ominous: false,
has_display: false,
spin: 0.0,
o_spin: 0.0,
dispensing: None,
}
}
}
/// `VaultConfig`.
#[derive(Clone, Debug, PartialEq)]
pub struct VaultConfig {
pub loot_table: String,
pub activation_range: f64,
pub deactivation_range: f64,
/// None when the config has no key (`ItemStack.EMPTY`): the vault never opens.
pub key_item: Option<ItemStack>,
pub override_loot_table_to_display: Option<String>,
}
/// `VaultBlockEntity`: its config, server data and shared data.
#[derive(Clone, Debug, PartialEq)]
pub struct Vault {
pub config: VaultConfig,
/// `VaultServerData`, oldest rewarded player first.
pub rewarded_players: Vec<Uuid>,
pub state_updating_resumes_at: i64,
/// Popped from the end.
pub items_to_eject: Vec<ItemStack>,
pub total_ejections_needed: i32,
pub last_insert_fail_timestamp: i64,
/// `VaultSharedData`.
pub display_item: Option<ItemStack>,
pub connected_players: Vec<Uuid>,
pub connected_particles_range: f64,
/// The shared data changed since the last tick sent it.
pub shared_dirty: bool,
/// `VaultClientData`: the display item's spin and the previous tick's, in degrees.
pub spin: f32,
pub o_spin: f32,
}
/// `BeehiveBlockEntity`: the bees inside and the flower one of them knew.
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Beehive {
pub bees: Vec<Occupant>,
pub flower: Option<[i32; 3]>,
}
/// `BeehiveBlockEntity.Occupant`: a bee's tag (with its `id`), its time in
/// the hive and the time it stays at least.
#[derive(Clone, Debug, PartialEq)]
pub struct Occupant {
pub entity_data: Tag,
pub ticks_in_hive: i32,
pub min_ticks_in_hive: i32,
}
#[derive(Clone, Debug, PartialEq)]
pub struct CommandBlock {
pub command: String,
pub success_count: i32,
/// `CustomName` as saved (a text component).
pub custom_name: Option<Tag>,
pub track_output: bool,
pub last_output: Option<Text>,
pub update_last_execution: bool,
/// The game time of the last run, -1 for none.
pub last_execution: i64,
pub powered: bool,
pub auto: bool,
pub condition_met: bool,
}
@@ -179,6 +179,8 @@ impl Mesh {
layer: 4,
color: u32::MAX,
light: shade,
glint_uv: [0.0, 0.0],
});
}
};
@@ -54,6 +54,27 @@ fn strider_liquid_shape() -> VoxelShape {
VoxelShape::from_cube([1, 2, 1], &[true, false]).expect("a valid half-block column")
}
// Vanilla scopes this direction to Entity.move while a piston pushes it.
thread_local! {
static PISTON_NOCLIP: std::cell::Cell<Option<horizon_core::geometry::Direction>> = const { std::cell::Cell::new(None) };
}
/// Suppress the moving part of pistons moving in this direction during a push.
pub fn with_piston_noclip<T>(
direction: horizon_core::geometry::Direction,
f: impl FnOnce() -> T,
) -> T {
struct Reset(Option<horizon_core::geometry::Direction>);
impl Drop for Reset {
fn drop(&mut self) {
PISTON_NOCLIP.with(|value| value.set(self.0));
}
}
let previous = PISTON_NOCLIP.with(|value| value.replace(Some(direction)));
let _reset = Reset(previous);
f()
}
pub struct SceneCollisions<'w, 'r> {
pub world: &'w World,
pub binding: &'w BlockCollisionRegistry<'r>,
@@ -140,8 +161,21 @@ impl<W: EntityMovementWorld<Error = io::Error>> EntityMovementWorld for EntityCo
border.collision_shape().map(Some).map_err(io::Error::other)
}
}
impl EntityCollisions<'_, SceneCollisions<'_, '_>> {
/// `SceneCollisions::resolve` that also stops at the entity boxes.
impl<'a, W> EntityCollisions<'a, W>
where
W: BlockCollisionWorld<Error = io::Error> + EntityMovementWorld<Error = io::Error>,
{
/// `EntityCollisions` for a body the world border also stops: the border
/// and the body's own x and z (`WorldBorder.isInsideCloseToBorder`).
pub fn with_border(blocks: W, border: &'a BorderGeometry, at: [f64; 2]) -> Self {
Self {
blocks,
entities: &[],
border: Some((border, at)),
}
}
/// `SceneCollisions::resolve` that also stops at the entity boxes and,
/// with a border, at the world border's walls.
pub fn resolve(
&mut self,
bounds: Aabb,
@@ -149,12 +183,11 @@ impl EntityCollisions<'_, SceneCollisions<'_, '_>> {
max_step: f32,
on_ground: bool,
) -> Result<Vec3> {
let context = self.blocks.context(bounds);
Ok(resolve_world_movement(
self,
MovementRequest {
entity: EntityIdentity(0),
context,
context: CollisionContext::position(bounds.min_position().y),
bounds,
movement,
max_step,
@@ -324,7 +357,7 @@ impl DynamicSupportWorld for SceneCollisions<'_, '_> {
return Ok(None);
};
pistons
.collision_shape(self, p, piston, None)
.collision_shape(self, p, piston, PISTON_NOCLIP.with(|value| value.get()))
.map(Some)
.map_err(io::Error::other)
}
@@ -388,6 +421,42 @@ impl PowderSnowWorld for SceneCollisions<'_, '_> {
}
}
/// A collision world without blocks: no chunk is resident, so only the
/// world border stops a body.
pub struct EmptyCollisions;
impl BlockCollisionWorld for EmptyCollisions {
type Error = io::Error;
type Chunk = ChunkPos;
fn collision_chunk(&mut self, _: ChunkPos) -> io::Result<Option<ChunkPos>> {
Ok(None)
}
fn collision_block(&mut self, _: ChunkPos, _: BlockPos) -> io::Result<CollisionBlock> {
Err(io::Error::other("no chunk is resident"))
}
fn is_suffocating(&mut self, _: ChunkPos, _: CollisionBlock, _: BlockPos) -> io::Result<bool> {
Ok(false)
}
fn context_shape(
&mut self,
_: CollisionBlock,
_: BlockPos,
_: &CollisionContext<'_>,
) -> io::Result<VoxelShape> {
Ok(VoxelShape::empty())
}
}
impl EntityMovementWorld for EmptyCollisions {
type Error = io::Error;
fn entity_collisions(&mut self, _: EntityIdentity, _: Aabb) -> io::Result<Vec<VoxelShape>> {
Ok(Vec::new())
}
fn movement_border(&mut self, _: EntityIdentity, _: Aabb) -> io::Result<Option<VoxelShape>> {
Ok(None)
}
}
/// Collision queries over any block lookup (test worlds without chunks):
/// every chunk is present, nothing is suffocating, no entities collide.
pub struct LookupCollisions<'w, 'r> {
@@ -475,3 +544,70 @@ impl PowderSnowWorld for LookupCollisions<'_, '_> {
Ok(false)
}
}
#[cfg(test)]
mod piston_noclip_tests {
use super::*;
use horizon_core::geometry::Direction;
#[test]
fn piston_noclip_is_scoped_and_restored_after_unwind() {
assert_eq!(PISTON_NOCLIP.with(|value| value.get()), None);
with_piston_noclip(Direction::East, || {
assert_eq!(
PISTON_NOCLIP.with(|value| value.get()),
Some(Direction::East)
);
let _ =
std::panic::catch_unwind(|| with_piston_noclip(Direction::West, || panic!("test")));
assert_eq!(
PISTON_NOCLIP.with(|value| value.get()),
Some(Direction::East)
);
});
assert_eq!(PISTON_NOCLIP.with(|value| value.get()), None);
}
}
#[cfg(test)]
mod border_movement_tests {
use super::*;
/// A border 10 wide around the origin: its wall's inner face is at x = 5.
fn border() -> BorderGeometry {
BorderGeometry::new([0.0, 0.0], 10.0, 29_999_984).expect("a border")
}
/// `WorldBorder.isInsideCloseToBorder` and `WorldBorder.getCollisionShape`:
/// a body inside close to the border collides with the border's walls.
#[test]
fn a_body_next_to_the_border_stops_at_its_wall() {
// A body 0.6 wide, 0.4 blocks short of the wall, moved east into it.
let bounds = Aabb::new(Vec3::new(4.0, 0.0, 0.0), Vec3::new(4.6, 1.8, 0.6));
let border = border();
let mut collisions = EntityCollisions::with_border(EmptyCollisions, &border, [4.3, 0.3]);
let moved = collisions
.resolve(bounds, Vec3::new(0.51, 0.0, 0.0), 0.0, false)
.expect("the movement resolves");
// The wall stops the body 0.4 blocks short of the whole move.
assert!(
(moved.x - 0.4).abs() < 1.0e-9,
"the body was not stopped by the border: {moved:?}"
);
}
/// Away from the border, the same body moves the whole way.
#[test]
fn a_body_away_from_the_border_moves_whole() {
let bounds = Aabb::new(Vec3::new(0.0, 0.0, 0.0), Vec3::new(0.6, 1.8, 0.6));
let border = border();
let mut collisions = EntityCollisions::with_border(EmptyCollisions, &border, [0.3, 0.3]);
let moved = collisions
.resolve(bounds, Vec3::new(0.51, 0.0, 0.0), 0.0, false)
.expect("the movement resolves");
assert!(
(moved.x - 0.51).abs() < 1.0e-9,
"the body did not move the whole way: {moved:?}"
);
}
}
@@ -1,8 +1,10 @@
//! `CommandBlockEntity` with its `BaseCommandBlock`: the command, the success
//! count comparators read, the output it keeps, and the powered, automatic
//! and condition flags `CommandBlock.tick` runs on.
use crate::{
save::{number, text},
pub use crate::chunk::block_entity::CommandBlock;
use horizon_client_core::{
nbt_value::{number, text},
scoreboard,
text::Text,
};
use horizon_world::nbt::Tag;
@@ -76,22 +78,6 @@ pub fn automatic_block(name: &str) -> bool {
name == "minecraft:chain_command_block"
}
#[derive(Clone, Debug, PartialEq)]
pub struct CommandBlock {
pub command: String,
pub success_count: i32,
/// `CustomName` as saved (a text component).
pub custom_name: Option<Tag>,
pub track_output: bool,
pub last_output: Option<Text>,
pub update_last_execution: bool,
/// The game time of the last run, -1 for none.
pub last_execution: i64,
pub powered: bool,
pub auto: bool,
pub condition_met: bool,
}
impl Default for CommandBlock {
fn default() -> Self {
Self {
@@ -123,7 +109,7 @@ impl CommandBlock {
last_output: track_output
.then(|| tag.get("LastOutput"))
.flatten()
.map(|t| crate::scoreboard::comp_text(&crate::scoreboard::tag_to_json(t))),
.map(|t| scoreboard::comp_text(&scoreboard::tag_to_json(t))),
update_last_execution,
last_execution: if update_last_execution {
number(tag.get("LastExecution")).unwrap_or(-1)
@@ -179,7 +165,7 @@ impl CommandBlock {
pub fn name(&self) -> Text {
self.custom_name.as_ref().map_or_else(
|| Text::literal("@"),
|t| crate::scoreboard::comp_text(&crate::scoreboard::tag_to_json(t)),
|t| scoreboard::comp_text(&scoreboard::tag_to_json(t)),
)
}
}
@@ -0,0 +1,580 @@
//! Container block entities: chests/barrels (27 slots) and the three furnace
//! kinds, ticking as `AbstractFurnaceBlockEntity.serverTick` with fuel times
//! from the data-driven `context_int_provider`/`context_float_provider` files.
pub use crate::chunk::block_entity::{
Banner, Beacon, BlockEntity, Brushable, Campfire, ChiseledBookshelf, Crafter, CreakingHeart,
EndGateway, Furnace, FurnaceKind, Jukebox, Lectern, Shelf, Skull, SkullSkin,
};
use crate::items::ItemStack;
use std::collections::HashMap;
impl FurnaceKind {
pub fn recipe_type(self) -> &'static str {
match self {
Self::Furnace => "minecraft:smelting",
Self::BlastFurnace => "minecraft:blasting",
Self::Smoker => "minecraft:smoking",
}
}
pub fn fast(self) -> bool {
self != Self::Furnace
}
pub fn from_block(name: &str) -> Option<Self> {
match name {
"minecraft:furnace" => Some(Self::Furnace),
"minecraft:blast_furnace" => Some(Self::BlastFurnace),
"minecraft:smoker" => Some(Self::Smoker),
_ => None,
}
}
}
/// The key `recipes_used` keeps a recipe under: its full id, as vanilla's `ResourceKey<Recipe>`
/// writes it in `RecipesUsed`. Horizon's own recipe ids are file names without the namespace, so
/// keeping both forms would write a key twice, and vanilla refuses such a furnace
/// ("Duplicate entry for key").
pub fn recipe_used_key(id: &str) -> String {
if id.contains(':') {
id.to_owned()
} else {
format!("minecraft:{id}")
}
}
/// A hopper's slots: the only plain container with five.
pub const HOPPER_SLOTS: usize = 5;
/// `getDefaultName` of the `BaseContainerBlockEntity` that `block` holds: the
/// translation key of its menu's title when it has no custom name. Copper
/// chests hold a `ChestBlockEntity`, shulker boxes of every colour a
/// `ShulkerBoxBlockEntity`.
pub fn default_name_key(block: &str) -> Option<&'static str> {
Some(match block {
"minecraft:chest" | "minecraft:trapped_chest" => "container.chest",
n if n.ends_with("copper_chest") => "container.chest",
"minecraft:barrel" => "container.barrel",
n if n.ends_with("shulker_box") => "container.shulkerBox",
"minecraft:dispenser" => "container.dispenser",
"minecraft:dropper" => "container.dropper",
"minecraft:hopper" => "container.hopper",
"minecraft:furnace" => "container.furnace",
"minecraft:smoker" => "container.smoker",
"minecraft:blast_furnace" => "container.blast_furnace",
"minecraft:brewing_stand" => "container.brewing",
"minecraft:crafter" => "container.crafter",
_ => return None,
})
}
/// `BaseContainerBlockEntity.getDisplayName` (`getName`), the title of the
/// `ClientboundOpenScreenPacket` that opens it: the block entity's custom
/// name (`CustomName`, a literal), else its default name.
pub fn display_name(custom: Option<&str>, default_key: &str) -> horizon_world::nbt::Tag {
match custom {
Some(name) => horizon_protocol::text::literal(name),
None => horizon_protocol::text::translatable(default_key, &[]),
}
}
/// `ChestBlock.MENU_PROVIDER_COMBINER`'s `getDisplayName` for a double chest:
/// the first half's custom name (the right half, `DoubleBlockCombiner`'s
/// `FIRST`), else the second's, else `container.chestDouble`.
pub fn double_chest_name(first: Option<&str>, second: Option<&str>) -> horizon_world::nbt::Tag {
display_name(first.or(second), "container.chestDouble")
}
impl Campfire {
/// `placeFood`'s slot: the first empty one, given the item's
/// `campfire_cooking` time; it takes one item of `held`.
pub fn place_food(&mut self, held: &mut ItemStack, cooking_time: i32) -> bool {
let Some(slot) = self.items.iter().position(Option::is_none) else {
return false;
};
self.total[slot] = cooking_time;
self.progress[slot] = 0;
self.items[slot] = Some(ItemStack {
count: 1,
..held.clone()
});
held.count = held.count.saturating_sub(1);
true
}
/// `cookTick` (lit): each item's progress rises by one; the slots whose
/// progress reached their cooking time give up their item (the caller
/// drops its result).
pub fn cook_tick(&mut self) -> Vec<(usize, ItemStack)> {
let mut done = Vec::new();
for i in 0..4 {
if self.items[i].is_none() {
continue;
}
self.progress[i] += 1;
if self.progress[i] >= self.total[i]
&& let Some(item) = self.items[i].take()
{
done.push((i, item));
}
}
done
}
/// `cooldownTick` (unlit): the progress falls by two, no lower than 0.
/// Whether anything changed (`setChanged`).
pub fn cooldown_tick(&mut self) -> bool {
let mut changed = false;
for i in 0..4 {
if self.progress[i] > 0 {
changed = true;
self.progress[i] = (self.progress[i] - 2).clamp(0, self.total[i]);
}
}
changed
}
}
impl Skull {
pub fn new(
profile: Option<horizon_world::nbt::Tag>,
note_block_sound: Option<String>,
custom_name: Option<horizon_world::nbt::Tag>,
) -> Self {
let skin = profile.as_ref().map(SkullSkin::of);
Self {
profile,
note_block_sound,
custom_name,
skin,
}
}
}
impl SkullSkin {
pub fn of(profile: &horizon_world::nbt::Tag) -> Self {
let download = crate::skins::profile_skin_url_nbt(profile).and_then(|url| {
let location = crate::skins::skin_location(&crate::skins::texture_hash(&url)?);
Some((url, location))
});
// A `Dynamic` profile (a name or id only) resolves through the
// services when the skull is drawn.
let dynamic = crate::skins::dynamic_nbt(profile);
// DefaultPlayerSkin.get(id), or getDefaultSkin (slim steve) without an id.
let fallback = crate::skins::profile_uuid_nbt(profile)
.or_else(|| {
crate::skins::nbt_text(profile.get("name"))
.map(|name| horizon_protocol::crypto::offline_uuid(&name))
})
.map_or("entity/player/slim/steve", crate::tab_list::default_skin);
Self {
download,
dynamic,
fallback,
}
}
}
/// The skull and head blocks (`SkullBlock`, `WallSkullBlock`): every
/// `*_skull` and `*_head` but the piston's head.
fn is_skull(name: &str) -> bool {
name.ends_with("_skull") || (name.ends_with("_head") && name != "minecraft:piston_head")
}
impl Crafter {
/// `slotCanBeDisabled`: only empty slots.
pub fn slot_can_be_disabled(&self, slot: usize) -> bool {
slot < 9 && self.items[slot].is_none()
}
/// `setSlotState`.
pub fn set_slot_state(&mut self, slot: usize, enabled: bool) {
if self.slot_can_be_disabled(slot) {
self.disabled[slot] = !enabled;
}
}
/// `getRedstoneSignal`: filled or disabled slots.
pub fn redstone_signal(&self) -> i32 {
(0..9)
.filter(|&i| self.items[i].is_some() || self.disabled[i])
.count() as i32
}
/// `canPlaceItem`: a hopper fills the smallest stacks first.
pub fn can_place(&self, slot: usize, _stack: &ItemStack, max_stack: u32) -> bool {
if slot >= 9 || self.disabled[slot] {
return false;
}
let Some(current) = &self.items[slot] else {
return true;
};
if current.count >= max_stack {
return false;
}
!(slot + 1..9).any(|i| {
!self.disabled[i]
&& match &self.items[i] {
None => true,
Some(other) => {
other.count < current.count && other.same_item_same_components(current)
}
}
})
}
}
impl BlockEntity {
pub fn for_block(name: &str) -> Option<Self> {
if let Some(hanging) = crate::signs::sign_kind(name) {
return Some(Self::Sign(crate::signs::SignData {
hanging,
..Default::default()
}));
}
if let Some(kind) = FurnaceKind::from_block(name) {
return Some(Self::Furnace(Furnace {
kind,
items: [None, None, None],
lit_remaining: 0,
lit_total: 0,
cooking_timer: 0,
cooking_total: 0,
speed: 1.0,
recipes_used: HashMap::new(),
}));
}
if matches!(
name,
"minecraft:suspicious_sand"
| "minecraft:suspicious_gravel"
| "minecraft:brushable_block"
) {
return Some(Self::Brushable(Box::default()));
}
if name == "minecraft:creaking_heart" {
return Some(Self::CreakingHeart(Box::default()));
}
if name == "minecraft:spawner" {
return Some(Self::Spawner(Box::default()));
}
if name == "minecraft:trial_spawner" {
return Some(Self::TrialSpawner(Box::default()));
}
if name == "minecraft:vault" {
return Some(Self::Vault(Box::default()));
}
if name == "minecraft:beehive" || name == "minecraft:bee_nest" {
return Some(Self::Beehive(Box::default()));
}
if name == "minecraft:lectern" {
return Some(Self::Lectern(Lectern::default()));
}
if crate::command_block::Mode::of_block(name).is_some() {
// CommandBlock.newBlockEntity: setAutomatic(this.automatic).
return Some(Self::CommandBlock(Box::new(
crate::command_block::CommandBlock {
auto: crate::command_block::automatic_block(name),
..Default::default()
},
)));
}
if let Some(sculk) = crate::sculk::SculkEntity::for_block(name) {
return Some(Self::Sculk(Box::new(sculk)));
}
if name == "minecraft:crafter" {
// CrafterBlock.newBlockEntity copies the placed state's flag: the
// first tick sees it as a change from untriggered.
return Some(Self::Crafter(Crafter {
state_triggered: Some(false),
..Crafter::default()
}));
}
if name.ends_with("_banner") {
return Some(Self::Banner(Banner::default()));
}
if is_skull(name) {
return Some(Self::Skull(Skull::default()));
}
if name == "minecraft:end_gateway" {
return Some(Self::EndGateway(EndGateway::default()));
}
if name == "minecraft:daylight_detector" {
return Some(Self::DaylightDetector);
}
if name == "minecraft:beacon" {
return Some(Self::Beacon(Box::new(Beacon {
last_check_y: i32::MIN / 2,
..Default::default()
})));
}
if name == "minecraft:brewing_stand" {
return Some(Self::Brewing(Box::new(crate::brewing::BrewingStand::new())));
}
if name == "minecraft:campfire" || name == "minecraft:soul_campfire" {
return Some(Self::Campfire(Box::default()));
}
if name == "minecraft:shelf" || name.ends_with("_shelf") {
return Some(Self::Shelf(Box::default()));
}
if name == "minecraft:chiseled_bookshelf" {
return Some(Self::ChiseledBookshelf(Box::default()));
}
let size = match name {
"minecraft:chest" | "minecraft:trapped_chest" | "minecraft:barrel" => 27,
n if n.ends_with("copper_chest") => 27,
"minecraft:hopper" => HOPPER_SLOTS,
"minecraft:dispenser" | "minecraft:dropper" => 9,
n if n.ends_with("shulker_box") => 27,
_ => return None,
};
Some(Self::Chest(vec![None; size]))
}
pub fn id(&self) -> &'static str {
match self {
Self::Chest(items) => match items.len() {
HOPPER_SLOTS => "minecraft:hopper",
9 => "minecraft:dispenser",
_ => "minecraft:chest",
},
Self::Furnace(f) => match f.kind {
FurnaceKind::Furnace => "minecraft:furnace",
FurnaceKind::BlastFurnace => "minecraft:blast_furnace",
FurnaceKind::Smoker => "minecraft:smoker",
},
Self::Brewing(_) => "minecraft:brewing_stand",
Self::Sign(s) => s.id(),
Self::Jukebox(_) => "minecraft:jukebox",
Self::Beacon(_) => "minecraft:beacon",
Self::Lectern(_) => "minecraft:lectern",
Self::Sculk(s) => s.id(),
Self::Banner(_) => "minecraft:banner",
Self::Crafter(_) => "minecraft:crafter",
Self::CreakingHeart(_) => "minecraft:creaking_heart",
Self::Spawner(_) => "minecraft:mob_spawner",
Self::TrialSpawner(_) => "minecraft:trial_spawner",
Self::Vault(_) => "minecraft:vault",
Self::Beehive(_) => "minecraft:beehive",
Self::CommandBlock(_) => "minecraft:command_block",
Self::Skull(_) => "minecraft:skull",
Self::EndGateway(_) => "minecraft:end_gateway",
Self::DaylightDetector => "minecraft:daylight_detector",
Self::Brushable(_) => "minecraft:brushable_block",
Self::Campfire(_) => "minecraft:campfire",
Self::ChiseledBookshelf(_) => "minecraft:chiseled_bookshelf",
Self::Shelf(_) => "minecraft:shelf",
}
}
pub fn contents(&self) -> Vec<ItemStack> {
match self {
Self::Chest(items) => items.iter().flatten().cloned().collect(),
Self::Furnace(f) => f.items.iter().flatten().cloned().collect(),
Self::Brewing(b) => b.items.iter().flatten().cloned().collect(),
Self::Sign(_) => Vec::new(),
Self::Jukebox(j) => j.record.iter().cloned().collect(),
Self::Beacon(_)
| Self::CreakingHeart(_)
| Self::Spawner(_)
| Self::TrialSpawner(_)
| Self::Vault(_)
| Self::Beehive(_)
| Self::CommandBlock(_)
| Self::EndGateway(_)
| Self::Skull(_)
| Self::DaylightDetector
| Self::Brushable(_) => Vec::new(),
Self::Lectern(l) => l.book.iter().cloned().collect(),
Self::Sculk(_) => Vec::new(),
Self::Banner(_) => Vec::new(),
Self::Crafter(c) => c.items.iter().flatten().cloned().collect(),
Self::Campfire(c) => c.items.iter().flatten().cloned().collect(),
Self::ChiseledBookshelf(b) => b.items.iter().flatten().cloned().collect(),
Self::Shelf(b) => b.items.iter().flatten().cloned().collect(),
}
}
pub fn slots(&self) -> &[Option<ItemStack>] {
match self {
Self::Chest(items) => items,
Self::Furnace(f) => &f.items,
Self::Brewing(b) => &b.items,
Self::Sign(_)
| Self::Jukebox(_)
| Self::Beacon(_)
| Self::Lectern(_)
| Self::Sculk(_)
| Self::Banner(_)
| Self::EndGateway(_)
| Self::Skull(_)
| Self::DaylightDetector
| Self::Brushable(_) => &[],
Self::Crafter(c) => &c.items,
Self::ChiseledBookshelf(b) => &b.items,
Self::Shelf(b) => &b.items,
Self::CreakingHeart(_)
| Self::Spawner(_)
| Self::TrialSpawner(_)
| Self::Vault(_)
| Self::Beehive(_)
| Self::CommandBlock(_)
// Not a Container (only Clearable): hoppers don't reach in.
| Self::Campfire(_) => &[],
}
}
pub fn slots_mut(&mut self) -> &mut [Option<ItemStack>] {
match self {
Self::Chest(items) => items,
Self::Furnace(f) => &mut f.items,
Self::Brewing(b) => &mut b.items,
Self::Sign(_)
| Self::Jukebox(_)
| Self::Beacon(_)
| Self::Lectern(_)
| Self::Sculk(_)
| Self::Banner(_)
| Self::EndGateway(_)
| Self::Skull(_)
| Self::DaylightDetector
| Self::Brushable(_) => &mut [],
Self::Crafter(c) => &mut c.items,
Self::ChiseledBookshelf(b) => &mut b.items,
Self::Shelf(b) => &mut b.items,
Self::CreakingHeart(_)
| Self::Spawner(_)
| Self::TrialSpawner(_)
| Self::Vault(_)
| Self::Beehive(_)
| Self::CommandBlock(_)
| Self::Campfire(_) => &mut [],
}
}
}
pub use crate::providers::Providers;
/// A `ChestBlock`, which `ChestBlock.combine` can join into a double chest:
/// chests, trapped chests and copper chests in every weathering state, waxed
/// or not (`CopperChestBlock` extends `ChestBlock`). Ender chests are
/// `EnderChestBlock`s and always single.
pub fn is_chest_block(name: &str) -> bool {
matches!(name, "minecraft:chest" | "minecraft:trapped_chest") || name.ends_with("copper_chest")
}
/// A chest block's name with its `type` and `facing` properties.
#[derive(Clone, Copy, Debug)]
pub struct ChestHalf<'a> {
pub block: &'a str,
pub kind: &'a str,
pub facing: &'a str,
}
impl ChestHalf<'_> {
/// `ChestBlock.getConnectedDirection` as an (x, z) step to the other
/// half: clockwise from the facing for a left half, counter-clockwise for
/// a right one. None for a single chest.
pub fn connected_step(&self) -> Option<(i32, i32)> {
let step = |facing: &str| match facing {
"north" => Some((0, -1)),
"south" => Some((0, 1)),
"west" => Some((-1, 0)),
"east" => Some((1, 0)),
_ => None,
};
let (x, z) = step(self.facing)?;
// Turning clockwise seen from above takes (x, z) to (-z, x).
match self.kind {
"left" => Some((-z, x)),
"right" => Some((z, -x)),
_ => None,
}
}
/// `DoubleBlockCombiner.combineWithNeigbour` as `ChestBlock.combine` calls
/// it: `other`, the block in this half's connected direction, is its other
/// half when it is the same block, a half of the other side and faces the
/// same way. Copper chests in different weathering states never join
/// (`CopperChestBlock` keeps both halves in one state instead).
pub fn pairs_with(&self, other: &ChestHalf) -> bool {
is_chest_block(self.block)
&& self.kind != "single"
&& other.block == self.block
&& other.kind != "single"
&& other.kind != self.kind
&& other.facing == self.facing
}
}
/// `AbstractFurnaceBlockEntity.createExperience`: `count * experience`, floored, and one
/// more with the chance of what is left over (a level random draw only when there is some).
pub fn create_experience(
count: u32,
experience: f32,
random: &mut impl horizon_core::random::RandomSource,
) -> u32 {
let total = count as f32 * experience;
let whole = total.floor();
let fraction = total - whole;
let mut xp = whole.max(0.0) as u32;
if fraction != 0.0 && random.next_float() < fraction {
xp += 1;
}
xp
}
/// The `MenuType` the block entity of a one-block storage container opens
/// (`createMenu`): `ChestMenu.threeRows` for chests, trapped and copper chests
/// and barrels, `ShulkerBoxMenu` for shulker boxes of any colour (its
/// `ShulkerBoxSlot`s refuse shulker boxes), `DispenserMenu` for dispensers
/// and droppers, `HopperMenu` for hoppers. None for other blocks.
pub fn block_menu_type(block: &str) -> Option<&'static str> {
Some(match block {
"minecraft:chest" | "minecraft:trapped_chest" | "minecraft:barrel" => {
"minecraft:generic_9x3"
}
n if n.ends_with("copper_chest") => "minecraft:generic_9x3",
n if n.ends_with("shulker_box") => "minecraft:shulker_box",
"minecraft:dispenser" | "minecraft:dropper" => "minecraft:generic_3x3",
"minecraft:hopper" => "minecraft:hopper",
_ => return None,
})
}
#[cfg(test)]
mod tests {
use super::*;
use horizon_core::random::{LegacyRandom, RandomSource};
#[test]
fn campfire_slots_take_food_cook_it_and_cool_down() {
let mut c = Campfire::default();
let mut beef = ItemStack::new("minecraft:beef", 2);
// placeFood: the first free slot, one item, its recipe's time.
assert!(c.place_food(&mut beef, 600));
assert_eq!(beef.count, 1);
assert_eq!(c.items[0].as_ref().map(|s| s.count), Some(1));
assert_eq!((c.progress[0], c.total[0]), (0, 600));
let mut fish = ItemStack::new("minecraft:cod", 1);
assert!(c.place_food(&mut fish, 3));
assert_eq!(c.items[1].as_ref().map(|s| &*s.item), Some("minecraft:cod"));
// cookTick: the cod is done on its third tick, the beef keeps cooking.
assert!(c.cook_tick().is_empty());
assert!(c.cook_tick().is_empty());
let done = c.cook_tick();
assert_eq!(done.len(), 1);
assert_eq!((done[0].0, &*done[0].1.item), (1, "minecraft:cod"));
assert!(c.items[1].is_none());
assert_eq!(c.progress[0], 3);
// cooldownTick: two a tick, never below zero.
assert!(c.cooldown_tick());
assert_eq!(c.progress[0], 1);
assert!(c.cooldown_tick());
assert_eq!(c.progress[0], 0);
// Full: four slots, no fifth.
for _ in 0..3 {
let mut more = ItemStack::new("minecraft:potato", 1);
assert!(c.place_food(&mut more, 600));
}
let mut fifth = ItemStack::new("minecraft:potato", 1);
assert!(!c.place_food(&mut fifth, 600));
assert_eq!(fifth.count, 1);
}
/// A whole amount pays itself without a draw; a fraction is a chance of one more.
#[test]
fn create_experience_rounds_the_fraction_by_chance() {
let mut random = LegacyRandom::new(7);
let mut untouched = LegacyRandom::new(7);
assert_eq!(create_experience(4, 0.5, &mut random), 2);
assert_eq!(create_experience(3, 0.0, &mut random), 0);
assert_eq!(random.next_long(), untouched.next_long());
let paid: u32 = (0..1000)
.map(|_| create_experience(1, 0.35, &mut random))
.sum();
assert!((250..450).contains(&paid), "{paid}");
assert!((0..100).all(|_| (1..=2).contains(&create_experience(1, 1.7, &mut random))));
}
}
@@ -3,6 +3,7 @@
//! Written to `crash-reports/crash-<yyyy-MM-dd_HH.mm.ss>-client.txt` in the game folder when
//! the main thread panics or `main` gets an error back. It leaves the machine only scrubbed, and
//! only if the player allows crash reports (`telemetry`).
use std::cell::{Cell, RefCell};
use std::fmt::Write as _;
use std::path::{Path, PathBuf};
use std::sync::Mutex;
@@ -14,6 +15,51 @@ use chrono::{DateTime, Local};
static DETAILS: Mutex<Vec<(&'static str, String)>> = Mutex::new(Vec::new());
static REPORTED: AtomicBool = AtomicBool::new(false);
thread_local! {
/// How many `contain` calls this thread is inside.
static CONTAINED: Cell<u32> = const { Cell::new(0) };
/// The message of the last panic `contain` caught on this thread, from the hook.
static CAUGHT: RefCell<Option<String>> = const { RefCell::new(None) };
}
/// Runs `f`, and returns a panic in it as an error with the panic's message instead of letting
/// it end the game. For work that is not the game's own and whose failure the caller recovers
/// from, such as handing a shader pack's programs to the GPU driver's compiler: the panic goes
/// to the log with its stack, and no crash report is made for it.
pub fn contain<T>(f: impl FnOnce() -> T) -> Result<T, String> {
CONTAINED.with(|c| c.set(c.get() + 1));
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f));
CONTAINED.with(|c| c.set(c.get() - 1));
result.map_err(|payload| {
CAUGHT
.with(|c| c.borrow_mut().take())
.unwrap_or_else(|| payload_text(&*payload))
})
}
/// For the hook: a panic inside `contain` is logged with its stack and kept for `contain` to
/// return. False for any other panic.
fn contained_panic(info: &std::panic::PanicHookInfo) -> bool {
if CONTAINED.with(Cell::get) == 0 {
return false;
}
let message = panic_message(info);
let stack = stack_lines(
&std::backtrace::Backtrace::force_capture().to_string(),
true,
);
log::error!(
"Recovered from an error: {message}\n{}",
stack
.lines()
.map(|l| format!("\tat {l}"))
.collect::<Vec<_>>()
.join("\n")
);
CAUGHT.with(|c| *c.borrow_mut() = Some(message));
true
}
/// Sets one line of the report's details (replacing the last value under `key`).
pub fn set_detail(key: &'static str, value: impl Into<String>) {
let mut details = DETAILS.lock().unwrap_or_else(|e| e.into_inner());
@@ -105,9 +151,13 @@ impl Report {
}
}
/// The release this binary is (`v0.2.3`); for a release from before 0.2, its tag and the client's
/// crate version; for a build from source, that version alone.
/// The release this binary is (`v0.2.3`); a nightly's label (`Nightly 2026-10-28 · 0c02067`); for a
/// release from before 0.2, its tag and the client's crate version; for a build from source, that
/// version alone.
pub fn version() -> String {
if let Some(label) = horizon_core::release::nightly_label() {
return label;
}
let version = env!("CARGO_PKG_VERSION");
match (horizon_core::release::RELEASE, release_tag()) {
(Some(_), Some(tag)) => tag,
@@ -117,8 +167,12 @@ pub fn version() -> String {
}
/// The release this binary came with: the version release.yml built in (docs/versioning.md), else
/// the folder the launcher installed it in, named after the tag (releases from before 0.2).
/// the folder the launcher installed it in, named after the tag (releases from before 0.2). A
/// nightly is its version as the launcher installs it (`nightly-2026-10-28-0c02067`).
pub fn release_tag() -> Option<String> {
if let Some(nightly) = horizon_core::release::nightly_version() {
return Some(nightly);
}
if let Some(version) = horizon_core::release::RELEASE {
return Some(format!("v{version}"));
}
@@ -300,13 +354,35 @@ fn deliver(home: &Path, report: Report) -> Option<PathBuf> {
}
}
fn panic_message(info: &std::panic::PanicHookInfo) -> String {
let payload = info.payload();
let text = payload
/// The report for a hung server tick, written by the watchdog thread
/// (`ServerWatchdog`): the stuck thread's stack is not available to Rust, so
/// the report carries none.
pub fn report_watchdog(home: &Path, side: &'static str, message: String) -> Option<PathBuf> {
deliver(
home,
Report {
time: Local::now(),
description: "Watching Server".into(),
error: format!("{message} (Watching Server)"),
stack: String::new(),
thread: thread_name(),
side,
details: system_details(),
log: crate::logging::recent_lines(),
},
)
}
fn payload_text(payload: &(dyn std::any::Any + Send)) -> String {
payload
.downcast_ref::<&str>()
.map(|s| s.to_string())
.or_else(|| payload.downcast_ref::<String>().cloned())
.unwrap_or_else(|| "Box<dyn Any>".into());
.unwrap_or_else(|| "Box<dyn Any>".into())
}
fn panic_message(info: &std::panic::PanicHookInfo) -> String {
let text = payload_text(info.payload());
match info.location() {
Some(at) => format!("panicked at {at}:\n{text}"),
None => format!("panicked:\n{text}"),
@@ -318,16 +394,18 @@ fn panic_message(info: &std::panic::PanicHookInfo) -> String {
/// frames go. Paths inside the workspace lose the build machine's prefix.
pub fn stack_lines(backtrace: &str, from_panic: bool) -> String {
// The workspace as it was on the machine that built this binary (it need not exist here).
let manifest = env!("CARGO_MANIFEST_DIR");
// A report names workspace files the same way on every host, whatever
// separator the build machine used.
let manifest = env!("CARGO_MANIFEST_DIR").replace('\\', "/");
let root = manifest
.strip_suffix("crates/horizon-client")
.unwrap_or(manifest);
.strip_suffix("crates/horizon-client-render")
.unwrap_or(&manifest);
let mut frames: Vec<(String, Option<String>)> = Vec::new();
for line in backtrace.lines() {
let line = line.trim();
if let Some(at) = line.strip_prefix("at ") {
if let Some(frame) = frames.last_mut() {
frame.1 = Some(short_path(at, root).to_owned());
frame.1 = Some(short_path(&at.replace('\\', "/"), root).to_owned());
}
} else if let Some((n, name)) = line.split_once(": ")
&& n.chars().all(|c| c.is_ascii_digit())
@@ -407,10 +485,13 @@ pub fn panic_report(info: &std::panic::PanicHookInfo, side: &'static str) -> Rep
/// Reports panics: on the main thread a crash report (the panic then ends the game, exit code
/// 101); on other threads the error goes to the log, as vanilla's uncaught exception handler
/// does. The default hook still prints the panic.
/// does. The default hook still prints the panic. A panic inside `contain` is only logged.
pub fn install(home: PathBuf, side: &'static str) {
let default = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
if contained_panic(info) {
return;
}
default(info);
let main = std::thread::current().name() == Some("main");
if main {
@@ -525,6 +606,11 @@ mod tests {
#[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.
if let Some(label) = horizon_core::release::nightly_label() {
assert_eq!(super::version(), label);
assert_eq!(release_tag(), horizon_core::release::nightly_version());
return;
}
match horizon_core::release::RELEASE {
Some(version) => {
assert_eq!(release_tag(), Some(format!("v{version}")));
@@ -590,10 +676,21 @@ mod tests {
);
assert_eq!(
stack_lines(&bt, true),
"horizon_client::crash::debug_crash_tick (crates/horizon-client/src/crash.rs:359:9)\nwinit::dispatch (winit-0.30.13/src/event_loop.rs:642:56)\nmain\n"
"horizon_client::crash::debug_crash_tick (crates/horizon-client-render/src/crash.rs:359:9)\nwinit::dispatch (winit-0.30.13/src/event_loop.rs:642:56)\nmain\n"
);
}
#[test]
fn contained_panics_come_back_as_errors() {
assert_eq!(contain(|| 7), Ok(7));
let error = contain(|| -> u8 { panic!("a pack's program") }).unwrap_err();
assert!(error.contains("a pack's program"), "{error}");
// Nested, and the thread is no longer inside one after.
let outer = contain(|| contain(|| -> u8 { panic!("inner") }));
assert!(outer.unwrap().unwrap_err().contains("inner"));
assert_eq!(CONTAINED.with(Cell::get), 0);
}
#[test]
fn the_debug_crash_is_reported_as_vanillas() {
let error: Box<dyn std::error::Error> = DebugCrash::new().into();
@@ -2,7 +2,10 @@
//! the parts of them commands use: `.mcfunction` functions (with macros and
//! line continuation), function tags and predicates.
use crate::zip::Zip;
use horizon_world::nbt::Tag;
use horizon_world::{
nbt::Tag,
worldgen::overrides::{self, PackFile, WorldgenOverrides},
};
use std::{
collections::BTreeMap,
path::{Path, PathBuf},
@@ -125,6 +128,114 @@ pub fn discover(world: &Path) -> Vec<DataPack> {
packs
}
// ---- Create World ------------------------------------------------------------
/// Create World's Data Packs screen (`PackSelectionScreen` on vanilla's
/// `mcworld-` folder): the folder's packs (`PackSource.WORLD`) and the
/// built-in one (`resources` holds its `pack.png`), which can be unselected
/// and stays at the bottom. Horizon has no experiments, so their feature
/// packs are not offered.
pub fn screen_packs(dir: &Path, resources: &Path) -> Vec<crate::pack_screen::PackInfo> {
use crate::{
pack_screen::{Compat, PackInfo, Source, load_icon},
packs::Position,
text::{Style, Text},
};
let format = horizon_core::version::DATA_PACK_FORMAT;
let mut infos: Vec<PackInfo> = crate::packs::folder(dir, format)
.into_iter()
.map(|p| PackInfo {
icon: p.icon_png().as_deref().and_then(load_icon),
id: p.id,
title: p.title,
description: p.raw_description,
description_style: p.description_style,
source: Source::World,
compat: p.compatibility,
required: false,
position: Position::Top,
fixed: false,
})
.collect();
infos.push(PackInfo {
id: "vanilla".into(),
title: Text::translate("dataPack.vanilla.name", &[]),
description: Text::translate("dataPack.vanilla.description", &[]),
description_style: Style::default(),
source: Source::BuiltIn,
compat: Compat::Compatible,
icon: std::fs::read(resources.join("pack.png"))
.ok()
.as_deref()
.and_then(load_icon),
required: false,
position: Position::Bottom,
fixed: false,
});
infos
}
/// A new empty folder for Create World's packs
/// (`Files.createTempDirectory("mcworld-")`).
pub fn temp_folder() -> std::io::Result<PathBuf> {
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos();
let dir = std::env::temp_dir().join(format!("mcworld-{}-{nanos}", std::process::id()));
std::fs::create_dir_all(&dir)?;
Ok(dir)
}
/// What's in a folder of packs, to notice packs added or removed while the
/// screen shows it (`PackSelectionScreen.Watcher`).
pub fn folder_stamp(dir: &Path) -> Vec<(String, Option<std::time::SystemTime>)> {
let mut stamp: Vec<_> = std::fs::read_dir(dir)
.into_iter()
.flatten()
.flatten()
.map(|e| {
let modified = e.metadata().and_then(|m| m.modified()).ok();
(e.file_name().to_string_lossy().into_owned(), modified)
})
.collect();
stamp.sort();
stamp
}
/// Create World's packs into the new world before it opens
/// (`copyTempDataPackDirToNewWorld`): every pack of `dir` goes to
/// `<world>/datapacks`, and `datapacks.json` enables those `selected`
/// (lowest priority first) and disables the rest.
pub fn install(dir: &Path, world: &Path, selected: &[String]) -> Result<(), String> {
let err = |p: &Path, e: std::io::Error| format!("{}: {e}", p.display());
let target = world.join("datapacks");
std::fs::create_dir_all(&target).map_err(|e| err(&target, e))?;
let mut disabled = Vec::new();
for pack in crate::packs::folder(dir, horizon_core::version::DATA_PACK_FORMAT) {
let (crate::packs::Source::Dir(from) | crate::packs::Source::Zip(from)) = &pack.source;
copy(from, &target.join(pack.id.trim_start_matches("file/")))?;
if !selected.contains(&pack.id) {
disabled.push(pack.id);
}
}
if !selected.iter().any(|id| id == "vanilla") {
disabled.push("vanilla".into());
}
let json = serde_json::json!({ "enabled": selected, "disabled": disabled });
let file = world.join("datapacks.json");
std::fs::write(&file, json.to_string()).map_err(|e| err(&file, e))
}
/// A file, or a folder and everything in it.
fn copy(from: &Path, to: &Path) -> Result<(), String> {
let err = |e: std::io::Error| format!("{} -> {}: {e}", from.display(), to.display());
if !from.is_dir() {
return std::fs::copy(from, to).map(drop).map_err(err);
}
std::fs::create_dir_all(to).map_err(err)?;
for e in std::fs::read_dir(from).map_err(err)?.flatten() {
copy(&e.path(), &to.join(e.file_name()))?;
}
Ok(())
}
// ---- functions ---------------------------------------------------------------
/// A `.mcfunction`: plain command lines and macro lines.
@@ -368,11 +479,21 @@ impl DataStore {
/// included, each function once).
pub fn tag_functions(&self, id: &str) -> Vec<String> {
let mut out = Vec::new();
self.expand(id, &mut out, 0);
let mut seen_fns = std::collections::HashSet::new();
let mut seen_tags = std::collections::HashSet::new();
self.expand(id, &mut out, &mut seen_fns, &mut seen_tags);
out
}
fn expand(&self, id: &str, out: &mut Vec<String>, depth: usize) {
if depth > 16 {
/// Depth-first walk keeping first-seen order. Each tag is expanded at most
/// once, so reference cycles end and nesting depth is not capped.
fn expand<'a>(
&'a self,
id: &'a str,
out: &mut Vec<String>,
seen_fns: &mut std::collections::HashSet<&'a str>,
seen_tags: &mut std::collections::HashSet<&'a str>,
) {
if !seen_tags.insert(id) {
return;
}
let Some(entries) = self.function_tags.get(id) else {
@@ -380,9 +501,9 @@ impl DataStore {
};
for (e, required) in entries {
match e.strip_prefix('#') {
Some(inner) => self.expand(inner, out, depth + 1),
Some(inner) => self.expand(inner, out, seen_fns, seen_tags),
None => {
if (self.functions.contains_key(e) || *required) && !out.contains(e) {
if (self.functions.contains_key(e) || *required) && seen_fns.insert(e) {
out.push(e.clone());
}
}
@@ -421,9 +542,138 @@ fn hash_normalize(s: &str) -> String {
}
}
/// The ids of `packs` (a world's, from `discover`) that are enabled, lowest
/// priority first: those `<world>/datapacks.json` lists as enabled, in its
/// order, then every pack it lists neither way, in name order.
pub fn enabled_ids(world: &Path, packs: &[DataPack]) -> Vec<String> {
let saved: Option<serde_json::Value> = std::fs::read(world.join("datapacks.json"))
.ok()
.and_then(|b| serde_json::from_slice(&b).ok());
let list = |key: &str| -> Vec<String> {
saved
.as_ref()
.and_then(|v| v[key].as_array())
.map(|a| {
a.iter()
.filter_map(|s| s.as_str().map(str::to_owned))
.collect()
})
.unwrap_or_default()
};
let disabled = list("disabled");
let mut enabled: Vec<String> = list("enabled")
.into_iter()
.filter(|id| packs.iter().any(|p| &p.id == id))
.collect();
for p in packs {
if !enabled.contains(&p.id) && !disabled.contains(&p.id) {
enabled.push(p.id.clone());
}
}
enabled
}
// ---- worldgen ----------------------------------------------------------------
/// The worldgen entries of `packs` (lowest priority first, the built-in pack
/// left out: the prepared bundles are it) for the chunk generator, and what
/// it leaves out, for the log. `biomes` says which biomes the game has: a
/// pack's new biome is left out (the game has no colours, sounds or id for it).
pub fn worldgen_overrides(
packs: &[&DataPack],
biomes: impl Fn(&str) -> bool,
) -> (WorldgenOverrides, Vec<String>) {
let mut out = WorldgenOverrides::default();
let mut not_read: BTreeMap<String, usize> = BTreeMap::new();
let mut warnings = Vec::new();
for pack in packs {
for file in pack.list_kinds(&overrides::PACK_KINDS) {
let read = || String::from_utf8(pack.read(&file)?).ok();
match out.add_file(&pack.id, &file, read) {
PackFile::Read | PackFile::Other => {}
PackFile::DimensionType { id, min_y, height } => warnings.push(format!(
"{}: dimension type {id} (min_y {min_y}, height {height}) is not used: \
the dimension keeps its vanilla build height",
pack.id
)),
PackFile::NotRead(kind) => {
*not_read.entry(format!("{}: {kind}", pack.id)).or_default() += 1
}
PackFile::Unreadable => warnings.push(format!("{}: {file}: unreadable", pack.id)),
}
}
}
warnings.extend(out.finish(biomes));
for (kind, files) in not_read {
warnings.push(format!("{kind}: {files} files the generator does not read"));
}
(out, warnings)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn enabled_packs_override_worldgen_in_order() {
let dir = std::env::temp_dir().join(format!("hz-datapack-worldgen-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
let write = |pack: &str, p: &str, t: &str| {
let path = dir.join("datapacks").join(pack).join(p);
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(path, t).unwrap();
};
for pack in ["a", "b", "off"] {
write(pack, "pack.mcmeta", "{}");
write(pack, "data/minecraft/worldgen/noise/cave.json", pack);
write(
pack,
"data/minecraft/tags/worldgen/biome/is_hill.json",
"{\"values\":[]}",
);
}
write("a", "data/minecraft/worldgen/biome/new_peaks.json", "{}");
write("a", "data/minecraft/worldgen/biome/plains.json", "{}");
write(
"a",
"data/minecraft/worldgen/world_preset/normal.json",
"{}",
);
write(
"b",
"data/minecraft/dimension_type/overworld.json",
"{\"min_y\":-64,\"height\":2096}",
);
// `b` is enabled first, so `a` (enabled after it) wins.
std::fs::write(
dir.join("datapacks.json"),
"{\"enabled\":[\"file/b\",\"file/a\"],\"disabled\":[\"file/off\"]}",
)
.unwrap();
let packs = discover(&dir);
let enabled = enabled_ids(&dir, &packs);
assert_eq!(enabled, ["file/b", "file/a"]);
let enabled: Vec<&DataPack> = enabled
.iter()
.map(|id| packs.iter().find(|p| &p.id == id).unwrap())
.collect();
let (o, warnings) = worldgen_overrides(&enabled, |b| b == "minecraft:plains");
assert_eq!(
o.entries("noises").collect::<Vec<_>>(),
[("minecraft:cave", "a")]
);
assert_eq!(
o.entries("biomes").collect::<Vec<_>>(),
[("minecraft:plains", "{}")]
);
let tag_packs: Vec<&str> = o.tags("biome_tags").map(|(_, p, _)| p).collect();
assert_eq!(tag_packs, ["file/b", "file/a"]);
assert_eq!(warnings.len(), 3, "{warnings:?}");
assert!(warnings[0].contains("height 2096"));
assert!(warnings[1].contains("minecraft:new_peaks"));
assert!(warnings[2].contains("worldgen/world_preset"));
let _ = std::fs::remove_dir_all(&dir);
}
use horizon_world::nbt::parse_snbt;
#[test]
@@ -527,6 +777,40 @@ mod tests {
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn tag_resolution_dedupes_fast_ends_cycles_and_nests_deeply() {
let mut store = DataStore::default();
let big: Vec<(String, bool)> = (0..50_000)
.map(|i| (format!("t:f{}", i % 25_000), true))
.collect();
store.function_tags.insert("t:big".into(), big);
let start = std::time::Instant::now();
let got = store.tag_functions("t:big");
assert_eq!(got.len(), 25_000);
assert_eq!(got[0], "t:f0");
assert!(start.elapsed() < std::time::Duration::from_secs(5));
// A cycle terminates and keeps first-seen order.
store.function_tags.insert(
"t:a".into(),
vec![("#t:b".into(), true), ("t:x".into(), true)],
);
store.function_tags.insert(
"t:b".into(),
vec![("#t:a".into(), true), ("t:y".into(), true)],
);
assert_eq!(store.tag_functions("t:a"), ["t:y", "t:x"]);
// Nesting deeper than the old cap of 16 still resolves.
for i in 0..40 {
store
.function_tags
.insert(format!("d:{i}"), vec![(format!("#d:{}", i + 1), true)]);
}
store
.function_tags
.insert("d:40".into(), vec![("t:deep".into(), true)]);
assert_eq!(store.tag_functions("d:0"), ["t:deep"]);
}
#[test]
fn folder_packs_list_the_needed_kinds_in_walk_order() {
let dir = std::env::temp_dir().join(format!("hz-datapack-kinds-{}", std::process::id()));
@@ -566,4 +850,51 @@ mod tests {
assert_eq!(p.list_kinds(&kinds), wanted);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn create_world_installs_its_packs() {
let root = std::env::temp_dir().join(format!("hz-datapack-install-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&root);
let (dir, world) = (root.join("mcworld"), root.join("world"));
let write = |pack: &str, p: &str, t: &str| {
let path = dir.join(pack).join(p);
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(path, t).unwrap();
};
write("fns", "pack.mcmeta", r#"{"pack":{"pack_format":121}}"#);
write("fns", "data/x/function/f.mcfunction", "say hi");
write("off", "pack.mcmeta", r#"{"pack":{"pack_format":121}}"#);
std::fs::create_dir_all(&dir).unwrap();
let mut zip = zip::ZipWriter::new(std::fs::File::create(dir.join("old.zip")).unwrap());
zip.start_file("pack.mcmeta", zip::write::SimpleFileOptions::default())
.unwrap();
std::io::Write::write_all(&mut zip, br#"{"pack":{"pack_format":48}}"#).unwrap();
zip.finish().unwrap();
let infos = screen_packs(&dir, &root);
let ids: Vec<&str> = infos.iter().map(|p| p.id.as_str()).collect();
assert_eq!(ids, ["file/fns", "file/off", "file/old.zip", "vanilla"]);
let compat: Vec<_> = infos.iter().map(|p| p.compat).collect();
use crate::pack_screen::Compat;
assert_eq!(
compat,
[
Compat::Compatible,
Compat::Compatible,
Compat::TooOld,
Compat::Compatible
]
);
let selected = ["vanilla", "file/old.zip", "file/fns"].map(String::from);
install(&dir, &world, &selected).unwrap();
let packs = discover(&world);
let ids: Vec<&str> = packs.iter().map(|p| p.id.as_str()).collect();
assert_eq!(ids, ["file/fns", "file/off", "file/old.zip"]);
let saved: serde_json::Value =
serde_json::from_slice(&std::fs::read(world.join("datapacks.json")).unwrap()).unwrap();
assert_eq!(
saved,
serde_json::json!({"enabled": selected, "disabled": ["file/off"]})
);
let _ = std::fs::remove_dir_all(&root);
}
}
@@ -2,7 +2,7 @@
//! add/update with hidden effects, per-tick application of the periodic
//! effects (regeneration, poison, wither, hunger, saturation, instant
//! effects), and the derived flags the movement and damage code read.
use crate::brewing;
use horizon_client_core::potions;
#[derive(Clone, Debug, PartialEq)]
pub struct EffectInstance {
@@ -29,6 +29,10 @@ impl EffectInstance {
fn infinite(&self) -> bool {
self.duration == -1
}
/// `MobEffectInstance.endsWithin`.
pub fn ends_within(&self, ticks: i32) -> bool {
!self.infinite() && self.duration <= ticks
}
fn shorter_than(&self, other: &Self) -> bool {
!self.infinite() && (self.duration < other.duration || other.infinite())
}
@@ -129,15 +133,16 @@ fn periodic(id: &str, tick: i32, amp: i32) -> Option<EffectAction> {
_ => None,
}
}
/// `InstantenousMobEffect.applyInstantenousEffect` (potions apply these directly).
/// `HealOrHarmMobEffect.applyInstantaneousEffect` (potions apply these directly;
/// the amount rounds to the nearest whole point).
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))) as i32 as f32,
(scale * f64::from(4 << amp.min(30)) + 0.5) as i32 as f32,
)),
"instant_damage" => Some(EffectAction::Hurt(
"minecraft:magic",
(scale * f64::from(6 << amp.min(30))) as i32 as f32,
(scale * f64::from(6 << amp.min(30)) + 0.5) as i32 as f32,
)),
"saturation" => Some(EffectAction::Eat(amp + 1, 1.0)),
_ => None,
@@ -183,7 +188,7 @@ impl PlayerEffects {
pub fn from_saved(saved: &[(String, i32, i32, bool, bool)]) -> Self {
let mut effects = Self::default();
for (id, duration, amplifier, ambient, particles) in saved {
if let Some(def) = brewing::effect(id) {
if let Some(def) = potions::effect(id) {
let mut e = EffectInstance::new(def.id, *duration, *amplifier);
e.ambient = *ambient;
e.visible = *particles;
@@ -306,10 +311,10 @@ impl PlayerEffects {
) -> (Vec<EffectInstance>, Vec<EffectAction>) {
let mut lasting = Vec::new();
let mut now = Vec::new();
if let Some(def) = potion.and_then(brewing::potion) {
if let Some(def) = potion.and_then(potions::potion) {
for (id, duration, amp) in def.effects {
let id = brewing::effect(id).map_or("speed", |e| e.id);
if brewing::effect(id).is_some_and(|e| e.instant) {
let id = potions::effect(id).map_or("speed", |e| e.id);
if potions::effect(id).is_some_and(|e| e.instant) {
now.extend(instant(id, *amp, 1.0));
} else {
// withScaledDuration: max(floor(d * scale), 1).
@@ -14,19 +14,27 @@ struct Globals {
// Atlas rects (u0, v0, width, height) of end_sky.png and end_portal.png.
end_portal_sprites: array<vec4<f32>, 2>,
// x: vanilla's GameTime ((game time % 24000 + partial tick) / 24000)
// y: Nether cardinal lighting, z: how many dynamic_lights are set.
// y: Nether cardinal lighting, z: how many dynamic_lights are set,
// w: the glint scroll clock (`Glint::scroll_ticks`).
time: vec4<f32>,
crumbling_sprite: vec4<f32>,
// Selection outline parameters (read by terrain.wgsl).
outline_params: vec4<f32>,
// Horizon Extras' dynamic lights: camera-relative position and level (dynamic_light.rs).
dynamic_lights: array<vec4<f32>, 16>,
sunrise_fan: array<vec4<f32>, 32>,
}
@group(0) @binding(0) var<uniform> globals: Globals;
@group(0) @binding(1) var atlas: texture_2d<f32>;
@group(0) @binding(2) var atlas_sampler: sampler;
@group(0) @binding(3) var lightmap: texture_2d<f32>;
@group(0) @binding(4) var lightmap_sampler: sampler;
// enchanted_glint_item on its own repeating texture (vanilla's GlintSampler).
@group(0) @binding(5) var glint_tex: texture_2d<f32>;
@group(0) @binding(6) var glint_sampler: sampler;
// The armour layers' GlintSampler: vanilla samples enchanted_glint_armor with
// the same repeating, linearly filtered sampler as the item glint.
@group(0) @binding(7) var glint_armor_tex: texture_2d<f32>;
struct Out {
@builtin(position) clip: vec4<f32>,
@location(0) uv: vec2<f32>,
@@ -44,6 +52,14 @@ struct Out {
// entity.vsh's lightMapColor, multiplied in after the overlay.
@location(9) light_color: vec4<f32>,
@location(10) @interpolate(flat) weather_rect: vec4<f32>,
// The `GLINT_TEXTURING` sample for overlay 7 (item foil), or the model UV
// `ARMOR_ENTITY_GLINT_TEXTURING` scrolls for overlay 13 (armour).
@location(11) glint_uv: vec2<f32>,
// 1 drops the quad's back-facing pixels (Vertex::normal w = 1), like a culling render type.
@location(12) @interpolate(flat) cull: u32,
// HDR tier of a light-giving particle (mesher::hdr_tier), from the overlay's
// high bits (particles.rs): fs_hdr_mark marks the frame with it.
@location(13) @interpolate(flat) tier: u32,
}
// Shipped light.glsl; normals are already transformed and RGBA8_SNORM packed.
fn entity_light(normal: vec3<f32>, nether: bool) -> f32 {
@@ -92,7 +108,10 @@ fn entity_vertex(position: vec3<f32>, uv: vec2<f32>,
// minecraft_mix_light: the vertex colour times the cardinal light only.
var c = color;
var back = c;
if normal.w != 0.0 {
// Normal w: 0 keeps the baked colour, +-127 shades by the normal, and a small positive 1
// keeps the baked colour but culls back faces (entitySolid models such as the decorated pot).
out.cull = select(0u, 1u, normal.w > 0.0 && normal.w < 0.5);
if abs(normal.w) >= 0.5 {
c = vec4(c.rgb * entity_light(normal.xyz, globals.time.y != 0.0), c.a);
back = select(c, vec4(back.rgb * entity_light(-normal.xyz, globals.time.y != 0.0), back.a), normal.w < 0.0);
}
@@ -103,18 +122,24 @@ fn entity_vertex(position: vec3<f32>, uv: vec2<f32>,
out.spherical = select(length(position), 0.0, overlay >= 2u && overlay <= 4u);
out.cylindrical = select(max(length(position.xz), abs(position.y)), 0.0, overlay >= 2u && overlay <= 4u);
out.overlay = overlay;
out.glint_uv = vec2(0.0);
if overlay == 4u { out.spherical = length(position); }
return out;
}
@vertex fn vs_entity(@location(0) position: vec3<f32>, @location(1) uv: vec2<f32>,
@location(2) overlay: u32, @location(3) color: vec4<f32>, @location(4) light: vec4<u32>, @location(5) normal: vec4<f32>) -> Out {
return entity_vertex(position, uv, overlay, color, light, normal);
@location(2) overlay: u32, @location(3) color: vec4<f32>, @location(4) light: vec4<u32>,
@location(5) normal: vec4<f32>, @location(6) glint_uv: vec2<f32>) -> Out {
var out = entity_vertex(position, uv, overlay & 0xffffu, color, light, normal);
out.glint_uv = glint_uv;
out.tier = overlay >> 16u;
return out;
}
// Retained bolt-local geometry, translated using the same f64 camera
// subtraction as the former CPU path before adding each local f32 position.
// The bolt offset is instance attribute 7: 6 is the entity vertex's glint UV.
@vertex fn vs_lightning(@location(0) position: vec3<f32>, @location(1) uv: vec2<f32>,
@location(2) overlay: u32, @location(3) color: vec4<f32>, @location(4) light: vec4<u32>, @location(5) normal: vec4<f32>,
@location(6) offset: vec3<f32>) -> Out {
@location(7) offset: vec3<f32>) -> Out {
return entity_vertex(offset + position, uv, overlay, color, light, normal);
}
struct CloudInfo {
@@ -166,6 +191,27 @@ fn linear_fog(d: f32, start: f32, end: f32) -> f32 {
if d >= end { return 1.0; }
return (d - start) / (end - start);
}
// TextureTransform.setupGlintTexturing(0.16), ARMOR_ENTITY_GLINT_TEXTURING.
// globals.time.w is (Util.getMillis() * glintSpeed * 8) modulo 330000, the
// common period of both offsets.
fn armor_glint_uv(model: vec2<f32>) -> vec2<f32> {
let ticks = globals.time.w;
let offset0 = (ticks % 110000.0) / 110000.0;
let offset1 = (ticks % 30000.0) / 30000.0;
let angle = 0.17453292;
let c = cos(angle);
let s = sin(angle);
let scaled = model * 0.16;
return vec2(scaled.x * c - scaled.y * s - offset0,
scaled.x * s + scaled.y * c + offset1);
}
// RenderPipelines.GLINT: the source colour is scaled by itself, so `GlintAlpha`
// is squared along with the sample.
fn glint_over(color: vec4<f32>, glint: vec4<f32>) -> vec4<f32> {
let alpha = globals.fog_ends.w;
let g = alpha * glint;
return vec4(color.rgb + g.rgb * g.rgb, max(color.a, alpha));
}
// CRUMBLING_UV_BEGIN
fn crumbling_uv(uv: vec2<f32>, packed_origin: vec2<f32>, sprite_size: vec2<f32>, dimensions: vec2<u32>) -> vec2<f32> {
let size = vec2<f32>(dimensions);
@@ -198,27 +244,53 @@ fn crumbling_uv(uv: vec2<f32>, packed_origin: vec2<f32>, sprite_size: vec2<f32>,
// clouds.fsh: alpha fades out towards the cloud fog end.
return vec4(color.rgb, color.a * (1.0 - linear_fog(in.spherical, 0.0, globals.fog_ends.y)));
}
if color.a < 0.1 { discard; }
if color.a < 0.1 || (in.cull != 0u && !front) { discard; }
// entity.fsh calculateFinalColor: the overlay (hurt red, TNT white) over the
// lit texture, then the lightmap, then the glint and fog.
color = vec4(mix(in.overlay_color.rgb, color.rgb, in.overlay_color.a), color.a);
color *= in.light_color;
if in.overlay == 7u || in.overlay == 8u || in.overlay == 33u {
// Enchantment glint (overlay 7, 8 with the hurt tint): the purple
// enchanted_glint sheen drifting diagonally over the item's shape.
let px = in.uv * vec2<f32>(textureDimensions(atlas));
let t = globals.misc.y * 0.05;
let band = sin((px.x + px.y) * 0.9 - t * 6.0) * 0.5 + 0.5;
let band2 = sin((px.x - px.y) * 0.6 + t * 4.0) * 0.5 + 0.5;
let shimmer = pow(band * 0.6 + band2 * 0.4, 2.0);
let glint = vec3(0.50, 0.25, 0.80) * (0.25 + 0.75 * shimmer);
color = vec4(min(color.rgb + glint * 0.6, vec3(1.0)), color.a);
// 33: a translucent item with the glint.
// Enchantment glint: item.fsh's GLINT path, sampled from
// enchanted_glint_item at the `GLINT_TEXTURING` UV.
color = glint_over(color, textureSample(glint_tex, glint_sampler, in.glint_uv));
} else if in.overlay == 13u {
// Enchanted armour: enchanted_glint_armor at the scrolling model UV.
color = glint_over(
color,
textureSample(glint_armor_tex, glint_sampler, armor_glint_uv(in.glint_uv)),
);
}
let f = max(linear_fog(in.spherical, globals.fog.x, globals.fog.y),
linear_fog(in.cylindrical, globals.fog.z, globals.fog.w));
return vec4(mix(color.rgb, globals.fog_color.rgb, f * globals.fog_color.a), color.a);
}
// rendertype_outline.fsh: the vertex colour (the team colour; outline vertices carry no
// normal, so no cardinal light) wherever the texture is not fully clear.
// rendertype_entity_shadow.fsh: the shadow texture times the vertex colour,
// then fog, with no alpha cutout, so a faint shadow in dim light still draws.
@fragment fn fs_entity_shadow(in: Out) -> @location(0) vec4<f32> {
var color = textureSample(atlas, atlas_sampler, in.uv) * in.color;
color *= in.light_color;
let f = max(linear_fog(in.spherical, globals.fog.x, globals.fog.y),
linear_fog(in.cylindrical, globals.fog.z, globals.fog.w));
return vec4(mix(color.rgb, globals.fog_color.rgb, f * globals.fog_color.a), color.a);
}
@fragment fn fs_outline(in: Out) -> @location(0) vec4<f32> {
if textureSample(atlas, atlas_sampler, in.uv).a == 0.0 { discard; }
return vec4(in.color.rgb, 1.0);
}
// HDR output: a light-giving particle's pixels marked in the frame's alpha the
// way terrain.wgsl marks a light source's, drawn again after the entities with
// only alpha written, so its colour stays as blended.
@fragment fn fs_hdr_mark(in: Out) -> @location(0) vec4<f32> {
if in.tier == 0u || textureSample(atlas, atlas_sampler, in.uv).a < 0.1 { discard; }
return vec4(0.0, 0.0, 0.0, 0.7 * f32(4u - min(in.tier, 4u)) / 4.0);
}
fn celestial_color(in: Out) -> vec4<f32> {
return textureSample(atlas, atlas_sampler, in.uv) * in.color * in.light_color;
}
@@ -237,6 +309,20 @@ fn celestial_color(in: Out) -> vec4<f32> {
return vec4(added, smoothstep(0.1, 0.3, max(added.r, max(added.g, added.b))));
}
// RenderPipelines.ENERGY_SWIRL (entity.fsh with EMISSIVE and NO_OVERLAY): the sprite, scrolled by
// the UV and repeated within itself, times the layer colour 0xFF808080, unlit; the pipeline adds
// it to the frame. The light bytes carry the sprite's origin and the colour bytes its size, both
// as breaking::stage_light packs them.
@fragment fn fs_energy_swirl(in: Out) -> @location(0) vec4<f32> {
let bytes = round(in.color * 255.0);
let size = vec2(bytes.x + bytes.y * 256.0, bytes.z + bytes.w * 256.0) / 65535.0;
let color = textureSample(atlas, atlas_sampler, crumbling_uv(in.uv, in.wrap, size, textureDimensions(atlas)));
if color.a < 0.1 { discard; }
let f = max(linear_fog(in.spherical, globals.fog.x, globals.fog.y),
linear_fog(in.cylindrical, globals.fog.z, globals.fog.w));
return vec4(mix(color.rgb * (128.0 / 255.0), globals.fog_color.rgb, f * globals.fog_color.a), color.a);
}
// RenderPipelines.WATER_MASK: depth only (the pipeline masks the colour off).
@fragment fn fs_water_mask() -> @location(0) vec4<f32> {
return vec4(0.0);
@@ -389,3 +389,36 @@ pub fn apply_weather(sky: &mut Sky, rain: f32, thunder: f32) {
);
}
}
pub fn sky_darken(environment: &Environment, day_time: i64, rain: f32, thunder: f32) -> i32 {
let mut sky = environment.sample(day_time, None, None);
apply_weather(&mut sky, rain, thunder);
// Level.updateSkyBrightness: darkening from the sky light level.
(15.0 - 15.0 * sky.sky_light_level).round() as i32
}
#[cfg(test)]
mod tests {
use super::*;
fn overworld() -> Option<Environment> {
let root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../.local/horizon-client/26.3");
root.join("data/minecraft/timeline/day.json")
.exists()
.then(|| Environment::load(&root).unwrap())
}
#[test]
fn dark_outside_follows_time_and_weather_levels() {
let Some(env) = overworld() else {
return;
};
let dark = |time, rain, thunder| sky_darken(&env, time, rain, thunder) >= 4;
assert!(!dark(6000, 0.0, 0.0), "clear noon");
assert!(!dark(6000, 1.0, 0.0), "rainy noon");
assert!(dark(6000, 1.0, 1.0), "thunderstorm at noon");
assert!(dark(18000, 0.0, 0.0), "clear midnight");
assert!(!dark(1000, 0.0, 0.0), "clear morning");
}
}
@@ -72,9 +72,8 @@ pub struct Extras {
pub zoom_smooth: bool,
/// Divide mouse turning by the zoom.
pub zoom_sensitivity: bool,
/// The FPS counter and keystrokes extras are official mods now
/// (`moved_extras`): their old settings stay here until the game has
/// installed the mod for a player who had one on.
/// The FPS counter and keystrokes extras are official mods now: their
/// old settings stay here, kept as they were.
pub fps: bool,
pub fps_corner: Corner,
/// Also show the minimum and 1% low.
@@ -83,8 +82,8 @@ pub struct Extras {
pub keystrokes_corner: Corner,
/// Clicks per second under the mouse buttons.
pub keystrokes_cps: bool,
/// The official mods installed for the extras they replace, by mod id
/// (`movedToMods`): each is done once.
/// The official mods once installed for the extras they replace, by mod
/// id (`movedToMods`).
pub moved: Vec<String>,
pub minimap: bool,
pub minimap_corner: Corner,
@@ -97,6 +96,8 @@ pub struct Extras {
pub minimap_rotate: bool,
/// Retry lost multiplayer connections (`reconnect`).
pub auto_reconnect: bool,
/// The player's own name tag over its model in third person.
pub own_name_tag: bool,
/// Mouse Tweaks-style drags in the inventory and storage containers.
pub mouse_tweaks: bool,
/// With them, the wheel over a slot moves its items one at a time.
@@ -137,6 +138,7 @@ impl Default for Extras {
minimap_zoom: 1.0,
minimap_rotate: false,
auto_reconnect: false,
own_name_tag: false,
mouse_tweaks: false,
mouse_tweaks_wheel: true,
sort: false,
@@ -211,6 +213,7 @@ impl Extras {
}
"minimapRotate" => e.minimap_rotate = flag(e.minimap_rotate),
"autoReconnect" => e.auto_reconnect = flag(e.auto_reconnect),
"ownNameTag" => e.own_name_tag = flag(e.own_name_tag),
"mouseTweaks" => e.mouse_tweaks = flag(e.mouse_tweaks),
"mouseTweaksWheel" => e.mouse_tweaks_wheel = flag(e.mouse_tweaks_wheel),
"sort" => e.sort = flag(e.sort),
@@ -225,7 +228,7 @@ impl Extras {
}
pub fn serialize(&self) -> String {
format!(
"fullbright:{}\ndynamicLights:{}\nzoom:{}\nzoomKey:{}\nzoomLevel:{}\nzoomSmooth:{}\nzoomSensitivity:{}\nfps:{}\nfpsCorner:{}\nfpsLows:{}\nkeystrokes:{}\nkeystrokesCorner:{}\nkeystrokesCps:{}\nmovedToMods:{}\nminimap:{}\nminimapCorner:{}\nminimapSize:{}\nminimapShape:{}\nminimapZoom:{}\nminimapRotate:{}\nautoReconnect:{}\nmouseTweaks:{}\nmouseTweaksWheel:{}\nsort:{}\nsortKey:{}\n{}:{}\nagentControl:{}\ndiscordPresence:{}\n",
"fullbright:{}\ndynamicLights:{}\nzoom:{}\nzoomKey:{}\nzoomLevel:{}\nzoomSmooth:{}\nzoomSensitivity:{}\nfps:{}\nfpsCorner:{}\nfpsLows:{}\nkeystrokes:{}\nkeystrokesCorner:{}\nkeystrokesCps:{}\nmovedToMods:{}\nminimap:{}\nminimapCorner:{}\nminimapSize:{}\nminimapShape:{}\nminimapZoom:{}\nminimapRotate:{}\nautoReconnect:{}\nownNameTag:{}\nmouseTweaks:{}\nmouseTweaksWheel:{}\nsort:{}\nsortKey:{}\n{}:{}\nagentControl:{}\ndiscordPresence:{}\n",
bool_text(self.fullbright),
bool_text(self.dynamic_lights),
bool_text(self.zoom),
@@ -251,6 +254,7 @@ impl Extras {
self.minimap_zoom,
bool_text(self.minimap_rotate),
bool_text(self.auto_reconnect),
bool_text(self.own_name_tag),
bool_text(self.mouse_tweaks),
bool_text(self.mouse_tweaks_wheel),
bool_text(self.sort),
@@ -282,7 +286,7 @@ impl Extras {
}
/// The field-of-view modifier target `Camera.tickFov` eases toward: vanilla's
/// (`getFieldOfViewModifier`) divided by a mod's zoom (`render.set-zoom`). A
/// (`getFieldOfViewModifier`) divided by a mod's zoom (`native_zoom`). A
/// zoom of 1 leaves it as is.
pub fn fov_target(vanilla: f32, zoom: f32) -> f32 {
if zoom == 1.0 { vanilla } else { vanilla / zoom }
@@ -378,6 +382,7 @@ impl Cps {
pub enum Item {
Fullbright,
DynamicLights,
OwnNameTag,
AutoReconnect,
MouseTweaks,
MouseTweaksWheel,
@@ -396,6 +401,8 @@ pub fn rows() -> Vec<Result<Item, &'static str>> {
Err("Lighting"),
Ok(Fullbright),
Ok(DynamicLights),
Err("Third person"),
Ok(OwnNameTag),
Err("Inventory"),
Ok(MouseTweaks),
Ok(MouseTweaksWheel),
@@ -410,6 +417,7 @@ impl Item {
match self {
Item::Fullbright => "Fullbright",
Item::DynamicLights => "Dynamic lights",
Item::OwnNameTag => "Own name tag",
Item::AutoReconnect => "Auto-reconnect",
Item::MouseTweaks => "Mouse Tweaks controls",
Item::MouseTweaksWheel => "Scroll wheel moves items",
@@ -426,7 +434,7 @@ impl Item {
Some(match self {
MouseTweaksWheel => MouseTweaks,
SortKey => Sort,
Fullbright | DynamicLights | MouseTweaks | Sort | AutoReconnect | NoAi
Fullbright | DynamicLights | OwnNameTag | MouseTweaks | Sort | AutoReconnect | NoAi
| AgentControl | DiscordPresence => {
return None;
}
@@ -439,6 +447,9 @@ impl Item {
Item::DynamicLights => {
"Torches and other light sources held, dropped or on fire light the blocks around them."
}
Item::OwnNameTag => {
"Shows your name above your head in third person, as other players see it. Hidden when the server hides your team's names or you're invisible."
}
Item::AutoReconnect => {
"Rejoins a server after a lost connection or a restart, never after a kick or ban."
}
@@ -471,7 +482,9 @@ impl Item {
match self {
Item::Fullbright => Some(Cost::None),
Item::DynamicLights => Some(Cost::Low),
Item::AutoReconnect | Item::MouseTweaks | Item::Sort => Some(Cost::None),
Item::OwnNameTag | Item::AutoReconnect | Item::MouseTweaks | Item::Sort => {
Some(Cost::None)
}
_ => None,
}
}
@@ -480,6 +493,7 @@ impl Item {
Some(match self {
Item::Fullbright => e.fullbright,
Item::DynamicLights => e.dynamic_lights,
Item::OwnNameTag => e.own_name_tag,
Item::AutoReconnect => e.auto_reconnect,
Item::MouseTweaks => e.mouse_tweaks,
Item::MouseTweaksWheel => e.mouse_tweaks_wheel,
@@ -506,6 +520,7 @@ impl Item {
match self {
Item::Fullbright => e.fullbright = !e.fullbright,
Item::DynamicLights => e.dynamic_lights = !e.dynamic_lights,
Item::OwnNameTag => e.own_name_tag = !e.own_name_tag,
Item::AutoReconnect => e.auto_reconnect = !e.auto_reconnect,
Item::MouseTweaks => e.mouse_tweaks = !e.mouse_tweaks,
Item::MouseTweaksWheel => e.mouse_tweaks_wheel = !e.mouse_tweaks_wheel,
@@ -534,7 +549,7 @@ mod tests {
let d = Extras::default();
assert!(!d.fullbright && !d.dynamic_lights);
assert!(!d.zoom && !d.fps && !d.keystrokes && !d.minimap && !d.auto_reconnect);
assert!(!d.mouse_tweaks && !d.sort);
assert!(!d.mouse_tweaks && !d.sort && !d.own_name_tag);
assert!(d.discord_presence);
assert_eq!(Extras::parse(""), d);
assert_eq!(Extras::parse(&d.serialize()), d);
@@ -558,6 +573,8 @@ mod tests {
e.minimap_rotate = true;
e.minimap_corner = Corner::BottomLeft;
e.auto_reconnect = true;
Item::OwnNameTag.cycle(&mut e, false);
assert_eq!(Item::OwnNameTag.switch(&e), Some(true));
e.mouse_tweaks = true;
e.mouse_tweaks_wheel = false;
e.sort = true;
Loaded 100 of 1148 files, more files were not shown because too many files have changed in this diff. Show more