🤖 Agent: The CurseForge rows now reuse #9583's curseforge_instance_target (minecraftinstance.json first, manifest.json only if it has no version), so copy-from and `instance others` read the same triple the pack import does. A loader Horizon doesn't name, or one without a version, stays none, as before.
🤖 Agent: verified on playhorizon merged with main: launcher fmt, clippy -D warnings, 541 lib + 102 binary tests. The failure file's tag line is what keeps a launcher already at that release from showing a stale 'update failed'; files from older installers have no tag and still show.
🤖 Agent: #9298 merged; main merged in (clean, nothing since the verified tree touches net.rs, remote.rs or the wire tools). Verified on playhorizon at the stack's top 72f38687b: all five vanilla_wire_e2e rules pass against vanilla 26.3 with the Login held 1 s, known-packs-as-vanilla and known-data-used included; fmt, clippy -p horizon-client --all-targets -D warnings, 910 horizon-client tests.
The failure file now starts with the release that failed, and a launcher
already at it or newer (another copy, the command line or the site put it
in) doesn't say its update failed. Moving the data folder quit the window
with an update still staged, so the installer could start while the
relocation worker copied the folder holding the stage; the move now
drops the staged update and its stage, and the restarted launcher checks
again. From martipops's review of #9311.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: The contact now follows Player.aiStep (inflate 1/0.5/1, or the vehicle union inflated 1/0/1) and Pufferfish.playerTouch (doTeamsAllowDamage, hurt first, then poison with the fish as source), and it runs per guest even with a frozen level. One gap left for later: vanilla sends PUFFER_FISH_STING to the player only when the hurt lands, while the sting sound here still comes from the fish AI every 10 ticks.
🤖 Agent: Every place that removes or renames a listed file now goes through Entry::path, which accepts only plain names under the five install folders (mods, resourcepacks, shaderpacks, horizon-mods, horizon-native-mods), and imports from packs or other launchers can no longer bring their own mod-browser.json. Data packs never enter that list, so nothing the browser installs today is dropped.
🤖 Agent: The wrong-guess total and the retired-code count now last for the whole sharing session, and only as many relayed key exchanges run at once as wrong guesses are left, so re-registering no longer resets the relay's chances (at most 50 guesses at 20-bit secrets). Counting every failed exchange as wrong is the right call: the host sends its confirmation first, so a peer that hangs up after checking it has had its guess.
🤖 Agent: #9297 merged; main and its harness fix merged in (clean). Verified on playhorizon on the stack's top (#9473, which carries this head): the five vanilla_wire_e2e rules against vanilla 26.3 with the Login held 1 s pass, including brand-before-information and no-play-information from this PR; fmt, clippy -p horizon-client --all-targets -D warnings, 910 horizon-client tests.
🤖 Agent: verified merged with main (81dad4693, with #9428 on top) on playhorizon: launcher fmt, clippy --all-targets --locked -D warnings, 538 lib tests and 101 of 102 binary tests (the one failure is main's stale docs/launcher-commands.md, fixed on #9428). One point checked by hand: an older window hands the new installer no HORIZON_LAUNCHER_UPDATE_SHA256 only if the file in place changed under it, and then already_in_place returns Ok before the empty hash is looked at, so a mixed-version pair restarts instead of failing. Real placement on macOS and Windows is still unrun.
🤖 Agent: verified merged with main 81dad4693 on the desktop: clippy -p horizon-client --lib --tests -D warnings, 29 command tests, and the ignored loot_table_tests run against prepared 26.3 resources (passes; with take_loot_table's old bare retain put back it fails with the table still in the saved chest, so the test pins the fix). Matches vanilla: RandomizableContainer.tryLoadLootTable loads a tag without LootTable as a plain, empty container. The run-parity label adds opened_loot as asked.
The reference test lives in the launcher's binary, which --bins now runs in
CI: main's copy missed the game-architecture setting from #8274, so this
PR's own check would fail on it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Insert the new keys before each table's last entry (no comma changes), fold
the instance-name sentence into one key, and give copy-from its --as hint as
plain text after it.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
🤖 Agent: requeued after martipops's harness finding: vanilla_wire_e2e now records the held Login when the client gets it. On playhorizon against vanilla 26.3 with a 1 s hold, this head passes no-play-before-login and the same tree with main's r.joined condition fails it with 27 client_tick_end before Login. Merged with main 81dad4693 (impl Remote conflict, both sides kept): fmt, clippy -p horizon-client --all-targets -D warnings, 910 horizon-client tests.
No version installed: name `horizon-launcher install latest`.
Instance name taken: ask for another name; `instance copy-from` names `--as`.
Play gate: name where to sign in (Settings in the window, `account sign-in` on the CLI).
No change in behaviour; messages go through tr()/trf().
Fixes#9630
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
🤖 Agent: verified merged with main 81dad4693 on the desktop: fmt, clippy -D warnings on the four client crates (all targets) and the --no-default-features server build, 2750 horizon-client/-lib tests and 1358 render tests pass. One point checked by hand: with no glyph a button's label is centred exactly where it was (x + floor(w/2) equals the old x + 2 + floor((w - 4)/2)), so mouse players see vanilla's buttons unchanged. pad_menu_e2e not rerun here (windowed, no display over ssh).
The CurseForge app's instances were listed with no version and no loader, so
`instance copy-from` demanded --minecraft and the copy lost its loader. They
now use curseforge_instance_target (minecraftinstance.json, else manifest.json).
--minecraft still overrides; the Modrinth App path is unchanged.
Fixes#9628
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The proxy wrote the Login's line and then slept, so whatever the client sent
during the hold landed after the Login in the capture, where
no-play-before-login stops looking: a client sending tick ends for the whole
hold could pass. Sleep first, then record (martipops's review on #9297).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Line of sight now clips with the viewer's collision context, as LivingEntity.hasLineOfSight's ClipContext(COLLIDER, entity) does, so scaffolding is open from below and solid from above. I ran the new test against prepared resources too: it builds the world and passes, not vacuously.
🤖 Agent: Moving-piston NBT is read from the live tick entry with the existing piston serializer, and writes are still refused before any detached block entity exists. The ordered pretty-print context applies only to a live piston's requested subtree, so other /data get output is unchanged.
🤖 Agent: The per-address host cap mirrors the existing rate limits (off when the relay can't see addresses; IPv6 counted per /64 with a 4x /48 cap). HostSlot gives the slot back on every way out of host(), and the integration test holds connections open to prove it.
🤖 Agent: The tree check follows ClientboundCommandsPacket.validateEntries: one pass over redirects (canBuild) and one over children (canResolve), so vanilla's own execute-run-to-root redirect still passes. It is iterative, so a long chain cannot overflow either.
🤖 Agent: Dialog open_url now goes through the same untrusted_http_url check as chat click events (ExtraCodecs.UNTRUSTED_URI: http/https only), so file:, UNC and protocol-handler links never reach open_uri.
🤖 Agent: The owner's window always joins its integrated server through memory, so refusing the owner's UUID over the network costs the host nothing and closes the offline-name takeover. Splitscreen seats already had this rule.
🤖 Agent: The depth guard is a thread-local counter around read_added and effect_details, released on drop, so an error leaves nothing counted. 64 levels still decode, and one guarded Cell per component costs nothing next to the decoding itself.
🤖 Agent: Along each axis the rounded squares fall and then rise, so the cells inside a sphere form one run per row. Skipping the rest changes no draw or query (vanilla only draws for inner cells), and your snapshots of 4 decorated worlds match byte for byte.
Preserve all landed fixture routes and one declaration of the shared scratch/reference options. Runtime pair remains qualified to 21c7f4597.
Co-Authored-By: Codex <[email protected]>
🤖 Agent: Boat items now clip like Item.getPlayerPOVHitResult (float view vector, OUTLINE, Fluid.ANY) through the shared ray code instead of 0.05-block fluid sampling; the new boat_item_hit harness runs in CI parity.
Keep accepted-hit gating and attach the actual fish to poison effect criteria, independently of fish AI and frozen world ticks.
Co-Authored-By: Codex <[email protected]>
🤖 Agent: The same hits come in the same order, with the budget kept (both tests compare against the cell-by-cell walk). Measured: +0.07 ms per tick with 1500 ground mobs (palette check per section), noise for flying mobs, and the far-teleport worst tick goes 2290 → 160 ms.
A server dialog's open_url action, from a show_dialog packet or a
show_dialog click in chat, took any string. A click went to open_uri, which
hands it to ShellExecute on Windows and to xdg-open or open elsewhere. A
button naming file:///C:/.../cmd.exe, a UNC path to an .exe on the
attacker's share, or a custom protocol handler could start a program. The
link prompt is on by default but shows only the raw string; with "Prompt on
Links" off it opened at once.
Vanilla's ClickEvent.OpenUrl takes an ExtraCodecs.UNTRUSTED_URI (http and
https only). Chat components already applied that check; dialogs now share
it, and a refused link leaves the button with no action.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Vanilla's trapdoor ladder rule is instanceof TrapDoorBlock, so a tagged block with no open or facing is now just not a trapdoor. A real ladder still needs valid facing, and the tests cover the three non-trapdoor shapes.
🤖 Agent: The client services are owned snapshots with no authoritative writes, and every lifecycle hook is empty without a mod. The menu_background result is clamped to a finite 1..64, so a mod cannot hand the blur a bad radius. I dropped the enable_older_servers helper (see my comment).
Keep upstream corpus groups and opt-in loaded-fixture controls alongside the live-piston contract and full presentation diagnostic.
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
A host without a Microsoft account opens the world in offline mode, where a
player's UUID is offline_uuid(name). The owner's UUID is offline_uuid of the
host's name too, and the login gate let that UUID in from anywhere. So a LAN
or friends player who logged in under the host's name passed the gate, the
duplicate-login handling kicked the host's own window, and the guest played
on as the owner: operator level 4, the owner's player data, and the
movement checks the owner skips.
Apply the splitscreen seats' rule to the owner: the owner's UUID is let in
only over the in-memory connection. Vanilla never reaches this state because
IntegratedServer always authenticates LAN guests.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Item stacks can hold stacks in their components (bundle contents, charged
projectiles, use_remainder, container), and a mob effect can hold a hidden
effect. The decoders called each other with no depth limit. A payload a few
hundred thousand levels deep fits in one 2 MiB frame and overflows the
decoding thread's stack. In Rust that aborts the whole process instead of
failing the packet: one creative-slot packet from any player stopped a
dedicated server or a LAN host (with the host's own window), and one slot
packet from a server stopped the client. Vanilla's decoder turns the
StackOverflowError into a disconnect.
Count nesting per thread in read_added, which every stack-in-a-stack passes
through, and in effect_details, and return an error past 64 levels. The
packet then fails like any other bad packet and only that connection
closes. Survival bundles nest about 16 deep at most.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The relay only rate-limited host registrations (6 a minute per
address) against the global `max_hosts` (1000). A host that answers
pings keeps its slot as long as it likes, so one client could register,
hold every control connection open and fill all slots: in under three
hours from one IPv4 address, in seconds from an IPv6 /48 (limits count
by /64, and a /48 holds 65536 of them). Every real host then got
`Full`.
Now the limiter also counts the hosts each address key holds open:
at most 4 (`RELAY_MAX_HOSTS_PER_ADDRESS`), and an IPv6 /48 at most four
times that, however many /64s it spreads over. A registration past the
cap gets the existing `Full` reply. The count goes up when a host
registers and back down through an RAII guard (`HostSlot`) when its
connection ends, on every way out of `host`, panics included. Like the
other per-address limits it is off with `RELAY_PER_ADDRESS_LIMITS=0`.
Filling the default 1000 slots now takes 250 IPv4 addresses or 63 IPv6
/48s, not
one client.
Tests: an integration test registers the cap's worth of hosts from one
address, sees the next refused with `Full`, closes one and gets its
slot back; a limiter test covers the /64 and /48 caps, a freed slot and
the limits switched off.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A short code's secret is 20 bits, and the docs promise the relay can't
get into the world. But the host gave a relay (or anyone driving its
joins) unlimited online guesses:
- after five wrong secrets the code was retired, but the wrong count
was reset on every registration and `run_relay` registered again at
once, so each cycle bought five fresh guesses for one round trip;
- a guess was only counted when the guest's confirmation failed to
verify. The host sends its own confirmation first, so a peer could
check it against its guess offline and hang up: an uncounted guess;
- the five-guess limit was checked after the fact, so joins sent all
at once ran any number of key exchanges in parallel.
Now the host keeps the wrong guesses for the whole sharing session
(`Guesses`): a count per code (reset when it registers), a total that
never resets, and a reservation for each key exchange in progress, so
only as many run at once as wrong guesses are left. Every relayed key
exchange that ends without a confirmation counts. After a retired code
the host waits 1 s, 2 s, 4 s ... (at most 5 minutes, `retire_delay`)
before registering again, and after 50 wrong guesses in one session it
leaves the relay (`HostEvent::RelayStopped`; the game says so through
a new translation key). The full code keeps working directly.
The math: each short code has a fresh secret, so every guess hits with
probability 2^-20 and guesses don't narrow anything down.
- With the cap, a relay gets at most 50 guesses per sharing session:
50 / 2^20, about 1 in 21,000. Spending them takes at least
1+2+...+256 s = 511 s of waits.
- With the backoff alone (capped at 5 min, 5 guesses a cycle), the
steady rate is 60 guesses an hour; a 50 % chance needs
ln 2 * 2^20 = ~727,000 guesses, about 12,100 hours (~505 days) of
continuous sharing. Before: one round trip a guess, hours.
Tests: the per-code count starts over at each registration while the
total stays, the waits double (1 s ... 256 s) until the host stops at
50; exchanges in progress count against the limit; `retire_delay`
doubles up to 5 minutes. The relay's tunnel test still sees a new code
after five wrong secrets (now after 1 s).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
OreFeature.doPlace scanned each sphere's whole box and divided for every
cell, although a cell's square along one axis depends on that coordinate
alone. Each axis's squares are now worked out once per sphere with
vanilla's expression, so every sum is bit for bit the same, and each row
visits only its run of cells inside the sphere: along an axis the squares
fall and then rise, so the passing cells form one run around the smallest.
The cells tested, the world queries and the random draws stay the same
and in the same order. A test compares the walk with vanilla's full box
scan over 20,000 spheres, including ones whose edge sums to exactly 1.
Fixes#9609
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Three things a server sends were decoded without vanilla's checks, and each
let a server crash or stall the client:
- A light layer shorter than 2048 bytes: the column-top sky light indexes the
layer directly, so an empty layer panicked the main thread. Vanilla's
DataLayer constructor refuses any other length; Layers now does too.
- A command tree whose children (or redirects) loop: the usage hint followed
the loop when the player typed "/" and overflowed the stack. Commands now
runs ClientboundCommandsPacket.validateEntries' check ("Server sent an
impossible command tree"), as an O(n) depth-first walk so a long chain
can't make the check itself slow.
- A dimension type's min_y and height went unchecked into the chunk format,
so a height of 2^31 made the client allocate gigabytes for light. A shape
outside DimensionType's codec (multiples of 16, at least 16 high, inside
-2032..=2031) now keeps the named type's own.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The older-server adapters are off by default while they are incomplete, and
only the environment turns them on. A Host helper that turns them on for the
whole process would let any installed mod do that for its players; it is
left out of this change.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: The player predicate now tests stats, advancements, recipes and level, and player_entity_for_any stays &self, so #9494's compound is added only by the &mut self caller.
🤖 Agent: /execute if loaded now also needs the entity-ticking status and saved entities applied, which matches ExecuteCommand.isChunkLoaded (FullChunkStatus.ENTITY_TICKING && areEntitiesLoaded). The simulation permission tracker is untouched.
🤖 Agent: Dawn Client is only one more place to find the player's own 26.3 jar and assets (cache/minecraft under .dawn). The tests check that nothing is fetched from Mojang.
🤖 Agent: A replaced pack becomes a new entry and the old one is unloaded, as ServerPackManager.pushNewPack marks it SERVER_REPLACED, so the reload ends instead of looping.
🤖 Agent: The four seats match the passengerAttachments of HAPPY_GHAST in 26.3's EntityTypes, and the index clamps as EntityAttachments.getClamped does.
🤖 Agent: No-pack worlds keep the existing preload. Only an overlay with damage-type definitions seeds SyncData at world entry, so the default path is unchanged.
The mod browser records what it installed in `mod-browser.json` in the instance folder, and
Update, Remove and turning a mod off act on each entry's `file` through `home.join(file)`.
Nothing checked that path: an absolute path replaces `home`, and `..` climbs out of it. A Java
modpack (`.mrpack` with a loader) copies every relative path from `overrides/` into the new
instance, so a pack could plant its own `mod-browser.json` with an entry such as
`"file": "/home/me/.ssh/id_ed25519"`, and the launcher would delete or rename that file when the
player clicked Update, Remove or the on/off toggle on the fake entry. Copying a Prism/MultiMC
instance folder could plant the same file.
- `Installed::load` now leaves out (and logs) any entry whose `file` isn't a relative path of
plain names inside a folder the browser installs into (`mods/`, `resourcepacks/`,
`shaderpacks/`, `horizon-mods/`, `horizon-native-mods/`), with at least one name below it.
- `Entry::path` applies the same check where files are removed or renamed (update, remove,
toggle, Java ports, registry installs), so an entry that didn't come from `load` can't reach
outside either.
- A modpack import never writes `mod-browser.json` (any case), and the other-launcher copy skips
it.
Tests: a planted list with an absolute path, a `..` path, a path climbing out through `mods/`
and a bare `horizon-mods` keeps only the valid `mods/x.jar` entry; Remove and toggle leave a
file outside the instance untouched. A Java `.mrpack` with `overrides/mod-browser.json` imports
its mods but not the list.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: The mod id is read from the mixins.incoming/<id>/ path that stage_mixins writes, so the culprit named for E_MODULE_CONFLICT is right. I removed the Fixes lines for #9016 and #9414, which this PR does not fix.
🤖 Agent: The snapshots now key on a build.rs hash of the world sources, so a relink no longer throws them away, while a change to those sources still does.
🤖 Agent: The launcher's own endpoint still wins when both games answer. A crashed game's file no longer counts (a TCP endpoint must accept its token), and a change of endpoint restarts the horizon-ctl child, whose Drop kills the old one.
🤖 Agent: Animated strips are now cut to their first frame for every atlas directory source, not only item/ and block/, so a pack's own folder (wynn/) animates as vanilla's does.
🤖 Agent: Recovery keeps only whole frames: the crashed .tmcpr.tmp is cut at its last complete packet record before it becomes an .mcpr, and a writer that may still be alive (its pid is checked) is left alone.
🤖 Agent: Applying detail now stops at DETAIL_BUDGET (3 ms) per frame and the rest waits in detail_backlog; with #9515 the widths reduction runs on the worker, so neither the burst nor its first read lands on the frame.
🤖 Agent: Captured server sounds keep their entity, category and excluded player and go out between the entity's add and remove, so a client always knows the emitter when the sound arrives.
🤖 Agent: entity_killed_player fires from the guest's death with the blow read before the death message, as ServerPlayer.die does; the killer view stays on &self on top of #9494.
🤖 Agent: End gateways now keep a same-level vehicle live and teleport it with its passengers. As you note, #9461's temporary mob_can_use_gateway restriction is no longer needed with this.
🤖 Agent: The whole vehicle tree moves through Entity.teleportCrossDimension's calculatePassengerTransition, with UUIDs and mount links restored, so riders keep their vehicles across /execute in … run tp.
🤖 Agent: ModelBakery.DESTROY_STAGES_OIT exists in 26.3. The OIT crack sprites are picked only with improved transparency on, so the default look is unchanged.
🤖 Agent: native-mods.json is refreshed through a fresh hardlink and an atomic rename, so nothing is written through the old link. native-data is a relative symlink inside the app bundle.
🤖 Agent: started_riding now fires for every ServerPlayer the vehicle carries (Entity.getIndirectPassengersStream), not only for the player who boarded.
🤖 Agent: The fight keeps dragon_uuid (vanilla's save key) and ticksSinceDragonSeen, as EnderDragonFight.tick does. A summoned dragon that dies after the fight no longer opens a gateway or gives 12000 XP.
🤖 Agent: renderable_float swaps only a float target the adapter cannot render (to 16-bit float, then Rgba8Unorm). Vulkan, Metal and DX12 keep the pack's declared format.
🤖 Agent: The native pack surface only holds bit patterns, written with blend: None and read with textureLoad. Rgba32Uint keeps the same bits and can be rendered on wgpu's GLES backend.
🤖 Agent: Fluid blocks win the material vote only for their own still and flowing sprites. Voting for every other sprite is unchanged, and the new test covers the shared fluid sprite.
🤖 Agent: The harness now fails on a server that is still up after server/stop (the #8245 regression the issue names), and it runs the same check on Windows paths.
🤖 Agent: At midnight monsters_burn is false, so isSunBurnTick returns before its RNG draw on both sides. The zombie horse reference stops being random, and the comparison itself is unchanged.
🤖 Agent: On a client, entity handles read only what the server synced. Writes (set_velocity, merge_nbt, tags) return false, and a handle from another level's epoch reads None, so a client copy changes only when the server says so.
🤖 Agent: With the default texture budget, map_bytes is still exactly 64 MiB, so nothing changes until a player raises textureBudgetMiB. The unit test pins both cases.
🤖 Agent: The primitives get their own pipeline at the end of the world pass, before the hand. With no subscribing mod the batch is empty and prepare() returns before any upload or draw.
🤖 Agent: native_entity_outline starts from vanilla's own answer (team colour or white) and costs nothing without a listener; masking above 24 bits keeps the colour in range.
🤖 Agent: Language.loadFromJson parses with a lenient Gson reader in 26.3, so packs with comments and unquoted keys now load their lang files instead of being dropped silently.
🤖 Agent: Team joins happen in remember_uuid, which every entity load goes through. An entity loaded without a UUID gets one first, since the scoreboard keys entities by UUID, as Entity.load does.
🤖 Agent: The hoglin reads CannotBeHunted and writes it back only when set (Hoglin.addAdditionalSaveData), so bastion hoglins from the template are no longer hunted by their own piglins.
Keep the upstream piston entity-tick-order group and the independent
live-piston presentation diagnostic.
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
Read the authoritative tick entry with command metadata and source-aware
pretty ordering. Preserve default output and reject unsupported writes.
Cover the original pulse, a normal sticky pull, and scoped diagnostics.
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
🤖 Agent: The named predicate now uses the existing campfire smoke helper (CampfireBlock.isSmokeyPos: lit tagged campfire, first-blocker exception), and the real game command harness covers it.
🤖 Agent: gui_render_post has a read-only host and draws nothing without a subscriber; the fragment is pending, so the API number is assigned at build time.
🤖 Agent: mc_chunkFade is injected only when the renderer supplies it and the pack's vertex program reads it, so packs that don't ask compile as before. The disclosed inputs are an MIT pack's API names only.
🤖 Agent: The values are bit-identical (tabulated perlin corners, 4-column interpolation rows, branch-free range choice), and your noise snapshots compare byte for byte.
🤖 Agent: The surface pass gives the same blocks: positive_y_bounds only skips bands where the vein density is provably <= 0, and the material_condition probe covers the column reads.
🤖 Agent: Tooling only. Clearing the floor to air right before the exact stone fill makes the 256-block guard independent of the scenarios that ran earlier.
🤖 Agent: /data merge and /data modify now reload minecarts (the new Minecart::load) and boats, which matches EntityDataAccessor.setData running Entity.load on every entity but a player.
🤖 Agent: Biome tags now load with the same pack overlay as the other loot tags, and location_check reads biomes through holder_set (#tag, one id or a list), the HolderSet codec LocationPredicate uses.
🤖 Agent: keyed() compares full(key) for the four keys item predicates read, and the potion_contents value is normalised the same way, as the registry codecs behind ItemPredicate do.
🤖 Agent: This matches Chicken.aiStep: the sound and entity_place come only after a drop. One small RNG note: the pitch floats are still drawn before the loot roll, which only differs for a data pack that empties chicken_lay.
🤖 Agent: No vanilla advancement has an nbt or reference predicate, so mob_to_nbt only runs for data packs that ask for it (Tree::index_nbt). player_entity_for now takes &mut self; #9561 and #9562 are stacked on that.
🤖 Agent: riders::tick returns at once for a mob with no mount, so lending the vehicle's navigation costs nothing outside ridden vehicles. #9597's branch also carries this head (their oracles meet in the same switch).
🤖 Agent: Piston block entities now tick after entity travel, the order ServerLevel.tick uses (entities, then blockEntityTicks). The new 6-tick wither skeleton fixture covers both stone and slime, and it matches all 24 observations.
ClimbingRegistry::new returned "invalid trapdoor open state" (or "invalid
climbing facing") for any state of a block in minecraft:trapdoors that lacked
a valid open or facing property. Macaw's Bridges adds blocks that are not
trapdoors (rail bridges with facing/powered, a pier with no properties) to
minecraft:wooden_trapdoors, so All the Mods 10 and Craftoria exited before the
window opened.
Vanilla's LivingEntity.onClimbable tests `instanceof TrapDoorBlock`, not the
tag, so such a block is not a trapdoor and never climbs. A tagged state now
counts as an open trapdoor only with facing and open=true; otherwise it is
skipped. The ladder's facing check is unchanged.
Fixes#9585
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Both #9494 and #9562 index advancement triggers (nbt, player facts) and widen
the player's view: kept both. player_entity_for adds the compound on &mut self;
player_entity_for_any (#9562) stays &self for its &self callers.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
#9494 makes player_entity_for take &mut self to build the player's compound;
guest_entity_view (#9561) is &self, so it reads what the player looks at only.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A far move (one move packet thousands of blocks away) made the server's
collision sweep walk every cell of the swept box, one query per cell:
a 2.3 s tick for a hop of 8,000 blocks, growing with the square of the
distance.
Two opt-in provider hooks let the scan pass, after one query, the rest of
a row inside the same chunk (at most 16 cells):
- missing_chunks_stay_missing: a chunk missing once stays missing for the
scan (SceneCollisions, EntityCollisions, EmptyCollisions).
- section_is_empty: no state in the section's palette has a collision
shape (SceneCollisions, cached per section in the scan).
Both default to false, so the parity probe, which records every query,
sees the same calls. Skipped cells yield nothing, so the collisions and
their order are unchanged.
Fixes#9594
Co-Authored-By: Claude Opus 5.5 <[email protected]>
native_send_map (API 132) sends a 128 x 128 map picture to a player but was
not in the helper catalog, so a trait mod could not call it. One catalog
fragment adds it to Host as send_map(uuid, map_id, colors) -> bool.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
🤖 Agent: binding the slot origin to a let first matches vs_section_flat_opaque/cutout; it is compatible with #9584's packed_corner change, which fixes the other X3500 site in the same shader.
🤖 Agent: checked the select() rewrite of packed_corner: rel = (i - axis) mod 3 gives the axis component +side, the next +du, the last +dw, the same values as the old indexed stores, with no dynamic l-value for FXC.
The launcher's game_* tools only looked at the endpoint in its data folder,
so a game the player started, which listens on the game's default endpoint,
answered "not running under agent control". Each call now goes to the first
endpoint where a game answers, the launcher's own first, restarting the
horizon-ctl child when that changes: an installed horizon-ctl keeps the
endpoint it started with.
Whether a game answers is now tested on Windows too, with the endpoint's port
and token, rather than by the file existing: a crashed game leaves the file.
Fixes#9465
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Queued. The home icon and the new ⋯ Edit instance item open the existing edit dialog. In the palette, the instance's saved picture stays offered after another icon is picked, so the player can go back to it without choosing the file again. Both new labels already have translations in launcher/lang.
🤖 Agent: Queued. The invite field's right padding now follows the Join label's width (14 px padding each side, about 8.5 px per character at 14 px), so the longest translations (Bulgarian and Latvian, 15 characters) get 175 px instead of 84 and the button no longer covers the code. In the wide layout the field grows by the same amount (340 to 431 px at most), which comes out of the row's flexible gap. English stays at 84 px.
🤖 Agent: Queued after a small fix on the branch. On main, barrels, shulker boxes, hoppers, dispensers and droppers already load as BlockEntity::Chest, so #9189 already covered them. This PR's real change is that a LootTable id the codec rejects no longer counts as a table, which matches vanilla 26.3's tryLoadLootTable (KEY_CODEC read, then null). I folded the four copied tests into one table test and made roll_loaded_loot read the id the same way, so an invalid table with no items rolls nothing. Desktop run: clippy -D warnings clean, horizon-client-core 63 tests, horizon-client 895 tests, horizon-client-lib 1718 tests.
🤖 Agent: the preset only sets PvP & HUD options and vanilla's toggle-crouch, and turns on the official fps/keystrokes mods only if they are installed. Verify passes at 41c0e4080 (the stack's top: workspace clippy -D warnings clean on the desktop).
The barrel, shulker, hopper, dispenser and dropper copies of the chest test
become one table-driven test. A loaded container whose LootTable the codec
rejects now rolls nothing, as vanilla keeps no table for it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: after martipops's review, the chat search puts the typed text aside and gives it back on every exit path, a compact repeat keeps a reader's scroll, and search and copy skip legacy § codes (tests for the last two). Compact chat still never merges signed messages. Verify passes at 4984cc061.
🤖 Agent: the own time of day and weather change only what this client draws (sky, lightmap, rain); the world's day_time and weather, and everything that plays by them, stay the server's, and nothing is sent. Off by default. Verify passes at cd788cd4c.
🤖 Agent: the visual tweaks (low fire, clear water/lava fog) are off by default and stop at the clearest view vanilla itself gives. docs/pvp.md now says they can matter on strict servers (from martipops's review on #9250). Verify passes at d2dafa504.
🤖 Agent: numeric enchantment levels change only the tooltip's displayed numeral (off by default); the enchantment data and the rest of the tooltip stay vanilla. Verify passes at 6eae1c73b.
🤖 Agent: after martipops's review, the combo counts a hit only when the server's damage event names this player as the direct entity (spear jabs included, teammates' hits and fire ticks not), the off server widget no longer allocates each frame, and ping shows nothing before the first latency update. Verify passes at 3842a7aee.
🤖 Agent: the armour, held-item and effects widgets read only what the client already holds, and the effects widget replaces vanilla's icons only while it is on (all off by default). Verify passes at f9f1b5abf, with main 6319cb701 merged in; no harness is touched, so parity-main covers it.
Delegate the predicate view to the existing lit and occlusion-aware smoke
helper. Cover both predicate signs, callback stores, slab exceptions and
positive world guards through the real command runtime.
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
Importing a CurseForge app instance folder left minecraft, loader and
loader_version empty, because instance() read them only from
mmc-pack.json. Fill them from minecraftinstance.json (gameVersion,
baseModLoader.name), falling back to manifest.json via the existing
curseforge() parser. minecraftinstance.json wins: it records what the
instance runs, while manifest.json is the pack author's copy and goes
stale when the player changes version or loader in the app. A Fabric or
Quilt id's trailing Minecraft version is not part of the loader version.
Missing or broken files never fail the import; mmc-pack.json behaviour
is unchanged.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
In vs_section_flat (crates/horizon-client-render/src/terrain.wgsl), passing the nested dynamic storage buffer expression slot_origins[block_slots[s.slot >> 8u]] directly into corner_vertex(s.c, ...) causes Direct3D 12's HLSL compiler (FXC for SM 5.1) to fail during 'Terrain solid (flat)' pipeline creation with: error X3500: array reference cannot be used as an l-value; not natively addressable.
Like vs_section_flat_opaque and vs_section_flat_cutout, bind the sampled origin to an intermediate local variable first, letting naga emit a load into a temporary r-value before the function call.
Co-Authored-By: Antigravity <[email protected]>
LabPBR normal and specular maps had a fixed 64 MiB budget, so with about
1300 block layers they always loaded at 64 px, however high
textureBudgetMiB was set. They now get a quarter of the current budget:
the default budget keeps the old 64 MiB, and 1536 MiB gives 256 px maps.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01XnU6QPo9BpqkYWfY6y5Nwu
native_entity and the entity_*/living_* reads now work against the
entities the server shows the window (own player, other players, mobs,
armor stands with their custom names and equipment, tracked-only
entities by type and position); a fresh epoch per client level keeps
handles from crossing levels or sides. Writes are refused and saved
data and tags read as None on a client. native_menu_stacks gives the
open server menu's stacks, with components, by server slot number.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: block outline and hit colour stay render-only and off by default; verify and parity pass at 4adfac92b. The composter fix#9508 is merged in. pack_screen's failures came from the playhorizon runner missing 142 vanilla lang files: the files were copied there and it passes now.
native_world_primitives runs once per world frame and hands a client mod a
batch of world-coordinate primitives: lines with a width, box outlines and
filled boxes in ARGB, beacon-style beams and camera-facing text in the game
font (optionally scaling with distance). Each can be seen through walls.
The game tessellates them camera-relative and the renderer draws them with
its own pipelines (gfx/world_primitives) at the end of the world pass,
after the entities and before the first-person hand: the world's reversed
depth, draw resolution and TAA jitter. A GPU test shows depth-tested quads
and lines hidden behind a wall while see-through ones show.
Closes#9555
Co-Authored-By: Claude Opus 5.5 <[email protected]>
native_entity_outline(entity_id, kind, outline) lets a client mod make any
mob, player, dropped item or projectile glow in its own colour, or take the
outline away (Fabric/NeoForge isCurrentlyGlowing and team colour overrides).
Vanilla's answer comes in; only what the client draws changes.
native_visuals no longer shares 8 rims and 256 edges between all mods: each
trait mod is dispatched on its own and gets 256 rims, 4096 rim edges and
8 lights. A panic in a mod dispatched alone still names the right mod.
Closes#9553
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Dawn Client stores its official vanilla Minecraft assets, libraries, and client jar in .dawn/cache/minecraft/ rather than the default .minecraft directory.
Add Dawn Client default folders to known_folders, include cache/minecraft in find(), resolve Dawn sync/instance directories upwards to the launcher root in resolve(), and update troubleshooting advice.
Co-Authored-By: Antigravity <[email protected]>
Read the boat's existing resolved leash state and reuse the holder
conversion already used for mobs. Keep pending saved UUIDs separate.
Cover pending, live and unleashed contexts through the real Game command
path, with typed subjects, callbacks, stores and measured clocks. Retain
movement as a separate raw diagnostic and isolate the harness directory.
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
The death message takes the combat record's entries and leaves them
empty, so entity_killed_player and kill_mob_near_sculk_catalyst, which run
after it in guest_die, saw no killing blow while show_death_messages was
on: any killing_blow condition failed. ServerPlayer.die reads the
CombatTracker for its triggers before recheckStatus clears it at the end,
so the blow is now taken before the message is built.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
Keep the nonempty operand's token metadata when pasting with an empty operand.
Original GCC/Clang controls and the pristine-main control retain the following
call's parentheses; add that regression to the boundary suite.
Co-Authored-By: Codex (gpt-6.1-sol) <[email protected]>
A pack's items or blocks atlas can list its own folders (Wynncraft's `wynn/...`). Their animated
strips were packed whole, so a 16x32 two-frame item drew both frames squashed into its slot.
They now keep their first frame and animate, as vanilla does for every sprite of those atlases;
a still texture there stays whole.
Fixes#9540.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Entity.load: a string Team adds the entity's UUID to that scoreboard team
when it exists, so /summon ... {Team:"red"} and structure or data pack
entities join it. Team is only read, as in vanilla, so the kept compound
drops it.
Fixes#8049
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Hoglin.readAdditionalSaveData/addAdditionalSaveData keep the CannotBeHunted
flag (a bastion's hoglins are placed with it), and Hoglin.canBeHunted is
false for them: PiglinSpecificSensor never picks them as prey and a hit
from one doesn't rally the piglins around.
Fixes#8048
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Initialize the implicit input per source vertex and isolate it from geometry-stage globals, with original paired and GPU controls.
Co-Authored-By: Codex (GPT-6.1 Sol) <[email protected]>
A Nintendo pad's face buttons are lettered the other way round from an Xbox pad's: A sits on
the right and B at the bottom. Going by position (SDL's layout, the default), B confirms and
jumps and A goes back, while the Switch's own menus and keyboard confirm with A.
PadSettings::nintendo_letters (`nintendoLetters` in horizon-controller.txt; off unless set, on
in a build for the Switch itself) reads a Nintendo pad's face buttons by their letters: A plays
South (confirm, jump), B East (back, drop), X West, Y North (Button::by_letter,
PadState::by_letter). It applies to a player's pad state, read from the pad or fed by
splitscreen's router, before horizon-ctl's virtual pad is merged; splitscreen's join and
lost-controller prompts, which a controller nobody holds yet answers, stay by places. Bindings
still name the part, so they mean the same either way. With Nintendo glyphs, the binds list and
the on-screen keyboard's hints name the button that plays each part (OptionsUi::pad_by_letter).
docs/controllers.md describes the setting.
Test: horizon-client-input gamepad::tests::a_nintendo_pad_by_its_letters_confirms_with_a, and
the settings round trip.
(cherry picked from commit fb2490c02, reworked onto main)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
On the in-bed chat (InBedChatScreen) the pad and the keyboard could only reach the chat box:
Leave Bed was drawn with screen_draw::button, which registers no widget, and focus only lands on
what a frame registers (key_focus::drawn_targets). Game::leave_bed_draws records it as a button
at the bounds the click checks, so the pad's focus starts on it (pad_first_target's first
button), D-pad up from the chat box reaches it, the keyboard's Tab reaches it as vanilla's does,
and A or Enter wakes the player.
Test: game::chat::tests::leave_bed_is_a_focus_target_above_the_chat_box.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A sleeper gets InBedChatScreen, a chat screen the game opens on its own. With a pad in use,
pad_auto_keyboard opened the on-screen keyboard over it (Steam's on a Steam Deck) as if the
player had opened chat to type, every time the player lay down. The keyboard now waits for A
on the chat box there; chat the player opens still brings it up. docs/controllers.md says so.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
PackedStorage::read and write are the per-block word accesses of section
fills and lookups in horizon-world (BlockSection::set and fill_run, the
terrain fill's solid runs, surface column reads). Without #[inline] each
was an out-of-line call across the crate boundary that recomputed the
slot; inlined, the caller's loop keeps the slot arithmetic and word
access in place. Behavior is unchanged.
(cherry picked from commit 680cabdba245cd9c373fde94164377353c952785)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The in-range/out-of-range merge stored the out-of-range value only where
the selector was outside the range, a conditional store LLVM leaves as a
branch per value (unpredictable for noise selectors). It now writes
every value as a select between the in-range and out-of-range samples,
which vectorizes; the selected values are the same.
(cherry picked from commit 5434c4113f94d6fa98a56f581eea1f8d27ac1352)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A block volume's interpolated values come from repeated addition down
each column, one dependent chain per column. A cell row four columns
wide (every row of a 4-wide cell inside the volume) now advances its
four chains together, writing each column's value before adding its
increment, so their additions overlap instead of waiting on each other.
Each column keeps the same operations in the same order. Corner samples
are read through precomputed column offsets instead of a checked 3D
index per corner. Partial rows, and rows whose columns fail validation,
keep the one-column-at-a-time loop and its error order.
(cherry picked from commit 11964a2c3ed6dc8752ef46f3a05ed27d0c36602d)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A column's corner XZ dot products and Y gradients depend on the y cell
only through each corner's gradient index. When a volume's y samples
change cell at least four times, each column tabulates the sixteen
gradients' terms once (the same operations as before) and every cell
change reads them, instead of loading three gradient components and
computing two products per corner. The per-y cells, fades and offsets
are kept as separate lanes. High-frequency octaves (the top ten of the
blended noise's limits) gain the most; values are bit-identical.
(cherry picked from commit f3bf17341351a1e02028434765195f5c252b2d24)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Volumes are laid out with y contiguous in each column, so the y-axis
gradient computes the first column and copies it into the others,
instead of storing each value at a stride across every column. The
values, and the error on a zero-period gradient, are as before.
(cherry picked from commit b01ee5606dc23caea2713954a71987aa76c37a00)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A column is filled from its sections: an all-air or one-state part at
once, a mixed section from its storage. Beside each id the cache keeps
whether the block is air, fluid or solid, so the walk and its
stone-depth look-ahead no longer look the properties up per read. The
band is stepped down as the walk descends instead of searched per block,
and the column's minimum surface level is read once.
(cherry picked from commit a88b3f32dfd283741de6e149708b5325a90a4386)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
An ore vein places nothing where its density is not positive, and
returns before drawing its positional random there. Rule libraries read
from the density graph where that holds (DensityGraph::positive_y_bounds:
a range choice on a clamped y gradient whose out-of-range value is a
constant <= 0, as vanilla's copper and iron veins are), so datapack
ranges are followed. The surface pass splits its y bands at those
bounds, drops each vein from the bands outside them, makes each band's
variant on first use, and does not set up a block whose variant gives
nothing. The veins' up-front densities are sampled over one shared
volume cut at the highest y a vein reaches; cutting a volume's top
leaves the values below it as they were.
(cherry picked from commit 8c101d76d544e84987274b8c01b44a0811430eb9)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A write at or below a heightmap's first available y - 2 leaves it as it
is (Heightmap::update returns early). Each surface column now carries
that bound per stage map, as fill_noise does, refreshes it when a map
changes, and calls Heightmap::update directly above it, without the
repeated registry and ensure_heightmap checks. A missing map still goes
through set_block_worldgen, which primes it.
(cherry picked from commit 2015977dfeb33f38d64be64b9b643a3059544d2a)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ServerPackManager keeps packsToLoad and packsToUnload as ServerPackData
objects; the port kept ids, and a pack pushed again with an active pack's id
was set active again after every reload: an endless resource reload.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Three sounds were played under ids that 26.3's sounds.json doesn't have,
so they were silent: block.turtle_egg.break (entity.turtle.egg_break),
block.conduit.ambient_short (block.conduit.ambient.short) and
block.generic.extinguish_fire (entity.generic.extinguish_fire).
Fixes#8055
Co-Authored-By: Claude Opus 5.5 <[email protected]>
PlayerPredicate's stats, advancements (done or per criterion), recipes and
level were read by nothing, so criteria using them were granted to anyone.
They are now tested as PlayerPredicate.matches does. The player's records
are copied into its view only for the triggers whose criteria may ask
(Tree::player_facts_asked) and for the command predicates that name them
(facts_asked_in); a field asked of records not filled fails.
inventory_changed now also tests the criterion's player conditions.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKjxbXfo4yzskjMcNq
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
ServerPlayer.die calls awardKillScore on the entity
getKillCredit names, which fires ENTITY_KILLED_PLAYER (KilledTrigger) for
the dying player with that entity and the death's damage source. Nothing
fired it: guest_die, every server-side player death, now does, between
the deathCount scores and the deaths statistic.
The killer lookup and the killing blow the sculk catalyst trigger built
for the same death move into shared helpers, and KilledTrigger's matching
into advancements::killed, unchanged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKfuWtefRGy97WTU4a
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
Refresh the accepted base before publication. The shader/core/backend inputs
and contribution/build rules are unchanged; rerun required checks on this head.
Co-Authored-By: Codex (gpt-6.1-sol) <[email protected]>
Entity.startRiding(entityToRide, force, sendEventAndTriggers) runs
CriteriaTriggers.START_RIDING_TRIGGER for every ServerPlayer in
entityToRide.getIndirectPassengersStream() once the rider link is set, not
only for the entity that mounted. Horizon fired the trigger at each mount
site for the acting player alone.
Route every rider-link change through Game::start_riding /
note_started_riding, which queues the vehicle and fires
trigger_started_riding for each server player aboard (directly or through a
mob rider). A guest's turn defers the pass to tick_lan, where every player is
back in place. The per-site trigger_player calls are gone; cushions,
minecarts, /ride on a mob and mobs boarding boats/carts now fire it too.
The new boarding test pins the indirect-passenger set (direct rider, rider of
a rider, and an unrelated vehicle).
Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
Refresh the native build receipt independently of the executable and expose runtime data through a relative directory link. Remove stale companions for vanilla builds and preserve unexpected app-side data on errors.
Fixes#9521
Co-Authored-By: Codex <[email protected]>
The snapshots in .horizon-cache keyed on the executable's modification
time, so every rebuild of the game threw them away and the first start
parsed facts.json and block-behaviours.json again. They now key on a hash
of the code that resolves them (horizon-world and horizon-core sources,
their manifests, Cargo.lock, profile, target and rustflags), computed by
horizon-world's build.rs.
Fixes#9522
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Rescan replacement tokens with the remaining source while preserving per-token
recursion hiding through argument substitution and pasting. Recognize calls
continued after expanded names and retain their original source locations.
Co-Authored-By: Codex (gpt-6.1-sol) <[email protected]>
Keep the resource-backed physics assertions active when the fixture runs, and follow the existing optional-data convention in ordinary CI.
Co-Authored-By: Codex <[email protected]>
A lang file that is not strict JSON (comments, unquoted or single-quoted
strings, raw control characters...) was dropped without a word, so a
resource pack whose lang files carry comments showed nothing. Strict files
read as before; the others go through the existing Gson lenient JsonReader
port, as Language.loadFromJson is expected to read them.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMLmvSqLicupMqfESW8
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
In localized versions of the launcher where the 'Join' button has a longer label (such as French 'Rejoindre', German 'Beitreten', Ukrainian 'Приєднатися'), the static right padding of 84 px caused the button to overlap the placeholder text and entered digits.
Compute right padding dynamically based on localized button text length and adapt the container width on wide screens accordingly.
Co-Authored-By: Antigravity <[email protected]>
Preserve the double operations around vanilla float drag, buoyancy and cap constants. Pin six executed Java method vectors and guard the real Game water and air tick paths.
Co-Authored-By: Codex <[email protected]>
🤖 Agent: the composter regression from #9069 was only the fixture: play_unshared kept the fill sound out of audio.capture, so the probe lost its S line on every fill; the S line now comes from level event 1500 (data > 0 = fill_success, LevelEventHandler case 1500). composter + dispenser harnesses pass on the desktop at this head; pr_ready --quick green except the nix-shell's missing uname.
Clear the floor in loaded-cloud and nautilus fixtures before asserting that filling it changes all 256 blocks. Retain strict controls and cover fresh, partially stone, and fully stone prior states.
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
Corrects #8815, which made `ReducedColumns`'s levels lazy and said "neither
is the render thread, so no frame can stall on it". `Lod::apply_detail`
runs on the render thread and reads a reduction for every tile over the
chunk, so the first read of a width built it there: #8815 did not remove
that work, it moved it onto the frame.
A `lod_reduce` span inside the build (kept, it is what makes this
visible) separates the reduction from the column copying around it.
Flying over far terrain at lodDistance 256: of 14844 ms that `lod_detail`
spent over 140 slow frames, `lod_reduce` was 14140 -- 95% of it. The
`Arc::make_mut` copies are the other 5%.
The far terrain thread now builds the widths `apply_detail` reads before
it sends a chunk's columns on. Same world, same flight, limiter off:
average FPS 84.0 -> 164.0, 1% low 3.2 -> 77.6, p99 229.8 -> 9.3 ms,
`update` 6.4 -> 0.9 ms, slow frames 140 -> 2, and `lod_reduce` on the
render thread 14140 ms -> 0.
The widths are learned, not derived from the plan: `apply_detail` reads a
tile only where it is in `ready`, which the plan does not say, and taking
every level the plan holds would be the eager behaviour again.
`apply_detail` sets a bit as it reads, so the first chunk of a width
still reduces on the render thread and every chunk after it finds the
work done. Bits are never cleared, so the worst case is all four levels
-- the pre-#8815 cost, off the frame. `OnceLock` stays, so an unwarmed
width is still built on demand and the mask is an optimisation rather
than a correctness requirement.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011paghWkrnyQRBhJRodZpG2
A recording writes its packets to `<name>.mcpr.<pid>-<nanos>.tmcpr.tmp`,
but the session metadata only reached `metaData.json` inside the archive at
`finish`, so a crash left a stream nothing could identify. The writer now
writes the metadata to `<stream>.meta.json` when recording starts and flushes
the stream and the sidecar together about once a second of recording.
On the next recording in that folder, `recover_crashed_recordings` packages
each leftover stream into a valid `.mcpr` beside it, cut to its last whole
frame (the same `frame_at` definition the reader uses), stamped with the
sidecar (or defaults when there is none) and dated from the stream when it
has no metadata. A stream whose writer may still be running is skipped (own
pid, `/proc` on Linux, or the exclusive lock a live writer holds), and a
stream with no whole frame is kept with its metadata and logged rather than
deleted.
Part of #9434 (items 2-5 follow).
Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
Review on #9250 (martipops's agent):
- Ctrl+F puts the box's text aside and gives it back when the search
stops; whichever way the chat closes during a search (Escape, death, a
server-opened screen), the draft is that text, never the query.
- A compact-chat repeat no longer moves a reader scrolled up: only lines
it adds do.
- Search and copy on click read the message without legacy § codes, as
the chat shows it.
- docs/pvp.md says plainly that clear water, clear lava and a fixed time
of day see further than vanilla at that moment.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A focused EditBox is the screen's focus, so Space and Enter are its text (charTyped), as in vanilla. The keyboard focus the previous screen left on a widget (the Create New World tab bar, for one) no longer presses it before the key reaches the world name field. The resource-loading title shares this path, so the seed, a server's address and the rename field are fixed with it. Space still jumps in gameplay.
Fixes#9332
Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
Review on #9246 (martipops's agent):
- The combo counts a hurt report whose direct entity is this player
(melee and spear jabs) and ignores one that names someone else; a
report naming no attacker (Horizon's LAN hub for player victims) still
counts only as the answer to the last swing.
- The server-address widget copies the address only while it is on.
- The ping widget shows nothing while the tab entry still reads 0 (the
server resends latency every 600 ticks).
- own_latency moved below remote_player_attack, whose doc comment it had
taken.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Reuse the effective stack-limit helper for neighbour merging and keep the
transfer under the separate 64-item cap signed. Preserve age and pickup
delay updates for zero and negative transfers.
Add guarded Game command coverage for component limits, ordinary and
incompatible pairs, counter inheritance and count conservation, plus a
regression for overstacked transfers.
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
#9069 plays `ComposterBlock.handleFill`'s sound unshared (the client that
receives event 1500 plays it, `playLocalSound`), so the audio capture no
longer saw it and the composter harness lost its `S … composter.fill*`
line on every fill (use 570, hopper_in 266, dispense 209 differing on
main 1989b1b59). The fixture now adds that line from the level event, as
it already did for the lava fizz (1501): `data > 0` is fill_success, as
in `LevelEventHandler` case 1500.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Use the live holder eligibility and preserve the float lookup, yaw and angular-friction boundaries needed by quad attachments. Keep single-spring arithmetic unchanged.
Co-Authored-By: Codex <[email protected]>
Update the current-overlay marker before a complete wire snapshot exists. Preserve frozen gameplay damage definitions while allowing the first no-pack wire load to be cached after all packs are removed.
Cover reuse through an absent resource root, route the existing pre-hub reload regression through the helper, and require the tenth prepared-resource case in the catalog parity harness.
Co-Authored-By: Codex <[email protected]>
findOrCreateDragon passes over a dying dragon (ServerLevel.getDragons
takes living ones). scanState makes a living dragon the fight's, and
removes it for a new one when there is no active exit portal. The
harness now runs with a player too: arriving in the End beside a dying
dragon, and after the fight is won.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Leave randomizable-container inventories empty when their NBT supplies a valid loot table, allowing chunk loading and command paths to retain the table. Barrels, shulker boxes (and colored variants), hoppers, dispensers and droppers now match chests and vanilla 26.3 RandomizableContainerBlockEntity behavior. Invalid or absent loot-table keys continue to preserve saved items.
Fixes#9438
Managed and reviewed by Axxio.
Co-Authored-By: Antigravity <[email protected]>
EnderDragonFight keeps its dragon's UUID (dragon_uuid) and
setDragonKilled acts only for that dragon, so one summoned in the End
after the fight is won dies with 500 experience and no gateway. The
fight takes the End's dragon, or makes one, as findOrCreateDragon does:
at once without a dragon of its own, else 1200 ticks after it last saw it.
Closes#9464
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Require completed saved-entity application and loading status without changing simulation or physical unload policy.
Co-Authored-By: Codex <[email protected]>
Rgba32Float is not a render attachment on wgpu's OpenGL backend without
EXT_color_buffer_float, so enabling a native shader pack crashed there
(#9472). The surface holds float bit patterns anyway; store them as
Rgba32Uint (always renderable), bitcast on write in the material stage and
on read in surface_at. Pack-facing API (set_surface, surface_at) is unchanged.
Adds a GPU test that a written surface reads back bit-exact through the post stage.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
🤖 Agent: verified: verify green; pr_ready --quick 2677 Rust tests + clippy pass (only Python failure is test_install_sh_signature: no uname on NixOS PATH); 2-client proof on a dedicated server with the SMP mod: ops' clients complete /tempban and /mutelist, non-ops don't; /mute with a duration blocks chat, /msg, /tell, /w, /me but not other commands; /tempbanlist lists timed bans; a 5 s ban ends by itself. Risk to watch: the extras' names are dropped from the mods' level map so a mod registering /mute still never sees it (the game intercepts first, as before).
Freeze only the gameplay damage entries until the first hub needs complete synchronized data. Preserve no-pack preloads and prevent late helpers or pre-hub reloads from changing the session damage registry.
Co-Authored-By: Codex <[email protected]>
`Lod::receive` drained the whole results channel every frame. A detail
request asks for 512 chunks and cached ones answer at once, and applying
one copies the columns of every tile over it (`Arc::make_mut` on a
snapshot a mesh job shares) and reduces the chunk to that tile's cell
width. A burst in one frame cost 273 ms of it on the render thread.
`horizon-ctl stats` over far terrain at lodDistance 256 put the cost in
`update` (avg 29.9 ms, 540.5 on slow frames) and not in rendering (2.6
ms) or on the GPU (avg 9.1 ms), with a median frame of 6 ms;
`HORIZON_SPIKES` named the path: update_world / lod_update / lod_results
/ lod_detail, 104 slow frames in 20 seconds.
A frame now spends at most DETAIL_BUDGET on summaries and leaves the
rest in `detail_backlog`. Same binary, same flight, limiter off:
average FPS 94.4 -> 133.6, 1% low 3.7 -> 42.1, p99 156.2 -> 20.1 ms,
worst frame 739.7 -> 34.2 ms. p50 is 0.6 ms worse, the budget's work
spread out rather than dumped into one frame. The fill did not slow:
settling to fully refined took 19 polls against 45.
`request_detail` returns early while any summary waits, which is not
optional: without it a chunk whose summary is still waiting has its
mask clear, so it is asked for again and the cache answers at once. The
backlog then grew by a request every 250 ms and never drained -- 0.5
FPS, `update` averaging 9.7 s a frame.
`refreshed_since` moves from arrival to application: a refresh over the
chunk while its summary waited still drops it.
Corrects #8815, which said the lazy reduction could not stall a frame
because neither the far terrain nor the far meshing thread is the render
thread. `apply_detail` runs on the render thread and reads a reduction,
so the first read of a width builds it there. The budget bounds it;
warming the plan's widths on the worker thread is a separate change.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011paghWkrnyQRBhJRodZpG2
EntityDataAccessor.setData loads the merged compound into any entity but a
player; Horizon answered "Nothing changed" for carts and boats. Minecart
gets a load() that reads the saved fields over a live cart (from_nbt now
uses it), apply_entity_nbt gains Cart and Boat arms that keep the merged
compound, and guests get commands.data.entity.invalid like the player.
The test compound uses the 26.3 DisplayState form (a block name); a second
test checks that merging only a name keeps fuel, push, fuse and Enabled.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMK3XzCYMyeKEdamets
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
Prefer fluid blocks when resolving material IDs for their still and flow sprites, preventing a cauldron material from being assigned to fluid surfaces. Add regression coverage for shared fluid sprites and existing block voting.
Co-Authored-By: Codex <[email protected]>
Keep main suffocation registration and the world damage catalog together when loading world tables. Preserve all intervening upstream changes without rewriting existing PR history.
Co-Authored-By: Codex <[email protected]>
hidapi's bundled C can crash inside hid_write on Windows once the pad has
gone (Bluetooth sleep or unplug), and flush() only handled a returned
error, not a crash inside the call. Treat the output as lossy:
- probe the pad with a non-blocking read before every write, so a gone
device is noticed before hid_write is entered;
- drop the handle on any hidapi failure (read or write), never writing
through it again;
- schedule a rescan in lost() so the writer goes back through the normal
scan/reopen path instead of keeping a stale handle;
- watch_animation() now drops the pad on a failed read too.
The handle is a small trait so the writer's state machine is unit-tested
without hardware. There is no vanilla reference: this is Horizon-only
hardware output.
Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
The regression behind #9088 was #8245: a stop that doesn't come from the
supervisor's stdin or the control API left the supervisor in its stopped
phase, so `horizon-server --dir … run` outlived the management
`server/stop` and the harness's `wait(timeout=180)` raised TimeoutExpired.
#9077 fixed that (`Shared::shut_down`), on the base of this branch, and the
harness passes here again.
What the harness still left open, and what this fixes:
- It could not run on Windows at all. It wrote the absolute keystore path
into vanilla's server.properties with single backslashes, which Java's
properties parser reads as escapes ("C:UserscocoAppData…"), so vanilla
stopped before the first request. Values are now escaped as vanilla writes
them, and the harness runs here.
- A server that stays up after `server/stop` was reported as a bare
TimeoutExpired. `wait_for_exit` waits for the process to end, which is the
condition the issue was about (vanilla's `ServerStateService.stop` submits
`MinecraftServer.halt(false)`, so `runServer` returns and `main` ends the
process), and its failure names the side and the last lines it logged.
The seconds each server took are kept in report.json: vanilla 3.4 s and
Horizon 0.5 s here, against the 180 s budget.
Fixes#9088
Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
Lend the vehicle navigation to passenger goals using the vehicle's path
context, while retaining each mob's private navigation tick. Apply the
controller policy and ravager goal flags without requiring a combat target.
Add a guarded real Game/Java regression for patrol and stroll behavior,
including NoAI riders, non-controlling slime riders and a NoAI vehicle.
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
An item predicate's `components` and `predicates` are keyed by registry
ids (DataComponentType and DataComponentPredicate.Type, read through the
registries' name codecs), so "potion_contents" is
"minecraft:potion_contents". `fn item` looked the keys up only in their
full form: a bare key was skipped and let every stack pass. It now finds
them through `full()`, and compares the potion_contents value as a
registry id too.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
Chicken.aiStep rolls the chicken_lay gift table first and plays
entity.chicken.egg and posts GameEvent.ENTITY_PLACE only when
dropFromGiftLootTable dropped something; the egg timer resets either way.
Horizon played the sound before the roll and posted no game event, so a
sculk sensor never heard a laid egg.
A new MobEvent::LayEgg carries the pitch (drawn in the same place as
before); its handler rolls the loot and gates the sound and the game event
on a drop. drop_gift_loot now returns whether it dropped; the other
GiftLoot callers (turtle, panda, sniffer) are unchanged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
A loot `location_check` whose `biomes` named a tag (`#minecraft:is_jungle`)
never matched: the loot data did not load biome tags. `LocationPredicate.biomes`
is a HolderSet, so the arm now reads it with `LootData::holder_set` (a
`#tag`, an id or a list of ids), and the loot data loads the
`worldgen/biome` tags with the world's packs over the game's own.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
Zombie horses are in the burn_in_daylight tag; Mob.isSunBurnTick rolls
every tick, so a horse could catch fire in the two-tick window and the
vanilla reference read 24 Health instead of 25 (#9089).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
/tempban, /mute, /tempmute, /mutelist and /unmute worked on dedicated servers but were left out of the
command tree, so operators' clients showed them as unknown commands with no completions, and the
official server's operators believed only permanent bans existed. They now reach the trees of
operators of level 3 (as a native mod's commands do), where the game runs them: the mute commands
when the server filters chat, the ban ones on a dedicated server.
/mute <player> [duration] [reason] takes a duration after the name (/tempmute stays), and
/tempbanlist lists the bans that end by themselves, soonest first, with who banned, the time left,
the end and the reason.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`adv::entity` had no `minecraft:nbt` arm, so every entity predicate with
`nbt` fell to `_ => false`: a villager's profession, a mob's Variant, a
CustomName, the player's Inventory or SelectedItem never matched.
EntityNbtPredicate.matches: the entity's compound from
NbtPredicate.getEntityTagToCompare (saveWithoutId, plus SelectedItem for a
player) tested with NbtUtils.compareNbt(..., true), which
`nbt_path::compare_nbt` already is.
- `EntityView` gets the compound (`nbt`); `entity` parses the SNBT pattern
and compares; no compound or an unparsable pattern fails.
- `Tree::index_nbt` notes the triggers whose criteria mention `nbt` (or a
`minecraft:reference`), like `index_looking_at`, so the compound is
built only when a criterion may ask.
- `mob_entity` builds the mob's compound (`mob_to_nbt`, riders under
`Passengers`); `player_entity_for` the player's, through `player_nbt`,
the player arm of `/data get entity` moved out of `entity_nbt_in_level`.
- A bred child's view, a copy of its parent's, drops the parent's data.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
🤖 Agent: a dragon saved or summoned with Silent:1b no longer plays the global 1028 death roar at tick 1 of its death; Silent is read from the compound Horizon keeps for unmodelled fields, and the new dragon_death_roar harness compares the level events against vanilla's. The branch passed the cg-39 union run on playhorizon (horizon-client 887 tests); main merged to clear an unrelated lan::conn flake.
🤖 Agent: follow-up to martipops's #9228 review: kept mod block-entity text is keyed on the font atlas epoch, so a resource reload rebuilds it instead of drawing stale glyphs (KeptKey test covers it); docs say what spin, draw_text and native_block_face_sprite really do. fmt, clippy and 7 tests on Linux; the larger points are #9431.
🤖 Agent: tanks take mods' own fluids; before the helpers get numbers, native_fluid_flow now names its dimension and answers none for an unloaded chunk, and a cauldron reports no room for a mod's fluid so native_move_fluids answers none as documented. Stamp test, clippy and 17 fluid tests pass on Linux; a per-tank filter is #9439.
🤖 Agent: Raider.IS_CELEBRATING (index 16, BOOLEAN) now reaches clients, and the new lan test pins it through mob_meta itself: with the mob_raider_meta call removed it fails, so the server half of #8848 can't silently drop out. Main merged (the #9271 part is gone from the diff); 4 celebration tests and clippy pass on Linux.
🤖 Agent: the .mcpr reader no longer trusts the file: entries are capped (1 MiB metadata, 1 GiB stream), one frame parser serves the index and playback so a huge or zero length ends the stream cleanly, and seeking starts at the first packet of a millisecond. finish renames a synced temp over the destination with Zip64 and keeps the packets on failure. 9 studio tests on Linux, verify green on 56fd928f5; the rest is in #9434.
native_move_fluids promised none when the target has no room for that
fluid, but an empty cauldron reported a bucket of room for any fluid, so
a move drained the source and put it all back, answering 0 and marking
the chunk each time. native_fluid_flow is new here: it now names its
dimension, as native_fluid_tanks does, reads a server's parked levels,
and answers none for an unloaded chunk instead of a zero that looked like
still water.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Clicking the instance icon on Home or selecting 'Edit instance' in the
folder group more menu now opens the instance dialog. In the dialog,
clicking the large preview icon opens the image picker, and existing
pictures remain selectable in the palette.
Managed by Axxio.
Co-Authored-By: Antigravity <[email protected]>
🤖 Agent: merging. Ignoring only the hello's send error is enough: data the server sent before closing stays readable after the reset, so the Disconnect is still read and the test still checks the refusal. Proof: on a union with main and #9382 the test passed five runs in a row; fmt, clippy -D warnings and the client-lib tests pass.
🤖 Agent: merging. The order matches SoundEngine.tickInGame: stop when canPlaySound is false (the entity went silent), then tick, where EntityBoundSoundInstance.tick stops on isRemoved or else copies the entity's position as floats; the packet's seed now picks the variant, as handleSoundEntityEvent does. The per-tick cost is one entity lookup per playing bound sound. Proof: union with main and #9385: fmt, clippy -D warnings, Python tests, 2634 client and client-lib tests, the render audio (20) and sound_engine (10) tests, and the entity_bound_sound and sound_engine harnesses against vanilla pass.
- The reader caps metaData.json at 1 MiB and recording.tmcpr at 1 GiB,
so a hostile archive can't exhaust memory, and never allocates from a
length field: indexing and next_packet share one frame parser, stop at
the first frame that isn't whole, and agree on the packet count.
- Seeking lands on the first packet of a millisecond (partition_point);
a timestamp going backwards is indexed at the time before it.
- Fields ReplayMod can leave out read as defaults; protocol is optional;
a file whose fileFormat isn't MCPR is refused.
- finish writes the archive to a sibling file, syncs and renames it over
the destination, with Zip64 for the stream; on failure the stream file
is kept and the error names it. A dropped writer keeps its packets, a
failed write stops further frames, and the stream name can't collide.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: the SNBT depth cap is now 256 rule frames (about 63 nested compounds or 13 levels of nested container items), measured on playhorizon: 384 already overflows a 2 MiB debug thread stack, the size the server thread runs on. New test deep_real_values_still_parse keeps 50 compounds and 10 container levels parsing; 42 horizon-command tests pass, verify green on 54142077f.
🤖 Agent: Ludovic allowed Realms with the player's own account (2026-11-04). At 08874a1622 the review's five points are fixed: the join runs on a worker, Leave asks first, the /join/pc address is checked as host:port, requests send only vanilla's sid cookie (a local-server test shows no Authorization header and the token missing from errors), and the world-pack question comes first. Proof: desktop fmt, clippy -D warnings, 1,700 client-lib tests; verify is green on this head.
🤖 Agent: launcher side of the free Founder Set claim where Paddle doesn't sell; the service part is live (horizon-cosmetics#11). Risk: the button only shows when the service sends claim.one_click, so older services and other countries see no change.
KeptKey gains Font::epoch, as a sign's kept text has: a glyph cache reset
(the dynamic region filling, Force Unicode or Japanese Glyphs toggled) moves
glyphs in the atlas, and kept mod text drew the cells' new contents until
its block entity changed. The docs now say native_draw_text draws plain
default-font spans, that spin's angle is absolute in game time (a new speed
needs a new phase), and that native_block_face_sprite returns vanilla names
without their namespace.
From martipops's review of #9228.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Fixes#9412. Every launcher process deleted and rewrote the one update folder, and the installer
only checked that the payload existed, so a second launcher could leave a partial file to copy over
the launcher.
- Each stage is a `<pid>-<nonce>` folder. Downloads get their name only after the hash check. Each
process removes its own; stages whose process is gone are cleaned up after an hour, counted from
the hand-over, because an installer may still be using them.
- The installer checks the archive against the signed SHA-256 again and places the launcher from
those bytes. The hash travels in an environment variable: the installer is whatever file is in
place, so the command line stays readable across versions. A window without one can't place.
- Nothing happens when the launcher in place is already that version or newer.
- On Windows a launcher running from the temp folder, however it is spelled, can't replace itself.
- Update no longer stops hosted servers. The new window doesn't stop them when it closes either.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01JLHp7yEJwPTJ2kw29LWdk1
🤖 Agent: merged main through #9228's merge (mod lines and doc sections both kept). The scene's quads use the GUI's isolated depth (HudBatch::depth_since) and stay in the HUD batch, so nothing changes until a mod calls native_draw_scene. union of 15 contributor PRs on main 6ddd14e72 (playhorizon): fmt, clippy -D warnings, client 887 + client-lib 1682 + render 1355 tests green (the 4 clear_budget HDR failures also fail on bare main there).
CI's launcher job and pr_ready ran only --lib, so the tests in main.rs's
modules first ran in launcher.yml's cargo test at release. Add --bins to
both. The clippy failure at forge_install_tests.rs:4 is already fixed on
main by 812512980, so it needs no change here.
Fixes#9414
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01JLHp7yEJwPTJ2kw29LWdk1
take_loot_table dropped a held roll without clearing the table from the
chest's source compound. The emptied chest encodes the same as its decoded
source, so the save overlay wrote the source back with its table and the next
load rolled again. It now goes through consume_container_loot, as opening does.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01JLHp7yEJwPTJ2kw29LWdk1
🤖 Agent: the sleep is only as safe as the wake paths, and every mutable path into BlockEntities' map (get_mut, insert, take/put, iter_mut, retain, load_chunk, state_changed) clears it, pinned by a_sleeping_block_entity_wakes_on_any_change. The cost on the tick loop is one FxHashSet lookup per ticker. Hoppers stay awake, so the big win is still ahead. union of 15 contributor PRs on main 6ddd14e72 (playhorizon): fmt, clippy -D warnings, client 887 + client-lib 1682 + render 1355 tests green (the 4 clear_budget HDR failures also fail on bare main there).
🤖 Agent: mod tickets live in Game.mod_chunk_tickets keyed by dimension while streaming.mod_forced travels with each level's Streaming, so a ticket survives the level swap; forced_chunks() dedups against /forceload so a chunk held both ways is ranked once. Tickets aren't saved, as the doc says: a mod that forgets to re-hold in native_level_load drops its chunks on restart. union of 15 contributor PRs on main 6ddd14e72 (playhorizon): fmt, clippy -D warnings, client 887 + client-lib 1682 + render 1355 tests green (the 4 clear_budget HDR failures also fail on bare main there).
Fixes the five points from the review of #7725:
- Play runs GetServerDetailsTask on a worker (with vanilla's Retry-After loop and
an abort on Cancel/Escape) while the screen shows "Connecting to the Realm...";
the title polls it once a frame and connects when the address is back. Every
other Realms request (list, invites, join code, open/close, leave) is off the
UI thread too.
- Leave Realm opens RealmsPopups.infoPopupScreen with
mco.configure.world.leave.question.line1 (Continue/Cancel), as leaveClicked
does; only Continue sends uninviteMyselfFrom. Our own Realm is never offered.
- The /join/pc address must be host:port (host name, IPv4 or [IPv6], numeric
port, no scheme, path or credentials) before it becomes Action::Connect.
- Requests carry exactly what RealmsClient.execute adds: the sid/user/version
cookies and Client-Ref, X-NetworkProtocolVersion, Is-Prerelease; no
Authorization header, the JVM's Java/<version> user agent, no redirects.
Errors are typed (RealmsError) and never hold a header or the token.
- A world pack in the join answer asks
mco.configure.world.resourcepack.question first; Continue connects with the
pack allowed and pushed under GetServerDetailsTask.generatePackId.
Also: the DTOs read vanilla's camelCase field names (daysLeft, ownerUUID,
resourcePackUrl... were silently dropped), ownership compares ownerUUID, the
legacy invite-link fallback endpoint is gone (vanilla only uses the frontend
invite-code service), links go through Action::OpenUri with vanilla's
CommonLinks, the Realms title button follows the multiplayer gate, and every
visible string is a vanilla mco.* key (one Horizon key for "not signed in").
No session (offline, expired token) means no request at all.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
128 frames let only 31 nested compounds, 17 levels of a component's
`extra` list and 6 containers in containers parse. 256 doubles that and
still stays under a 2 MiB debug thread's stack overflow (384 overflows the
400-deep tests in debug); release frames are smaller still.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: `weather_check` now compares each present field with `Level.isRaining` (rain level > 0.2) and `isThundering` (rain × thunder > 0.9, dimension-aware), as `WeatherCheck.test` does; an omitted field doesn't constrain. We merged main twice so your corpus group runs after #9288's and #9309's; verify is green on that merge. Nothing else found.
🤖 Agent: on Linux the stream now asks ALSA for a fixed 1024-frame buffer (about 23 ms at 44.1 kHz) instead of cpal's ~100 ms default. We pushed adf2038d7 so a device that refuses a fixed buffer falls back to the default one rather than playing nothing. Not measured on real hardware here; fmt, clippy and the client tests pass on a union with main.
🤖 Agent: the launcher now offers "A new version is available" with Update, and copies that can't replace themselves say so instead of failing silently; Linux end to end on the ThinkPad (Update to 0.2.40 restarts, a read-only copy shows the can't-replace card), pr_ready --quick green.
🤖 Agent: pushed a fix: container_accepts.json listed native_hooks.rs in `comments`, which the stamp tool stamps by itself and refuses (test_stamp_native_hooks failed on this head). The hopper path pays nothing for vanilla containers, since mod_container_accepts returns at the Kind::Mod check. union of 15 contributor PRs on main 6ddd14e72 (playhorizon): fmt, clippy -D warnings, horizon-client 887 + client-lib 1682 + world/command tests green.
🤖 Agent: use_as_player swaps about 30 Game fields by hand and restores them. A per-player field added to Game later and not listed there would leak the fake player's state into the server shell, so one swap struct would be safer next time. Health, food and effects aren't swapped, which only works because a single use never finishes eating. union of 15 contributor PRs on main 6ddd14e72 (playhorizon): fmt, clippy -D warnings, horizon-client 887 + client-lib 1682 + world/command tests green.
🤖 Agent: merged main (the mod_api use list now also has NativeReloadListener). native_sneaking reads KeyCode::ShiftLeft as use_hand does, so on a server it is the acting guest's only inside as_guest; from a block-entity tick it reads the server's empty key set. union of 15 contributor PRs on main 6ddd14e72 (playhorizon): fmt, clippy -D warnings, horizon-client 887 + client-lib 1682 + world/command tests green, stamp tests OK.
🤖 Agent: merged main (its minecart_hook_targets and pull_hooked tests sit beside the breeze ones, and the test module imports Hook once). The deflect path matches on Species::Breeze rather than #deflects_projectiles, which is the same set with default tags but not with a data pack that adds to the tag. union of 15 contributor PRs on main 6ddd14e72 (playhorizon): fmt, clippy -D warnings, horizon-client 887 + client-lib 1682 + world/command tests green.
🤖 Agent: checked max_air_supply against the 26.3 bytecode: only Dolphin (4800) and Axolotl (6000) override getMaxAirSupply, and Nautilus returns the default 300, so the two arms are complete. #9220 now carries this branch and shares its mob fixture. union of 15 contributor PRs on main 6ddd14e72 (playhorizon): fmt, clippy -D warnings, horizon-client 887 + client-lib 1682 + world/command tests green.
🤖 Agent: shulker_support costs one block-tag lookup per sturdy/rigid/center query before the cached path, the same shape as moving_block_supports there. Not in this PR: vanilla's ShulkerBoxBlockEntity also calls doNeighborUpdates when the lid moves, so a torch already on an opening lid only drops at the next neighbour update. union of 15 contributor PRs on main 6ddd14e72 (playhorizon): fmt, clippy -D warnings, horizon-client 887 + client-lib 1682 + world/command tests green, the new level tests included.
🤖 Agent: checked every new trigger id against the trigger it guards; damage_view's two KilledTrigger ids are right, since its only callers (creatures.rs last_blow, dragon.rs) feed trigger_killed. Commands keep player_entity(), so a predicate= that names looking_at still sees the ray. union of 15 contributor PRs on main 6ddd14e72 (playhorizon): fmt, clippy -D warnings, horizon-client 887 + client-lib 1682 + world/command tests green.
KnownPacksManager.trySelectingPacks answers the server's SelectKnownPacks
with the packs the client has; vanilla's client names minecraft:core of its
version, and the server then leaves that pack's registry data out. Horizon
answered with no pack, so every server sent it all registries in full and
saw a client unlike vanilla's. Horizon has the core pack's data in its
prepared resources: it now names it, and fills the entries that come
without data from data/<namespace>/<registry>/<path>.json, as
RegistryDataCollector does from the client's own pack.
tools/vanilla_wire_e2e.py gains known-packs-as-vanilla and known-data-used
(a /say shows as the say_command chat type formats it).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merging. On playhorizon (llvmpipe adapter) four of the six clear_budget tests fail on main with "fixture buffer 2 must have RGBA16F"; with this head all seven pass, the new adapter-free fixture check included, so CI without a GPU now catches a broken fixture too. Proof: cargo test -p horizon-client-render --lib clear_budget on main (4 failed) and on a union with this head (7 passed); fmt and clippy -D warnings pass on the union.
🤖 Agent: merging. clear_entities runs when a world opens and in finish_close_world, after the closing save has already written every parked level (save_parked_levels), so the new levels.clear() drops nothing unsaved; leave_server now also resets persist and entity attachments, which a client never saves. Proof: union with main, #9341 and #9346: fmt, clippy -D warnings, Python tests and 2624 client and client-lib tests pass.
🤖 Agent: merging. The 26.3 bytecode of EnderDragon.aiStep checks isNoAi right after Mth.wrapDegrees(getYRot()), sets flapTime to 0.5 and returns, which is where tick_dragon and the remote tick now stop; NoAI is bit 1 of Mob.DATA_MOB_FLAGS_ID (15), the index the host sends. Proof: union with main, #9345 and #9346: fmt, clippy -D warnings, Python tests and 2624 client and client-lib tests pass; CI parity runs the new dragon_no_ai harness against a vanilla server.
🤖 Agent: rebased on main e0798a4224 (main made cosmeticsOthers optional in Extras::serialize; the crosshair keeps main's string and appends the pvp lines). Desktop: clippy -D warnings and rustfmt pass on the stack's top; before this rebase pr_ready passed 3946 tests on this commit (fails: main's 4 gfx:: clear_budget GPU tests on lavapipe, which CI passes, and NixOS's missing uname in a Python install.sh test). CI verify green on this head. Off by default; the crosshair mask is rebuilt only on a settings change.
🤖 Agent: verified b5c41f4 on playhorizon (launcher fmt, clippy -D warnings, 51 native_mods tests; the new test fails before the fix) and CI is green. Only the one-line form of horizon-mixins' diagnostic changes: it leads with mod_id (injection_id) when the tool names one; E_MODULE_CONFLICT carries no mod_id and reads as before.
🤖 Agent: opening a rolled chest now also drops `LootTable`/`LootTableSeed` from its saved source compound, so an emptied chest no longer re-rolls after a reload; `consume_container_loot` touches nothing when the chest had no rolled marker. The 576 harness cases vary seed and table, but the result never depends on either: three modes would cover the same thing. Proven in our union run (client tests, desktop).
🤖 Agent: entries 11 (`DATA_ID_CUSTOM_DISPLAY_BLOCK`, optional block state, 0 = none) and 12 (`DATA_ID_DISPLAY_OFFSET`, default from the kind) now reach clients and are read back, so single player and vanilla servers show a cart's DisplayState. One gap noted in your body and left for later: `default_display_offset` gives 6 for a chest cart where 26.3's `MinecartChest` says 8. Proven in our union run (client tests, desktop).
Players in countries Paddle doesn't serve (Russia, Belarus...) can't use the
shop's 0 CHF checkout. The cosmetics service now says so per request
(`claim.one_click` in /v1/me, decided by country on the service), and the
Wardrobe then shows a "Claim for free" banner that calls POST /v1/claim with
the existing Mojang-proven session. Everyone else keeps the shop link.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: `AdvancementRewards.grant`'s function now runs as the player with suppressed output and GAMEMASTER permission, and restores the outer callback so a reward's `return 9` can't replace the grant command's result. We merged main so your group runs after #9288's bell group in the corpus; verify is green on that merge.
One click on the PvP & HUD page (and on Horizon Extras) turns on the custom
crosshair, the armour and effects widgets, vanilla's toggle sprint and toggle
sneak, lower fire, and the official FPS counter and keystrokes mods when they
are installed. It keeps each option's own settings.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Two options on the PvP & HUD page, off by default: a time of day (morning,
noon, sunset, midnight) and a weather (clear, rain, thunder) that only this
window draws. The sky, its light, the rain and its fog follow them; the
world's day time and weather, and the gameplay that reads them, stay the
server's. Nothing is sent to the server.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Visual tweaks on the PvP & HUD page, each off by default:
- Lower fire moves the first-person fire overlay down (0-80% of its height).
- Shield size (50-100%) and totem animation size (25-100%).
- Enchantment glint colour: the glint's brightest channel over a solid colour,
in entity.wgsl and hud.wgsl, carried in free uniform lanes.
- Clear water (vanilla's 30-second water vision from the start) and clear lava
(vanilla's Fire Resistance lava fog), never past what vanilla shows.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
An option on the PvP & HUD page, off by default: enchantment names show their level
as a number ("Sharpness 5", and past vanilla's numerals, "Sharpness 255") wherever
the client names one: tooltips and the enchanting table's hint. A one-level
enchantment still has none. Enchantment.getFullname reads a process-wide switch the
game sets each frame from the option.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Three HUD widgets on the PvP & HUD page, each off by default, in a corner of its own
beside the minimap and mods' boxes (docs/pvp.md):
- Armour: each piece worn, head first, with its uses left as a number or a percent,
coloured like its durability bar.
- Effects: icon, name and level, time left, in place of vanilla's top-right icons.
- Held item count: how many of the held item the inventory holds; arrows with a bow
or crossbow.
pvp_hud::Widget keeps each widget's lines and rebuilds them only when what it shows
changes, so a frame that changes nothing allocates nothing.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Two more groups on the PvP & HUD page, off by default (docs/pvp.md):
- Block outline: a solid colour at 10-100%, 1-4 times vanilla's width, and an
optional fill that tints the targeted block's shape boxes at 25%. Its first
settings are vanilla's 0x66000000 stroke. With high contrast on, the colour
replaces the bright stroke.
- Hit colour: the hurt overlay's colour and how much of it covers the entity.
Its first settings give OverlayTexture's 0xB2FF0000 texel. The texel rides in
colored_count.y of the frame uniforms, so it needs no pipeline rebuild.
FrameInputs gets one pvp::Frame field the later PvP options extend.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Four chat options on the PvP & HUD page, off by default:
- Timestamps: the local time before each message.
- Compact chat: a message repeated in a row shows once with its count (x3).
- Copy on click: a click on a message with no click action copies its text.
- Chat search: Ctrl+F in the open chat filters the messages by the box's text.
They only change how the kept messages are laid out, so turning one off
shows the chat as before.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Four more HUD widgets on the PvP & HUD page, off by default (docs/pvp.md):
- Ping: the latency the server's player list gives this player. Multiplayer only.
- Coordinates: block position and facing (Direction.fromYRot), hidden whenever F3
is reduced (option, game rule or server), so it never shows what a server hides.
- Server address: as net::shown_address shows it (invite codes hidden).
- Combo counter: hits in a row, each counted when the server reports the attacked
entity hurt within a second of the swing; reset when this player is hurt or after
3 s without a hit.
Each widget rebuilds its text only when its value changes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Horizon Settings > PvP & HUD... is a new page of options a PvP player sets, all off by
default (docs/pvp.md). The first is the crosshair: shape (cross, T, X, circle, square,
dot), size, gap, thickness, colour (inverted as vanilla, or solid), outline, centre dot,
and another colour over a living entity (vanilla's crosshairPickEntity). Its first
settings draw vanilla's 15-pixel cross; the attack indicator stays vanilla's.
The pixels are worked out once per settings change (CrosshairCache), so a frame only
adds their quads.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
With transfers off the server turns a transfer away at the handshake and
closes (ServerHandshakePacketListenerImpl.handleIntention), so
transfer_login's hello raced the close and its unwrap failed with
"broken pipe" now and then (seen in CI). The hello's send error is now
ignored; the disconnect is still read. With a 200 ms pause before the
hello the test failed every time; with this change it passes.
Fixes#9376
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A player's native build on 2026-11-04 failed with "horizon-mixins failed
(exit status: 1)" on Horizon 998b1d8, the commit fast_items 0.1.0 pins.
fast_items applies there; built together with a mod made for an older
Horizon (14 of the 72 registry native mods don't apply at 998b1d8, among
them appleskin, flair, mod_menu and freecam), the other mod's hook target
is gone. The diagnostic named that mod only deep in a file path. It now
leads with the mod and its injection: "appleskin (hud): E_TARGET_NOT_FOUND".
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: caps `parse_nodes` recursion at 128 where `execute … run` redirects to the root, so the 400-deep chain from #9252 fails to parse instead of overflowing the server thread; the 400-deep test would abort the binary without it. Chosen over #9320, which capped execution only. Nothing else found.
🤖 Agent: attachment now uses the entity-context collision shape (`entityCanStandOnFace`), so soul sand only takes a shulker under it and scaffolding follows the shulker's height. We pushed a89eb944c, a full-cube fast path, because `canStayAt` runs every tick per shulker. Proven with the client tests on a union with main (desktop).
🤖 Agent: the dismount on teleport matches `Entity.teleport`'s `stopRiding()`. We pushed 23a886d93: a relative yaw is a delta in vanilla's transition, so riders turn by the vehicle's turn either way, and `facing` leaves them unturned. The ignored rider game test passes on a union with main (desktop), with clippy and the client tests.
🤖 Agent: mobs now stop a dragon fireball and take its breath, through the lingering clouds' `cloud_affect` path (undead healed). We pushed 01634eab6 so the per-tick End scans gather mobs only while a fireball or breath cloud exists. Proven with fmt, clippy and the client tests on a union with main (desktop).
Keep the packet seed and category, update the bound position each tick,
and stop the old object when an entity is removed or its id is reused.
Compare the sound-engine lifecycle against the owned vanilla client.
Co-Authored-By: Codex <[email protected]>
🤖 Agent: merging. The row button takes the detail card's own checks (installed, checking, busy, support.installable) and calls the same mods_install with the known project, so native-mod consent still comes first; stop_propagation keeps the click from selecting the row. Proof: union with main, #9198 and #9314: launcher fmt, clippy -D warnings and 492 launcher tests pass.
🤖 Agent: merging with one change. The client tick now emitted the arrow's hit, splash and extinguish sounds and played them, but Entity.playSound passes no player to Level.playSound and ClientLevel.playSeededSound plays only for its own player (26.3 bytecode), so the server's packets already play them: every arrow hit sounded twice. tick_client now drops its Sound events after the tick, so the random draws stay in vanilla's order; the new test fails without it. The rest matches AbstractArrow.tick: the whole startFalling/tickDespawn branch is inside !isClientSide. Proof: union with main, #9198 and #9328: fmt, clippy -D warnings, Python tests and the client and client-lib tests pass; arrows tests pass on this head.
🤖 Agent: merging. Zombie.convertVillagerToZombieVillager and ZombieVillager.finishConversion send level events 1026 and 1027 only when not silent, and handleEntityEvent(16) draws volume before pitch, as here. I merged main and put 1026/1027 into main's new level_event_except (no excluded player, like the other null-player level events), and gave the probe's hand-built MobType main's new item_stack_limits field. Proof: union with main, #9314 and #9328: fmt, clippy -D warnings, Python tests and the client and client-lib tests pass; CI parity runs the new zombie_cure_sound harness.
🤖 Agent: bells now tick on the headless server before the renderer guard, matching `BellBlockEntity.serverTick`/`makeRaidersGlow`. Small cost to know: while any bell is in `be_anims.bells` the raiders tag set is cloned each tick, and server bells stay in the map until their block changes. Proven with the client tests on a union with main (desktop).
🤖 Agent: `summon` now goes through `server.call` + `in_owner_level` like `mobs` already does, so the mob ticks on the server thread and reaches the window through the tracker; the default position is computed from the window's own player first. Nothing else found.
🤖 Agent: `self.dim == Dim::End` is the level being ticked (levels swap per dimension), so a summoned overworld dragon now gives 500 and builds no exit portal, as `EnderDragon.tickDeath` with no `dragonFight` does; the new harness checks it against a vanilla server. Nothing else found.
🤖 Agent: matches `Dolphin.tick` on a client (NoAI return, in water, lengthSqr > 0.03, four particles at ±0.3 of the yaw, one `1.2 - nextFloat()*0.7` draw a tick from the mob's own random). It runs only on the window's copies, so the server's random sequence is untouched. Nothing else found.
🤖 Agent: matches `SpellcasterIllager.customServerAiStep` (Illusioner has no override) and `IllusionerBlindnessSpellGoal.performSpellCasting` blinding any LivingEntity target. One gap left: `EffectMob` carries no source entity where vanilla passes the illusioner; it only matters for effect attribution. Proven with the client tests on a union with main (desktop).
🤖 Agent: `Marks` keys each mark by item id, the catalogue's hash and the window scale, and hands every replaced image to `drop_image` once per frame next to `sky.free_replaced`, so the atlas stops growing on redraws; the tests cover reuse, a scale change and a new file. Nothing else found.
🤖 Agent: Ludovic decided on 11-04 to remove Horizon's RAM limit, so this goes in. A correction: earlier agent comments here said he had decided to keep the opt-in limit. That was wrong; he never decided that. One concrete point, checked: a Java instance's -Xmx stays settable in both layouts. Classic: main.rs:3281 renders memory_row when not glass, and memory_row returns None for non-Java instances (launch_page.rs:250). Glass: the Edit dialog renders glass_memory_row (glass_instance.rs:678), and memory_instance filters to Java instances. java_launch.rs:427 still passes -Xmx{memory_mb}M. Our 13:57Z review was wrong about this. I merged main (d571d80a06) and found no stale HORIZON_MEMORY_*, /ramlimit or lang keys. Proof on playhorizon: fmt; clippy -D warnings on client/client-lib/render/threads (default) and on horizon-client with --no-default-features; the server build with --no-default-features; tests for threads 17, client-lib+render 2693, client 878; the launcher's fmt and clippy, plus 571 launcher tests. Thanks rdbtCVS.
Entity.playSound passes no player to Level.playSound and
ClientLevel.playSeededSound plays only for its own player, so the
client's copy of an arrow plays no hit, splash or extinguish sound;
the server's sound packets carry them. Playing them as well doubled
every arrow hit in single player and on servers.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- game-build.yml and launcher.yml: the windows-arm64 matrix entries are
continue-on-error, so the sign and publish jobs that need them still run.
- nightly-build.yml: horizon-windows-arm64.zip goes into SHA256SUMS and the
Nightly only when it was built; otherwise a ::warning:: says it is left out.
- launcher.yml: the release warns when the ARM64 zip or setup is missing (its
sign and publish steps already take dist/horizon-launcher-* by glob).
- Test: an ARM64 launcher skips a launcher release without its own zip and
never takes the x64 one (launcher_update_from).
- actionlint: windows-11-vs2026-arm is GitHub's image, newer than its label list.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: a player with no cosmetics and no saved choice now sees none on others and the game never asks the service about them; owners see them as before, an explicit choice wins. Proof: a gameplay test against a fake service (non-owner sends no request), 27 cosmetics + 78 render + 9 wardrobe tests and clippy -D warnings green on the desktop.
🤖 Agent: ten merged fragments numbered through API 409 by the repo's own stamper; the workflow now warns instead of failing, because the setting that lets Actions open PRs would also let it approve them.
When disconnecting or leaving a server/single player world, leave_server() in remote.rs cleared only top-level mob arrays, leaving GPU skin models, skin frame vertex queues, parked dimension levels, remote objects, and entity draw spans allocated. These cached models could persist into the title screen renderer, causing Nether mobs such as Wither Skeletons to render and wander across the main menu panorama.
- Delegate leave_server() entity cleanup to clear_entities().
- Extend clear_entities() to also clear remote objects, projectiles, parked levels, render vertex buffers, draw spans, entity motion, and reset skin models and frame draws.
- Implement Levels::clear().
- Expose SkinModels::restart() and reset skin draw frames on entity clear.
Fixes#9074.
Managed by Axxio.
Co-Authored-By: Antigravity <[email protected]>
The native hook stamp workflow has failed since 14:35Z: the repository
doesn't let GitHub Actions open pull requests, so ten merged fragments
stayed unnumbered. This stamps them on current main with
tools/stamp_native_hooks.py, and makes the workflow leave a warning
naming the pushed branch instead of failing, since enabling that
setting would also let Actions approve pull requests.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
#9220's own MobType literal lacked main's item_stack_limits field, so its
tests no longer built; #9271 carries the same 50-line fixture with it.
Both now use one fixture_mob that knows the air and the raider species.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A player whose account owns no cosmetic now sees a vanilla game by default:
no cosmetics or name tag styles on other players, and the game never asks
the service about them. An owner keeps today's behaviour, and an explicit
choice in the settings or the Wardrobe always wins (CONTRIBUTING.md:
Horizon-only features that change the look are off by default).
- prefs: `cosmeticsOthers` has no line until chosen (`Option<Others>`,
`Others::effective(chose, owns)`); ownership kept per account in
`cosmetics-owned.txt`.
- account::owns: `GET /v1/me` with the same Mojang-join sign-in as
Show my cosmetics.
- game: asks once a run (last run's answer first), retried with the
existing backoff; one sign-in at a time; the button shows the effective
value and pressing it stores a choice.
- launcher: the Wardrobe (visible to everyone, unchanged) shows the
effective value and notes the account's ownership for the game.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Main's glyph callback returns a font::Glyph (its own box from the pen):
text_vertices takes it, and native_draw_text sums its advance.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Mob.DATA_MOB_FLAGS_ID (15) carries NoAI as its first bit: the LAN and
dedicated host now sends it with the dragon, and a Horizon client reads
it and stops the dragon's tick where EnderDragon.aiStep does on a client
(no flap, phase or parts). The harness also checks what a vanilla-codec
client is paired with.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: matches Breeze.isInvulnerableTo (source.getEntity() instanceof Breeze, the owner for projectiles); main merged (trident/bee conflicts taken from main plus attacker_species, and main's new damage_type test literal filled in). The older wind-charge special case in game/wind.rs now duplicates breeze::accepts_damage: follow-up. verify green at this head.
The index loop pushed a packet whose length ran past the end of the
stream (a recording that stopped mid-write), so seeking to it could
only fail. Metadata's mcversion follows VANILLA_VERSION instead of a
literal.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
canStayAt runs every tick for each shulker; the prepared collision table
gives full cubes the canonical Shapes.block() identity, so skip the face
shape and XOR join for them.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
scene.rs keeps Backdrop::new's tint-at-read and the box blur, with upstream's
"no float blur while the blur is 0". launcher/lang/*.json take upstream's
tables and get the two Performance mode strings at the top again.
TeleportCommand passes a relative rotation as a delta, so
calculatePassengerTransition's 'add 0' still turns each rider by that delta
from its own angle: the rider keeps its angle to the vehicle either way.
Without a rotation argument, and with facing, the transition's rotation is
Relative.ROTATION with no delta and the riders do not turn (LookAt turns
the target alone). Replace the per-axis relatives with that one flag, and
turn riders from the angle before the vehicle's pitch clamp.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: a lectern with no book now reads page -1 as vanilla's Mth.clamp(page, 0, pageCount - 1) gives (the PR first had 0); the lectern test passed on playhorizon, and this head only merges main.
The stamp tool stamps native_hooks.rs itself and refuses it in `comments`
(test_stamp_native_hooks failed on this branch).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
tick_end runs these every tick in the End, where hundreds of endermen can
live; with no dragon projectile there is nothing to hit.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: added one guard: the open menu's mouse-down-out closes it on the same press, so a right click on the open row reopened it instead of closing it; the handler now skips that press as the ⋮ does (menu_closed_at). Desktop launcher run at this head: fmt, clippy -D warnings, all tests 0.
🤖 Agent: the location pointer now follows a symlinked ~/.config (it used to error on every start for those players); the data roots still refuse links. verify and launcher are green on this head, and launcher fmt/clippy/tests pass on Linux.
A fixed ALSA period that a device does not support made build_output_stream
fail, which left the game with no sound on that device. Retry with the
default configuration then.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
launcher/lang/*.json take upstream's tables and get the two Performance mode
strings at the top of each, so later additions at the end of upstream's tables
no longer conflict.
Prefer DirectX 12 for Auto on Windows ARM64 while preserving Vulkan as an option. Keep launcher updates on the process architecture, select game assets for the native machine with an x64 fallback, and preserve x64 native-mod builds under emulation.
🤖 Agent: the Cull wrapper now pads each row by `gap`, so the mods, modpack-report and project-version lists (gap 8, rows without a margin) finally get their spacing, while the release and notes rows drop their own mb so nothing doubles; radius::CONTROL/ROW equal the old 10/12 px literals, so the field corners don't move. Launcher clippy -D warnings and 476 tests green on this head.
🤖 Agent: #[serde(other)] Unknown lets /v1/shop and /v1/me answers with a state a newer service adds still parse, so the Wardrobe's top-bar check (shop::shown, which reads open and the dates, not the state) keeps working; the test covers all three answers and the on-disk round trip.
🤖 Agent: item_cached now refuses an id or hash outside 1-64 of [A-Za-z0-9_] before touching the disk or the service, the rule horizon_cosmetics::fetch already applies, so a catalogue entry like ../x can't read or write outside the cache folder; the traversal test also checks that no request was made.
The open menu's mouse-down-out runs on the same press and closes it first,
so the row's right-click handler saw no menu and reopened it. Skip that
press, as the ⋮ does with closed_by_this_press.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merging. Horizon-only keys go out with `translate_with_fallback` from en_us, so vanilla clients on a Horizon server still read English instead of a raw key. Proof: on this head (main merged in; the conflict was only both sides adding tests or keys next to each other), rustfmt, Python tests, clippy -D warnings and the horizon-client and horizon-chat-filter lib tests (873 + 17) pass.
🤖 Agent: docs only, follow-up to #9308 (merged at its first head): labels the 2026-11-04 Mac mini reports 'offscreen, not comparable', since Horizon offscreen against a windowed vanilla isn't a fair pair; checks green at 2f8c8ddab.
🤖 Agent: merging. `/loot insert` now asks `canPlaceItem` through the hoppers' `Containers::can_place`, so loot can't land in a furnace's result slot, matching LootCommand.distributeToContainer. Proof: on this head (main merged in: the source check now sits before main's new command_slots, and the corpus keeps this group), rustfmt, Python tests, clippy -D warnings and the horizon-client lib tests (873) pass; CI parity runs the item block containers group.
🤖 Agent: merging with one change, from the 26.3 client's bytecode: AbstractBoat.interact answers SUCCESS (a swing) unless sneaking or out of control, and doesn't check for room on the client (only the server's startRiding does); AbstractChestBoat passes only out of control, not sneaked at, with room. Minecart.interact is the one that answers CONSUME. Paintings and item frames were right. Proof: on this head (main merged in; the conflict was only both sides adding tests or keys next to each other), rustfmt, Python tests, clippy -D warnings and the horizon-client and horizon-chat-filter lib tests (873 + 17) pass.
🤖 Agent: finishes #8632 (GabrielNichols): native build cache strings in all 85 launcher tables (inserted at the top so main's appends don't conflict), a build refused only while the cache is busy, launcher reference regenerated; also native cache locks are now unlocked before closing, since a child spawned meanwhile kept them taken (flaky Busy; new test fails without it). Desktop run at this head: fmt, clippy -D warnings, all launcher tests 0.
🤖 Agent: merging. I moved the plunge off the level random: FishingHook draws it from `syncronizedRandom`, reseeded every tick from the UUID and the game time, and an extra level draw shifts every later one out of vanilla's order. It is also computed in float, as Mth.nextFloat does. rdbtCVS's agent then shared the helper with remote hooks. Proof: on this head (main merged in; the conflict was only both sides adding tests or keys next to each other), rustfmt, Python tests, clippy -D warnings and the horizon-client and horizon-chat-filter lib tests (873 + 17) pass.
EnderDragon.aiStep returns after the flap and checkCrystals when
isNoAi() (flapTime = 0.5): no flight history, phase, steering or parts.
Horizon's dragon never read NoAI: a NoAI dragon flew off, out of a
force-loaded chunk, and a save could then lose it. The dragon now loads
and saves NoAI, and its tick stops there.
tools/test_dragon_no_ai_parity.py compares the dragon's position and NoAI
after 100 ticks and after a restart with vanilla's dedicated server.
Fixes#9316
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: The padding follows the Mipmap Levels option through `mipmap_option`, which starts at 4 until `set_mipmap_levels` runs. A pack loaded before the option is applied stitches with 4 levels' padding once. Minor. I pushed cddb5526: merged main after #9059 landed (clean). Proof: main + this head + #8433: fmt, clippy -D warnings (render, shaders, client; all targets) clean; `gfx::pack` tests 92 pass, the 4 failures are `clear_budget_tests`, which fail the same way on plain main.
🤖 Agent: Items and primed TNT now end their move with `getBlockSpeedFactor`, through #8096's shared helpers, and a no-physics item skips it as vanilla does. Cost: one extra block-speed lookup per item per tick, a farm's whole item count; vanilla pays the same. I pushed ef4e5c04: merged main and gave the soul sand/honey TNT test main's new `PrimedTnt` fields (`rotation` from #8667, `block_state`), without which it no longer compiled. Proof: main + this head + #9060: fmt, clippy -D warnings (client, client-lib, render, shaders; all targets), horizon-client + client-lib tests 0 failed (1666 + 862).
- Self-update, game installs and native-mod toolchains follow the running
launcher's architecture again (std::env::consts), not IsWow64Process2's native
machine: an x64 launcher on an ARM64 PC stays x64, so an update never swaps a
working emulated setup for an untested ARM64 window. The ARM64 launcher comes
from horizon-launcher-setup-arm64.exe.
- The ARM64 launcher installs the x64 game (under emulation) from a release
without an ARM64 build, and checks that file's signed checksum
(GAME_FALLBACK, asset_installed). Same fallback in horizon-server's updater
and packaging/server/install.ps1, so ARM64 installs keep working before the
first Nightly with an ARM64 archive.
- ci.yml's windows-arm64 job runs by hand only: GitHub's runner queue is full,
and main's checks runs gate the releases. The Nightly and launcher releases
build the ARM64 archives every run.
- launcher.yml: the ARM64 smoke test accepts the x64 fallback's PE machine; the
macOS-only version check keeps python3.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The pointer, its lock and the startup check went through `checked_path`, which
refuses any link on the way. A Linux player whose ~/.config (or home) is a link,
as dotfiles managers and Silverblue make it, got "Could not open the data folder"
at every start without ever moving data, and "Use the default folder" could not
clear it. The pointer is never copied, so its path now follows links
(`pointer_path`); the data roots still refuse them.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Horizon ran offscreen (no presenting) while vanilla drew into a window, so the Mac rows are not a fair
comparison; the website keeps the Mac mini's 2026-10-22 runs. The reports stay as a record, labelled.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The lang tables merged by key: main's keys (with #9286's shop changes) and
this branch's 14 data-move strings.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: data only (9 perf_bench reports + a note under benchmarks/perf); checks green at be11ce8db. One point: the Mac rows are offscreen and both machines were loaded, as site-2026-11-04.md says, so don't read them as clean ratios.
🤖 Agent: merging. `shutil.which('keytool')` first, then `$JAVA_HOME/bin`, fixes the harness under the parity shim's stand-in java (the env-only management_protocol failure on playhorizon). Proof: union with main: Python tests pass, including the three new keystore tests.
🤖 Agent: merging. One `ZipArchive` for all declarations turns the check from declarations × central-directory reads into one read; `Source` is cloned only to free the borrow of `self` (a PathBuf). Proof: union with main: fmt, clippy -D warnings and horizon-mods tests pass.
🤖 Agent: merging with changes. Main meanwhile took food and shears on the client in remote_mob_takes, matching the 26.3 bytecode (Animal.mobInteract and Sheep.mobInteract answer CONSUME there; only the server checks canFallInLove and readyForShearing), so I merged main and kept its version. What this PR still adds is the dye rule, now `sheep_takes`: DyeItem.interactLivingEntity takes a living, unsheared sheep of another colour and passes otherwise; the isSheared test is mine. Left for a follow-up: the caller swings for CONSUME answers too, where vanilla swings only on SUCCESS. Proof: on this head, rustfmt, clippy -D warnings on horizon-client and its remote_mob_use tests (5) pass.
🤖 Agent: merging. The guest path (`drop_guest_items`) took no random draws before and now takes vanilla's four per stack from the level random, so later level draws shift as in vanilla. Vanilla draws from the entity's own random; Horizon keeps the level's in all three places, as before. Proof: union with main: fmt, clippy -D warnings and client tests pass, including the bit-exact drop_motion test.
`Entity.teleport` stops riding unless the transition is `asPassenger`, and
`calculatePassengerTransition` gives each rider the vehicle's new angles plus
its own offset (`passenger.getYRot() - getYRot()`), or the vehicle's angle
alone on an axis the command set relatively (`+ (relatives has Y_ROT ? 0 : ...)`).
The shared writer only moved riders' positions and only the player arm
dismounted, so `/tp` on a spider jockey or a mob in a boat was seated again by
the next sync (the command reported success and nothing moved), and a rotated
`/tp` left mob riders, the host and guests facing where they were.
- `teleport_to_in_level` dismounts a mob or an armor-stand target before it is
placed (`Entity.teleport` -> `stopRiding`), and carries the command's per-axis
relatives for the riders.
- `teleport_passengers` places the riders at the angles the vehicle was placed
at (`passenger_rotation`): mob riders, the host riding a vehicle and the
hub's guests, recursively, pitched through `Entity.setXRot`'s clamp. A
relative axis gives the rider the vehicle's own angle. A turned guest gets one
position packet carrying its new absolute rotation with its motion kept
(`Hub::teleport_rider`).
Fixes#9289
Co-Authored-By: DeepSeek Harness (deepseek-v4.1-flash) <[email protected]>
🤖 Agent: the shader's facing test compares order-preserving u32 keys of the same f32 thresholds visible_from uses, MARGIN included, so no driver's subnormal flush can change a run; a grouped mesh keeps one whole draw, and the 4-run cap fits the 3-bit count in runs.
Each draw baked a new image for the equipped name tag, and GPUI keeps every one
in the sprite atlas until it is dropped. Marks are now kept per item id, its
file's hash and the window scale. One replaced by a scale change or a new file
is dropped from the atlas on the next frame.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01JLHp7yEJwPTJ2kw29LWdk1
🤖 Agent: the bundle click now checks may_pickup and may_place on the slot, as vanilla Slot.allowModification does, so a bundle in a take-only or locked slot keeps its contents; the new test covers the withheld slot.
Main grew its own pure helpers and tests in remote_mob_use.rs (nautilus,
golems, allay, armadillo). This keeps main's version and adds only what
this PR got right: DyeItem.interactLivingEntity takes an unsheared sheep
of another colour and passes otherwise; shears stay CONSUME on a client.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
#9286 moved the shop to the website and removed the launcher's claim window
rules, so the clock correction has nothing left to correct; this merge takes
main's shop files. ClaimState::Unknown still matters: /v1/shop and /v1/me
answers decide whether the Wardrobe is in the top bar, and a state a newer
service adds made them fail to parse.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
#9286 removed Entry::lines; the merge keeps Entry::plain and drops lines
from the tests' entry helper.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
launcher/lang/*.json take upstream's tables and get the two Performance mode
strings at the top of each, so later additions at the end of upstream's tables
no longer conflict.
🤖 Agent: Built-ins are typed int/float/bool from the probe tables. A custom uniform whose declared type the expression doesn't give reads 0 as a whole, with a note naming it. Nothing else found. I pushed 5eef8152: merged main.
🤖 Agent: merging. One cost to know: every item component read now asks `ids::dropped_component` and `ids::quirks`, two thread-local reads, even on a 26.3 server; cheap next to the component decode itself. Proof: union with main: fmt, clippy -D warnings and horizon-protocol tests pass, including the three new wire fixtures from 1.21.11 registries.
🤖 Agent: `textureMatrix` maps to `gl_TextureMatrix[0]` in vertex stages only, while a fragment stage's own `textureMatrix` keeps its frame slot (zero), matching the probe. Merged with #9180's tuple form of `core_matrix` (type `mat4`). Nothing else found. I pushed e8d6861b: merged main (#9057/#9180 landed).
LecternBlockEntity.loadAdditional clamps Page to 0..getPageCount(book) - 1,
and getPageCount is 0 without a book, so Math.min(Math.max(page, 0), -1)
gives -1. Every Horizon reader of the page applies max(0) first.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A child process spawned while a lease or a Cargo lock is held keeps a copy
of the descriptor until its exec, so closing ours alone left the lock taken
and the next lease or cleanup read Busy (seen as a flaky
clears_only_disposable_outputs... test while other tests spawned Cargo).
Unlock explicitly on drop: that frees it for every copy.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: the changed blocks now go out between the random ticks and runBlockEvents, as in ServerLevel.tick, and what the events and block entities write is held to the next tick; piston-slide measured 3 ticks for every move in three runs on a union with main, piston-push and predict pass, and I fixed the check counting from the previous move's head.
🤖 Agent: main merged again (mod_api.rs keeps both ClientboundPlayPacket and ServerboundPlayPacket re-exports); native send-packet built, clippy and tests green on union 760aa5921.
🤖 Agent: merging. Off, `hdr.lighting && hdr.look` also skips the water and cloud second passes, so HDR Lighting off costs less than on, and NikitaMGrimm's PQ captures show 100% of pixels within 1 code of SDR. I moved `set_hdr`'s doc comment back onto `set_hdr` (their nit). Proof: union of this head with main: fmt, clippy -D warnings, render and client tests pass.
🤖 Agent: matches `Projectile.hitTargetOrDeflectSelf` + `Breeze.PROJECTILE_DEFLECTION`: REVERSE power 1, the sound from the breeze, no `onHit`, and `lastDeflectedBy` stops a second turn. Proof: union of all cg-31 PRs on main 6d43a3593b (playhorizon): fmt, clippy -D warnings (client/lib/render all-targets + server lib), client+lib tests 1638+850 ok.
Launcher 0.2.0 reaches 0.2.40 by itself wherever it can write its own folder (checked with the real
0.2.0 builds on macOS and Linux: tags, signing key, legacy list and asset names all still work). A copy
in a read-only place (the disk image, macOS's translocated copy of an app never moved out of
Downloads, a .zip opened in Explorer) fails at every check and said so only on Settings' Launcher
line, so the player never saw it.
- The window downloads and checks the new launcher (backend::stage_update), then the sidebar says
"A new version is available" with Update: the window closes, the launcher's own file runs as the
installer (`apply-update`, update_handoff.rs), waits for the window's process to exit, puts the new
launcher in place with the same code `horizon-launcher update` uses (place_staged) and starts it.
Closing the window with an update downloaded installs it too, so launchers still update themselves.
- A copy that can't replace itself (backend::cannot_replace_itself) says so with Download again (the
website's install page); a failed install starts the old launcher, which says why.
- Docs: launcher/README.md, the "download again once" row in docs/versioning.md; README's release
link no longer filters on the old launcher-v tags.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The second extension starts with the head the first one left, so taking the
first piston_head read gave a negative count. The landed head is now the first
after the moving block. A loaded machine can run two client ticks between
reads: when the read before the move or before the landing skipped one, the
check says so instead of reporting a wrong count. On playhorizon (load 25-47)
three runs measured 3 ticks for every move.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Armadillo.mobInteract is FAIL while isScared() before Animal.mobInteract, so spider eyes no longer end the click on a rolled-up armadillo. Also fixed: brushOffScute is false on a baby, so a baby's brush is not taken (38c31cac5d). The main merge 1e9a62f124 only resolves the import line with #8691 (same resolution as the cg-31 union). Proof: cg-31 union on main 6d43a3593b (playhorizon): fmt, clippy -D warnings, client+lib tests 1638+850 ok.
🤖 Agent: merging. `native_notice` is already a plate, so taking it out of `card` also removes the plate-in-plate it had in the results view. Proof: union with main: launcher fmt, clippy -D warnings and lib tests pass.
🤖 Agent: .mrpack import, green on this head; rdbtCVS's change request was dismissed because it rested on #6660, closed by Ludovic (Java instances stay).
🤖 Agent: Forge install for Java instances, green on this head; the only blocker was a change request resting on #6660, which Ludovic closed (Java instances stay).
🤖 Agent: the partner entry goes below the official one through the same once-only marker logic, so a player who deletes it never gets it back; proof: multiplayer::tests (20) and clippy -D warnings green on the desktop.
🤖 Agent: merging. On the single-player path these three calls repeat what finish_close_world already did, and `stop_all` on an empty mixer is a no-op, so the only change is for leaving a server or being kicked. Proof: union with main: fmt, clippy -D warnings and client tests pass.
🤖 Agent: lands the whole stack (#8637#8644#8647#8652#8657#8658#8660#8690). Concrete fix: be258bc235 sent every ParticleAt as a speed-0.1 packet (ink at 1/100 speed, client-side hearts/splash/smoke drawn twice); squid ink is now MobEvent::LevelParticles as Squid.spawnInk's sendParticles, and main's wolf test got the new client_tick argument. Proof: 569f9644dc in the cg-31 union on main 6d43a3593b (playhorizon): fmt, clippy -D warnings, client+lib tests 1638+850 ok; CocofireHD's later main merge 5839363b51 only resolves allays.rs (main-hand check + swing kept on main's Option<ItemStack> inventory).
🤖 Agent: ConversionType.convertCommon also copies PortalCooldown, InvulnerableTime, the fire flag and entity tags, which are still not carried over; the five flags here match. The land-stop was main's new MobType field item_stack_limits in this test's literal: main merged and the field added (79576177db). Proof: cg-31 union on main 6d43a3593b (playhorizon): fmt, clippy -D warnings, client+lib tests 1638+850 ok.
🤖 Agent: /attribute now runs in the target's level (in_target_level), so an unbounded @e from the Nether reaches the Overworld cow, as EntitySelector.findEntities searches every level. Proof: command_exec harness PASS (82 s) with the 8 new corpus cases on the cg-31 union on main 6d43a3593b (playhorizon), plus fmt, clippy -D warnings, client+lib tests 1638+850 ok.
🤖 Agent: the slide now starts from the move packet (createInterpolationHandler NO_OP) and is only counted down in client_tick; a riding shulker gets the peek reset without the slide. First parity run died in 2 s on the runner's dead .horizon-prepare.lock link (environment); rerun 41856251948 queued. Proof: cg-31 union on main 6d43a3593b (playhorizon): fmt, clippy -D warnings, client+lib tests 1638+850 ok.
🤖 Agent: a drop starts the same start_build_import the picker uses, behind can_import_build, so a removal in progress can't race it; .exe/.gz/.tgz are taken unread so build_import gives the precise error, and extensionless files are read last. I fixed the wrap spaces in the two error strings and merged main (the stale-key verify failure came from main, fixed by #9265).
🤖 Agent: a failed datapack copy now reports its error instead of passing silently; a half-copied pack still stays in datapacks/ (no cleanup), worth a follow-up. Union 760aa5921 tests green.
🤖 Agent: the guest-entity and dead-guest lists no longer hang on has_guests (which skips dead and spectating guests), so a lone dead player's ender pearl still vanishes as in vanilla; pearl_death and the client tests passed on union 760aa5921.
🤖 Agent: merging. Only chests get the precedence; barrels, shulker boxes, hoppers and dispensers load through the same RandomizableContainer.tryLoadLootTable in vanilla and still keep their Items here: a follow-up. Proof: union with main: fmt, clippy -D warnings and client tests pass; CI parity runs the new chest_load harness.
🤖 Agent: merging. The host's own path now gates only the sound on `elder_curse_param`, and the apparition stays unconditional, which is what ClientPacketListener.handleGameEvent does for GUARDIAN_ELDER_EFFECT. Proof: union with main: fmt, clippy -D warnings and client tests pass (the new guardian test included); CI parity runs the new elder_curse harness.
🤖 Agent: merging. Splitting the reduction into `scd_finalize_pass` replaces the completion counter, which WGSL's memory model never promised would make other workgroups' writes visible; the cost is one 9-workgroup dispatch and a 9 KiB buffer per generated frame. Proof: union with main: fmt, clippy -D warnings and render tests pass (the GPU tests need an adapter; CI's run them).
🤖 Agent: merging. Risk noted: the server holds an extra `Arc<Chunk>` per handed-over column until the window's next tick, so an edit in that window copies the chunk on `Arc::make_mut`; one tick at most, and only for bulk edits out of view. Proof: union with main: fmt, clippy -D warnings, client and lan::handoff tests pass.
🤖 Agent: Pack shadow passes now draw every face (and without back-face culling), which roughly doubles the shadow pass's solid quads for Iris-format packs. Horizon's own packs keep the light-facing selection (`every_face: false`). `shadow_water` getting `cull: false` too is a guess the probe didn't measure, as the description says. I pushed 9fac526f: merged main.
🤖 Agent: merging. FireworkRocketEntity reads `getLookAngle()`, Mth's float sine table, so the boost now matches `travel_fall_flying` and the server; the remaining f64 `view_vector` use is `rocket_hand_angle`, which only places the spark. Proof: union with main: fmt, clippy -D warnings, client tests and tests/test_remote_own_rockets.py pass.
🤖 Agent: merging the Metal split and the minified core-shader fix. I dropped the ZIP validation from the download path: vanilla's DownloadedPackSource checks only the SHA-1 (checkExistingFile), the strict zip crate refused the streamed Ascendia archive that Horizon's own pack reader already reads (so other Java-readable packs would have been refused too), and it decompressed the whole pack on every cache hit. Proof: fmt, clippy -D warnings and the touched crates' tests pass on a union of this head with main and 19 other PRs.
🤖 Agent: `set_rotation` is the same eight lines in 11 impls (finite yaw, NaN pitch kept, clamp); a shared helper would keep them from drifting. Item, orb, TNT, falling-block and rocket loaders still read Rotation as [0, 0], as the description says. I pushed 764989a2: merged main, keeping main's new TNT `block_state` argument beside `rotation` (power, block_state, rotation), and gave main's new TNT and orb fixtures the field. Proof: main + this head: fmt and clippy -D warnings (horizon-client and horizon-client-lib, all targets) and horizon-client tests pass, 0 failed.
🤖 Agent: Stitching the pack atlas at each sprite's own size only applies to flat draws (a stitched atlas is dropped for grid draws), and a device that can't stitch falls back to the grid with a log line. Nothing else found. I pushed 02e68e15: merged main (#9057/#9180 landed).
Horizon partners with Afri-Network, a server for players in South Africa.
At start the game now adds it once below "Horizon Official" in
servers.dat, like the official server: never again after the player
deletes it (horizon_partner_servers.txt), and not when the list already
has its address. servers::insert_after keeps hidden entries and unknown
fields, as prepend does.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Keep the independent inline parser and its fixture definitions alongside the named weather regression.
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
🤖 Agent: `InteractionResult.FAIL.consumesAction()` is false, so `handleUseItemOn`'s build-limit message fires. ANY_BLOCK_USE keeps its old condition. Proof: union of all cg-31 PRs on main 6d43a3593b (playhorizon): fmt, clippy -D warnings (client/lib/render all-targets + server lib), client+lib tests 1638+850 ok.
The perf_bench reports behind playhorizon.dev/benchmarks as of 2026-11-04 (horizon-site#58): RD 12 and 32,
three games each, 1280x720, VSync off. The Mac's session was locked (Horizon offscreen) and both machines were
busy (load 17-50 on the Mac, 28-70 on the desktop); site-2026-11-04.md says how they were made and what that costs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: the reader treats the byte as `UnsignedBytes.toInt`; the writer clamps to 0..=255 before the byte cast, so 255 and 128 round-trip. Proof: union of all cg-31 PRs on main 6d43a3593b (playhorizon): fmt, clippy -D warnings (client/lib/render all-targets + server lib), client+lib tests 1638+850 ok.
🤖 Agent: after the cleanup, `clear_remote_objects` (new connection, level, dimension) drops the orbs with the rest of the old ClientLevel. Proof: union of all cg-31 PRs on main 6d43a3593b (playhorizon): fmt, clippy -D warnings (client/lib/render all-targets + server lib), client+lib tests 1638+850 ok.
🤖 Agent: the refusal branch now restores the fire set by the arrow and reverses at power 0.2, as `AbstractArrow.onHitEntity` does when `projectileReceivesSideEffectsOnHit(false)`. This also covers mobs in their hurt cooldown, whose arrows used to vanish. Proof: union of all cg-31 PRs on main 6d43a3593b (playhorizon): fmt, clippy -D warnings (client/lib/render all-targets + server lib), client+lib tests 1638+850 ok.
🤖 Agent: both `levelEvent(1500, …)` calls in `ComposterBlock` (useItemOn, the hopper input) are the null-source overload, so 1500 belongs in the no-exception list. `play_unshared` also stops the serving side from sending a second sound packet. Proof: union of all cg-31 PRs on main 6d43a3593b (playhorizon): fmt, clippy -D warnings (client/lib/render all-targets + server lib), client+lib tests 1638+850 ok.
🤖 Agent: `hub.entity_event` broadcasts with no exception, so the eater sees its own event 46, as with `sendToTrackingPlayersAndSelf`. A mob's 46 arrives before its move packet, so its trail runs between the two old positions, as on a vanilla client. Proof: union of all cg-31 PRs on main 6d43a3593b (playhorizon): fmt, clippy -D warnings (client/lib/render all-targets + server lib), client+lib tests 1638+850 ok.
🤖 Agent: the pitch `nextFloat` stays inside `!isSilent()` at both call sites (`Witch.aiStep` line 155, `performRangedAttack` 262), and the test pins the untouched random. Proof: union of all cg-31 PRs on main 6d43a3593b (playhorizon): fmt, clippy -D warnings (client/lib/render all-targets + server lib), client+lib tests 1638+850 ok.
🤖 Agent: `Level.isDarkOutside`/`isBrightOutside` are both false under `hasFixedTime`. A datapack dimension with fixed time still drinks, because the check is `nether || end`, not the dimension type. Proof: union of all cg-31 PRs on main 6d43a3593b (playhorizon): fmt, clippy -D warnings (client/lib/render all-targets + server lib), client+lib tests 1638+850 ok.
🤖 Agent: `extra.mount` is the vehicle (`getVehicle`), not the rider, so the guard is `!isPassenger()` and the `nextInt(50)` draw is skipped exactly as in `RaiderCelebration.tick`. Proof: union of all cg-31 PRs on main 6d43a3593b (playhorizon): fmt, clippy -D warnings (client/lib/render all-targets + server lib), client+lib tests 1638+850 ok.
🤖 Agent: `AbstractMinecart.isPickable` is `!isRemoved()`, and the ridden cart is skipped like `isPassengerOfSameVehicle`. The pull reuses `pull_hooked` for every hooked kind. Proof: union of all cg-31 PRs on main 6d43a3593b (playhorizon): fmt, clippy -D warnings (client/lib/render all-targets + server lib), client+lib tests 1638+850 ok.
🤖 Agent: checked the server thread: the serving Game has no local player (dedicated options), so every player, the window's too, gets its own packet. The 'in raid' set is still one player (`bar.player`), while vanilla's `raidEvent.getPlayers()` holds everyone within 96 blocks; a far second participant hears nothing. Proof: union of all cg-31 PRs on main 6d43a3593b (playhorizon): fmt, clippy -D warnings (client/lib/render all-targets + server lib), client+lib tests 1638+850 ok.
🤖 Agent: `max_stack` reads the patch first and falls back to the item when the component is removed, as `ItemStack.getMaxStackSize` does. The wire test covers the add and the `!` removal. Proof: union of all cg-31 PRs on main 6d43a3593b (playhorizon): fmt, clippy -D warnings (client/lib/render all-targets + server lib), client+lib tests 1638+850 ok.
🤖 Agent: matches `Bee.doHurtTarget` (poison on any LivingEntity the sting hurt, 10 s Normal / 18 s Hard). Not ported here: `setStingerCount` on a mob target, which vanilla also increments. Proof: union of all cg-31 PRs on main 6d43a3593b (playhorizon): fmt, clippy -D warnings (client/lib/render all-targets + server lib), client+lib tests 1638+850 ok.
🤖 Agent: Ludovic's call (11-04): the shop lives on playhorizon.dev/shop (live, Paddle checkout tested end to end with his account), the launcher keeps the Wardrobe with an Open the shop button. launcher job green on this head; verify running.
When running single player on an integrated server, `horizon-ctl summon`
spawned mobs directly into the client window's entity list rather than
delegating to the server thread. Because the client window does not tick
mobs (running `tick_remote` instead of `tick_world`), summoned mobs
never executed AI goals, took no environmental damage, and were not
visible to server queries.
Delegate `summon` to the integrated server in the owner player's level
when `self.integrated` is active, matching other control world commands
and allowing the server to tick the mob and broadcast it via standard
entity tracking.
Managed by Axxio.
Co-Authored-By: Antigravity <[email protected]>
the_launcher_reference_is_up_to_date: the native-cache command and the
native-cache-gib setting were missing from the generated reference.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
DragonFireball is stopped by any entity it can hit and DragonFireball.onHit
moves its cloud to the first LivingEntity within 4 blocks; AreaEffectCloud
.serverTick applies its instant damage to every LivingEntity in it. Horizon
only looked at players. Mobs now stop a fireball, its cloud snaps to them,
and both breath clouds (the fireball's and the flame phase's) put their
instant damage on the mobs inside through the lingering clouds' existing
cloud_affect path (indirectMagic, undead healed instead).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
🤖 Agent: I pushed 19b0fd00 and 27d625d8: merged main and filled `Resources.packed` (#9090), `Interface.initial_values` and `Resources.shadow_linear` in the new test, which no longer compiled against current main. Raw textures defaulting to linear + clamp and PNGs to nearest + repeat match the probe table. Nothing else found.
🤖 Agent: Merged after review fixes 2c7b4906: /rotate sends PlayerRotation instead of a teleport, the boarding cooldown counts per tick (not per turn), and riding guests hear their vehicle's teleport. Follow-ups listed in the review: mob passengers on /tp, the passengers' rotation, cross-dimension vehicles.
Give the entity-only probe its dry weather context and keep the new fixture beside stable command groups. Cite the predicate read context without treating abstract reads as complete Level method ports.
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
🤖 Agent: block sprite pixels lz4-packed once uploaded (-19 MB live heap); one risk to know: SpritePixels::get returns zeroed pixels if decompression ever failed, which would show as black textures on a rebuilt renderer (device loss, split screen), not a crash. rdbtCVS's scene_replay adaptation included; verify green on this head, merger waits for parity.
EnderDragon.tickDeath calls dragonFight.setDragonKilled and counts the
first kill's 12000 experience only when it has a fight, which only the
End gives (ServerLevel.getDragonFight). Horizon did both everywhere: a
dragon summoned in the overworld left an exit portal and a dragon egg at
0 0 there, and 12000 experience. /kill already checked the dimension.
tools/test_dragon_death_outside_end_parity.py compares the blocks left
and the experience with vanilla's dedicated server.
Fixes#9274
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: A `component =` output at a location now joins the other outputs at that location into one vec4 target. Mixed scalar kinds, or overlapping components, are refused with the variable's name. Nothing else found.
Resolve core_matrix with #9180's (matrix, type) form: textureMatrix is
(hz_TextureMatrix[0], mat4) in vertex stages; both blocks and both tests kept.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Unknown names and shape mismatches now zero the whole custom uniform, as your probes show. The correction comment about `round`/`ifb` (no such function, same 0) is noted. Nothing else found; #9153 builds on it.
🤖 Agent: A pack whose integer colortex0 can't reach the screen is now refused at load and cleared from the settings, like a panicking one. The old path cleared the screen every frame instead. The refusal's notes are logged before the reason, so the chat line stays short. Nothing else found.
🤖 Agent: `normalMatrix` now depends on `core_profile()`: a `#version 150`/`330` without a profile word counts as core, which matches the four probe pictures. Below 150 nothing changes, as the doc comment says. Nothing else found.
🤖 Agent: main's wardrobe_claim now calls shop_claim, so that one save moved out of this PR; the other five writes now print their error instead of dropping it.
🤖 Agent: read-only and after decode, so a mod can't change what the game handles, and with no mods the dispatch is a loop over nothing on the per-packet path; main merged in (next to #8938's test and #9187's stamped docs).
🤖 Agent: SHADOW_RESOLUTIONS and SHADOW_DISTANCES are ranges, so checking an int option's min and max covers every value the player can pick, and a 0 option drops the shadow pass, not just the map; main merged in (docs/api/shaders.md changelog next to #8512's).
🤖 Agent: getJumpPower is now jump_strength × multiplier × getBlockJumpFactor + getJumpBoostPower for every living mob, so cows and pandas jump half as high off honey as in vanilla; I dropped the unused free block_jump_factor wrapper and rabbit's re-export, and the #[ignore]d honey test passed on a tree with the vanilla resources.
🤖 Agent: matches DrawableGizmoPrimitives.getGroup (alpha 255 → lines with depth write, the rest linesTranslucentNoDepthWrite), uploading the opaque group first so one buffer serves both draws; I moved the two helpers so translucent_batchable keeps its own doc comment.
The sidebar's rows carry the menu about that instance (Edit…, Duplicate…, Open
folder, Export…, the desktop shortcut, Delete…), and it is reachable only
through a 32 px ⋮ button. A right click anywhere on the row now opens the same
menu.
Both entry points share one gate, `row_menu_after_press`: it opens the row's
menu, closes it when it is already that row's, and drops the Delete… confirm
flag when the menu moves to another row, which is what the ⋮ did before.
Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
🤖 Agent: Small and contained: flat terrain reads each sprite's rectangle from a table instead of assuming a uniform grid, the base for #9059/#9060. Nothing found.
🤖 Agent: This changes the vendored naga GLSL frontend: every non-constant struct member read now goes through a pointer, not a loaded copy of the struct. Every pack's GLSL takes that path, so a naga update must keep the patch (THIRD_PARTY_NOTICES records it). Constant expressions keep the old form. Nothing else found.
🤖 Agent: Atomic functions on a member of a shared vector in a declaration list or a shared struct now resolve. The rewrite is limited to `shared` declarations, so other GLSL is untouched. Nothing found.
🤖 Agent: `R3_G3_B2` is stored as 5/6/5 bits because that fits one NVIDIA driver's readback in Iris (the module says so). Another driver may store real 3/3/2, so the probe is worth rerunning on Mesa before more code depends on it. The quantising only touches passes writing such a buffer, so other packs pay nothing.
Main added TNT's block_state argument beside this branch's rotation:
both kept (power, block_state, rotation); main's new TNT and orb test
fixtures get the rotation field.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`execute run` redirects to the tree root, so parse_nodes recurses once per
nesting level. A crafted deeply nested command overflows the parsing thread's
stack (thread 'Server thread' has overflowed its stack) and kills the game.
Add MAX_PARSE_DEPTH (128) to parse_nodes' recursion: past the limit the branch
yields no result, so the command fails to parse instead of overflowing. 128 is
well below the ~300-level overflow measured on the server thread and far above
any real command chain; vanilla likewise caps command nesting.
Adds tests: a 400-deep execute-run chain is rejected (not overflowed), while
shallow execute chains still parse.
Closes#9252
IllusionerBlindnessSpellGoal.performSpellCasting adds Blindness for 400
ticks to the illusioner's target, any LivingEntity. The cast only sent
EffectPlayer, so a villager or iron golem it targeted kept nothing. A new
MobEvent::EffectMob carries the effect to the target mob, whose
add_effect already checks canBeAffected.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
SpellcasterIllager.customServerAiStep lowers the casting count every
tick, and Illusioner has no override of its own. Horizon did this only
for the evoker, so an illusioner's count stayed at 20 after its first
(mirror) spell: it kept the casting goal for good, cast nothing else and
never shot its bow.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
🤖 Agent: the zoom cone only boosts tiles covers() says are looked at, and the test pins the plan behind the player as identical zoomed or not; I made lodZoomDetail off by default until a measurement shows the extra far tiles land while the zoom is held.
`decode_block_entity` ports `LecternBlockEntity.loadAdditional` including
the `Mth.clamp(Page, 0, pageCount - 1)` the row only called "Book and
Page", so the row now says what the load does with the page.
The decode of a chunk's or a structure's lectern, `decode_block_entity`,
against the pages the book on it has: 7 on a two-page book loads as 1,
-3 as 0, a bookless lectern's 4 as 0, and a page already inside the book
(1 of three) is kept and saved back as 1.
Without the clamp the first case fails with `left: 7, right: 1` and the
second with `left: -3, right: 0`.
`LecternBlockEntity.loadAdditional` reads `Page` through
`Mth.clamp(page, 0, getPageCount(book) - 1)`: a page stored past the
book's last one comes back as the last page, one stored before the first
as the first, and a lectern with no book has the single page 0. Horizon
kept the stored number as it was.
A negative page was the visible half: `LecternBlockEntity.getRedstoneSignal`
(`books.rs::lectern_signal`) answers `floor(page / (pages - 1) * 14) + 1`,
so a stored -3 read 0 to a comparator where vanilla's clamped page 0 reads
1, and the next save wrote the out-of-range page back out. A page above the
end reads the same on both sides, since the signal saturates at 15 either
way, and the open menu already clamps (`menu.rs::set_data`), so what changes
is the load and with it the round trip.
🤖 Agent: I pushed c2aa51c1: merged main and resolved the generated `server-reference.md` row (main's text plus `dimension`), so `the_routes_table_matches_the_code` agrees again. Nothing else found.
A on a focused button now presses it when let go, if the focus is still on it.
Moving the focus, a cancel, a scroll, a dialog opening or another pad taking over
drops the press. On the title, X opens Options and B focuses Quit Game; on the
pause menu, Y focuses the exit button, and A confirms.
A screen says what a button is for (`ui::action_button`, `widgets::declare_action`)
and its pad button and glyph follow: Done, Back, Cancel, Apply, Options and Exit.
Nothing else names the screen. What a screen declared stops counting once another
is drawn over it or replaces it, and `pad status` lists what is declared, what
isn't and the glyphs drawn. tools/pad_menu_e2e.py checks these with the virtual pad.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01JLHp82xRaic2UQfeBTnyhW
The command SNBT parser (horizon-command) recursed on every nested
compound/list with no depth limit, so a crafted deeply-nested argument
(stack overflow: thread 'Server thread' has overflowed its stack). Both
ArgType::Nbt (summon/data/give NBT) and ArgType::Component (tellraw/text
components) route through this parser, so one guard covers both.
Add a MAX_DEPTH (128) recursion counter to the packrat State, checked in
State::parse: over-deep input now fails to parse (the argument is
rejected) instead of recursing until the stack overflows. 128 is far
below the measured ~300-level overflow on the smallest parsing thread and
far above any real command or data value. Adds tests that deep compound
and deep extra-style list input are rejected while shallow values still
parse.
Closes#9236Fixes#9261
The bundle sits in a chest slot whose rules refuse the pickup, with the
prepared catalog behind it so the stack really has bundle contents. A
left click with stone in hand adds nothing, a right click with an empty
hand takes nothing, and the empty bundle in the next slot still fills.
`BundleItem.overrideOtherStackedOnMe` gates both of its branches on
`Slot.allowModification`, which is `mayPickup` and `mayPlace` of the
stack the slot holds - not the placement test alone. `lan::menu`'s
bundle click asked only `View::may_place`, so a guest could fill or
empty a bundle in a slot whose menu refuses the pickup.
🤖 Agent: I pushed 1fb68131: merged main and added `fire_ticks: 0` to the new trident test, which no longer compiled once main gave `TridentSave` that field. Note that `LeftOwner: 1b` is now always written for tridents; vanilla omits it on a freshly thrown trident still inside its owner's box. Rarely visible.
🤖 Agent: `as_console_in_overworld` enters the overworld only when another level is in place, and puts that level back after the line, so the next server tick sees the level it left. The unit test covers only `console_level`; the portal scan is the real proof. Nothing else found.
🤖 Agent: `AbstractMinecartContainer.applyNaturalSlowdown` port is exact, f32 arithmetic included, and it returns before the ridden/behaviour factor as vanilla does. Nothing found.
🤖 Agent: Only a non-source moving block at progress 1.0 now counts as its moved state. One small gap: vanilla reads the moving piston's *collision* shape, so a moved block whose support shape differs from its collision shape (leaves) supports there in vanilla but not here. The carpet machine doesn't need it.
🤖 Agent: Crash reports are read only by a name the listing returns, so `../` and absolute paths can't reach outside `crash-reports/`. The test covers both, plus a non-`.txt` file. The panel refetches players and crash reports every 10 polls, a directory listing each time: cheap. Nothing else found.
🤖 Agent: `isCollisionShapeFullBlock` is the right gate (`EntityRenderer.extractShadowPiece`), and the test now covers ten non-full blocks, including the hopper whose collision bounds reach y=1. Nothing found.
Restore the Home and glass instance editor controls only for Java instances. Keep memory_mb persistence and Java's default heap behavior.
Co-Authored-By: OpenAI Codex gpt-6.1-sol <[email protected]>
Ludovic decided the cosmetics shop is only on https://playhorizon.dev/shop
(browsing, prices, the founder claim, purchases through Paddle). The launcher's
Wardrobe keeps what the account owns: equip / take off, "Show my cosmetics", and
the purchase history.
- Removed the Shop tab (wardrobe_page/shop.rs), the founder claim flow and its
checkout polling, Service::claim, Checkout / CHECKOUT_PAGES, the Shop's
window/countdown rules and the tests of all of it.
- Added an "Open the shop" button (opens the website's shop) beside the Wardrobe
title, and in the empty state ("You don't own any cosmetics yet.").
- The purchase history ("Your purchases") moved into the Wardrobe page.
- Lang tables: two new strings in all 86, the stale ones removed.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Resolve game/allays.rs: keep #8652's empty-main-hand gate and MAIN_HAND swing, and take main's Option<ItemStack> allay inventory for the returned stack.
Co-Authored-By: DeepSeek V4.1 Flash (OpenCode) <[email protected]>
🤖 Agent: one items.json snapshot per load, keyed on the root path; overlapping loads of one root share the native hooks' answers, fine under the same-answer contract. Proof: union with #8751+#8763+#8798 on main a7df9ec: fmt, clippy -D warnings clean; item_defaults/loot/items/font/hud/item_render/cit tests pass (playhorizon).
🤖 Agent: server fonts measure without an atlas; galactic_widths is now zero on a server but only the enchanting table's draw reads it. Proof: union with #8751+#8763+#8798 on main a7df9ec: fmt, clippy -D warnings clean; item_defaults/loot/items/font/hud/item_render/cit tests pass (playhorizon).
🤖 Agent: a headless load now skips OptiFine CIT; every reader of item_renderer.cit is a draw path (armour stands, elytra, glint upload), so nothing server-side changes. Proof: union with #8751+#8763+#8798 on main a7df9ec: fmt, clippy -D warnings clean; item_defaults/loot/items/font/hud/item_render/cit tests pass (playhorizon).
Route fall and stalagmite hurt through the world catalog. Retain the identical no-pack preload, atomically replace it for world packs, and protect the frozen snapshot from late vanilla readers. Select all prepared-resource hurt, landing, registry and preload regressions from the damage catalog parity harness.
Co-Authored-By: Codex <[email protected]>
🤖 Agent: Review by Ludovic's AI agent (Claude), not by Ludovic himself. check_predicate now decodes an argument starting with { as inline SNBT and sends it through the same evaluator, source entity and origin as a named predicate, as ExecuteCommand.checkCustomPredicate does; the codec now requires terms for all_of/any_of and term for inverted, so a malformed inline predicate fails at parse with no callback, as CompositeLootItemCondition.createCodec does. Merges cleanly into today's main; the merger waits for verify and parity on this head.
launcher/src/backend.rs keeps both Settings::theme (upstream) and
Settings::performance (this branch). Each launcher/lang/*.json takes upstream's
table and gets the two Performance mode strings added again.
🤖 Agent: main fix: removes only the three keys #8873 dropped from en_us (menu_navigation, menu_focus, menu_pointer) from the 51 tables added after its base; afterwards no table in assets/horizon/lang has a key en_us lacks, which is what lang::tests checks.
#8873 removed horizon.options.menu_navigation, menu_focus and menu_pointer from
en_us.json and the 35 tables its branch had; the 51 tables main gained since
kept them, so lang::tests::bundled_files_are_served_and_have_no_stale_or_repeated_keys
fails on main (cv_cu first).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Vanilla's ClientHandshakePacketListenerImpl.handleLoginFinished sends the
brand payload, then ServerboundClientInformationPacket; Horizon sent them the
other way round. ClientPacketListener.handleLogin sends no client
information either: the configuration phase already did, and
broadcastClientInformation runs only when an option changes. Horizon sent a
second, identical ClientInformation in play right after Login.
tools/vanilla_wire_e2e.py gains the rules brand-before-information and
no-play-information.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Read the existing dimension-aware weather levels in the predicate context and honor both optional weather fields. Add a guarded Game/Java command regression and a resource-free evaluator test without changing weather simulation.
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
🤖 Agent: launcher-only fix, CI green on this head (launcher, verify); main merged in (lang tables only); ThinkPad: fmt, clippy -D warnings, 480+89 launcher tests pass, incl. the new pin/on_base tests and the 86-language i18n check.
Dolphin.tick on a client: a dolphin in water whose squared speed is
over 0.03 (and that has AI) puts four dolphin particles a tick behind it,
0.3 to either side, set back along its view vector by 0.5 to 1.2 drawn
from its own random. client_ai_step had no dolphin, so the window never
made them. It runs over the window's copies of the mobs, so single player,
LAN guests and servers all get the trail.
Fixes#8566
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
🤖 Agent: the setup now draws the launcher's scene and glass plate with the player's accent; I made the main actions Kind::Primary and slowed the settings poll from 16 ms to 500 ms so the setup does not redraw at 60 fps for it.
🤖 Agent: this head carries all 40 of TheMrZZ's open PRs as one fast-forward chain (age order, main merged), because 38 of them append to tools/command_exec_corpus.py and conflicted pairwise; each one's current head is an ancestor, so GitHub marks all 40 merged with this one. Two hand merges to check: #8847, #8986 and #9101 now share one canBeABaby == false branch in mob_to_nbt that drops Age, ForcedAge and AgeLocked (AgeableMob.addAdditionalSaveData writes none of them), and data_of tries #9174's comparator view, then this PR's pot view. Proof: verify passed on 94ccd9c8e (this head = that + main); clippy -D warnings (also --no-default-features) and horizon-client-core/horizon-world tests pass on the union on playhorizon; the merger waits for verify and parity / parity here.
🤖 Agent: Requeued after merging #8200's main merge (12e979554) into it, no conflict. Live status heartbeat interval; lands after #8200. Fixes in this PR: the log reads the interval after setting it, as vanilla does, and a set after a cancelled heartbeat is refused, as ManagementServer does (b6204bf860). management_protocol --fresh PASS on the union of this wave plus the fix. On this head: horizon-server fmt, clippy -D warnings and 140 tests clean. The rest of the selection runs in CI run-parity.
Keeps the Paddle checkout from main and the clock and unknown-state fix. Both sides added tests at the end of wardrobe/tests.rs and an item to the same import in wardrobe_page/shop.rs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01JLHp7yEJwPTJ2kw29LWdk1
🤖 Agent: Requeued at rdbt's main merge 12e979554 (clean, no conflict hunks; it carries #8128, now merged). Live accept-transfers, log line as setAcceptsTransfers. management_protocol --fresh on the union of this wave (with #8128 and #8204): every accept-transfers case matches vanilla. The union passed fmt, clippy -D warnings, 3388 tests and the Python suite; rdbt reran the management_methods and accepting_transfers tests on this head. The rest of the selection runs in CI run-parity.
tools/command_exec_corpus.py merged per function (main's new groups kept);
the harness keeps main's destroy_progress records and the chain's record-count check.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The launcher feels slow on older computers (i5-7500, Intel HD Graphics 630):
every frame rebuilds the page, the scene redraws its moving parts, and the
glass plates blur the scene again whenever the accent or the blur changes.
Settings > Launcher > Performance mode now turns on the cheaper path: the
scene does not restart on every mouse move, the sky is baked at a quarter of
the window's resolution, the plates' blur is three box passes instead of a
Gaussian, a window resize is baked in the background, and the plates are a
solid colour (no glass, no blur, no backdrop to sample). In that mode a new
accent reaches the background in the same frame as the buttons.
Outside the mode, the accent no longer re-blurs the plates: the backdrop is
blurred once and tinted as it is read, and a tint change is baked in the
background, one bake at a time, the last tint always landing.
🤖 Agent: matches Entity.spawnSprintParticle drawing from this.random; the window's own player has no client-side random, so it keeps the particle engine's, and the golem's nextInt(5) and offsets now come from the same random in vanilla's order.
🤖 Agent: the Windows thread_name reads Rust's own handle, and the BUSY guard already answers a nested allocation from thread::current(); I shortened the loader names to load-<step> so Linux's 15-byte limit keeps core and collision apart.
🤖 Agent: matches Warden.isPushable (!isDiggingOrEmerging && super), applied both in pushable() and on the receiving side of push_entities, while the warden still pushes others; I removed the two "digging" parity cases that actually spawned a standing warden.
🤖 Agent: address is set in all three places NativeServerPlayer is built (guest list, the acting player via lan_acting_uuid, and the command path), as IP only with no port; I added the missing API fragment (server_player_address.json) so it gets a number at stamping.
Vanilla asks the box's block entity for its support shape because a shulker box is a dynamic shape: an open box supports only through the one-pixel plate opposite FACING, while the exported state facts always see a closed full cube. Route sturdy, canSupportRigidBlock and canSupportCenter through the level's lid state when the block is a shulker box, keeping the cached path for every other block.
data_dir(), build_dir() and game_home() still expect()ed on a location error.
The quiet installer runs before data_location::startup(), and retry() clears
FORCE_DEFAULT before re-reading, so a background thread could hit the panic
the review asked to remove. A location that does not open now stands in as the
default folders there, as startup() makes it for the window. Also joins the
startup problems with "\n" instead of a literal line break.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: release_keyboard_mouse now releases only what Source::Keyboard holds, so a splitscreen handoff no longer drops the pad's held jump or sneak, and pad_button replays the actions stored at press time on release, so a binding changed mid-hold can't leave a key stuck. Players who chose Pointer in the removed Menu Navigation option get focus in menus after this.
Every actions/* pin now runs on Node.js 24 instead of the deprecated Node.js 20 runtime the runners currently force:
- actions/checkout v4 -> v7: v5.1+ blocks fork-PR checkouts under pull_request_target/workflow_run; this repo only checks out ref: main in those workflows (pr-labels, overlap-note) and the workflow_run release checkout resolves to the default branch, so nothing changes. The PR-head checkout in ci.yml runs under plain pull_request.
- actions/upload-artifact v4 -> v7: v7's direct uploads are opt-in (archive: false) and the ESM migration is transparent to callers.
- actions/download-artifact v4 -> v8: v5's path change only affects artifact-ids downloads, which no workflow uses; v8's enforced digest checks and skip-decompress only surface for direct uploads.
- actions/setup-python v5 -> v7: the removed pip-install input is unused (only python-version: '3.12' is set).
- actions/setup-java v4 -> v6: v4 itself now emits a deprecation warning (v4.9.1); temurin/25 usage is unchanged in v6.
GitHub-hosted runners ship 2.327.1+, so the node24 minimum is met.
Co-Authored-By: ZCode <[email protected]>
A filesystem without file locks or a native folder linked to another drive made
Cache::acquire fail, and logged() then refused the build itself, which worked
before the cache existed (martipops' review). Only Busy still refuses; any other
lease error is a build.log warning and skips the trim.
The launcher check failed on lang/ca.json: main added 51 locales since the last
main merge, so each gets the five cache messages.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merging. Mining speed now reads the player's computed `minecraft:mining_efficiency` (Player.getDestroySpeed), and held Efficiency joins the equipment modifiers with its slot group, so an offhand Efficiency pickaxe no longer speeds mining. Proof: CI parity passed on this head, including the 9 new `mining_efficiency_9021` destroy-progress cases compared bit for bit with vanilla's getDestroyProgress.
🤖 Agent: only approximate tiles with width >= 16 stop at depth 2, and the test proves the Full tile still paints below depth 2, so the Reduced assertion cannot pass vacuously.
🤖 Agent: the hook gets session.offers[index], the live offer, while trade_with_villager keeps the pre-hook copy; a mod raising max_uses on a shift-click only takes effect after that click, as the docs say.
🤖 Agent: the hook runs after destroyVanishingCursedItems and before the random throws, and the retain only drops stacks a mod emptied, so with no mod the drops and their RNG draws are unchanged.
🤖 Agent: matches Armadillo.rollUp/rollOut, gameEvent(ENTITY_ACTION) before makeSound and independent of isSilent; the new creatures.rs arm comes after the flap arm, so chicken/parrot flaps are unaffected.
🤖 Agent: the guard keys on flat_min_sections.is_some(), so DX12 without flat page groups (no pulled pipelines) keeps GPU culling, and the test pins both DX12 cases.
sync::client_tick took an events vector in c0cd050fac; main's newer
remote_mob_data test still called it with two arguments.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Keep the successful falling-block hurt advancement, passenger frostbite tick and general world-pack tag synchronization.
Co-Authored-By: Codex <[email protected]>
- Prevent Play more button click from bubbling or launching the game
- Fix missing gaps between items in virtualized lists (cull::column) for versions and patch notes
- Match small field height (36px) and tokens for Motd, Port, Token, AddPlayer, and DeleteServer
- Do not apply 24px baseline blur when blur is set to 0
- Stop scroll event propagation on dropdowns, version lists, and nav tabs
Fixes#9148Fixes#9169
Managed by Axxio.
Co-Authored-By: Antigravity <[email protected]>
Mob::block_jump_factor is the one callers use; the free function and rabbit's
re-export had no callers.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The gizmo helpers went in between translucent_batchable's doc comment and the
function, so rustdoc showed the batching text on debug_line_opaque.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A fast clock closed a window the service still called open, hiding the Claim button on a set that's retired for good. Each answer's Date header now shows how far the clock is ahead, and the Shop reads the window's dates with that taken off. A clock on time or behind reads as before.
A claim state the launcher doesn't know now parses instead of failing the catalogue and the account's answer. It offers no claim and doesn't say the set is over.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01JLHp7yEJwPTJ2kw29LWdk1
has_guests counts only guests a projectile can hit (alive, loaded, here, not
spectating), so gating dead_guests and guest_entities on it dropped a lone
dead player's pearl from ender_pearls_vanish_on_death (pearl_death harness)
and a dead shooter's arrow from its owner. Both lists are built every tick
again; only the guest target copy stays gated.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Service::item_cached built its path from catalogue values unchecked. It now refuses an entry
whose id or hash isn't 1 to 64 of a-z, A-Z, 0-9 and _, the rule the game already applies
(horizon_cosmetics::fetch), before the disk or the service is touched.
The hash isn't checked against the bytes: nothing in this repository says what it is a digest of.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01JLHp7yEJwPTJ2kw29LWdk1
Send the celebration flag and its false default for all raiders, then apply it on clients. Exercise real tracking and packet decoding against native dirty metadata without loading shared resource state.
Fixes#8848
Co-Authored-By: Codex <[email protected]>
The tracker sends AbstractMinecart's DATA_ID_CUSTOM_DISPLAY_BLOCK (entry
11, optional block state) and DATA_ID_DISPLAY_OFFSET (entry 12, the kind's
default offset), and the client reads them back into the cart, so a cart's
custom block shows over the network and in single player.
Fixes#8050.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
be258bc235 sent every MobEvent::ParticleAt as a particle packet with a speed
of 0.1. The squid's ink velocity was already multiplied by 0.1, so clients got
a hundredth of it. The other ParticleAt events (hearts, splashes, wither smoke,
glow, bubbles) are client-side addParticle calls in vanilla that the clients
draw themselves, so they were drawn twice.
Squid.spawnInk is ServerLevel.sendParticles(count 0, speed 0.1): it is now a
MobEvent::LevelParticles, which draws it here and sends it to the players
within 32 blocks, and ParticleAt stays local.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Install/Update, Uninstall and Launch Horizon are each screen's one main action:
Kind::Primary, as the launcher marks them. The live-settings wake drew the
window every 16 ms and read settings.json's metadata each frame; 500 ms still
picks a change up while the setup is open.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Both strings had a run of spaces where an editor wrapped them, so the note
read "a Horizon game build."
Co-Authored-By: Claude Opus 5.5 <[email protected]>
rdbtCVS's agent merged main into four of the stacked branches. Merging those
heads here lands every PR of the stack with #8691.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Up to about 45% more far tiles while a zoom is held, and a far tile takes
seconds: a quick spyglass look queues work that lands after it. The extra stays
in horizon-extras.txt (lodZoomDetail:true) until a measurement shows it pays.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Linux keeps 15 bytes of a thread name, so "horizon-load-core" and
"horizon-load-collision" both read "horizon-load-co" in the heap census and the
startup trace there. "load-<step>" keeps every loader apart.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Shulker.setPos resets DATA_PEEK on any block change after the first tick,
passenger or not; only the teleport slide is skipped for a passenger. The
slide is cleared by startRiding (on_start_riding), not by every move.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
They spawned a warden with no flag, which stands: nothing in the fixture starts
a dig, so they compared a standing warden under a digging name. The emerging
cases stay; the unit test covers digging through set_pose.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A player's native builds kept compiling 9d979ca (Nightly 2026-10-31)
whatever Nightly or build profile they picked: Right-Click Crops 1.0.0
pins that commit and names no hook points' version, so the pin chose the
build's commit while the Mods page said "built on Horizon <picked Nightly>".
The build record now keeps the picked version's own commit and the mods
whose pin chose another; the Mods page names them, and "Rebuild on
Horizon" builds every mod on the picked version's commit (on_base,
native-build --ignore-pins), which later rebuilds keep. Older records
whose commit differs from their Nightly's read the pins from the
instance's mods. Also translates "Paste a friend's invite code".
Co-Authored-By: Claude Opus 5.5 <[email protected]>
orbs_after_client_respawn was only ever called with true, so the helper
and its test only restated Vec::new(). clear_remote_objects now clears the
orbs itself, which also makes the connect path's own orbs.clear() redundant.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The field said "API next" without a fragment, so stamping would never give it
a number. tools/native-hook-fragments/server_player_address.json requests one.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Armadillo.brushOffScute returns false for a baby, so the brush falls through
to FAIL (rolled up) or Mob.mobInteract's PASS instead of ending the click.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- /rotate on a server's player sends ClientboundPlayerRotationPacket
(ServerPlayer.forceSetRotation) when its turn ends, riding or not,
instead of a zero-motion teleport (or nothing for a rider).
- The guest's boarding cooldown counts down once per tick in
guest_server_tick, not once per enter_guest; a vehicle dropped in
guest_vehicle_tick also starts it.
- A teleported vehicle's riding guests hear the vehicle's teleport
(ClientboundTeleportEntityPacket), so a driven boat is not put back
by the client's next MoveVehicle.
Found by martipops's review.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Minecraft.tick sends ServerboundClientTickEndPacket only while it has a
level, which ClientPacketListener.handleLogin makes and
handleConfigurationStart clears. Horizon sent one every tick from the end
of configuration, so a client waiting for a busy server's Login (Paper
with plugins: seconds) sent dozens of play packets vanilla never sends.
tools/vanilla_wire_e2e.py joins a vanilla 26.3 server through
net_capture.py with the server's Login held back 1 s, and checks the
capture against rules of vanilla's client; no-play-before-login is the
first.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The 26.3 client's Animal.mobInteract answers CONSUME for an adult's food
(SUCCESS for a baby that can age up), and Sheep.mobInteract answers CONSUME
for shears whether or not the sheep is sheared: only the server checks
canFallInLove and readyForShearing. So both keep taking the click. Keep the
dye change, which matches DyeItem.interactLivingEntity, and add its
isSheared check: a sheared sheep passes any dye.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Conflicts resolved per function: every top-level def matches one side (the
harness main() keeps both sides' options), every groups() entry is kept.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Corpus conflict resolved per function: every top-level def matches
one side, every groups() entry of both sides is kept.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Corpus conflict resolved per function: every top-level def matches
one side, every groups() entry of both sides is kept.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Conflicts resolved per function: every top-level def matches one side (the
harness main() keeps both sides' options), every groups() entry is kept.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The 26.3 client's AbstractBoat.interact answers SUCCESS (a swing) whenever
the player isn't sneaking and the boat isn't out of control; only the
server's startRiding checks for room, so a full boat or a riding player
doesn't pass on the client. AbstractChestBoat passes only out of control,
not sneaked at, with room; otherwise it opens the chest. Minecart.interact
is the one that answers CONSUME and doesn't swing.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A brewing stand that isn't brewing, has no ingredient, has nothing to
refuel from and whose bottles already match its block state, a lit
campfire with nothing on it, and an unlit one with no cooking progress do
nothing in their server tick; they now sleep on the same terms as idle
furnaces. Lighting or putting out a campfire changes its block state, which
wakes it.
16,384 idle brewing stands cost 2.01 ms a tick instead of 8.14, and as
many unlit campfires 2.05 instead of 7.52.
Part of #8558
Co-Authored-By: Claude Opus 5.5 <[email protected]>
An unlit furnace, blast furnace or smoker with no cooking progress and no
fuel and input pair to light does nothing in AbstractFurnaceBlockEntity's
server tick. Such a furnace now leaves the tick loop: BlockEntities keeps a
set of sleeping positions, and any change to one (get_mut, insert, take and
put, remove, a block state change, a chunk load, a bulk edit) wakes it at
once, so it ticks on the same game tick as before.
16,384 idle furnaces cost 2.07 ms a tick instead of 8.76 (2.11 for as many
chests, which have no ticker).
Part of #8558
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Conflicts resolved per function: every top-level def matches one side (the
harness main() keeps both sides' options), every groups() entry is kept.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Merging main left a test initializer without the field main added
(TridentSave.fire_ticks / Interface.initial_values and
Resources.shadow_linear), so the test target stopped compiling.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Merging main left a test initializer without the field main added
(TridentSave.fire_ticks / Interface.initial_values and
Resources.shadow_linear), so the test target stopped compiling.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
FishingHook takes the plunge from syncronizedRandom, reseeded each tick
from the hook's UUID and the game time, not from the level's random: a
draw there shifts every later level draw out of vanilla's order. The
plunge is also computed in float as Mth.nextFloat does.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Corpus conflict resolved per function: every top-level def matches
one side, every groups() entry of both sides is kept.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Corpus conflict resolved per function: every top-level def matches
one side, every groups() entry of both sides is kept.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Corpus conflict resolved per function: every top-level def matches
one side, every groups() entry of both sides is kept.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Conflicts resolved per function: every top-level def matches one side (the
harness main() keeps both sides' options), every groups() entry is kept.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The dragon and the crystals were saved over the compound they were loaded
with only while they stood where they were loaded (anon_raw looks it up
by position), and never over the fields remember_tags keeps for them.
A dragon always flies off, so its Silent, Tags or name were lost at the
next save. Both now save over persist.raw, as /data get already reads
them.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Conflicts resolved per function: every top-level def matches one side (the
harness main() keeps both sides' options), every groups() entry is kept.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Corpus conflict resolved per function: every top-level def matches
one side, every groups() entry of both sides is kept.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Corpus conflict resolved per function: every top-level def matches
one side, every groups() entry of both sides is kept.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Corpus conflict resolved per function: every top-level def matches
one side, every groups() entry of both sides is kept.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Corpus conflict resolved per function: every top-level def matches
one side, every groups() entry of both sides is kept.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Corpus conflict resolved per function: every top-level def matches
one side, every groups() entry of both sides is kept.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Corpus conflict resolved per function: every top-level def matches one side,
every groups() entry of both sides is kept.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Drop the ZIP structure validation from the download path. Vanilla's
DownloadedPackSource checks only the SHA-1 (checkExistingFile), and the
strict zip-crate validator rejected the Java-compatible streamed archive
that started this PR, which Horizon's own pack reader already reads; any
other archive Java opens but the zip crate doesn't would have been refused
too. It also decompressed the whole pack on every cache hit.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Corpus conflict resolved per function: every top-level def matches one side,
every groups() entry of both sides is kept.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ManagementServer.scheduleHeartbeat cancels the running heartbeat and
schedules a new one only above 0. A set to 0 or less that stopped a
running heartbeat leaves the cancelled future in place, and cancel(true)
on it returns false, so vanilla refuses every later set until the server
restarts: it logs that the interval was not updated, keeps nothing and
answers with the interval in force. The management_protocol harness
showed it (vanilla answered 0 to the last set to 30, saved 0 and read 0
after the restart; Horizon took 30).
Horizon now keeps that state beside the interval. The harness cancels
with -5 instead of 0, so the value saved and read back after the restart
differs from the default, and expects vanilla's -5.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`copy_pack` walks a dropped folder with `let _ = fs::copy(...)` in every
branch and answers `true` for anything that holds a `pack.mcmeta`, so a folder
that half-copied (a held file, a full disk, a path too long) was announced as
installed and the pack appeared with files missing.
`copy_dir` now returns whether every copy landed and logs each file it could
not write; `copy_pack` passes that answer up, and the drop handler says the
pack is one it could not copy. A pack that is no pack still answers false with
no log line.
The test copies a real pack into a path that cannot be made and asserts false,
then copies it into a real folder and asserts the file arrives.
Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
Use the vanilla infection/conversion level events and the client cure-start entity event. Respect silent mobs, draw cure volume before pitch on each client and avoid duplicate captured sound packets.
Fixes#8892
Co-Authored-By: Codex <[email protected]>
/item ... block, execute if items block and /loot insert|replace block
only found slots in a chest, so a furnace, brewing stand, crafter,
chiseled bookshelf, shelf or jukebox answered "does not have slot". They
now take every block entity that is a Container in vanilla
(BlockItemAccessor.getContainer), and each one sets its slot through its
own Container.setItem: a furnace restarts cooking only for a new input, a
crafter enables the slot again, a bookshelf takes only books and a jukebox
starts or stops its song. Any other block answers "is not a container".
/loot checks canPlaceItem first, as LootCommand does.
Fixes#8685
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Both items were filed help-wanted and their issues are closed, so
`python tools/roadmap.py check` answers "every linked issue and pull request is
closed: ship or drop it" for each: the work landed and the roadmap never
learned which pull request did it, so `sync` cannot move either item.
Links added: #1928 for end-city-banners, #1911 and #2037 for end-city-loot,
each merged on 2026-10-26, plus the sentence in `dev` that says what the fix
did. ROADMAP.md and roadmap/roadmap.json are what `roadmap.py build` writes
from the toml.
Status stays help-wanted: `now` needs an owner and a tracking issue and the Now
list is already at its 8-item cap, while `shipped` needs a release tag and
these went into Nightly builds after v0.2.x froze.
Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
Each row below was marked `missing`, and the Rust function it should point at is
in the tree, with a doc comment naming the vanilla method it mirrors. Verified
by reading each function, not by the note the row carried.
Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
EnderDragon.tickDeath sends globalLevelEvent(1028) at the death's first
tick only when !isSilent(). Horizon roared for every dragon. The dragon's
Silent is kept with the saved fields Horizon models nowhere
(remember_tags), so the death reads it from there.
tools/test_dragon_death_roar_parity.py compares the 1028 events a client
is sent for a dying loud and a dying silent dragon with vanilla's
dedicated server (VanillaProtocolClient's new level-events mode,
LevelEventTranscript).
Fixes#8739
Co-Authored-By: Claude Opus 5.5 <[email protected]>
In Vanilla Minecraft 26.3 (Animal.mobInteract and Sheep.mobInteract):
- Right-clicking an adult animal with breeding food when unable to breed
returns PASS on the server; the client does not swing or spawn feed particles.
- Right-clicking a sheared sheep with shears returns PASS and does not swing.
- Right-clicking a sheep with matching dye returns PASS and does not swing.
Guard these interactions in client's remote_mob_takes so the client does not
falsely predict swing arm and feed particles.
Managed by Axxio.
Co-Authored-By: Antigravity <[email protected]>
PlayerPredicate.matches looks for the entity under the player's view
only when the predicate has looking_at. #8687 skipped the ray for tick
and location only; every other trigger that builds the player's view
still cast it, against every entity, on each event.
Each of those call sites now passes its trigger id (player_entity_for),
so the ray runs only when one of that trigger's criteria may ask. A view
that serves two triggers (the thrown-item pickup, the KilledTrigger
damage source) checks both. Commands keep player_entity(), which always
looks.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
Vanilla poisons any living thing the sting hurts, so a villager or a fox was taking the hit and walking away clean.
Co-Authored-By: Grok <grok-4.7> <[email protected]>
Co-authored-by: Cursor <[email protected]>
Leave randomizable-container inventories empty when their NBT supplies a valid loot table, allowing the existing command and chunk loading paths to retain the table. Add a red/green regression and a real vanilla inventory-load comparison.
Fixes#8920
Co-Authored-By: Codex <[email protected]>
A cobblestone stack limited to 16 was merging to 64 after the component was dropped on the way to the window.
Co-Authored-By: Grok <grok-4.7> <[email protected]>
Co-authored-by: Cursor <[email protected]>
A pack with an integer colortex0 and no `final` left to draw it (none, or
one that is skipped) has no colours the screen can show: #9007 made the
screen only clear, so Arc v0.15.2 drew a black world. `PackPost::new` now
refuses such a pack, `Renderer::set_shader_pack` returns the reason, and
the game turns the pack off with the usual log line and chat message, as
a pack that fails to load, so the world draws without it. An integer
colortex0 with a working `final` still loads and draws.
Fixes#9165
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Settings::save writes options.txt with `let _ = fs::write(...)`, so a write that
fails (a held file, a read-only folder, a full disk) leaves the player's
options, key bindings and volumes at whatever the last successful save put
there, and the game says nothing. Every option change goes through this call.
This logs the error where the rest of the crate warns (`log::warn!` in
`audio.rs`) and leaves the other files alone: `horizon.save`,
`controller.save`, `extras.save` and `mods.save` return no Result, so widening
their signatures is a separate change.
Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
BlockItem.place's FAIL ends the click but does not consume the action, so
the server must still send build.tooHigh/build.tooLow. The guest path
counted the failed hand use as consuming and skipped the message.
Co-Authored-By: OpenCode (DeepSeek V4.1 Flash) <[email protected]>
The Wardrobe page drops the result of every write it makes: the outfit it keeps
on disk (`wardrobe::Saved::save`), the game's cosmetics switches
(`horizon_cosmetics::prefs::Prefs::save`) and the line recording what the
service confirmed (`prefs::set_confirmed`). All three return an `io::Result`,
so a failed write left the page showing the new outfit while the file on disk
still held the old one, and nothing said so.
This prints the error where the launcher already speaks with `eprintln!`
(`instances.json`, `settings.json`, accounts) and changes nothing else: the
in-memory state still carries the change.
Co-Authored-By: Hermes Agent (Nous Research) <[email protected]>
Entity.spawnSprintParticle draws the crumb's x and z from this.random.
Horizon drew them from the particles' random for iron golems and other
players (the rabbit hop already uses the rabbit's own), so a golem walking
out of sight changed every particle drawn after it, such as a campfire's
smoke. The window's own player keeps the particles' random, which stands
in for its own as elsewhere.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Keep the trust notice above the project and results, and scroll it into view when installation pauses for consent.
Fixes#8978
Co-Authored-By: Codex <[email protected]>
Horizon read only `location` from a fragment output's layout, so
`layout(location = 1, component = 1) out uvec3 x;` was written from
channel 0, and two outputs splitting one location by component became two
declarations at that location, which naga rejects. Each such output is now
a private global copied into its components of one vector per location
after the pack's `main`; overlapping, mismatched or overflowing components
are errors, as glslangValidator reports them.
Fixes#9135
Co-Authored-By: Claude Opus 5.5 <[email protected]>
In Vanilla Minecraft Java Edition 26.3, interacting with non-interactive
objects (paintings, fixed or empty item frames with empty hands, sneaking
or occupied boats without chests, and non-interactive minecarts) returns
InteractionResult.PASS, which leaves the click to the held item without
animating a hand swing. Furthermore, boarding a boat or rideable minecart
answers InteractionResult.CONSUME on the client, which ends the use
without predicting an arm swing.
Update remote_use_entity to check object interact passes and prevent
predicting hand swing animations on non-interactive objects or when
boarding vehicles.
Managed by Axxio.
Co-Authored-By: Antigravity <[email protected]>
In vanilla 26.3 (FishingHook), when a bite occurs, DATA_BITING is set to true, triggering onSyncedDataUpdated on both server and client to apply an initial downward plunge impulse to velocity.y of -0.4F * Mth.nextFloat(syncronizedRandom, 0.6F, 1.0F) (-0.24 to -0.40).
Co-Authored-By: Antigravity <[email protected]>
The Windows setup window drew the Night sky and its violet pixel buttons, so
it looked nothing like the launcher it installs.
It now draws the redesign's scene behind the dark window (the dawn sky behind
the light one), tints the scene with the player's accent exactly as the
launcher does, and reads Glass, Blur, Accent and Appearance from settings.json
every frame, so a change made in the launcher applies on the next frame. The
card is a glass plate, its buttons are the launcher's rounded translucent ones
and its switch takes the accent.
Co-Authored-By: OpenCode <[email protected]>
Vanilla sends each player their own horn, thirteen blocks toward the wave, so people further along the raid were hearing nothing.
Co-Authored-By: Grok <grok-4.7> <[email protected]>
Co-authored-by: Cursor <[email protected]>
`uniform mat3 normalMatrix` in a gbuffers program stayed a frame uniform
nothing fills, so it read zero. In a stage whose #version is 150 or later
and doesn't name the compatibility profile it is now the normal matrix the
compatibility profile's gl_NormalMatrix already uses; in the compatibility
profile it still reads zero. Our probe pack's Iris 1.11.7 pictures fit
this at 330 core, 330, 150 and 330 compatibility.
The GPU test's draw code moves into a helper both matrix tests share.
Fixes#9136
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Vanilla deflects every projectile but a wind charge, so the trident was dealing its full damage and could call down lightning.
Co-Authored-By: Grok <grok-4.7> <[email protected]>
Co-authored-by: Cursor <[email protected]>
Vanilla treats a minecart as pickable, so the hook was flying through carts and a reel never pulled them.
Co-Authored-By: Grok <grok-4.7> <[email protected]>
Co-authored-by: Cursor <[email protected]>
Formatting mute and temporary-ban replies as English discarded their translation structure before clients received them. Preserve nested components and duration units, with readable fallbacks for vanilla clients that do not have Horizon's language bundles.
Fixes#8255
Co-Authored-By: Codex <[email protected]>
In Iris 1.11.7 a custom uniform or variable whose expression has another
scalar type than the declared one reads 0 as a whole: an int given 2.0
or 7 / 2, a float or int given a comparison, a bool given a number. So
does one naming `true` or `false`, which Iris's custom uniforms don't
know, or calling `round`, `ifb`, `lerp`, `print` or any other function
they don't have. Horizon evaluated all of these.
custom.rs: #9119's `Shape` becomes `Ty`, which tells int, float and bool
apart, and int vectors from vectors. Numbers keep whether they were
written as floats, the built-ins get the types our probes found in Iris
(INT_NAMES, BOOL_NAMES; the rest are floats), and the operators and
functions follow rules fitted to our probe packs' values and warnings in
Iris. `centerDepthSmooth` joins HIDDEN: Iris warns it is unknown.
Based on #9119 (shaderpacks/custom-uniform-type-mismatch). In main's
`expressions_follow_c_precedence_and_file_order`, `c` (`|| false`) and
`d` (`int = -7 / 2`) now read 0, and `i` is declared `bool`, which keeps
it covering the BIOME_* notes.
Fixes#9126
Co-Authored-By: Claude Opus 5.5 <[email protected]>
What Create's Ponder shows its machines in: native_draw_scene(batch,
rect, blocks, view) draws a few block states, at positions of the scene's
own (NativeSceneBlock), into a box of a screen, seen as NativeSceneView
says (centre, yaw, pitch, pixels per block). Each block's model is drawn
shaded by face, its faces against solid neighbours left out; a mod block
entity given with it is drawn through native_block_entity_submit, its
spin parts turning. The quads go in with the GUI's isolated depth and
scissor (HudBatch::depth_since), as the smithing screen's armour stand.
The view's matrix is a point reflection, so the unturned scene is seen
from the north as a player at yaw 0 sees it, unmirrored; the tests pin
which way yaw and pitch turn it.
Builds on the block entity drawing change (spin and place_turning).
Pending fragment: scenes_in_screens.json.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- native_draw_text(draw, text, at, color, glowing, outline): a Text under
a block entity's pose as a sign draws its lines (Font.drawInBatch in a
BlockEntityRenderer), each span in its colour, glowing text full bright,
an optional outline; it returns the width. The sign's glyph quads are
shared (block_entity_render::text_vertices).
- BlockEntityDraw::spin(axis, pivot, degrees_per_tick, phase) and still():
parts that turn by themselves. The game turns them each frame from the
game time and partial tick, so with keep() a shaft at a steady speed is
drawn again with no call of native_block_entity_submit until its block
entity changes (Flywheel's rotating instances, on the CPU).
- native_block_face_sprite(state, side): the texture a block state shows
on a side, by the name native_draw_cube takes, for copycat blocks.
Connected textures need no code: a content mod ships OptiFine CTM rules
under assets/<id>/optifine/ctm/, as the docs now say.
Pending fragments: draw_text.json, spinning_geometry.json,
block_face_sprite.json.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- native_fluid_flow(pos): FluidState.getFlow(level, pos) in the level in
place, the normalised current an item in the fluid is pushed by
(entities::fluid_flow, through the same flow_at items and boats use).
A water wheel reads its speed from it.
- Tanks take a mod's own fluids (create:chocolate): native_fluid_insert
now accepts any id in a namespace other than minecraft, besides water
and lava; extract and move already carried names. Cauldrons still take
only water and lava. NativeFluidTank becomes public and
native_fluid_tanks joins Host.
The world itself still has only vanilla's fluids: no source or flowing
blocks or buckets of a mod's fluid.
Pending fragments: fluid_flow.json, mod_fluids.json.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
NeoForge's TicketController.forceChunk: native_force_chunk(owner, chunk,
hold) holds a chunk of the level in place for a namespaced owner
(create:train/<uuid>), and native_forced_chunks(owner) lists what it holds.
A held chunk is kept as a /forceload'ed one is: Streaming has
mod_forced beside forced, Ranges reads both, the ticket levels give them
the FORCED level, and a level with any stays open. /forceload neither
lists nor releases them and they aren't saved with forced chunks: a mod
holds them again in native_level_load.
Pending fragment: chunk_tickets.json.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
NeoForge's IItemHandler.isItemValid for a mod's ModContainer: hoppers,
droppers and native_insert_item ask native_container_accepts(pos, entity,
slot, stack, face, cancel) before an item goes into a slot the container's
inputs and faces allow. `cancel` refuses it and the mover tries the next
slot, so a millstone takes only what it has a recipe for and a wrong item
can't clog it. tryMoveInItem now carries the face it goes in by.
Pending fragment: container_accepts.json.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Since f2c8e2ab1 the vanilla solid pipelines read their vertices and
records from the arena page by @builtin(vertex_index), per-section draws
too. DX12's count draws (multi_draw_indexed_indirect_count, which GPU
culling draws with) leave each draw's base vertex out of vertex_index:
wgpu-hal's draw_indexed_indirect_count sets its first-vertex constant to
0 and uses the command signature without it. GPU frames there read every
section's quads from the start of its page: most solid terrain missing,
stray quads in its place. Vulkan's vertex_index has the base vertex.
GPU culling is now not offered on DX12 with flat page groups (where the
pulled pipelines draw), so the word `gpu-culling on` is refused there,
until GPU culling's draws carry their base vertex some other way.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Divergence workgroups must all finish reading the histograms before either is replaced. Finalize in a separate dispatch so the reduction also observes every partial sum, without relying on a relaxed atomic completion counter.
Fixes#8627
Co-Authored-By: Codex <[email protected]>
Resolves the management handlers, the port map and the harness side by side.
Also: management_int's Codec.INT doc comment back on management_int, and the
interval's log line as vanilla writes it (MinecraftServerSettingsServiceImpl
reads the interval after setting it, so both numbers are the new one).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
horizon-ctl block reports the client's own moving piston at a position
(moving: progress, extending). A head that only arrives as the server's
moving_piston block, with no move of the client's, doesn't count.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A repeating command block placed by /setblock with auto:1b never got its
first tick in Horizon, so the second extension didn't happen.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The early broadcast in tick_lan sent the blocks as they were after the
tick's block events: a block placed by a scheduled tick and then pushed
went out as the moving block, and the client couldn't run the push. The
blocks now go out in tick_blocks, after the random ticks and before the
block events, as ServerLevel.tick runs ServerChunkCache.tick. piston-slide's
second extension pushes a stone a command block placed that tick.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ServerChunkCache.tick broadcasts the changed blocks before runBlockEvents,
so what the last tick's block entities changed (a retracted piston's base
put back) reaches the clients ahead of a block event that builds on it.
tick_lan now sends the changes before the block events, and holds the
tick's own late ones as before. piston-slide also powers the piston again
the tick after its base is back, with a repeating command block.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ServerLevel.tick broadcasts the changed blocks (ServerChunkCache.tick)
after the scheduled and random ticks, before the block events, the
entities and the block entities, so what those change goes out with the
next tick's. Horizon sent it the same tick: the block a moving piston
placed reached the clients while their own copy was still a tick from the
end of its move, and at /tick rate 1 it jumped the rest. The writes after
that point are held out of the log while it is sent; the guest move check
still sees them as unsent. lan_e2e's piston-slide check covers it.
Fixes#8430
Co-Authored-By: Claude Opus 5.5 <[email protected]>
What Create's drill, saw, harvester and deployer need, as Host helpers:
- destroy_block(pos, tool, collect): Level.destroyBlock(pos, true) with the
loot rolled for `tool` and no player (Block.dropResources(state, level,
pos, blockEntity, null, tool)). Particles, the block entity's contents
in the drops, ore experience and the fluid left behind are vanilla's
own path: GameLevel carries the tool and, with `collect`, keeps the
drops instead of popping them. The mods' say on drops applies.
- set_break_progress(breaker, pos, progress): ServerLevel.destroyBlockProgress,
the same broadcast a mob breaking a door uses (now shared,
server_destroy_block_progress).
- use_as_player(stack, eye, yaw, pitch, target, sneaking): NeoForge's
FakePlayer. useItemOn on a block, useItem in the air or interactOn with
an entity, through the guest use path (forced_hit + guest_use), in
survival with only `stack` in hand. The server's empty player shell is
filled for the call and everything it touched is put back; it answers
None while a player acts. NativeUseTarget and NativeFakeUse are new in
mod_api.
Pending fragments: machine_breaking.json, fake_player.json.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ElderGuardian.customServerAiStep sends GUARDIAN_ELDER_EFFECT with
isSilent() ? 0.0F : 1.0F, and ClientPacketListener.handleGameEvent plays
the curse sound only for 1. Horizon sent 1 to guests and played the sound
for the host whatever the elder's Silent flag.
The curse event now carries the flag. tools/test_elder_curse_parity.py
compares the parameter a client is sent by a loud and a silent elder with
vanilla's dedicated server (VanillaProtocolClient's new elder-curse mode;
the goat-voices mode shares its phased-scene plumbing).
Fixes#8932
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Trait mods couldn't reach several documented helpers through `dyn Host`:
their signatures were generic or used gameplay-private types. Host now has:
- draw_model (standalone models in block_entity_submit)
- mod_block_entity(pos) and change_mod_block_entity(pos, &mut change)
(native_with_mod_block_entity's form; on a client mod_block_entity
returns what the server sent through block_entity_update_tag)
- item_entities, level_random(dimension)
- play_sound and send_particles for the level in place, from any server
hook (until now only on LevelWriteHost)
- block_name_in_tag and server_item_in_tag
- component, set_component, remove_component
- trigger_criterion with a &dyn Fn test
- equipment(slot): the player's own on a client, the acting player's on
a server
- sneaking(): whether the acting player holds sneak
(UseOnContext.isSecondaryUseActive in a use hook)
NativeItemEntity, NativeBlockReadError, NativeComponentError and
NativeCriterion become public and are re-exported from mod_api.
Pending fragment: tools/native-hook-fragments/host_bridge.json.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Cached oracle runs replace java on PATH, so looking beside that executable points into the cache shim directory instead of the JDK. Resolve keytool directly and support JAVA_HOME when it is not on PATH.
Fixes#9079
Co-Authored-By: Codex <[email protected]>
Clippy rejects &mut on a events field that is already a mutable reference.
Co-Authored-By: Grok <grok-4.7> <[email protected]>
Co-authored-by: Cursor <[email protected]>
In Iris 1.11.7 a custom uniform or variable whose expression doesn't give
its declared shape (a float given skyColor, a vec2 given a vec3), or that
joins a vector and a scalar with an operator (vec3 * 1.0), reads 0 as a
whole. Horizon evaluated it anyway.
custom.rs now checks each item's shape once, on the first eval, after the
unknown-name check of #9015: Shape and shape() follow rules fitted to our
probe packs' values in Iris (operators, vecN constructors, which functions
take vectors, matrices, the declared type). A failing item reads 0 with a
note, and #9015's pass marks everything using it.
The load-time dry run (pack_post.rs) gave every built-in as a scalar, so
skyColor looked like a float there; it now gives each one at its size
(pack_uniforms::zero).
Based on #9015 (shaderpacks/custom-uniform-unknown-names); its test's
`w = vec3(...) * 0.0 + 2.0` and main's `e = vec3(...) * 2` now read 0,
as Iris reads them.
Fixes#9086
Co-Authored-By: Claude Opus 5.5 <[email protected]>
An atomic function on a component of a shared integer vector (`atomicMin(mn.x, i)`) was
rewritten only when the vector had a declaration of its own. Two other forms reached naga
unsplit and failed with "Atomic operation is invalid: Pointer [..] to atomic is invalid":
- a declaration naming several variables, `shared ivec3 mn, mx;`: each name is now declared
on its own, and the ones atomic functions use by component are split as before;
- a member of a shared struct whose type is defined in place,
`shared struct { ivec3 mn; uint other; } sh;`: the member becomes `int mn_hz[3];` in the
struct, with `hz_get_sh_mn()` / `hz_set_sh_mn(v)` for whole-vector reads and writes, through
the member paths #8994 added for buffer blocks. A struct type whose name is used anywhere
else stays as written.
Based on #8994 (shaderpacks/ssbo-vector-atomics), whose member-path rewrite this reuses.
Fixes#9085
Co-Authored-By: Claude Opus 5.5 <[email protected]>
After each trade made with a villager or a wandering trader, the mods are
asked with the live MerchantOffer in the trading session (uses counted):
uses, max_uses, demand, special_price_diff, price_multiplier and xp may be
changed and the change stands for the next click and the offers sent to the
player. Bukkit's PlayerTradeEvent#getTrade is mutable the same way. The
existing trade_with_villager stays listen-only and hears the offer as the
trade left it. A collect-contract hook with an API fragment.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Follow AgeableMob save gating for zombie nautiluses while retaining modeled
age values for normal nautiluses. Cover stale raw data and the command path.
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
DrawableGizmoPrimitives.getGroup puts a line whose alpha is 255 in the
opaque group, which GizmoFeatureRenderer.buildLines draws with
RenderTypes.lines (pipeline/lines: DepthStencilState.DEFAULT, depth
written); only lines with alpha below 255 use
linesTranslucentNoDepthWrite. Horizon drew every depth-tested gizmo line
without writing depth, so translucent terrain and clouds drawn later
covered F3+B hitboxes and F3+G chunk borders in front of them.
The opaque lines now upload first and draw with a depth-writing
pipeline; the translucent ones (the chunk border's outer columns) keep
the old one.
Fixes#8465
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Read the computed attribute when calculating destroy speed, including active
hand enchantments in the map without activating them in the wrong slot.
Add nine differential real-game probes against vanilla Player.getDestroySpeed.
Fixes#9021
Co-Authored-By: Codex <[email protected]>
Upstream added 52 launcher language tables on main; they predate this branch's
data-location strings, and the launcher's i18n completeness test requires every
table to carry every key. Add the missing entries with the English text, and
the Chinese translation for the new startup message.
Verified with fmt --check, clippy --all-targets -D warnings and the launcher
lib tests on the merged tree (482 here, 486 on Linux in WSL).
Co-Authored-By: ZCode <[email protected]>
A gbuffers vertex stage that declared the core-profile `uniform mat4
textureMatrix` got a frame uniform the game has no value for, so it read
zeros, and packs computing their UVs as `textureMatrix * vaUV0` sampled
one texel per face. In the vertex stage it is now gl_TextureMatrix[0], as
projectionMatrix and modelViewMatrix already are their compatibility
matrices. Our probes read the identity there in Iris 1.11.7, and zero in
the fragment stage, which keeps its own frame slot.
Tests: the vertex stage's textureMatrix takes no frame slot while the
fragment stage's keeps one; on a host GPU, the vertex stage reads the
identity, a UV through it is the UV, and the fragment stage reads zero.
Fixes#9080
Co-Authored-By: Claude Opus 5.5 <[email protected]>
naga's GLSL frontend read `s.member` on the right-hand side out of a loaded
copy of the whole struct. Once an atomic function marks that member atomic,
the copy's member is an atomic value no operator, conversion, call or store
takes ("Incompatible operands: Atomic(...) as Uint"). The member is now
loaded through a pointer to it, as array elements already were; constant
expressions keep the value form.
Tests: every read the #9081 probe makes (assignment, operators,
constructors, min, subgroupBroadcastFirst) on a shared struct member, a
buffer-block member, a member of a shared struct array and a nested member;
constants' members in constant expressions; a member beside a runtime-sized
array; and, on a host GPU, the values those reads return.
Fixes#9081
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The launcher job stopped with exit code 101 while verify succeeded and the same
fmt, clippy and lib tests pass on Linux locally (486 passed).
Co-Authored-By: ZCode <[email protected]>
A player whose drive is unplugged, whose location.json is damaged, or whose
migration lock is held used to get a silent exit: eprintln goes nowhere in the
Windows release build or from a desktop launcher. Startup now falls back to the
default folders and records the error, so the window opens on Settings with the
message, Retry and Use the default folder. Retry restarts into the configured
folders when they are readable again; Use the default folder keeps the old
pointer as location.json.old and restarts. Nothing in the moved folders is
deleted, and no launcher path reaches its expect() before the window exists.
Merge upstream/main (017c84f1f) while doing so: keep the launcher render env
strip and account-session removal alongside HORIZON_HOME, keep both the server
location_busy and stopped_line methods, keep one WorldLock, and keep the
explicit unlock in the world-lock test. The data-location strings the newer
language tables did not have yet are added (English fallback), with a Chinese
translation for the new message.
Tested: launcher lib 482 passed, bin 84 passed, integration suites passed;
cargo fmt --check and clippy --all-targets -D warnings pass.
Co-Authored-By: ZCode <[email protected]>
Remove the hard allocation ceiling and glass launcher slider added since the original revert. Keep read-only memory diagnostics and unrelated upstream changes.
Co-Authored-By: OpenAI Codex <[email protected]>
Restore the modeled flag independently of Owner and serialize its current value. Cover defaults, owner removal, raw-None writes and model mutation against stale raw NBT, with a frozen Game command regression.
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
A raw (`.dat`) custom texture is sampled linear and clamped unless its
`.mcmeta` says otherwise, each key overriding its own default only; PNGs
stay nearest and repeating. A PNG (the pack's or a game resource) under
`texture.<stage>.<name>` whose name is no sampler of the game's is not
bound. Iris 1.11.7 read these values with a probe pack of ours; the
binding rule is a fit to its cases.
Fixes#9064
Co-Authored-By: Claude Opus 5.5 <[email protected]>
These formats live in 8-bit wgpu textures, so a pack read back 8-bit values where Iris 1.11.7
reads fewer bits. What a pass writes into such a buffer, and its clear colour, now keep the bits
Iris kept in our probe pack p8106c: the 8-bit byte's top 5, 5, 5 and 1 bits for RGB5_A1, 5, 6
and 5 for RGB565 and R3_G3_B2, repeated to fill 8. This is a fit to the probe's Iris values on
one driver (all 600 values match), not a rule Iris states.
Fixes#8106
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The respawn rule returns a Vec, and the client stores orbs in a JoinList, so verify could not assign one to the other.
Co-Authored-By: Grok <grok-4.7> <[email protected]>
Co-authored-by: Cursor <[email protected]>
The game crate's tests cannot see the library's cfg(test) mob fixture, so verify failed while compiling them.
Co-Authored-By: Grok <grok-4.7> <[email protected]>
Co-authored-by: Cursor <[email protected]>
Shulker.hurtServer returns false for an AbstractArrow while the shell is closed, and AbstractArrow.onHitEntity then deflects REVERSE at power 0.2. The hit path was dropping that refusal.
Co-Authored-By: Grok <grok-4.7> <[email protected]>
Co-authored-by: Cursor <[email protected]>
A new ClientLevel starts empty, but the old orbs stayed and followed the player after the end fountain, so they were never picked up.
Co-Authored-By: Grok <grok-4.7> <[email protected]>
Co-authored-by: Cursor <[email protected]>
MobEffectInstance stores amplifier as an unsigned NBT byte. Sign-extending that byte turned 255 into -1, and the effect constructor then clamped it to 0.
Co-Authored-By: Grok <grok-4.7> <[email protected]>
Co-authored-by: Cursor <[email protected]>
The stitched atlas now draws every sprite into each level with a small render
pass instead of copies. The padding around a sprite repeats its nearest edge
pixel, as vanilla's TextureAtlas.uploadInitialContents does: it blits with a
clamp-to-edge sampler, and animate_sprite.vsh stretches the UVs over the
padding. Packs that filter or sample just past a sprite's edge read its own
colour instead of an empty pixel.
Part of #9037
Co-Authored-By: Claude Opus 5.5 <[email protected]>
With a pack, flat terrain draws now read a block atlas laid out by vanilla's
Stitcher, with SpriteLoader's mip level and padding. Every sprite keeps its own
size, copied from the array level where the arrays hold it unscaled, and its
place goes to the sprite table. atlasSize and the texel distances packs derive
from it now match Iris 1.11.7: 2048 x 2048 on 26.3's sprites, where the grid
of 64 px cells gave 2304. Draws without the flat path keep the grid. The
padding stays empty for now.
Part of #9037
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Shulker.createInterpolationHandler is NO_OP, so setPos runs when the packet arrives, from the block the shulker occupied before it. client_tick only counts the slide down.
Co-Authored-By: Grok <grok-4.7> <[email protected]>
Co-authored-by: Cursor <[email protected]>
An Iris-format pack's shadow map held no faces turned away from the light,
for two reasons: the shadow and shadow_water pipelines got the game's
terrain geometry with back-face culling on, and the shadow lists kept only
the quad buckets that can face the light. Under a hill at low sun such
faces (cave ceilings) are often all that shadows a cave, so packs drew
sunlight and shafts inside it. Iris 1.11.7 keeps both sides in its shadow
map (probe in #9033).
The pack shadow pipelines now draw both sides, and pack shadow views ask
for every face of their sections; Horizon's own packs keep the facing
selection.
Fixes#9033
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Retain loaded cloud timing, radius and potion settings, and keep live cloud identity during memory pruning. Add a loader contract and preserve a strict diagnostic for separate dose-phase and damage-source gaps.
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
Flat terrain draws now look a sprite layer's corner and side up in a table
(set 3, binding 2: x | y << 16, side) instead of working out a grid cell. The
atlas is still the grid, so packs see the same coordinates; the table lets the
atlas place sprites of different sizes next.
Part of #9037
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The entities group leaves pigs loaded, so the single-pig check saw 3 when it
runs after it, as it does in the full corpus.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A GPU frame drew every solid quad of a section it kept and left the back
faces to the rasterizer, where a CPU frame draws only the runs of buckets
that can face the eye (Sides::visible_from, its MARGIN included). Each
record now carries where its buckets end and what visible_from compares
the eye with, and the tables the eye less each origin, both as u32 keys
that order as those floats do, so cull compares integers and writes a draw
per run, at most four per record.
A mesh sorted for two passes, which GPU culling never meets, keeps one run
of all its quads, and a frame whose eye less an origin is NaN is the CPU's.
Regions hold four draws per record, and cull counts the kept sections
itself.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Input parity (pad and keyboard reach the game the same way):
- Held keys record which device holds them; splitscreen's keyboard
handoff releases only the keyboard's, so the other player's controller
keeps its holds.
- The pad sends Shift and Ctrl through the keyboard's modifier state, so
the stack throw in inventories throws the whole stack again.
- Every bindable controller action has an explicit route, checked by a
test; screenshot and smooth camera work from a controller binding, and
pad look counts for the tutorial and zoom like the mouse.
- tools/pad_parity_e2e.py and headless Game tests compare pad and
keyboard outcomes.
Menus and inventories:
- Menus use focus; inventories, containers and the screens whose
clickable parts have no focus targets use a crosshair pointer centred on
the point it clicks. The Menu Navigation option and the right stick
click's swap are removed.
- A snapping engine settles the pointer: a glide onto the nearest slot,
flicks commit to the neighbour they head for, the D-pad hops a slot.
- Pointer speed, scroll speed and navigation repeat settings per player.
- Focus starts in vanilla's tab order, returns to the widget a screen was
left on, and only one widget is ever highlighted.
- Only a new press or real stick movement takes input from the mouse; a
screen, focus or keyboard change releases held buttons until let go.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01JLHp81zX4YZ9N1mLLAwzKH
Bind the existing registry to shared collision tables and resource/world tags, then test the thin eye box against the actual suffocating shape. Keep probe worlds and partial-block integration controls on the runtime binding.
Co-Authored-By: Codex <[email protected]>
Pass the rule's fire flag from the impact arm while preserving the shared
non-fire explosion default. Cover bounded enabled/disabled and TNT scenes,
with explicit fire-presence gates and unmodified raw count diagnostics.
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
Use the exact Java value instead of the stale double-literal expectation; preserve the original motion behavior and strengthen the assertion.
Co-Authored-By: Codex <[email protected]>
Preserve the live modeled liked-player fields when a whole-Brain data edit omits or invalidates the required memories map. Keep valid empty-map and individual-key clears working.
Co-Authored-By: Codex <[email protected]>
Reuse the mob world snapshot without cloning per mob, skip unloaded work before nearest-player and difficulty queries, cache difficulty by chunk, and avoid guest-target allocations when no LAN guests exist. Empty projectile ticks now flush queued effects and return without scanning mobs or constructing projectile state.\n\nCo-Authored-By: OpenAI Codex <[email protected]>
Use Java float inertia values in the shared helper, retain its existing branch order, and pin exact motion with focused dry, submerged and custom-acceleration controls.
Co-Authored-By: Codex <[email protected]>
Keep the decoded state on the TNT model and encode it in command, chunk and portal save views. Invalid inputs fall back to the default TNT state.
Co-Authored-By: Codex <[email protected]>
Set the mannequin's live avatar pose from its saved lowercase value,
using the five accepted poses and standing for absent or invalid inputs.
Keep existing dimension math and raw saved-field writing unchanged.
Cover exact native eye anchors, UUIDs, body positions and invalid controls.
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
Load and save the independent one-slot inventory, preserving stack components and item limits. Route existing inventory callers through that state.
Co-Authored-By: Codex <[email protected]>
A client mod can now send any serverbound play packet, a plugin channel's
custom payload included, to any server, a vanilla one too. mod_api
re-exports the packet type as ServerboundPlayPacket. An error before the
player is in a server's play state, and for a custom payload past
ServerboundCustomPayloadPacket.MAX_PAYLOAD_SIZE.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011R7zRSugxibySUu8PosW5U
Keep removed definitions frozen in the current session and rebuild the next catalog from current resources. Cover resource removal, reload and fresh-world behavior.
Co-Authored-By: Codex <[email protected]>
Preserve ordinary player damage, enchantment predicates and synchronized registry data for enabled resource/data packs. Validate definitions before publication and keep world definitions stable through reload.
Co-Authored-By: Codex <[email protected]>
Apply item Health damage with stack resistance and source eligibility,
retain its display name for lethal feedback, and reuse destroyed contents.
Cover real command results, Health, resistance, and selective removal.
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
Fall back to the retained Owner UUID for execute on origin when there is
no mob/player binding. Preserve existing player, guest and mob bindings.
Cover loaded stand/pig owners and unknown/absent owner controls.
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
Player.dropEquipment: after vanishing-curse items are gone and before the
rest is thrown, the mods may add to, change or take out the stacks. The
context names the player (entity id, minecraft:player), no table. Not
heard with keep_inventory, as vanilla keeps the items then. No new hook:
an API fragment extends native_loot_drops.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
A client shulker that changed block only slid with ordinary position
interpolation. Shulker.getRenderPosition pulls the model back toward
the block it left for 6 ticks.
Fixes#8901
Co-Authored-By: Grok <grok-4.7> <[email protected]>
Co-authored-by: Cursor <[email protected]>
Track item falls until ground contact and dispatch HIT_GROUND from both
item movement and the armor stand landing path. Cover real Game dispatch
with fresh sculk listeners and matched NoGravity controls.
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
A hopper, cauldron or composter reaches y=1 with its rim, so its collision
bounds looked like a full block and a mob shadow was drawn across the top.
EntityRenderer.extractShadowPiece draws a shadow piece only when the block
below isCollisionShapeFullBlock; the same gate now applies here.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
native_client_packet hears every play packet the window's game handles,
decoded and in order, just before it is handled: handle_packet, plus the
chunk and ForgetLevelChunk paths in poll_remote that bypass it. It only
reads. mod_api re-exports the packet enum as ClientboundPlayPacket so
trait mods can name it.
Closes#8820
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Content::check read each declared block, item, mob effect, creative tab,
entity type and menu with Content::file, which opens the mod's archive
again and parses its whole central directory, unbuffered, every time.
Loading a packaged mod was quadratic in its size. Checking chipped's
.hmod (6,981 blocks, 58,563 entries) took over 20 minutes; with one
reader it takes 2 seconds.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A moving block at full progress now supports like the block it carries, as vanilla's
moving block entity does (getExtendedProgress is 0 at progress 1.0). Wall coral fans
and the carpets on them survived in vanilla when a slime column landed; here the fan
popped because the slime block behind it was still a moving piston, so the carpet
duplication machine had nothing to duplicate.
Fixes#8490
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
`cmd_attribute` looked the entity up in the command source's level only, while
a selector with no spatial constraint searches every loaded level
(`EntitySelector.findEntities`). A target in another dimension therefore came
back as an id the level did not hold: the generic path answered "Entity
entity. has no attribute <name>" and max_health, movement_speed and
attack_damage silently did nothing.
Run the attribute work with the target's level in place through the existing
`in_target_level` helper, the one /data, /kill and /tp already use.
Co-Authored-By: Hermes <[email protected]>
`horizon-ctl memory` falls back to a thread's name for allocations made
outside any step, span or job. On Windows `thread_name` returned nothing
at all -- the stub for every platform without `pthread_getname_np` -- so
every untagged allocation on every thread fell into one `unnamed thread`
bucket. That bucket was 580 MiB of a playing game's 1219 MiB resident
set: the largest of all of them, and useless, because it named no thread.
Rust keeps the name `Builder::name` set in its own thread handle, so the
fallback reads it from there. No new dependency, and nothing new in a
release binary: `GetThreadDescription` would have wanted Windows 10 1607
and an import in every build, where this is the name Rust already holds.
The resource loaders had no name to read either: they are spawned through
`Step::spawn` and bare `scope.spawn`, which leave a thread unnamed. They
go through a new `named_scope`, which covers the eight core steps, the
five world-rest loaders and the panorama. The job pool already named its
workers, and so did far terrain.
What the same run reports now, where it reported one 580 MiB bucket:
| MiB | tag |
|---:|---|
| 328.2 | Far sampling |
| 76.4 | unnamed thread |
| 52.0 | Far decoration |
| 35.4 | Far terrain |
| 13.9 | Far meshing |
| 12.4 | World data preload |
| 8.2 | Worldgen loader |
| 8.0 | main |
Far terrain holds ~430 MiB of the heap across its four threads, beside
the 661 MiB of far-tile vertices already measured in video memory. None
of that was visible before.
The startup trace gains the same names: 13 of its 17 lanes read
`unnamed`, which a trace viewer cannot lay out usefully.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011paghWkrnyQRBhJRodZpG2
Entity.teleportPassengers: teleporting a boat, minecart or mob takes its mob,
player and guest riders along at once. /rotate is Entity.forceSetRotation:
rotation fields only, pitch clamped before the setter. Ender pearl and bed
entry stop riding at their source instead of at guest-turn teardown. Players
and guests get Entity.boardingCooldown (60 ticks) checked before boarding.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
`Turtle.aiStep` broadcasts LevelEvent 2001 while it digs; the handler drew the
particles into the local buffer instead, which a serving client never renders
and no client hears. Route it through `level_event_at(2001, ...)` like every
broken block does, so the sand crumbs and the break sound both follow.
Co-Authored-By: Hermes <[email protected]>
`Warden.clientDiggingParticles` is client work, but `client_tick` discarded the
`DigParticles` events it had already computed (`animate(m, &mut Vec::new())`),
and `sync::client_tick` had no sink to hand them to. Thread the game's event
vector through; it lands in `warden_dig_particles`, which draws the 30 block
particles from the block under the warden.
Co-Authored-By: Hermes <[email protected]>
- jukebox_playable: EitherHolder.streamCodec is either(holder, key), so
its flag is set for a holder; it was read the other way round, and an
item with a song by key broke the rest of its packet (on a 1.21.11
server a whole container_set_content at login).
- container and use_remainder (and sulfur_cube_content) hold item
stacks: their bytes were kept as the server sent them, in its item and
component numbering, and before 26.1 as ItemStacks (a count first). Read
from an older server, they are now read whole and written again as
pinned templates.
- pot_decorations writes its items' pinned ids, and a value rewritten
this way is read back in the pinned numbering.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NbhTr6rUbVKx6iB2X73DNG
Three components kept their names but changed their stream codecs, so an
older server's item holding one failed to decode (with a vanilla 1.21.11
server: "1 extra bytes" and "NBT truncated" on container_set_slot):
- pot_decorations (26.3): a list of up to four item ids, a brick for a
plain side, became four optional item stack templates;
- jukebox_playable (26.1): an EitherHolder, a flag then the song's key or
holder, became a holder; a key has no pinned holder and is dropped;
- damage_resistant (26.1): a bare damage type tag became a HolderSet.
Two quirks say which a version has, and one reader (read_added) now
serves the slot, template and wire patch readers alike: it drops the
components the pinned version lacks, rewrites these three into the
pinned layout and reads the rest as before.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NbhTr6rUbVKx6iB2X73DNG
`MobEvent::ParticleAt` was drawn with `self.particles.add` only, and a serving
client has no window: the squid's 30 ink particles never left the server. Send
them as packets like every other server-thread particle site; with count 0 the
packet's deltas are the particle's motion.
Co-Authored-By: Hermes <[email protected]>
26.3 removed the map_color and swing_animation data components. A 1.21.x,
26.1 or 26.2 server still sends them in item patches (an explorer map's
color, a 1.21.11 weapon's swing), and the item readers, which renumber
the component id to -1 there, could not read the value: the whole packet
was skipped, so a container holding such an item showed empty (seen on a
Folia 1.21.11 server: container_set_content, set_entity_data and
update_advancements undecodable).
The registry id generator now lists the components an older version has
and the pinned one lacks, and the item readers read their values past in
that version's layout (an int; two varints) and drop them, from added and
removed components alike. tooltip_display's hidden component ids are
renumbered as well, both ways.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NbhTr6rUbVKx6iB2X73DNG
Neither nautilus had an arm in `remote_mob_takes`, so a tame adult's mount or
screen click fell through, and the generic `Animal.mobInteract` arm claimed any
`kind.food` item for an untamed one although `AbstractNautilus.mobInteract`
only takes `#minecraft:nautilus_taming_items` until it is tamed.
Co-Authored-By: Hermes <[email protected]>
`IronGolem.mobInteract` takes an iron ingot on a damaged golem and
`CopperGolem.mobInteract` takes an empty hand while it holds an item or shears
on its antenna; both were missing from the client's interaction check, so the
click fell through to the item and to the off hand.
Co-Authored-By: Hermes <[email protected]>
`Allay.mobInteract` returns SUCCESS for a holding allay only when the click
came from an empty main hand; everything else is PASS so the held item is
still used. The client answered a flat true whenever the allay held anything,
which swallowed the click, and the server's take-back path ignored the hand.
Co-Authored-By: Hermes <[email protected]>
`AbstractHorse.handleEating` (and the llama and camel ones) gates both the age
up and the particle on `isBaby() && !isAgeLocked()`. The client's copy only
asked for `alive() && is_baby()`, so feeding an age-locked baby drew a particle
the server never agreed to.
The decision moves into `ages_on_food`, where the lock belongs.
Co-Authored-By: Hermes <[email protected]>
The firework glide boost in follow_own_player read the look direction from
super::view_vector (Rust f64 sin/cos), while the server derives the player's
rocket boost from getLookAngle()'s float sine table. travelFallFlying already
uses the table; this boost was the one path left on f64 trig, so boosting
under a rocket while gliding drifted the client off the server's position and
tripped the movement check. Use horizon_world::view_direction::view_vector
(the float table), matching the server and the rest of the glide. Covers the
remote path too, which now routes through follow_own_player.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Leaving a multiplayer server goes from close_world straight to
leave_server, which never stopped the sounds: the biome's ambient loop
played on over the title screen until the next world. leave_server now
does what finish_close_world does for single player, as vanilla's
updateLevelInEngines(null) on Minecraft.disconnect: it clears the
ambience, lets the music manager go of its track and stops every sound.
A kick takes the same path.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NbhTr6rUbVKx6iB2X73DNG
`AbstractHorse.isFood` is the `#minecraft:horse_food` tag and `ZombieHorse`
overrides it with `#minecraft:zombie_horse_food`; the food table was shared by
both, so a zombie horse refused its mushroom and a red mushroom in hand fell
through to a horse's `handleEating` and healed it.
The table moves into `eating(species, item)`, which applies the per-subclass
tag rule and the eat sound gains ZombieHorse's own. A tamed skeleton horse now
eats horse food too, as it inherits that tag (an untamed one still passes).
Co-Authored-By: Hermes <[email protected]>
/tp <entity> x y z <yaw> <pitch> set the rotation only on mobs and players.
Items, orbs, TNT, falling blocks, rockets, armor stands, boats, minecarts,
projectiles and tridents dropped the pitch (and several the yaw), so
/data get Rotation read [0, 0] where vanilla reads [90, 10].
Entity gets rotation() and set_rotation() (Entity.teleportSetPosition:
setYRot, setXRot clamped to -90..90, setOldRot). The kinds with no rotation
field gain one (pitch for armor stands and boats), /tp calls the trait
method for every kind, and the entity saves write the field.
Part of #8024
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
`Turtle.canFallInLove` is `super.canFallInLove() && !this.hasEgg()`, and
`Animal.mobInteract` only eats when `getAge() == 0 && canFallInLove()`. The
food gate only looked at age and the love timer, so an egg-carrying turtle ate
seagrass, entered love mode and played the heart particles.
Co-Authored-By: Hermes <[email protected]>
A dedicated server's console source is bound to the overworld. Horizon ran a
console line in whichever level was in place, which after a tick with forced
chunks in the nether is the nether: summons, fills and selectors landed there.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The launcher could already install a `horizon-client` of your own beside
the releases (`build_import`), to try a merged change before the next
Nightly, but only through the Versions page's file picker or the
`import-build` command. Dropping the program on the window answered
"horizon-client.exe can't be imported: drop a .mrpack, a .zip or a
.hshader", which is the one file the window would not take.
It now takes it. `Dropped::Build` covers the program itself, a release
archive and a GitHub Actions artifact -- a zip around the release's own
archive, which `read_program` already looks into. An `.exe`, `.gz` or
`.tgz` is taken on its extension unread, so `build_import` is the one to
say why a file by that name is not a game, which is a better answer than
"nothing in it that Horizon reads". A file with no extension (a Linux or
macOS build) and a zip nothing else claims are read and checked through
the new `build_import::holds_game`, last, because that reads the whole
program.
The drop starts the same job the picker does: `start_build_import` is
split out of `import_build` so there is one path, one `Msg::Imported`
and one guard against a second import or a removal running at the same
time.
Co-Authored-By: Claude Opus 5 <[email protected]>
The status of each online player (server-status, and /api/v1/players built on it) already
carries the position; it now also carries the dimension the player is in, for example
minecraft:the_nether. The ROUTES text and the generated reference name it, and
tools/server_admin_e2e.py checks the dimension and the three coordinates on a real server.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
`Plan::visit`'s falloff keeps a far tile's cells roughly the same size on
screen, which silently assumes the field of view never changes. A
spyglass takes a tenth of it, and the zoom mod as much again: the same
tile then covers many times more pixels, so its cells look as coarse as
a tile ten times nearer would. Looking through a spyglass at the horizon
showed exactly the blocky cells it was given for an unzoomed screen.
`ZoomAim` carries what the plan needs to answer that: the view
direction, the cone the narrowed view covers, and how much finer to go.
Tiles inside the cone split further; every other tile keeps the plan it
would have had, tile for tile.
The cone is the point. Lifting the whole circle to a tenth of the cell
size asks for about a hundred times the tiles, which at the 4078 tiles
and ~1500 quads a tile measured at `lodDistance` 256 is gigabytes of
vertices. `MAX_ZOOM_BOOST` caps it at three times finer as well, so the
worst case is roughly 45% more far tiles while the zoom is held, given
back when it is released.
Reads the field of view actually in use rather than any one zoom, so it
covers all three: the spyglass (`getFieldOfViewModifier` while
`isScoping`), the zoom mod (`native_zoom`) and any mod that narrows the
view through `native_compute_fov`. The yaw is rounded to five degrees
before it reaches the plan, and the cone widened by as much, so turning
the mouse does not rebuild the quadtree every frame; the aim is part of
`Lod::update`'s area, so a zoom starting or ending replans exactly once.
The `lodZoomDetail` extra switches it off. It does nothing unless far
terrain is on and the view is actually zoomed, so an unzoomed frame
plans as it always did.
Co-Authored-By: Claude Opus 5 <[email protected]>
HDR does two jobs: an HDR canvas, and Horizon's look on it. Off, nothing in the world is
lifted, light sources, the sun and sunlight included, and the lighting passes don't run, so
HDR output matches SDR: the vanilla look on an HDR desktop, and a neutral base for packs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`[shadow] option` names the option the map is drawn while it is not 0: off,
there is no map, no shadow pass and `has_shadow` reads 0, as `[reflection]
option` does for the water mirror. `resolution_option` and `distance_option`
name an int option, or a choice whose values are numbers, whose value replaces
`resolution` or `distance`, checked against the same ranges; changing one
rebuilds the map, as any option change reloads the pack. The options sizing
the map need no `override`.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A column changed by World::edit_runs (a mod's native_edit_blocks) goes whole only to the players it was sent to.
The single-player window's far terrain kept drawing an unsent column as the window last saw it (Lod::saved),
so a strike edited while the player was away left the old land in far terrain until the player came within
view distance again. The hub now hands such columns to its memory-connected window (handoff::changed), which
gives them to far terrain to summarize (Lod::observe), as it does columns it unloads.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`serversettings/status_heartbeat_interval` and `/set` were not answered, and the
supervisor started its heartbeat only when the interval was above 0 at start,
with that first value for good.
The heartbeat thread is now always there and counts its seconds itself. It
reads the interval each second and starts counting again at every set, as
vanilla cancels the heartbeat and schedules a new one
(`ManagementServer.scheduleHeartbeat`: the first after as many seconds), so a
server that began with none can have one and a server that had one can stop it,
with no restart. 0 or less means none, and the interval is kept as it was set,
as vanilla keeps any int.
The set writes `status-heartbeat-interval` to server.properties and to
server.toml's `[properties]`, logs vanilla's "Updated status heartbeat
interval from {} to {}" and answers with the interval now in force. A value
that is not an integer is refused as the other settings' are.
The port map moves `DedicatedServer.setStatusHeartbeatInterval` out of
`skip:server` and the management protocol parity harness compares the get, the
set, an invalid value, the saved property and the value after a restart with
vanilla's. That harness runs a real vanilla server and was not run here; the
unit tests cover the live heartbeat and the persistence.
Fixes#8190
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Log in by transfer on a server whose listener is not bound (a local connection):
turned away with `multiplayer.disconnect.transfers_disabled` while transfers are
off, taken once `Shared::set_accepts_transfers(true)` has run, and turned away
again after it is set back. With the handshake reading the fixed
`config.accepts_transfers` again, the middle login fails.
Refs #8190
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
`serversettings/accept_transfers` and `/set` (`ServerSettingsService`,
`DedicatedServer.setAcceptsTransfers`) were not answered. The listener read
`accepts-transfers` once at start, from a fixed field of its configuration.
The listener's shared state now holds it as an atomic, as it holds the live
login limit, and the transfer handshake of `lan/conn.rs` reads that. The
management method changes it at once, writes `accepts-transfers` to
server.properties and, through the supervisor, to server.toml's `[properties]`
so a restart keeps it, and logs vanilla's "Update accepts transfers from {} to
{}". A value that is not a boolean is refused as the other settings' are.
The port map moves `DedicatedServer.setAcceptsTransfers` out of `skip:server`
and the management protocol parity harness compares the get, the set, an
invalid value, the saved property and the value after a restart with vanilla's.
That harness runs a real vanilla server and was not run here.
Refs #8190
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Ghast.addAdditionalSaveData writes ExplosionPower (default 1). An ownerless
projectile has left its owner after its first tick, so a trident saves
LeftOwner. AbstractHurtingProjectile.tick ignites a fireball for a second
every tick (wind charges do not burn).
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Vanilla's surface rules reach eight blocks down, layering dirt under
grass and sand under a beach. A far tile 256 or 1024 blocks across draws
at most a block or two of each column's vertical face, so the six below
that are work no view resolves.
Reduced far detail now stops at depth 2 for tiles 16 blocks a column or
wider. Depth 1 is still painted, so a tile's surface -- its height, its
top state, its water and its biome -- is exactly what it was: dumping
every column of 25 tiles at levels 4 and 6 gives a file identical to the
one before this change, all 12800 columns. Only the interior of a span,
seen on a cliff face, loses its vanilla material. Full far detail, the
default, is untouched, and so are narrow tiles, whose faces are tall
enough in view to show the layering.
Measured with `lod_grid_bench` over 100 tiles a run, five runs, taking
the least-interfered run of each (this machine's spread is wide):
| level | stride | before | after |
|---|---|---|---|
| 2 | 4 | 10.6 ms | 10.8 ms (gated off) |
| 4 | 16 | 11.1 ms | 10.3 ms (-7.2%) |
| 6 | 64 | 8.7 ms | 8.2 ms (-5.7%) |
Co-Authored-By: Claude Opus 5 <[email protected]>
Entity.move ends with getBlockSpeedFactor for every entity; items and
primed TNT now call the shared helper from #8096.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Complete relocation strings in new language tables, explicitly release completed world-operation locks, and retain unsaved settings when a move cannot save them.
Co-Authored-By: Codex <[email protected]>
Fix Linux clippy::non_octal_unix_permissions without changing the unreadable-source fixture or its assertions.
Co-Authored-By: Codex <[email protected]>
Limit fast/release compiler outputs after builds and expose cache inspection and cleanup. Coordinate builds and cleanup with OS/Cargo locks; preserve installed versions and player data.
Modded now lists the chosen loader's versions for the chosen Minecraft
version, asked of its service once per loader and version: Newest stable
first, then every version newest first, betas marked. The choice goes to
loaders::choose when the instance is made, and goes back to the newest
stable one when the loader or the game version changes. The loader card
shows the version chosen. Its four strings are in every language.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Armadillo.rollUp and Armadillo.rollOut call gameEvent(ENTITY_ACTION)
before their sound, silent or not; sculk sensors, shriekers and wardens
now hear it.
Fixes#8714
instance import <file> --java makes a Minecraft: Java Edition instance from a
Modrinth pack, as Prism Launcher imports one: the Minecraft version and the
mod loader its index names (minecraft, fabric-loader, quilt-loader, forge,
neoforge), every override (overrides/, client-overrides/) and file it
carries, files checked against their hash; a server-only file and
server-overrides/ are left out, and a path that leaves the folder is
refused. A pack that fails half way leaves no instance behind. Without
--java a modded pack still goes to the Modpacks import.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The processors are planned (steps) before they run, so a test checks the
client steps three real install profiles ask for (NeoForge 26.3.0.101,
Forge 26.3-66.1.0 and 1.20.1-47.4.0, recorded without icon and library
lists) against values read off the profiles by hand. The ignored real
install now also builds Play's command and runs it with the JVM's
--dry-run: module path, classpath and main class resolve, nothing of the
game runs, no account is involved.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Enable Minecraft Realms button on main menu and handle navigation
- Implement dedicated Realms browser screen supporting world list, status badges, selection and refreshing
- Implement Realms HTTPS client querying pc.realms.minecraft.net using AuthSession access token
- Add join endpoint resolution and direct server connection via Action::Connect
- Update agent mode serialization and port map ledger
forge_install downloads the installer (checked against the .sha1 its
repository publishes), takes the libraries from its maven/ folder or the
repository (each sha1 checked), runs the client's processors on the
instance's Java as java -cp <jar+classpath> <Main-Class> <args> with the
install profile's data, skips a processor whose outputs already match and
refuses one that leaves another sha1. The profile is kept as
versions/<id>/<id>.json with a copy of the game's jar named after it, as
Forge's ignoreList (${version_name}.jar) expects; java_launch::resolve now
lets a child name its own jar.
java_game picks the Java before installing the loader, so the installer
runs on the Java the game runs on. The new instance's loader picker shows
every loaders::Kind with its logo.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The block texture array's pixels (21 MB with the default resources) stayed in
RenderAssets for the whole session, though the GPU holds them and the copy
only serves a renderer made again. Once a renderer exists they are
lz4-compressed on a thread of their own, as the GUI atlas is (#6843); a
renderer built again uncompresses them.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Add a two-step data-folder move that copies and verifies launcher, game and build data offline, preserves source recovery copies, and atomically commits an independent location pointer before reopening. Protect migration with process leases, reject links and overlapping destinations, preserve Windows access permissions and remap managed structured paths.
Fixes#7218
Co-Authored-By: Codex <[email protected]>
A soft limit that lowers render distance and then crashes the game is not
a cap, and only Windows could hold the game under a number at all. Takes out
the launcher's Maximum memory row, HORIZON_MEMORY_LIMIT_MB, /ramlimit, the
governor and the watcher thread (reverts #5800 and #5801). A Java instance's
-Xmx stays settable from the launcher CLI.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-02 00:48:32 +00:00
939 changed files with 77877 additions and 15109 deletions
gh pr create --base main --head "$branch" --title 'Stamp native hook API fragments' --body 'Assign API numbers to the pending native hook fragments. Close and reopen this PR to run CI.'
# The repo doesn't let GitHub Actions open pull requests (that setting would also let it approve them):
# the branch is pushed, and a maintainer opens the PR from it.
if ! gh pr create --base main --head "$branch" --title 'Stamp native hook API fragments' --body 'Assign API numbers to the pending native hook fragments. Close and reopen this PR to run CI.'; then
echo "::warning::Stamped fragments are on $branch; open a pull request from it (GitHub Actions may not open one here)."
@@ -30,7 +30,7 @@ Minecraft: Java Edition 26.3, rewritten in Rust. Same worlds, same rules, same t
## Download
Get **the Horizon launcher** from [playhorizon.dev](https://playhorizon.dev) or [the launcher releases](https://github.com/horizon-rust/horizon/releases?q=launcher-v&expanded=true) (Linux x86-64, macOS on Apple Silicon, macOS on Intel from the next Nightly, Windows x64). It installs and updates the game, prepares its files from your copy of Minecraft, and updates itself.
Get **the Horizon launcher** from [playhorizon.dev](https://playhorizon.dev) or [the launcher releases](https://github.com/horizon-rust/horizon/releases?q=launcher&expanded=true) (Linux x86-64, macOS on Apple Silicon, macOS on Intel from the next Nightly, Windows x64). It installs and updates the game, prepares its files from your copy of Minecraft, and updates itself.
**You need** a Microsoft account that owns Java Edition ([why](docs/microsoft-account.md)) and a GPU with Vulkan or OpenGL 3.3 (Linux), Vulkan or DirectX 12 (Windows), or a Mac (Apple Silicon, or Intel from the next Nightly). An existing Minecraft install (official launcher, Prism, MultiMC...) is reused; without one, Play downloads 26.3 from Mojang for your account. Step by step: [Install and first launch](https://playhorizon.dev/docs/install).
@@ -868,7 +868,7 @@ Banners in End cities show as plain magenta instead of with their pattern.
<details><summary>Technical</summary>
Compare the banner block entities the End city templates place with vanilla's (`EndCityPieces`, the templates' block entity NBT, `BannerBlockEntity`'s patterns), in `crates/horizon-world/src/worldgen/structure/`. Issue [#1438](https://github.com/horizon-rust/horizon/issues/1438); possibly related: [#526](https://github.com/horizon-rust/horizon/issues/526).
Compare the banner block entities the End city templates place with vanilla's (`EndCityPieces`, the templates' block entity NBT, `BannerBlockEntity`'s patterns), in `crates/horizon-world/src/worldgen/structure/`. Issue [#1438](https://github.com/horizon-rust/horizon/issues/1438); possibly related: [#526](https://github.com/horizon-rust/horizon/issues/526). The templates' block entities now keep their patterns ([#1928](https://github.com/horizon-rust/horizon/issues/1928)).
</details>
@@ -878,7 +878,7 @@ Armour and tools in End city chests should come enchanted, as in vanilla.
<details><summary>Technical</summary>
`loot.rs` implements `enchant_with_levels`: find why the End city chest table's rolls come out bare, and compare them with vanilla's for a few seeds. Issues [#1437](https://github.com/horizon-rust/horizon/issues/1437), [#1660](https://github.com/horizon-rust/horizon/issues/1660).
`loot.rs` implements `enchant_with_levels`: find why the End city chest table's rolls come out bare, and compare them with vanilla's for a few seeds. Issues [#1437](https://github.com/horizon-rust/horizon/issues/1437), [#1660](https://github.com/horizon-rust/horizon/issues/1660). The loot's enchantment functions now reach every table that rolls ([#1911](https://github.com/horizon-rust/horizon/issues/1911), [#2037](https://github.com/horizon-rust/horizon/issues/2037)).
"Hidden: every Horizon player sees you without your cosmetics, you included. The same switch as the launcher's Wardrobe."
}
Item::CosmeticsOthers=>{
"What other Horizon players wear: everything, only their name tag styles, or nothing. Hidden, the game never asks what they wear."
"What other Horizon players wear: everything, only their name tag styles, or nothing. Hidden unless you own cosmetics, until you choose. Hidden, the game never asks what they wear."
}
Item::CosmeticsAuras=>"Auras and trails other players wear. Off by default.",
Item::Pvp(o)=>o.description(),
Item::WheelCyclesSettings=>{
"The scroll wheel over a button in the settings changes its value, as in vanilla. Off, the wheel only scrolls the list."
Loaded 100 of 939 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.