`horizon-launcher native-build --prebuilt-out DIR --version <Nightly> FOLDER` builds every
.hnmod in FOLDER through the player's own pipeline (pins, hook points, left-out mods), but
needs no prepared game files and installs nothing: it writes the program, its
native-mods.json and request.txt into DIR. request.txt is the build's request
(horizon_release_sign::native_build): target, the base's commit, each mod's id, version and
file SHA-256, so the registry's workflow and the launcher name a build alike. A set with a
post-build step is refused: the step runs on the player's game files.
native_mods: pipeline takes an export folder; the .hnmod unpacking is shared with
installed_mods; build_target names the Rust target of a build.
Measured on the desktop: zoom + minimap on Nightly 6560641, release profile, -j 4: 8 min,
request key equal to the one horizon-mods' plan computes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
horizon-release-sign gets `native_build`: a build's request (target, base commit, each mod's
id, version and registry file SHA-256, sorted) and its key, the release and asset names in
horizon-mods, and the signed message `horizon native build <key> <target>\n<sha256> <asset>\n`,
which no release or Nightly signature can pass for. The keys are their own
(native-build-keys.txt): horizon-mods' `native-builds` environment holds the private one,
because the registry compiles other people's code there.
tools/ci/sign_native_build.sh signs with OpenSSL as sign_release.sh does, refuses a key whose
public half isn't listed, and checks its own signature; tests sign with it and verify as the
launcher will.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: requeued at 8cd24bc4e5: the merger stopped on a main conflict, and the author's main merge resolves it. verify is green on this head; the run-parity label reruns parity on it before the merge.
🤖 Agent: test-only: Flame is Ignite(constant(100)) seconds, so 2000 ticks; a_flame_crossbow_shoots_burning_arrows passes with prepared resources on the union of this gate's PRs on main 1d63e0a5a (playhorizon: fmt, clippy -D warnings, client + client-lib tests, 26 harnesses all pass).
🤖 Agent: a re-angered suspect keeps its saved anger as increaseAnger does (test now expects 54); carries #10290 for the shared corpus; sculk, wolf_anger_writes and the save harnesses pass on the union of this gate's PRs on main 1d63e0a5a (playhorizon: fmt, clippy -D warnings, client + client-lib tests, 26 harnesses all pass).
🤖 Agent: added the hit's Glowing from the arrow's own Duration (SpectralArrow.doPostHurtEffects reads this.duration; Horizon used a fixed 200); carries #10344 since both extend the command corpus; spectral_duration and command_exec pass on the union of this gate's PRs on main 1d63e0a5a (playhorizon: fmt, clippy -D warnings, client + client-lib tests, 26 harnesses all pass).
🤖 Agent: merged main; the Health doc line now sits on item_entity_from_nbt next to main's numeric-UUID line instead of above the UUID helper; item_health_load and item_numeric_uuid pass on the union of this gate's PRs on main 1d63e0a5a (playhorizon: fmt, clippy -D warnings, client + client-lib tests, 26 harnesses all pass).
🤖 Agent: merged main, keeping main's arrow_owner_tests next to this PR's saved_tag_tests in data.rs; saved_empty_tags, arrow_owner and the explicit-empty ignored test pass on the union of this gate's PRs on main 1d63e0a5a (playhorizon: fmt, clippy -D warnings, client + client-lib tests, 26 harnesses all pass).
🤖 Agent: F1 stays the vanilla two-state toggle and resets the mod's hand override; set_hud_mode is a no-op on servers and during a guest's borrowed turn; hud_mode helper fragment passes tests.test_stamp_native_hooks with main merged in (union of 74 heads on main: fmt, clippy -D warnings, tests green).
hand_geometry: keeps this branch's ui.hands_drawn (native HUD modes) and
main's rule that a studio replay never draws the hands.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
SpectralArrow.doPostHurtEffects builds the effect from this.duration, which this PR now loads and saves; the hit still used a fixed 200.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Conflicts kept from both sides: crates/horizon-client/src/entity_nbt.rs tools/command_exec_corpus.py tools/test_command_exec_parity.py
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Realms subscription URL (subscriptions/$WORLD_ID), JSON-bool settings, InviteCodeList array/result parsing and the JSON Content-Type now match the 26.3 RealmsClient; same Microsoft session, no other auth (Mojang rules OK); footer buttons wired to the selected code. (union of 74 heads on main: fmt, clippy -D warnings, tests green).
🤖 Agent: #11443 by GabrielNichols with main merged (its fork refuses pushes): the Recipes panel keeps its editable search and query when a recipe returns to an empty result, and opens in Creative inventory. The only resolution is THIRD_PARTY_NOTICES, where the pinned JEI paragraph now names the GUI sprites under the MIT LICENSE.txt shipped beside them. Verify and launcher green; clippy -D warnings OK.
🤖 Agent: discord, friends and heartbeat become default-on Cargo features (compile gating only, no new network call), so the --no-default-features server build drops 14 crates; main's newer native_client_services invite code is gated too. (union of 74 heads on main: fmt, clippy -D warnings, tests green).
🤖 Agent: harness-only. Kills items and corpses between cases (the drop leak between the cow and item cases), and lists server_wool_vibration as known_flaky for the real-time player timing (1 of 4 on a loaded playhorizon).
🤖 Agent: exempts only STAB (action 8) from #11223's move hold, so a release still shoots from the position before the move (#8423). lunge_motion: parent 3/3 pass, #11223 3/3 fail, this branch 3/3 pass on playhorizon.
Show the newest build with a Latest badge, present Nightly dates in a player-friendly title, and separate older builds on the Versions page.
Co-Authored-By: Codex <[email protected]>
🤖 Agent: es_es now covers all 898 en_us keys (four recipe-transfer strings added after main grew), placeholders checked key by key; the old red verify/launcher was the horizon-checks-1 runner out of disk. (union of 74 heads on main: fmt, clippy -D warnings, tests green).
#7676 and #9222 each added Game::native_level_play_sound and
native_level_send_particles; the mod API generator needs one of each,
so every build on main panics in build.rs. The host-bridge pair becomes
native_play_sound_in_place / native_send_particles_in_place; the names
mods call (play_sound, send_particles) are unchanged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Brings in #11026 and #11534. docs/controllers.md keeps the guide's slider
hold beside main's server-list bumpers and Playing in the background.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
#11223 made every PlayerAction hold the move queued behind it for the next
tick. For a spear's STAB that moved the jump the move carries
(handlePlayerPositionChange's jumpFromGround) a tick after the Lunge's
impulse, and the server took two moves in the next tick: lunge_motion has
failed every jump phase on parity-main since. Releases, drops and digs keep
the hold.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Extras moves onto the new settings_shell (one row per setting, search, description strip), same items and saves; design-system.md now lists Extras with Graphics/Controller as Blocks-frame pages; size (+1748) accepted because ~860 lines are the shell the next pages reuse (#11487 plan). (union of 74 heads on main: fmt, clippy -D warnings, tests green).
🤖 Agent: Requeued after merging main twice (lan_e2e.py keep-both: this PR's entity-die check beside main's new mob-thorns check; then #11735's clippy/lock fix); verify green. Breaking a painting, cushion, boat, minecart, frame or armor stand emits ENTITY_DIE like vanilla's kill path, and its sculk/vibration harnesses passed on the earlier head.
🤖 Agent: Unbreaks main: clippy 1.99's manual_is_multiple_of in horizon-core (from #10553) and the socket2 0.5.10 entry missing from both lockfiles under --locked; both workspaces' cargo metadata --locked and clippy -p horizon-core -D warnings pass on this head.
Same break as the workspace lock: #11464 and #11569 left "socket2 0.5.10"
referenced with no package entry, so main's launcher build fails under
--locked. Regenerated with cargo metadata; nothing else moves.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
clippy::manual_is_multiple_of is in the stable toolchain now, and the workspace clippy step runs with -D warnings,
so main's own horizon-core fails verify before any PR's code is compiled.
Ensure advancement parent references and command lookups handle both bare
and namespaced identifiers, preventing orphaned advancement nodes and
matching vanilla's progression count on '/advancement grant @s everything'.
Co-Authored-By: Antigravity <gemini> <[email protected]>
🤖 Agent: Requeued at the author's main merge 552f2c1b4: same net patch as d8676fff0 (identical patch-id), whose parity failed only command_exec's 26 /say lines that #11687 has since fixed on main. The change: shader packs in the End draw the sky fan under the dragon fight's fog like the rest of the pack's sky.
🤖 Agent: a refused START_FALL_FLYING now flips the glide bit of sent_shared so a flags update goes out, as LivingEntity.stopFallFlying's set(true)+set(false) dirties the flag in vanilla (union of 74 heads on main: fmt, clippy -D warnings, tests green).
🤖 Agent: the Patch notes tab's three orphan strings are gone from the 86 lang files, so the launcher's i18n tests pass (union of 74 heads on main: fmt, clippy -D warnings, tests green).
🤖 Agent: /damage ids now come from the world's damage catalog and are replaced on each world open, so a pack's type doesn't leak into the next world (union of 74 heads on main: fmt, clippy -D warnings, tests green).
🤖 Agent: the single-player memory connection sends every ready view column in one batch before PlayerLoaded (about 0.5 s saved at render distance 12), then the 64-per-tick cap comes back; socket guests keep vanilla pacing. (union of 74 heads on main: fmt, clippy -D warnings, tests green).
🤖 Agent: new client-only, read-only native hook chat_suggestion_applied fires after Tab or a click applies a suggestion, with the replaced range in UTF-16 units of the list's original input; fragment stamps cleanly on main. (union of 74 heads on main: fmt, clippy -D warnings, tests green).
🤖 Agent: port-pack events answers VillagerTradesEvent/WandererTradesEvent with the trade_set and villager_trade registries 26.x moved trades to. (union of 74 heads on main: fmt, clippy -D warnings, tests green).
🤖 Agent: Studio playback of .mcpr recordings through the normal client handlers (B1 slice of #9712, +2120), only active when a recording is opened; F1 now hides chat and tab list as vanilla Hud.extractRenderState does. (union of 74 heads on main: fmt, clippy -D warnings, tests green).
🤖 Agent: new horizon-polytone crate reads Polytone targets and biome/dimension attribute layers from the public wiki only (Polytone is GPL, its source not read); no caller yet, first step of #11486. (union of 74 heads on main: fmt, clippy -D warnings, tests green).
🤖 Agent: the hook runs after the vanilla shuffle so RNG draws are unchanged and a no-mod dispatch is an empty loop; fragment wind_burst passes tests.test_stamp_native_hooks and --check --only on main merged in (union of 74 heads on main: fmt, clippy -D warnings, tests green).
🤖 Agent: Studio rows and buttons now register as focus targets and the wheel moves one whole 40 px row per notch, which also stops part-scrolled rows drawing over the title and makes the last recording reachable. (union of 74 heads on main: fmt, clippy -D warnings, tests green).
🤖 Agent: only block/ and item/ paths equal to the old id are renamed, so a mod's own pulley:block/chain and chain_command_block_* stay, and files are rewritten only when something changed (union of 74 heads on main: fmt, clippy -D warnings, tests green).
🤖 Agent: recipe suggestions reuse json_ids_of_every_namespace like /loot, and the dead ids_under goes (union of 74 heads on main: fmt, clippy -D warnings, tests green).
🤖 Agent: The unsupported placed features are now named in the log (capped at 20), which is what a pack author needs to fix them. (union of 74 heads on main: fmt, clippy -D warnings, tests green).
🤖 Agent: under TAA the flame gate now uses a frustum widened by one rendered pixel per side (jitter is at most half a pixel: 2× margin), one extra frustum per frame, shader packs still bypass. (union of 74 heads on main: fmt, clippy -D warnings, tests green).
🤖 Agent: with entity culling off the decision is Bypass before any bounds are read, so the posed-envelope walk over every model point of every copy is now skipped (TAA condition dropped), RNG order of cluster_poses pinned by a test. (union of 74 heads on main: fmt, clippy -D warnings, tests green).
🤖 Agent: TCP keepalive (10 s + 3×3 s) now ends dead friends tunnels and relay bridges in ~19 s instead of holding them until the 120 s idle; the stack buffers replace one Vec per connection, not per read. (union of 74 heads on main: fmt, clippy -D warnings, tests green).
🤖 Agent: Chunk-section block writes now do one position lookup instead of two on the worldgen hot path; output unchanged, and CI parity on main+head (worldgen, structures, light and the save harnesses it selects) passes in full.
A /kill-ed cow stays 20 ticks (LivingEntity.tickDeath) and still moves;
the fill that removes the carpet or slab under it drops it onto the floor
while the next case measures. item-slab read power 2 before its summon on
vanilla's side in a playhorizon run. Wait 25 server ticks after the kills.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
GitHub's runner moved its binaries to bin.<version>/ after a self-update, so
`pgrep -f "$runner/bin/Runner.Worker"` never matched: runner-gc took busy
runners for idle and deleted the probe builds and old prepared resources of
two running parity jobs (2026-11-07). Match bin*/ in runner-gc,
checks-runners.sh and linux-runner.sh.
runner-gc also counts shared-local/parity-target (19-24 GB per runner),
recreates it empty after removing it (a dangling .local link failed every
harness), and drops harness folders over 2 GB untouched for 6 hours.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Extend the spawn rule suite with Mob.checkSpawnObstruction: real entities
against water, lava, an obstructed level and Ocelot's special base, with
--no-obstruction reproducing the previous corpus.
The arena's /kill @e[type=!player] kills the previous case's cow, and the
cow drops beef and leather as it dies, after the selector was evaluated.
Those items fall onto the next arena and can hit the floor while the next
case measures, at a wall-clock moment: parity-main 42359055402 read power
2 before item-stone on vanilla's side, and playhorizon read 2 after
item-carpet in 1 of 3 runs at 67902962fd. Both cases follow a cow case.
A second /kill @e[type=item] removes the drops; setup lines aren't compared.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: fixes main's red clippy since #11561 (1.95 lints on interpolation.rs: is_multiple_of on non-zero unsigned divisors, as_chunks_mut::<4> quads); workspace clippy clean on main + this.
is_multiple_of on a usize cell size, and as_chunks_mut::<4> for the four
columns' quads (each quad is now a [f32; 4]).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Native mods can draw terrain models with an explicit material (shade, UV shift); the shading follows Flywheel's (MIT, credited). Main merged in after #11117 landed; its test now builds faces with #11356's BakedFace::new (clippy -D warnings passes).
LevelExtractor.setupRender calls allChanged when the effective render
distance changes; LevelRenderer has no allChanged in 26.3.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Main's backup screens (#9982) came in beside the invite codes screen.
The footer's Copy/Delete/Edit used ids 180-182, which no arm handled (the
100..=200 arm read them as rows 16-17) and which main's Backups arm
(170..=189) now takes first: they carry the selected code's row id
(130 + 3 * row + Copy 0 / Edit 1 / Delete 2) instead. Request.Post's
Content-Type is application/json; charset=utf-8.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: The brewing stand's fuel slot and quick-move both read the brewing_fuel component of the stack, as BrewingStandMenu.FuelSlot and quickMoveStack do.
Main added invite_public_part (horizon_friends::invite) after this branch
was cut; without the feature the server build's clippy
(--no-default-features --all-targets) could not resolve it. A friend's
world then shows no part of the invite, and the invite test needs the
feature.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: older clients 26.1→1.20.5 behind accept_older_clients (off by default); a 26.3 client never reaches ClientSession or the components_before_1_21_6 rewrites; per-step unit tests + lan_bot joins one bot per release; doc of older_client fixed.
🤖 Agent: attributes only, no code removed; the 30 kept allow(dead_code) now carry a reason (19 native mod API). Windows/macOS-only code is the risk, CI's other platforms cover it.
Extras moves onto settings_shell, which draws with graphics_style, so the
doc names the three settings screens that take the Blocks palette.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: socket2 0.6 keeps every call direct.rs makes (set_reuse_address, the unix-only set_reuse_port, set_only_v6, connect_timeout), so the friends socket code needs no change; lock loses 11 packages.
🤖 Agent: one workspace rust-version (1.95, which horizon-mods already needed); the second commit is clippy's let-chain/as_chunks/is_multiple_of rewrites, all on unsigned values with non-zero divisors, so no behaviour moves.
🤖 Agent: near sections no longer dither over far terrain (fade() is 0 or lod::HELD), and the replaced tile still stays behind for lod::FADE through previous, so no hole opens; the test bound lowered to 1,000 matches the half that now come in whole.
🤖 Agent: respawn_world_point and disconnect_crafting reuse generate/world only after a generate/finished marker, written after server.stop(); an old half-made world regenerates once.
🤖 Agent: LocatorBarClient.arc now flags a catch-up tick only before the jab (tick < stabTick); a catch-up right before it still changes the jab's motion, which the harness compares with vanilla, so lunge_motion keeps its signal.
🤖 Agent: drops the harness/probe/field counts from AGENTS.md and CONTRIBUTING.md; the commands and paths around them are unchanged, so nothing a reader runs moves.
🤖 Agent: horizon-ctl screenshot-sequence takes repeatable frames: each waits for the GPU (30 s at most) and for the pack's centre-depth copy, and F2 is refused while a sequence runs. Lands after #11238.
by_registry outlives a world, so a pack's type stayed accepted by /damage in
the next world opened. Same as sync_enchantment_registries.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The flame culling doc now says TAA uses the widened frustum; the run
notes and samples stay in the PR body (run logs are not docs).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
entity_cull_decision_in_bounds returns Bypass for EntityCullingMode::Off
before reading the bounds, so the TAA condition only kept an unused walk.
Drop the docs/performance run notes (run logs are not docs).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: mobs path-stop at shulkers and a held-still happy ghast; earlier parity failures were env + main's /say regression (#11687 fixed it); CI parity reruns with run-parity before landing.
#11532 and main since make loaded_pig_tags, loot_mine_pot_components, mob_discard
and command_exec run under a plain parity.py call, which is what ccb85ce45 and
aae27797d did here. Main's versions replace them, so this PR keeps only its own change.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
#11532 and main since make loaded_pig_tags, loot_mine_pot_components, mob_discard
and command_exec run under a plain parity.py call, which is what ccb85ce45 and
aae27797d did here. Main's versions replace them, so this PR keeps only its own change.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: raid wave positions (new and cached) now need an entity-ticking chunk in entity ticking range (ServerLevel.isPositionEntityTicking), not just a loaded one; raid, raid_village and raider_celebration harnesses passed.
🤖 Agent: the player save now writes and reads the recursive hidden_effect chain (Poison I under Poison II resumes after a reload); SavedEffect became a struct, guest data uses the same path; active_effects, effect_sync, player_effect_amplifier and status_effect harnesses passed.
🤖 Agent: HORIZON_STILL now settles a frozen world's rain, thunder and rain fog at the level instead of easing them; it only runs with that dev variable set.
🤖 Agent: The shader pack reset (both kinds) lands the in-flight frame before replacing targets, so the next frame draws as a fresh pack's first; dev tooling, no change without the reset command. #11239 follows it.
🤖 Agent: Shader pack shadow settings are now read from the stage that declares them (fragment const settings), with main's noiseTextureResolution kept on the formats stage; only shaders/ packs see a change.
🤖 Agent: Native mods can draw a still-fluid volume in a block; the fluid shading follows Ponder's FluidRenderHelper (MIT, credited in THIRD_PARTY_NOTICES) and nothing changes without a mod calling it.
🤖 Agent: /raid check now finds a raid still counting down (getRaidAt has no started filter), and /raid sound plays the horn only for "local" at volume 2 with the level random's seed; raid and raid_village harnesses passed.
#10523's settings gear now takes its width from the Recipes search box of
this layout; both Escape paths (mod_browser_back, close_transferred_recipe)
kept; one JEI notice for the sprites and the icon.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The commands' minecraft:damage_type ids came from the vanilla export
alone, so /damage refused a type from a content mod or a world's data
pack with argument.resource.not_found. Vanilla's ResourceArgument looks
the id up in the server's registries, packs included.
build_registries now adds the damage catalog's types to the registry's
ids after the export is applied, keeping the export's tags.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: The viewer's transfer is a separate server-side path: it only moves ingredients the player already has, leaves grid and inventory untouched when one is missing or the inventory is full, and never teaches the recipe; the vanilla recipe book keeps its own path. Second of the Recipes series, after #10523.
🤖 Agent: The focused text field is exposed to native mods before the character is inserted, so a hook can veto it; the hook fragments stamp cleanly on main. Main merged in (its ChatScreen is_draft argument added to the test).
Vanilla LivingEntity.onEquipItem posts GameEvent.EQUIP (or UNEQUIP) at
the mob and plays the equip sound at volume 1.0. Horizon only did that
for dispensers, copper golems and the player's armor, so saddling a pig
did not activate a sculk sensor and pig/strider/horse saddles played at
0.5.
Interact, inventory, pickup and /item paths now post the event from the
mob, and saddle/harness/armor sounds play at 1.0.
Fixes#9956
Co-Authored-By: OpenCode <[email protected]>
Co-authored-by: monkeycode-ai <[email protected]>
Co-authored-by: monkeycode-ai <[email protected]>
A memory connection that has not reported the view loaded now sends
every ready view column in one batch, and the recorded quota is raised
to cover that send so the following tick is capped again. A socket
guest, a player who has already loaded, and a respawn stay on the
64-column quota. Unlit columns stay withheld, the far ring still spends
only the leftover vanilla quota, and the gates that send nothing are
unchanged.
Fixes#6763
🤖 Agent: Transient block entity fields from the update tag reach the native API without being saved; only mods that ask for them see anything new. Main merged in (its new Joined field added to the test).
🤖 Agent: only the automatic count changes, and only where hw.perflevel0.physicalcpu answers (Apple silicon): M4 Pro 13 → 9 workers with no loading or pacing regression over 3 alternating rounds; off Apple the count is unchanged (tested on Linux), and a manual Worker threads setting still wins.
🤖 Agent: #10015's early return dropped /say from chat_capture, so command_exec lost 26 announcement lines; the broadcast now skips only the acting guest (lan_acting), whose copy stays in the capture in order. Console and command-block paths are unchanged. command_exec PASS fresh on the desktop; the control on main 004a39be0 FAILs 16320/16346.
🤖 Agent: a head-on stop on a straight rail no longer blows a TNT cart up (Entity.move zeroes the blocked axis and OldMinecartBehavior never re-applies it), and a glancing hit off its rails now does; tnt_saved_power, tnt_data_adoption and both vehicle harnesses passed.
🤖 Agent: The player-expect move now refuses a full server like a normal join would, and operators still bypass it as vanilla's PlayerList.canBypassPlayerLimit does.
🤖 Agent: 26.2 packet layouts are only used inside a session that negotiated V26_2, and accept_older_clients stays off by default, so a 26.3 client's path is unchanged; the lan_bot tests cover both.
🤖 Agent: The Recipes settings button opens from the inventory browser; the JEI-derived icon is MIT and credited in THIRD_PARTY_NOTICES. First of the Recipes series (#11442 → #11453 follow).
🤖 Agent: only reinforcement_room uses the oracle's light op, so the wait (ThreadedLevelLightEngine.waitForPendingTasks, ticking like settle in a frozen world) changes no other harness's records; command_reinforcements --fresh --repeat 3 PASS 3/3 at desktop load 40-48, where main failed 7 of 10.
🤖 Agent: code-owner approval given; pruning only touches marker-carrying Nightlies, manual/legacy ones are outside the quota (tests cover all four cases).
🤖 Agent: vanilla's CheckerboardColumnBiomeSource sets bitShift = size + 2 with no clamp, and Java's >> masks the distance; wrapping_add(2) as u32 with wrapping_shr does the same. biome_source PASS with fresh Java on the desktop (main + this head).
🤖 Agent: a map crafted from the grid gets Item.onCraftedBy on the taken stack and the scaling recipe reads the level's maps; merged with main's merchant on_take; craft_map_take and merchant_crafted pass.
The new touching_unloaded_chunk was inserted between eye_columns and its
doc comment, which then documented the wrong function.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: dropping the --reference-only assertion makes a plain run go through runner.main()'s full Java/native comparison, and then the strict tags/pot report, so CI now compares instead of exiting in 0 s. mob_discard's default --resources is lab.RESOURCES (the prepared 26.3 folder) and it gets the missing import os. The two sound harnesses take main's #11555 version.
🤖 Agent: data get on a leashed boat now reads its leash from the same save path the world writes (write_boat_leash); merged main twice, boat_leash_view harness passes and the u/3 union builds with clippy -D warnings.
🤖 Agent: breeding now adds one fresh orb of the whole 1-7 roll (no award split, no merge), and XP bottle orbs start 0.25 along the hit face's normal with their motion turned away from it; orb_lava/orb_water/orb_saved_uuid/bed_explosion harnesses passed.
🤖 Agent: OptiFine custom skies draw their layers over the vanilla sky, only when a pack ships optifine/sky and the Horizon sky option is on (a frame without one only clears an empty list); merged main, clippy and the sky tests pass.
The automatic pool size was every core but one. On Apple silicon the
efficiency cores count too, so a base M4 (4 performance + 6 efficiency
cores) ran 9 workers that kept the render and server threads waiting for a
performance core: fast flight's p99 frame went from 20 ms to 7 ms with 3
workers (#11294). The automatic count now leaves one of the performance
cores (hw.perflevel0.physicalcpu) instead; elsewhere it is unchanged, and
Graphics > Worker threads still overrides it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: a negative Age makes a baby only where AgeableMob.canBeABaby allows (frog, parrot, zombie nautilus, camel husk, slime, magma cube stay adults: box, voice, loot); ageable_integer_load, entity_death_loot and baby_pig_attachment pass.
🤖 Agent: Realms reset-world, templates and upload, stacked on #9982 (land after it); upload goes only to the endpoint the service returns; realms tests pass.
🤖 Agent: Realms backup list and backup info screens on main's Realms code (only the backups commit kept; the carried settings/invite copies already landed); restore asks the service and re-reads the list; realms tests pass.
🤖 Agent: advancements= in a selector reads the selected player's own progress, a guest's included (EntitySelectorOptions on the ServerPlayer); guest_advancement_selector passes on the merge, now runnable by parity.py.
The two reset-world commits rebuilt on the backups branch (both PRs extend the
same configure screen): the scroll wheel covers the product page, template and
upload lists, and the worlds tab has Options and Backup side by side with
Reset below.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This branch also carried early copies of the settings-tab and invite-code
commits, which landed on main as #9926 and #9968 and changed since. The tree
is main with only the backups commit on it, plus the configure screen's
tier fields that main added.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
#10015 broadcast /say from cmd_say to every guest and dropped the line
from chat_capture. The acting guest still saw it, but the command
harness reads the capture, so command_exec lost every /say line
("[Player] relative dimension", the h154 function's "[Pig] H154
fallthrough") and failed parity-main at ebcfa2263a.
When a guest's command is captured, the broadcast now skips that guest
and the line stays in the capture, in order with the other output, which
is delivered to them as before. The console path is unchanged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: roadmap.toml is the source; ROADMAP.md and roadmap.json regenerated (build --check passes). older-servers kept on purpose (1.8 PvP servers), only the sign-in item goes.
Mojang's approval covers Microsoft sign-in only; other account providers
(Ely.by, Yggdrasil servers) are against the rules Horizon plays by, so the
item can't ship. #1270 stays closed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ThreadedLevelLightEngine runs the checkBlock tasks a /fill queues at most
1000 per update. command_reinforcements fills ~100k blocks around its room
and then requires block and sky light 0 there; on a loaded runner the
vanilla side still read the old light (7, 9, 10 at different positions),
which failed parity-main and 7 of its bisect steps back to 11-05 merges.
The light op now waits for waitForPendingTasks on that chunk, ticking as
settle does.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: follow-up to #10129: with glass off, macOS still gets its 32 px title bar (plain side colour), so content no longer sits under the traffic lights.
🤖 Agent: follow-up to #10177: the unmaximize-before-fullscreen step runs on Windows only, and apply() no longer re-requests fullscreen each call while the window reports maximized.
parity.py runs a harness with no arguments; --side was required, so every
automatic selection failed it at once. The default now runs Java, then native
against Java's facts.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Raid.findRandomSpawnPos and the cached wave position both ask
ServerLevel.isPositionEntityTicking: an entity-ticking chunk in entity
ticking range. Horizon accepted any loaded chunk, so a wave could spawn
past the simulation distance, where its raiders would never tick.
Part of #11374 (the spawn positions; the raid/spawner order stays open).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: a copper chest someone has open now skips its weathering step (WeatheringCopperChestBlock.randomTick checks getEntitiesWithContainerOpen); random_tick and random_tick_sections harnesses passed.
🤖 Agent: using items (fishing rod, ender eye…) now posts vanilla's item-use game events through causeUseVibration, so sculk sensors react as in Java; sculk, server_player_sculk, server_wool_vibration and use_item_on pass against the 26.3 oracle in the gate's harness run (242 selected; failures were host setup only plus the lunge_motion timing flake).
🤖 Agent: crafting with a merchant result slot now fires vanilla's merchant crafted trigger and updates the trade, checked by the new --merchant-crafted-fixture in command_exec; merchant_crafted, command_exec and trading pass against the 26.3 oracle in the gate's harness run (242 selected; failures were host setup only plus the lunge_motion timing flake).
🤖 Agent: item entities, armor stands and boats now save the shared Entity flags (Invulnerable, CustomNameVisible, Silent, NoGravity, Glowing, HasVisualFire, Tags) as vanilla's saveWithoutId does. Its 242 selected harnesses ran on main + this head + #11017 + #11022: save_roundtrip, world_reopen, entity_nbt_boolean, the armor stand and boat ones pass; the only failures are setup (no sound/lang assets on the host, harness args) and the known lunge_motion tick-timing flake under software rendering.
MinecartTNT.tick reads the delta after AbstractMinecart.tick. The move
zeroes the blocked axes and no track step puts them back, so a cart
stopped head-on by a wall on a straight rail does not blow up, while one
glancing off a wall keeps its free axis and does, on or off its rails.
Horizon kept the speed the cart asked for before the move on the rail
paths and none off them.
Part of #11380 (the impact detonation; the team push points stay open).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: overflow honey now leaves through Player.drop (eye height − 0.3, thrown along the view, pickup delay 40) instead of popping at the feet; held_drop harness passed, plus Game tests for Peaceful anger and event 2011.
MobEffectInstance.Details nests the instance an effect hides under
hidden_effect, and the player save writes the whole chain. Horizon wrote
only the top instance, so a weaker, longer effect (Poison I under
Poison II) was gone after a reload instead of resuming. SavedEffect is
now a struct carrying that chain; guest player data goes through the
same code.
Part of #11384 (the player side; mobs still drop weaker instances).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Let native mods keep shared-item text aligned when a player accepts or
cycles chat suggestions. Report the original input, UTF-16 replacement
range, selected suggestion and resulting text. Cover keyboard and mouse
handling, server suggestions, emoji and length limits.
Part of #11212.
Co-Authored-By: Codex (GPT-6) <[email protected]>
🤖 Agent: the device list is now read on a worker thread, so a slow Windows audio driver no longer stalls a frame; the render thread only swaps in the result. No vanilla behaviour changes, no harness covers it.
RaidCommand.getRaid is getRaidAt, which returns a raid that has not
started its first wave yet. playSound plays the horn only for the text
"local", at volume 2 with the level random's seed. The status line
concatenates its floats the Java way (20.0, not 20).
Fixes#11372.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: effects now carry vanilla's showIcon apart from the particles (5-arg constructor, update, save codec default, packet bit 4), so warden Darkness and /effect hideParticles no longer show a HUD icon; 8 effect/HUD/sculk harnesses pass.
- Animal.finalizeSpawnChildFromBreeding adds one new ExperienceOrb of the
parent's 1-7 roll: breeding no longer splits it (4 into 3 + 1) or merges.
- ThrownExperienceBottle.onHit awards with direction: away from the block
face it hit, or back along its motion after an entity hit; the new orb
starts 0.25 that way and a random motion against it is turned round
(ExperienceOrb(level, pos, roughly, value)) (#11388).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: lightning now places fire (and draws its extra-source offsets) only when canSpreadFireAround passes at the bolt, so fire_spread_radius_around_player holds for strikes far from every player.
WeatheringCopperChestBlock.randomTick runs changeOverTime only when the
chest's getEntitiesWithContainerOpen is empty, so an open chest neither
oxidizes nor draws its odds (part of #11378).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`horizon-port-pack events` gives a fifth answer, data: NeoForge's
`VillagerTradesEvent` and `WandererTradesEvent` are gone since vanilla 26.1
made trades the `trade_set` and `villager_trade` registries (NeoForge's 26.1
primer), so a port writes trades as data instead of waiting for a hook.
On ATM10 that answers 33 uses (20 and 13 mods).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Without a keychain, account-secrets.json was sealed under SHA-256 of a
constant and /etc/machine-id, which every local user can read; on a
system with none of the machine-id files the key was a constant built
into every launcher. Only the file's 0600 mode protected the Microsoft
refresh token inside, so a copy restored with wider permissions, or a
shared home, gave it away.
Format 2 (HZSECv2) adds a random 32-byte install secret to the key,
kept beside the file in account-secrets.key (0600, made at the first
write through a temporary file and a rename, under the file's lock).
The key is never the bare constant any more, and a v2 file whose
secret is missing or belongs to another install is refused like any
unreadable file, never written over.
A file sealed by an older launcher (HZSECv1) is still read with the old
key and sealed again under the new one at once.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
🤖 Agent: the Mods page's install and port messages go through tr/trf with named arguments, so they translate; lang files merged key by key onto main, i18n tests pass.
The invite screen's list is the screen's one focusable beside its
footer, so its extractSelection outline is white; main's draw has no
list_focused parameter to ask.
Co-Authored-By: ZCode <[email protected]>
cargo audit flagged two crates in the locks:
- tokio 1.53.2 (RUSTSEC-2026-0386), reached through quinn in
horizon-mod-deps and in the launcher's lock. Patched in 1.53.3, a
patch release, so both locks move there.
- lru 0.16.4 (RUSTSEC-2026-0253), reached only through mysql 27 in
horizon-mod-deps. No 0.16 release carries the fix; mysql 28 depends
on lru 0.18, which does, so the crate mods may use moves to mysql 28
(mysql_common 0.37, pem 4, base64 0.23 and arc-swap come with it).
`cargo check -p horizon-mod-deps --features mysql` builds, and cargo
audit no longer reports either advisory. A native mod that asks for
`mysql = "27"` now has to ask for 28, since the lock is the allow list;
docs/native-mods.md names the new version.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
🤖 Agent: a mob's Absorption now refills on every accepted add (AbsorptionMobEffect.onEffectStarted), and heal/harm and effect intervals shift like Java's int << / >> (distance mod 32, wrapping) instead of clamping or panicking at amplifier 32+.
Guest break refusal is the sequence ack every PlayerAction arm sends, the packet rate limit is PacketRate::received,
and MinecraftServer.shouldRun has no haveTime budget to defer against in Horizon's loop.
neutralise breaks @mentions, #123 and github.com/ issue links in what
people write in the website's report form, so an anonymous report can't
put backlinks on the repository's issues. GitHub also links GH-123
(either case) to issue 123, which got through.
- At the start of a word, GH- followed by a digit gets a zero-width
space after GH, the way @ and # already do: the text reads the same
and links nothing. Other words (high-5, GH-pages, laugh-1) are left
alone.
- Test: GH-12, gh-7 and (Gh-8) are neutralised; look-alikes aren't.
Found in yahya-nusseibeh's review of Horizon (L2, 7 Nov).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
Two ways one client could lock every user out of the Play with Friends
relay, with no account:
(a) The only connection cap was global (4,096), counted before anything
is read, and each connection may stay silent for the 10 s first-line
wait: about 410 idle connections a second from one address kept the
relay full, so hosts couldn't register and guests couldn't join.
(b) Every join, known code or not, is refused while the global
unknown-code budget (120 a minute) is empty. Each /64 may try 5
unknown codes per 10 minutes, so ~240 /64s keep it empty, and one
residential /56 has 256 of them.
- Limiter::open_connection / close_connection: connections open at
once per address key (IPv6 /64; a /48 four times as many), 64 by
default (RELAY_MAX_OPEN_PER_ADDRESS; a host keeps one connection per
guest, hence the margin). Counted before anything is read, or right
after the PROXY line behind the PROXY protocol; past the cap the
connection is closed at once, not held for the first-line wait. An
RAII OpenSlot gives the place back however the connection ends.
- Each site (IPv4 /24, IPv6 /48) has its own unknown-code budget, 24 a
minute with a burst of 6, checked before the global one and spent on
every unknown code, so emptying the global budget takes many sites,
not one home.
- docs/play-with-friends.md: the new setting and the site budget.
- Tests: the per-address cap (IPv4, IPv6 /64 and /48, off without
per-address limits), the per-site budget (shared within a /24 and a
/48, refilling), and a running relay closing a fifth silent
connection at once with a cap of 4, then letting new ones in once
the held ones go.
Found in yahya-nusseibeh's review of Horizon (H2, 7 Nov).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
Pack::read preallocated from a zip entry's declared size and inflated it
with no limit. Shader packs are zips players download, so a deflate bomb
(or a forged size field) in any file under shaders/ aborted the game
with an allocation failure as soon as shaders.properties was read: 512
MiB of spaces zip to 509 KiB and took peak RSS from 4 to 516 MiB; at
1.5 GiB the review measured 1,543 MiB.
- Nothing is allocated from the declared size; a file that declares or
inflates to more than MAX_FILE (64 MiB, far above any .properties,
.lang or GLSL file, with room for the largest textures) reads as
missing, like a file the pack doesn't have. The same pack now stays
at 4 MiB.
- Folder packs are unchanged (files on disk, not inflated).
- Test: a small zip whose shaders.properties unpacks past a 16 KiB cap
reads as missing; the rest of the pack still reads.
Found in yahya-nusseibeh's review of Horizon (M1, 7 Nov).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
Send pause-menu and death-screen exits through close_studio_replay when playback is active. Clear the replay timer and restore the prior camera so the title screen and the next world use normal player and hand rendering.
Co-Authored-By: Codex <[email protected]>
Open and validate a replay archive before replacing the current screen. Track standalone replay sessions so closing one disconnects its synthetic remote and restores the title screen. Start playback in first person after applying timestamp-zero world packets.
Co-Authored-By: Codex <[email protected]>
Inflate recording streams to a bounded temporary file, verify the stream CRC during the copy, and index a sparse set of seek anchors instead of retaining every packet in memory. Preserve whole-frame recovery and timestamp-group seeking.
Co-Authored-By: Codex <[email protected]>
- Dump resident chunk columns, player pose, center chunk, and dimension login into .mcpr stream at timestamp 0ms on recording start
- Allow all chunks in remote_chunk_in_range during studio replay and freecam so distant chunks are never discarded
- Reset title mode to Mode::Main when closing studio replay so the user returns to the main menu instead of multiplayer
- Add interactive glass button for '▶ Edit' on recording cards in StudioUi
- Allow single-clicking '▶ Edit' on any card to immediately load and open the editor
- Allow clicking the bottom 'Edit Replay' button to open the selected or first replay
- Handle Event::OpenStudioReplay in Loading::menu_option_events by queueing PendingWorldEntry::StudioReplay
- Handle PendingWorldEntry::StudioReplay in Game::poll_pending_world_entry to enter replay
- Add Session::replay and init_remote_for_replay to initialize client state when entering replay from title screen
- Dismiss title screen and uncapture cursor so timeline and studio HUD controls are interactive
- Release mouse cursor on Escape key in studio replay mode before opening pause menu
- Implement standard .mcpr container reader/writer (metaData.json + VarInt timestamped recording.tmcpr stream)
- Add asynchronous StudioRecorder worker to record clientbound packets with zero impact on gameplay FPS
- Add ReplayDriver with timeline seeking, variable speed control, pausing, and looping
- Wire zero-alloc packet recording tap into Session for singleplayer, LAN, and remote multiplayer
- Add in-game /studio chat commands and control socket commands for recording and replay management
- NeutralMob.isValidPlayerTarget also fails on Peaceful: a bee's anger at
the player ends there.
- BeeGrowCropGoal sends level event 2011 (data 15) before the crop grows;
the client draws it like 2012 (spawnParticleInBlock, happy villager).
- A honey bottle that no longer fits is player.drop(stack, false): from
below the eyes, thrown along the view, no thrower (#11377).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
RealmsInviteCodesScreen lays out a footer of two rows (Copy/Delete/Edit
97 wide over Create/Back 150 wide, FOOTER_HEIGHT 64), titles itself
Invite Codes (n/5) once fetched with the gray subtitle over an empty
list, and shows the loading dots without a list behind them. Its rows
are 28 high with only the code's text, selected like every
ObjectSelectionList (the port's extractSelection); Copy answers the
clipboard and Delete/Edit act on the selection (ids 180-182), not on
per-row buttons.
Co-Authored-By: ZCode <[email protected]>
Pack::read inflated each of up to 512 zip entries to 1 MiB + 1 and kept
them all, and only Pack::from_files then checked the pack's 8 MiB total.
511 entries of zeros take 561 KiB zipped, and the mod registry reads
every submitted .hshader (index::inspect, Kind::Shader) on its blocking
pool: one upload made it hold 1 GiB (measured: peak RSS 3 -> 1,026 MiB),
a few in parallel could get it OOM-killed.
- Each entry is read with room for no more than what is left of
MAX_TOTAL (and its own 1 MiB cap), and refused as soon as it passes
it: "the pack is over 8 MiB", or the file named when it alone is over.
The same package now peaks at 6 MiB.
- A folder pack counts its total the same way.
- Tests: 64 MiB of zeros in a small zip, one file over its cap, and a
folder over the total are all refused.
Found in yahya-nusseibeh's review of Horizon (H1, 7 Nov).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
InviteCodeList.parse reads a JSON array, or an object whose 'result' is
one, dropping entries without a code; the port waited for an
'inviteCodes' object the service never sends, so the codes screen
listed nothing to manage.
Co-Authored-By: ZCode <[email protected]>
- RealmsConfigureWorldScreen.fetchServerData replaces its serverData with
getOwnRealm's answer; the port kept the server list's copy, whose
regionSelectionPreference is stale, so a saved region read back as
automatic. The detail answer now becomes the screen's server.
- Request.post sends Content-Type: application/json; the frontend
answered 415 to the invite codes' text/plain POST.
- RealmsPreferredRegionSelectionScreen draws each region's ServiceQuality
icon (ServiceQuality.getIcon, 10x8 at the row's right).
Co-Authored-By: ZCode <[email protected]>
Done used to rebuild the Configure screen with detail and tier cleared,
so the save that leaving the tab triggers found no slot to send and the
chosen region was dropped. The screen now carries both back, and the
save runs on the data it had.
Co-Authored-By: ZCode <[email protected]>
The click arm that handles the screens' drawn rows was guarded to
SubScreen::Configure, so the region preference's rows (and the create
screen's fields) never saw a press. The guard now covers the screens
whose rows it handles; the region selection draws extractSelection's
black entry with its white outline and the footer is vanilla's
150-wide Done/Cancel pair.
Co-Authored-By: ZCode <[email protected]>
The service sends settings like {"name":"hardcore","value":false},
a JSON bool where the DTO says string; Gson's JsonPrimitive.getAsString
accepts it, serde did not, and the detail answer the Worlds and Players
tabs need failed to parse.
Co-Authored-By: ZCode <[email protected]>
RealmsClient.subscriptionFor builds 'subscriptions/'; the port's
'worlds/{id}/subscription' answered 404, which the configure screen showed
as a fatal service error instead of its tabs.
Co-Authored-By: ZCode <[email protected]>
🤖 Agent: an empty frame now refuses a map tracking more than 256 counted markers (ItemFrame.interact's isTrackedCountOverLimit FAIL): the map stays held, no swing; exactly 256 still goes in.
🤖 Agent: /say from a command block reaches every player under the block's name, and a captured guest command no longer echoes it back as feedback (kacpherek's point); lan_e2e command-block-chat not run here.
LightningBolt.spawnFire places fire, and draws its extra sources, only when
ServerLevel.canSpreadFireAround passes at the bolt: the
fire_spread_radius_around_player rule now holds for lightning (#11376).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: the omen sound now plays from the player's addEffect for any newly added omen (command, potion, mod) at the feet, as MobEffect.onEffectAdded does; the three special paths moved from the eye to the feet too.
🤖 Agent: TNT, falling blocks and XP orbs now collide with boats, shulkers and a held happy ghast (Entity.collide's canBeCollidedWith); sand landing on a boat over air breaks into an item like vanilla's wouldContinueFalling.
🤖 Agent: 0.2 shader packs get the float HDR frame only under HORIZON_HDR_FRAME=float; the default and 0.1 paths keep their formats (frame_format tests cover both), so the vanilla look is unchanged.
MobEffectInstance.showIcon was not modelled: every effect showed its HUD
icon and every UpdateMobEffect carried the icon flag. Warden Darkness
(no particles, no icon) showed an icon. The flag now follows vanilla's
constructors, update, saves (show_icon defaults to show_particles),
consume effects and the packet flags (#11385).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: a loaded furnace keeps its saved cooking total until the item finishes, and a hopper or dropper filling the empty input slot restarts the cooking as setItem does (kacpherek's point); furnace_loaded_total and furnace_output_components pass.
🤖 Agent: a restored chat draft draws grey italic until edited, as ChatScreen's isDraft does; text a mod opens chat with is not a draft. Merge with main builds and the chat tests pass.
ItemFrame.interact returns FAIL when MapItemSavedData.isTrackedCountOverLimit(256):
the map stays in the hand and the frame stays empty (part of #11383).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Frost Walker's speed modifier now follows a boots change made by a guest too (the guest's armor is swapped in before the comparison); armor_changes copies only changed slots. armor_stand_frost passes on a union with main.
respawn_world_point and disconnect_crafting reused generate/world whenever the
folder existed, so one crash left them failing until someone deleted it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Since #11223 the server takes what follows a jab on the next tick, so the
harness's time query comes back a tick later and every jump failed as a
catch-up. The motions are identical to vanilla's and stay compared.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: a spectator using a portal block travels to its destination, never builds a Nether exit, and gets spectator.cannot_teleport without one (ServerPlayerGameMode.useItemOn); nether_portal harness passes on a union with main.
🤖 Agent: damage a mob's absorption takes is credited to the thrower (guest throws settle inside as_guest, kacpherek's point); mace and trident harnesses pass on a union with main.
tools/parity.py runs a harness with no arguments, and these three required
--binary, --work-dir or --resources (mob_discard also used os without importing it).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: region saves reuse one deflater per writer instead of building one per chunk; the test checks the compressed bytes are identical to a fresh deflater's. Union check (main + the gate's 9 heads): fmt, clippy -D warnings, 5666 tests green; verify green after the runner's full-disk rerun.
🤖 Agent: the second rider's PlayerInput/MoveVehicle is now ignored like vanilla's getControllingPassenger check; riders are listed in boarding order so the first one in keeps the tiller.
🤖 Agent: only SUCCESS_SERVER block uses (beds, light, a charged anchor, signs) swing back to their own player; item and entity uses still echo, as the body says. use_item_on, flat_place and dedicated_server pass locally.
🤖 Agent: each moving block's collision view now advances right after its own push, so later pushes in the same tick see it at its new progress; the cost is one lookup per moving piston. piston_push and piston_movement pass locally.
LivingEntity.addEffect runs onEffectStarted for every effect it accepts,
so Absorption given again refills a mob's shield to 4 per level of the
effect given. Horizon refilled it only for a new or stronger effect.
HealOrHarmMobEffect and the periodic effects use Java's shifts, which take
the distance modulo 32 and wrap: amplifier 32 is amplifier 0 again, and a
ticked heal is at least 0. Horizon clamped the distance to 30, and an
amplifier of 32 or more panicked on Regeneration's interval in a debug
build (#11386).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Select the library artifact that owns Game, retaining the exact one-test assertion so a thin executable with no tests cannot count as a pass.
Co-Authored-By: Codex <[email protected]>
Under `unittest discover -s tests`, the bare name test_rust_parity is
tests/test_rust_parity.py, so the drivers' fallback import took the wrong
module and four tests failed (no attribute verify_artifacts) in verify.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Under `unittest discover -s tests`, the bare name test_rust_parity is
tests/test_rust_parity.py, so the drivers' fallback import took the wrong
module and four tests failed (no attribute verify_artifacts) in verify.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: horn and brush uses end with stopUsingItem (the interaction vibration), and a mob in front of the brushed block stops the brush out to the block reach; item_use_catalog harness passes on a union with main.
MobEffect.onEffectAdded plays Bad Omen's, Trial Omen's and Raid Omen's
sound (withSoundOnAdded) whenever LivingEntity.addEffect adds one that
is new, at the entity's feet. Horizon played them only on the raid, bottle
and trial spawner paths, at the eye, so /effect give, a potion's custom
contents or a mod adding one was silent (#11373). The generic add now
plays it for players and, from /effect, for mobs; the three special paths
play at the feet.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: the item name and the action bar take fixed heights above the hotbar (Hud.extractSelectedItemName 59, extractOverlayMessage 68) instead of following the bars; hud harness passes on a union with main.
🤖 Agent: a breeze turns a thrown potion back at half speed (Breeze.deflection) once in a row, stopping at the hit point that tick; the REVERSE draw uses the potion's own random.
🤖 Agent: advancements with no criteria get their rewards on load (checkForAutomaticTriggers) but stay not done, as AdvancementRequirements.test answers false; checked on a union with main.
🤖 Agent: the fade out ending at startFadeIn now runs to the end of the day (since()==0 read as DAY), which was kacpherek's review point; checked on a union with main (fmt, clippy -D warnings, tests).
🤖 Agent: the solid terrain pipelines are built with HDR_LIGHTING off and the lit copy only exists when hdr_output is Some, so HDR players switch per frame by self.hdr; frame-identity label makes CI check HDR off, offscreen HDR and HDR toggled off in a running game. −16 to −22% GPU on M4 Pro, image identical in-process.
Reuse the native summon spawn-bounds predicate so negative endpoints stay valid, and use Java's integer width for the yaw direction lookup. Record 26 independent outcomes from both pinned vanilla jars and add a differential harness for the actual native calculations.
Co-Authored-By: Codex <[email protected]>
🤖 Agent: merging. Empty region files (13 303 of 13 620 in the test world) are no longer rewritten and synced on world open, and region files convert on all cores but one with byte-identical output; datafix harness 6/6.
terrain.wgsl's solid shaders carry HDR output's lighting behind a runtime
`hdr_light.z` test. The branch is never taken without HDR, yet on Apple GPUs its
code alone costs ~15% of the solid pass (register pressure). A pipeline
constant `HDR_LIGHTING` now guards it: the solid pipelines (per-section, pulled
and flat) are built without it, and a copy with it only where the display
offers HDR output, picked per frame by whether HDR is on.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: the session state a client HUD reads (world id, screen kind) comes from fields the window already keeps; merged with #10195 and checked with clippy, the native_client tests and the hook stamp test.
Reuse the current forceSetRotation path so turning an entity preserves its mount, position and velocity. Extend the existing resource-backed rider regression.
Co-Authored-By: Codex <[email protected]>
Preserve the current teleport passenger handling and supply its rotation policy from the native movement methods.
Co-Authored-By: Codex <[email protected]>
🤖 Agent: client_hurt reports the packet's causing entity (the shooter, not the arrow) and treats an untracked entity as not a player, so a mod never gets a false PvP signal. Main merged in (test conflict with #11512's open-chat test, both kept); verify green on this head.
Audio::should_change_device listed the output devices on the render
thread once a second. Windows has no cheap fingerprint to skip the list
(Linux reads /proc/asound/cards, macOS names the devices), so every
second the render thread waited ~90-120 ms on WASAPI's endpoints, even in
the main menu (#11490). The look now runs on a short-lived thread every 20
ticks; a later tick applies what it found with the same rules.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
PrimedTnt, FallingBlockEntity and ExperienceOrb move with Entity.move,
whose Entity.collide includes EntityGetter.getEntityCollisions: a boat,
a living shulker and a happy ghast held still stop them, as they stop a
dropped item. Horizon resolved their moves against blocks and the world
border only (#11518). The ordered tick gathers those boxes once per run
(again after an explosion) and passes the nearby ones to each move.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A boat's passengers went to the clients in the order the players joined
the server, so the player who got in second could hear it sits first and
take the boat over, and the server took MoveVehicle from either seat: with
two players in one boat it kept snapping back (#11541). The passengers now
go in the order they boarded, and handleMoveVehicle ignores a player who
isn't the vehicle's controlling passenger, as vanilla does.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: native_set_chat_input filters text to what typing allows (no control chars, DEL or §) and clamps the cursor, so a mod can't inject formatting codes into the chat box.
🤖 Agent: Launcher-only follow-up to #10529: the Home tab strip is flex_none on wide windows again (glass::tabbed keeps its notch at 1440 px) and only scrolls when compact; settings rows keep 40 px between name and control and wrap the control only when narrow. No new strings; fmt/clippy/tests green on the union of this wave's heads.
🤖 Agent: follow-ups to #11033/#11044/#11043/#11016 that missed the merges: death protection is one prototype lookup per hand per tick (was a full component map for every armed mob), checked against the component map for patched and stripped stacks. Union check: fmt, clippy -D warnings, 5666 tests green.
🤖 Agent: the interpolation change is checked bit for bit against the old formula over the test's whole range, so no position moves; one multiply fewer per entity per frame. Union check (main + the gate's 9 heads): fmt, clippy -D warnings, 5666 tests green.
🤖 Agent: inflates each region chunk in place instead of through a second buffer; the differential test decodes the same chunks with the old block decoder and compares every section. Union check (main + the gate's 9 heads): fmt, clippy -D warnings, 5666 tests green.
🤖 Agent: Optimize World now upgrades region files in parallel with one sync per file instead of one per chunk (F_FULLFSYNC on macOS was the cost); the 1-vs-4-thread test checks the counts against the fixture's own layout, and a canceled recreate leaves every file untouched. Union check: fmt, clippy -D warnings, 5666 tests green.
🤖 Agent: the launcher keeps each native shader's identity and options across the settings screen rewrite; its parent #10168 is merged, and the union with main passes clippy and tests.
🤖 Agent: client mods read entities and textures through the Host only, no new write path; #10196 conflicts with it in native_client_services.rs and gets main merged after this lands.
🤖 Agent: the invite-code calls go through one frontend() helper with x-mojangauth, only to the Realms frontend URL; builds and tests clean on a union with current main.
PistonMovingBlockEntity.tick pushes entities from each block entity's own
tick, and getCollisionShape reads its progress: the moving blocks that
ticked before it have advanced, the ones after it have not. Horizon
pushed every entity against last tick's progress of all of them, so a
TNT duper's TNT was pushed through a block that was about to move
(#11399). Each piston's collision view now advances right after its own
push.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: native_take_screenshot refuses while another screenshot waits for the frame, so a mod's call can't overwrite F2's or the world icon's pending grab.
🤖 Agent: parity.py starts harnesses with no arguments; these two required --binary/--work-dir and failed parity-main in 0 s. On the desktop both now reach their servers (the Java sides ran); the full runs there timed out only because the desktop's swap was full.
Removing each one and asking the compiler (the workspace's targets and the
server build): most guarded nothing, since `pub` methods of `pub` types already
count as used. The ones native mods need stay, now saying so, as do the fixtures
that share another fixture's source and code whose caller has not landed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: removes only the minecart boxes from the item's collision list; boats, shulkers and a held happy ghast still hold items, as AbstractMinecart doesn't override canBeCollidedWith. entity_collision, held_drop, shulker_space, boat_item_hit pass locally.
🤖 Agent: merged after #11019 and main (hook and doc blocks kept whole, quinn kept beside the new optional crates). Without a mod, PlayerSender::send pays one relaxed WATCHED load and sends the bytes as they were, and worldgen, LOD columns and redstone keep vanilla's values.
arboard 3.7 moves to windows-sys 0.61 (removing 0.60, windows-targets 0.53 and
its eight platform crates). socket2 0.6 and if-addrs 0.15 in horizon-friends
remove socket2 0.5; the launcher's lock also loses windows-sys 0.59.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
#11158 took the player's boarding cooldown out (Player.removeVehicle zeroes it), and
may_board with it, so this branch no longer built once merged with main.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: the check, not the 1.8 rules, was broken: the switch row sat half under the list's edge so the click missed it; verified 7/7 legacy, 9/3.6 modern on the Mac.
🤖 Agent: the fight bots' swing carried a hand byte 26.3's swing packet doesn't have, so every server kicked them on the first hit; it now sends the same Punch as --attack.
🤖 Agent: twelve hook groups land together. Without a mod, the move, teleport and trample hooks are fn pointers over an empty mods() slice, so a player's move costs one indirect call and no allocation; a mod command's permission node falls back to its own level when no provider answers.
Id tables from the vanilla data reports (1.20.6's packet list rebuilt from
the archived wiki page, which names no resources, via 1.21.1's report), with
the renames game_profile, set_carried_item and horse_screen_open. Packet
layouts from minecraft.wiki's archived pages for protocols 766 to 769 and the
release notes: 1.21.5's chunk format and per-entity data, hashed container
clicks, 1.21.2's window ids, teleports, movement flags, input and sea level,
1.21's particles, explosion sound and trades, 1.20.5's strict error handling.
1.21.5 item components (attribute modifier display, equippable shearing)
behind a quirk. A 1.21.1 client gets no recipe book.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: only the slider's step table changes; saved values between steps snap to the nearest step (test covers 3000 → 13/23), so existing options.txt files still load.
🤖 Agent: quinn is optional behind the quic feature with default features off (runtime-tokio + rustls-ring only), so no extra TLS backend lands in Cargo.lock beyond the 7 packages.
Discord Rich Presence, Play with Friends (horizon-friends) and the
anonymous "I'm playing" ping (horizon-telemetry/live) were built
unconditionally, though a server never runs them. Each is now a
default-on feature of horizon-client and horizon-client-lib, so the
normal build is unchanged and the server build (--no-default-features,
horizon-engine) leaves them out: horizon-friends and 13 crypto and
network crates drop from its dependency tree.
Without `friends` the game keeps working: game/friends_off.rs stubs the
host side (no Friends button in the pause menu, as on another server),
no code reads as an invite, and a friend: address is refused as an
invalid address.
Fixes#11539
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Clippy follows rust-version, so crates that move from 1.85 now get its
collapsible let chains, as_chunks and is_multiple_of suggestions. Almost all
from `cargo clippy --fix`, none changes what the code does.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The check failed on main with 1.8 rules that work: the switch row now sits
under the PvP page's HUD rows, the scroll stopped as soon as the row showed
at the list's lower edge, and the click at its centre landed outside the
list, so the rules never turned on (legacy hit 9, not 7). It now scrolls
until the row stops moving and asserts the click flipped it.
The swap and back went out in one tick: since the client sends a slot
change from its next tick, as vanilla does (#10084), that sends nothing,
and the modern hit kept its full charge. The stick is now held 3 ticks.
Mac mini: legacy (7, 7), modern (9, 3.6).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
#11226 clamped scale to 0 before the + 2. Vanilla has no clamp and Java masks the shift
distance, so tools/test_biome_source_parity.py failed at scale -1 and below.
Fixes#11519
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: .github only; the by-hand ARM64 job's launcher --version step now runs from bash as launcher.yml does, and passes on the ARM64 runner (run 42320852405), where the pwsh version failed on any code.
Opening a world saved by an older version runs the converter in
datafix/files.rs (upgrade_world), which rewrote every region file on one
thread. Now:
- upgrade_regions spreads a folder's region files over worker_threads()
workers (all cores but one). Each file is still read, fixed and written
whole by one worker through the same staging folder, so the bytes do not
depend on the thread count. A cancel or an error stops every worker
before its next file; the error returned is the first file's in order.
- A region file without chunks is left as it is instead of being rewritten
as an empty 8 KiB file and synced. The game leaves many of them: 13 303
of the 13 620 overworld files of a 1.21.1 ATM10 test world.
Tests: 4 workers give byte-identical files to 1 and every chunk at the
current version, an empty file stays empty, and a cancel from the first
chunk converts at most one file per worker and leaves the rest old.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Both asserted --reference-only when no --reference-dir was given, so tools/parity.py,
which passes no arguments, failed them before they ran anything.
Fixes#11520
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Members declared 1.85 to 1.95, or nothing. horizon-mods needs 1.95 and sits
under horizon-client-lib, cargo-horizon-mod and the launcher, so 1.95 is the
version the game really needs; the lower numbers promised builds that could
not happen. Set it once in [workspace.package] and inherit it everywhere.
The separately built workspaces (launcher, telemetry server, mod registry,
third_party) cannot inherit it and keep their own.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: one line in docs/hosting.md (Intel Macs have had server builds since the 10-31 Nightly; install.sh already maps Darwin-x86_64); the README half is already on main via bf4657da26.
🤖 Agent: Auto's backend order now reads the machine's arch (IsWow64Process2), so the x64 build on a Snapdragon PC starts on DX12 like the ARM64 one; the ARM64 runner built and started the game on this head. Unverified on real Adreno hardware (asked #7513's reporter).
🤖 Agent: merged main in again (only the test module list in pack_post.rs collided, with #11196's sky_disc_blend_tests). The last parity failure on 4fa3bf699 was command_exec's '/advancement grant @s everything' count (116 vs 115), which fails identically without this PR; this head reruns parity. sizes_differ is a few array reads per pass per frame.
The fight behaviour's swing went out as a raw Punch with a hand byte
26.3's ServerboundSwingPacket doesn't have: every server kicked the bots
on their first hit ("packet 46 was larger than expected: 1 extra bytes").
It now sends the same Punch as --attack.
--crits makes fight bots swing at a sword's full strength (13 ticks) and
jump into every other swing, landing it on the way down: a critical hit,
then a sweep from the ground. tools/perf PvP scenes get crit and sweep
particles from real hits.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
tools/parity.py starts every harness with no arguments, and both scripts
required --binary and --work-dir, so parity-main failed them in 0 s
(argparse usage error) since #10524 and #10266. Like
hostile_sound_categories, they now build the game through net.sides and
pick a work folder under .local/ when none is given.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The harness, probe, Game field, impl file and module counts had drifted
(324 harnesses against 502 on disk). Nothing regenerates prose, so name the
things and let the tree give the numbers. CONTRIBUTING.md's "about 290"
harnesses goes with them.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Follow-up to #10571 (Skr1zzzz). The cache checked obfuscation on the raw
spans, so a plugin hologram sending §k in literal JSON stopped
animating, and an inline head whose skin arrives later stayed stale: the
animated test now runs once on the resolved text, as drawing reads it.
The key also holds lang::bidirectional() as the name tag cache does, and
a display out of view keeps its cached quads instead of dropping them
before the view check.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Follow-up to #10529 (atsauban's responsive wrapping). The Home tab strip
took the full width at every size, so glass::tabbed drew one plate and
lost its notch even at 1440 px: it is full width and scrolls only when
compact. setting_row went from 40 px to 16 px with justify_between for
every row; it keeps 40 px between name and control and wraps the
control under the name (12 px) when the window is too narrow. The
breakpoints are named constants beside WIDE.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
#10129 made the native title bar transparent on macOS but drew the bar only with
glass on, so with glass off the page started under the traffic lights. The bar
is always there: glass, or the side colour as on Windows and Linux.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
#10177 unmaximized before every fullscreen request on every system, and apply()
asked for fullscreen again on each call while the window still reported
maximized. enter_fullscreen keeps the #9984 fix where it applies (winit keeps a
maximized window inside the work area on Windows); the resumed() checks share
one helper.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
#10532 builds a pack on a thread of its own, but dropping PackLoading (a
pack switch, packs off, another dimension) only detached that thread,
which went on for 5 to 50 s; switching back and forth stacked builds.
PackLoading now holds a cancel flag set when it is dropped: PackPost::new
checks it per program and compile() stops handing out jobs, and the
refusal it returns (is_cancelled) is not reported as the pack failing.
A thread that cannot be spawned now turns the pack off for the
dimension instead of retranslating it every frame.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
#10166 landed with its messages as raw strings. Every message a player reads
goes through tr/trf with named placeholders, and every language file gets the
English stand-ins.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Main gave a sky program two more pipelines (its vanilla blends for the
disc and void, and for the stars). Here they are RenderJobs made with the
others, in their own slots, and the draw reads them with get().
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Death protection: one prototype lookup per hand instead of a component
map rebuilt from the stack's patch. update_totem_ready runs for both of
the player's hands and sync_mob_death_protection for every armed mob,
every tick; only a stack that patches the component takes the old path.
- Remote projectiles: the swept bubble-column cells are deduplicated
without a HashSet per projectile per tick.
- Ident::read hands a namespaced identifier back without a second String.
- parked_worlds keeps its doc comment (#11016 put parked_border under it).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
handleUseItemOn sends the use's swing to the player's own client only when
the result is not SwingSource.PREDICTED. Horizon sent every swing back, so
on a slow connection the echo of a placement's swing arrived after the first
half of the client's own swing and swung the arm again (#9887). A placement
and a block's own SUCCESS now count as predicted; the beds, light block,
respawn anchor and sign stay SUCCESS_SERVER and are still sent back.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The check runs every tick for every player; cloning all four stacks each
time allocated their components for nothing.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
BrushItem.calculateHitResult picks entities with blockInteractionRange, not
the entity range: a mob 3 to 4.5 blocks away in front of the block stops it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: the AMP update stage runs the egg's own install script byte for byte (test pins it), so both panels install and SHA-256-check the same way; not imported into a live AMP.
a_flame_crossbow_shoots_burning_arrows (#10488) expected 100 fire ticks and failed with prepared resources. Enchantments.FLAME is Ignite(LevelBasedValue.constant(100.0F)), and Ignite calls igniteForSeconds: 100 seconds, 2000 ticks, as the bow path already gives. The code was right; only the expectation in the test was off.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A client mod that converts text as it's typed (Emoji Type's :code: to emoji)
reads the chat box's text and cursor and writes the result back as an edit,
so completions refresh as for typing. Only what typing lets in reaches the
box. Stacked on native_open_chat.
Part of #10107
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Its first runs failed at "horizon-launcher --version failed": pwsh read no
output from the GUI-subsystem launcher, while launcher.yml's same check, from
bash on the same runner, passes on every main push.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Once the search field had the focus it kept the keyboard, so Enter did nothing and letters filtered
the list with the ring on a row. Moving the focus away now ends typing.
A key that moves nothing shows no ring, a row greyed out while focused does nothing, the wheel stops
behind a resource error as `scroll_at` does, and a description too long for the strip ends in an
ellipsis, with room for one more line on Extras.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A screen now implements settings_shell::Screen, describing its pages, groups,
rows and values, and Shell does the drawing, clicks, scrolling, the controller's
focus and declared buttons, keys and each row's description. OptionsUi reaches
it through one accessor, so the wheel and a stick's scrolling can't part ways.
Horizon Extras is the first screen on it. Its settings do what they did and are
saved the same way. It gains the shell's look and search, and its descriptions
move from tooltips to the strip, which grows for them.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A new crate, horizon-polytone, reads the Polytone resource pack format
clean-room from the public wiki: modifier targets (implicit path id, ids,
tags, regex, required) and the attributes_modifiers layers of biome and
dimension modifiers (groups, REMOVE, values, modifier objects, colors,
colormap references, expressions kept as source). Nothing uses it yet.
Part of #11486
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Packet and registry id tables generated from the vanilla data reports of the
three releases; packet layouts from the public protocol documentation
(minecraft.wiki's archived packet pages for protocols 770, 771 and 772).
Each step has a reader (older server) and a writer (older client) with round
trips; entity data serializers without 1.21.9's additions. The end-to-end
bot test now joins as every release down to 1.21.5.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
es_es.json held 271 of the 894 keys en_us.json now ships, so 623 strings
(70% of the file) fell back to English on Spanish screens: the whole PvP
and HUD page, splitscreen setup and profiles, Horizon extras, the mod
browser, the resource pack browser, friends, the on-screen keyboard,
Replay Studio, the music page, cosmetics and the shader and graphics
descriptions.
This fills the table: 894/894 keys, no missing or extra ones, every
placeholder kept. The four values that were still English (Particle
Culling and its three descriptions) are translated, and the shader page
name now says "shaders" like the rest of the file.
lang.rs also serves es_ar, es_cl, es_ec, es_mx, es_uy and es_ve from this
file.
Co-Authored-By: OpenCode (DeepSeek V4.1 Flash) <[email protected]>
A 1.21.1 model's minecraft:block/chain or item/chain (parent or texture) became
iron_chain in 26.3, so a mod block drawing vanilla's chain baked with the missing
texture. Carried models and block states get the RENAMED_IDS rename data files
already had.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The recipe index reads every content namespace since 31756ebfe, but /recipe's
suggestions still listed only minecraft: ids. List data/<ns>/recipe of every
namespace, as /loot's loot tables already are.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Its rows and buttons only painted, so a pad reached them through the crosshair alone. The D-pad now
walks the recordings to the buttons, the right stick scrolls them, and Done declares its button.
A notch is now one whole row, since nothing clips a row that's partly scrolled off, and the scroll
limit counts the rows that fit, so the last recording can be reached with the mouse too.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Mirror translate's per-release readers as writers (client_session/step_*),
keep sent entities for entity data and 26.1's spectate by UUID, drop the
world_clock registry and split 1.21.11's interact into interact and attack.
End-to-end test joins a bot of each release through the adapter's Session.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The warning only gave their count, so a data pack or content mod author
could not tell which features were skipped or why. Log each one with its
reason, up to 20.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Use a conservative input-pixel guard for the billboard camera frustum
without changing the ordinary model frustum, flame envelope or shader
pack fallback. Keep this PR independent of experience-orb culling.
Retain the separate flame measurements and image controls, with their
original binary provenance and an explicit note about the PR split.
Co-Authored-By: Codex (GPT-6) <[email protected]>
Reuse resolved local model depth when placing dropped-item stack copies.
Skip the unused posed occlusion envelope with entity culling and TAA off,
and retain the original preparation under TAA after measured tail regressions.
Keep the existing cluster API and random sequence, with a regression test,
paired production measurements and explicit rendering-validation limits.
Co-Authored-By: Codex (GPT-6) <[email protected]>
Play in Background, off by default and per player, lets the pad keep driving the game while its
window has no focus, and a game the pad is playing doesn't pause on losing focus.
While several play, the router now plays each player's rumble on that player's pad. Rumble no
longer stops when the window loses focus: nothing asked for that, and it's useful to an AFK player.
Starting or leaving splitscreen builds a new renderer, whose atlas has none of what was drawn into
it at run time. Those two paths only forgot the skins, so the on-screen keyboard lost its letters
and rounded corners, and maps were exposed the same way. All renderer swaps now forget the same
things.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`tools/port_map/animal-1.tsv` had no answer for
`Armadillo.addAdditionalSaveData` or `Armadillo.readAdditionalSaveData`. Both
now name `entity_nbt::mob_to_nbt` / `mob_from_nbt`: the state written as its
`ArmadilloState` name beside `scute_time`, and read back through
`switchToState`, idle when the save names none.
`mob_from_nbt` and `mob_to_nbt` are the pair `Game::load_mob` and
`Game::mob_tree_nbt` go through. An armadillo whose save names
`state: rolling` loads rolling and takes 4.5 of a hit for 10; six ticks into
the roll its shell is out; a save that names no state, or a name that is no
`ArmadilloState`, loads idle and takes the whole 10. Saving a rolling armadillo
writes `rolling` even when the compound it came in with said `idle`, and the
reloaded one has its time in the state restarted, so the shell is not out until
it has been rolling for six ticks again.
Without the state in the pair the loaded armadillo is idle (left: 0, right: 1)
and takes 10 for a 10 hit where it took 4.5 rolled up.
`Armadillo.addAdditionalSaveData` writes the state the armadillo is in beside
its scute timer, and `readAdditionalSaveData` switches to it:
output.store("state", Armadillo.ArmadilloState.CODEC, this.getState());
output.putInt("scute_time", this.scuteTime);
...
this.switchToState(input.<Armadillo.ArmadilloState>read("state", Armadillo.ArmadilloState.CODEC)
.orElse(Armadillo.ArmadilloState.IDLE));
`ArmadilloState.CODEC` is a `StringRepresentable.fromEnum`, so the key holds
`idle`, `rolling`, `scared` or `unrolling`; `switchToState` sets the synched
state, and `onSyncedDataUpdated` restarts `inStateTicks` when it changes.
Horizon models the same state (`extra.armadillo.state`, the value
`Armadillo.getState` is synched with) and wrote only `scute_time`, so an
armadillo that unloaded while rolled up came back idle: a hit for 10 went from
4.5 back to 10.0 (`Armadillo.hurtServer`: a ball takes half of the damage past
the first point), `hiding_in_shell` went false so the model lost its shell, and a
brush or a food click was answered by an idle armadillo instead of a ball.
`state` is now written from the live state - `idle` for one that is not rolled
up, as vanilla writes it unconditionally - and read back through `set_state`,
which is `switchToState` plus `onSyncedDataUpdated`: a save that names no state
(or names one that is not an `ArmadilloState`) loads as idle, and the time in
the state starts again at 0.
The brewing suite stopped at recipe matching, assemble and the slot filters;
nothing advanced the block entity. The tick family now runs the real
BrewingStandBlockEntity.serverTick and BrewingStand::tick for 0..8001 ticks
and compares slots, brew time, total brew time, fuel, total fuel, speed,
bottle bits, ingredient and the brew-event count. It covers the 400-tick brew
divided by speed_multiplier and its ceiling (2, 3, 0.3 and 0), blaze powder's
twenty uses and speed 1, fuel loaded with nothing to brew, splash and
lingering outputs, ingredient shrink and mismatch, finishing without fuel and
twenty brews from one blaze powder. --no-tick leaves the tick corpus out.
2860/2860 cases match the pinned client.
Vanilla reads each level's DimensionType and its attributes (fast lava, water
evaporates, the day timeline, infiniburn, sky light) rather than naming the
Overworld, Nether and End, so a datapack dimension using the Nether's type
behaves as the Nether. horizon-world block behaviour now does the same through
LevelAccess::dimension_type(); sites vanilla keys on the level compare LevelKey.
No behaviour change for the three built-in levels.
Part of #11282
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Code that matches on the Overworld, the Nether or the End should read the
level's dimension type, as vanilla does. Add DimensionFacts: the
DimensionType fields and gameplay attributes Horizon reads, the four
vanilla types built in code, and from_json for packs. A test checks the
built-in table against the prepared 26.3 dimension_type and timeline tag
files.
No callers yet; nothing changes for players, saves or packets.
Part of #11282.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Vanilla makes one level per entry of the dimension registry and keys each
by its ResourceKey<Level>. Add LevelKey: the three built-in levels as
constants, any other id validated as Identifier.parse does and interned,
plus builtin_or_overworld for today's lossy lookups from saves, guests
and packets.
No callers yet; nothing changes for players, saves or packets.
Part of #11282.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
player-expect is refused as "the server is full" once the places the
player limit counts and those of the other expected players reach
max-players: each expectation holds a place, so two proxies sending
players at once cannot fill a server past its limit. As at login, an op
who bypasses the limit is never refused, and a renewal holds no second
place.
Once a stop has begun (/stop from the console, a player or the control
endpoint, or a stop signal), player-expect, player-import and
player-confirm are refused as "the server is stopping": a player must not
arrive on a server that is about to close. A retry after the commit is
still answered, and export, release and drop stay open, since they are how
players leave. transfer-status says whether the server is stopping.
Refs #7484
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Use the existing unshared audio path for level event 3007 and cover actual event capture, local playback, and random continuation.
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
Reuse bounded exact local geometry and normal rotations for GPU mob posing.
Keep the original walk and transform helpers when TAA is enabled, with the
policy refreshed every frame from the existing anti-aliasing setting.
Add byte-exact churn and bypass-switching coverage and measured crowd results.
Co-Authored-By: Codex (GPT-6) <[email protected]>
No parity suite reached the brewing recipes: brewing.rs's Brewing::find,
PotionIngredient and the BrewingStandMenu slot filters had no differential
corpus. A new suite loads the vanilla data pack's real RecipeManager and
compares every bundled BrewingRecipe's assemble with Horizon's find output,
the brewing_input and brewing_reagent property sets against is_bottle and
is_reagent, and the fuel slot's BrewingFuel against brewing_fuel. Cases cover
every recipe, every potion item and potion id against every reagent, the whole
item registry as a slot candidate, and non-matching pairs. --baseline keeps
only the exact recipe pairs and reproduces the corpus before the cross-product.
2811/2811 cases match the pinned client. The brewing-stand tick, brew time,
fuel use and remainders remain pending.
Agents had to open issues one by one to see if they were taken, and the rule
did not mention Discord claims (Kira's `in progress` label). Give them the
ready-made search and name both claim signals.
Fixes#11367
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Portals and an empty lectern showed Open, and a block that keeps the click and does nothing
(an empty flower pot or bookshelf slot, a pressed button) showed Use, or the held item's own
row. An ender chest said Use and now says Open.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
LivingEntity.getDamageAfterMagicAbsorb applies Resistance, returns 0 when
nothing is left, returns the damage as is for #bypasses_enchantments (the
warden's sonic_boom), and only then the enchantment protection. Horizon's
Mob::hurt_server ran the protection first and never read the tag, so a
zombie in a Protection IV chestplate kept 1.6 of a 10-point sonic boom.
DamageKind::bypasses_enchantments (SonicBoom, or a NamedDamage with the
new BYPASSES_ENCHANTMENTS tag) gates the protection, which now follows
Resistance as vanilla orders them.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
A server seating the player on a block display (sit mods, NPC plugins) took the vehicle for a minecart; none was
found, so riding was dropped and the player fell into the slab under the seat (y -61.0 where the server keeps
-60.1). The seat is the vehicle's riding position less the player's vehicle attachment (0.6, scaled).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The firework tooltip corpus sampled flight durations 0-3, 7 and 255 and runs
of at most six equal explosions, so Fireworks.addToTooltip's run count above
six and the rest of the unsigned-byte flight range were never compared. A
deterministic edge corpus now adds all 256 flight durations, runs of 0..256
equal explosions in every shape, colour and fade lists up to 32 entries
(custom, negative and duplicate colours), equality-sensitive runs (same shape
with different colours), both components on one stack, firework components on
non-rocket/non-star items, and every combination of hidden components.
--no-edges writes the random corpus exactly as before.
3331/3331 cases match the pinned client.
AbstractFurnaceBlockEntity.setItem resets the time; without the early reset a hopper-fed furnace would smelt its first item in one tick. Review feedback on #10170.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ServerPlayer.doTick runs super.tick() (Player.aiStep's tickRegeneration)
and foodData.tick for every player but a spectator touching an unloaded
chunk. Horizon's guest tick returned for a spectator before the damage
clock, peaceful_regeneration and food_tick, so health, saturation and
exhaustion froze while watching and were still there on the way back to
survival.
A spectator whose box, inflated by one block, lies in loaded chunks
(Entity.touchingUnloadedChunk) now runs those three; hazards, travel
and movement statistics stay skipped. food_tick already counts a
spectator as invulnerable, so starvation hurts none.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
`ServerExplosion.hurtEntities` damages every entity in reach, and
`ItemEntity.hurtServer` writes the result back:
this.health = (int)((float)this.health - amount);
if (this.health <= 0) { this.getItem().onDestroyed(this); this.discard(); }
Horizon's item branch only asked whether the damage reached 5 and destroyed the
item, so an item that lived through a blast kept all 5 points of health: a
power-5 bed blast that leaves vanilla's item at 1 left Horizon's at 5.
The damage is now subtracted in f32 and truncated after the subtraction, as the
cast does, and the item is destroyed at zero health as before. The nether star
stays immune through `ItemStack.canBeHurtBy`'s explosion exception, and a blast
whose source is an entity with `mobGriefing` off never reaches the branch
(`items_ignore`).
A dropped item starts with 5 health and `ItemEntity.hurtServer` takes the
blast's damage off it, so a survivor's `Health` says how much of the blast it
took. The pinned compound the ring already reads carries it, so the compared
line grows one fact:
data push -0.03 -0.00 -0.05 health 1 keep 0.9800
Red on main: Horizon reports `health 5` where vanilla reports `1`.
BoneMealItem.addGrowthParticles places a NEIGHBOR_SPREADER's happy
villager sparks at BonemealableBlock.getParticlePos, which is the block
above for grass, nylium, netherrack and moss; a GROWER keeps the block
itself, rooted dirt and mangrove leaves the one below. Horizon used the
bone-mealed block for spreaders too, a block lower than vanilla.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Player.actuallyHurt awards DAMAGE_TAKEN as Math.round(dmg * 10.0F) only
while dmg < 3.4028235E37F. Horizon awarded every hurt with Rust's round,
so /damage @s 3.4e38 added a saturated i32::MAX where vanilla adds
nothing. A damage_taken_award helper applies the threshold and
jmath::round_f32 (Math.round(float): ties up, saturating).
Co-Authored-By: Claude Fable 5.1 <[email protected]>
A modded 1.21.11 server's own entity type (Sit's sit:entity_sit is 157) kept its number, which in 26.3 is
the zombie villager: every sitting player showed one under them. Chunks already dropped such block entities;
their updates now go too.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The knee eases HDR lighting's gain into the peak; light written as light (alpha 1) has no
gain, and on a float frame the knee would compress it before the clamp to the peak. On a
unorm frame such pixels never reached the knee, so nothing changes there.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Marked pixels (light sources, the sun, HDR lighting) clip at white before their lift, as on
a unorm frame; only unmarked ones show light over 1, which is how shaders will write light.
Without that, the float frame tinted the sun and its halo instead of lifting them white.
The native world-text pipeline targeted the swapchain's 8-bit format, which a 16-bit or
float frame's pass refuses.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Creeper.readAdditionalSaveData reads ExplosionRadius as a byte (default
3), addAdditionalSaveData writes it back, and explodeCreeper blows with
it, twice it when powered. Horizon used a fixed 3 or 6 and never read the
key, so a summoned creeper with ExplosionRadius:12b blew with 3.
Extra.explosion_radius holds it (3 from Mob::new), entity_nbt reads any
numeric tag narrowed to a byte and writes the byte, and creeper_tick
scales it by the powered multiplier.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
The survivor the harness gained at the edge of the blast is read through the
item's own tick counter. A push on an item decays every tick
(`Entity.getAirDrag`, 0.98), and the two servers do not advance the same number
of game ticks between the same commands: at three script points of one case the
vanilla survivor was 31, 33 and 48 ticks old and Horizon's 38, 41 and 75, so a
`Motion` read there measures the pacing rather than the blast.
The edge item's `Age` is now read before the blast, and its whole NBT (`Age`,
`Pos` and `Motion` from one snapshot) at two pinned ages after it. The case
compares the push carried back over the ticks since the blast, to two decimals
because a tick of slack between the blast and the read is worth 2%, and the
decay the two snapshots show per tick, to four, which is `getAirDrag`'s 0.98.
Both agree with vanilla's in all twelve cases.
Cache each baked face's shape for both collision-fullness states and pack
its own UVs once, preserving the original floating-point operation order.
Address neighboring blocks, AO samples and fluid samples from padded cell
indices instead of recomputing coordinate indices for each lookup.
Add asset-independent checks against coordinate lookups for padded steps
and all AO sample tables, plus literal shape bounds and packed UV values.
Co-Authored-By: pi coding agent (DeepSeek-V4.1-Flash) <[email protected]>
Co-Authored-By: Codex (gpt-6.1-sol) <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WZzq8KX3vDMZy53y1vXqeT
HDR design step 3, the frame and the display pass (docs/design/hdr-linear-light.md):
with the switch, HDR on and no shader pack, the renderer draws into Rgba16Float where
the surface offers HDR output and the device draws it, whatever Smooth Gradients is.
0.1 and shaders/ packs keep today's frame; HDR off is untouched.
The HDR pass keeps light over 1 where the world shows, up to the peak, and clips it at
white under the HUD and menus and with HDR Lighting off, as a unorm frame does. On
today's frames, which hold nothing over 1, the pass computes the same values.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
SnbtGrammar accepts one trailing comma before the closing brace or
bracket; a comma alone and two in a row stay errors. Both readers take it:
horizon-world's parse_snbt (NBT arguments, item components) and the text
component reader (tellraw and other component arguments).
Fixes#11285
Co-Authored-By: Claude Opus 5.5 <[email protected]>
LocalPlayer.updateAutoJump reads Level.getCollisions, which returns the
collidable entities' boxes (EntityGetter.getEntityCollisions, query
inflated by 1e-7) before the block shapes. Horizon's auto-jump only saw
blocks, so walking into a boat, a shulker or a happy ghast never
auto-jumped onto it.
Co-Authored-By: Codex (gpt-6.1-terra) <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ThrownEnderpearl.onHit calls resetCurrentImpulseContext after the fall
distance reset, so a wind charge's or mace smash's impact height no
longer limits the next fall.
Co-Authored-By: Codex (gpt-6.1-terra) <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Record the actual block entity or its existing pure save projection in a local Pending, then attach existing component copies to the unchanged block drops. Keep source blocks and loot replacement policy unchanged.
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
Document native_client_entities with its new fields,
native_client_items, native_client_texture and skins by UUID, which
entities the list leaves out, and an example loop.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Client mods such as radars, name tags and spectator ESP need the
entities the client shows, with what they look like, without reaching
into Game's internals. API 411's native_client_entities had ids,
positions and player profiles, but no type, babyhood or texture, and no
dropped items.
- NativeClientEntity gains kind, yaw, baby, spectator and the texture
its body draws with (mobs::render::body), plus position_at, the
renderer's interpolation. The struct is non_exhaustive, so existing
mods keep building.
- native_client_items lists the dropped items the client shows.
native_item_entities stays server-only, so server logic never acts on
a client's copies.
- native_client_texture returns TexturePixels by the name texture and
NativeClientProfile::skin give: a downloaded skin from memory, else
native_client_resources().texture(name).
Pending fragment client_entity_list. Tests: position_at between two
ticks, and a client fed the server's play packets reading a zombie, a
calf, a spectating player with its default skin and a dropped item
through Host, plus texture pixels and a refused path out of the packs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Add the `native_client_resources()` helper. It returns `ClientResources`
over the resource tree the game loaded, which is vanilla's with the
resource packs turned on and the content mods' files over it. A client
mod fetches it on the game thread and can then use it on any thread.
`read(path)` returns a file's bytes for a plain path under `assets/`,
such as `assets/minecraft/textures/entity/cow/cow_temperate.png`.
`texture(name)` decodes a texture by name (`block/stone`,
`entity/cow/cow_temperate`) into `TexturePixels`. Paths outside
`assets/` return none, so Horizon's own prepared tables stay private.
Mods stop reading `options.resources` with `atlas_sources::texture_path`
and `prepared_files::read`. The tests build a folder resource pack over
a fake vanilla tree and check that the pack's texture wins, and that
paths outside `assets/` are refused.
Co-Authored-By: Claude Opus 5.5 <[邮箱]>
Add the `native_block_states()` helper. It returns `BlockStates`, a copy
of every block state keyed by its `BlockStateId`. A client mod fetches it
on the game thread and can then use it on any thread.
Each state's `BlockStateFacts` holds the block name, air, invisible,
`FluidState`, waterlogged, light emission, outline boxes, map colour and
whether the state can collide at all. `sprites(id)` returns what the
state's model draws on each side, as `native_block_sprites` does for a
block name. `texture(sprite)` names the texture behind a sprite, the
inverse of `native_texture_sprites`. `mod_api` now also exports
`FluidKind`, the type `FluidState::kind` returns.
The table copies the game's internal facts into stable public types.
Client mods that draw blocks their own way, such as minimaps, world maps
and radars, no longer need `res.facts()`, `res.collision()` or
`block_sprites::Lookup`, which change from commit to commit.
Tests run on real prepared resources (ignored without
HORIZON_TEST_RESOURCES): grass block facts, air and light, slab
outlines, and a grass block's face texture names.
Co-Authored-By: Claude Opus 5.5 <[邮箱]>
A mooshroom's mushrooms, an iron golem's poppy, a snow golem's pumpkin and a
copper golem's antenna block are submitted through BlockModelRenderState.submit,
whose block-item sheets (ITEM_CUTOUT, ITEM_TRANSLUCENT) cull back faces. Only the
enderman's carried block culled, so the coplanar faces of the other layers' cross
models z-fought as the mob moved (#5301).
The cull now goes by each quad's winding, as the GPU's does, rather than the baked
facing, which is the nearest axis and 45 degrees off for a cross model's planes.
Fixes#5301
Without a shader pack the rain also pulls the fog in
(AtmosphericFogEnvironment's rain fog), which eases toward the rain only in
tick_particles, and a frozen client skips that. With the rain at its level,
a frozen scene showed full rain but no rain fog. Under HORIZON_STILL a frozen
client now puts the rain fog at its target, from the same inputs the eased
path reads; the easing itself is unchanged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Root cause: #9335 follow-ups from #8733 left passenger rotation after /tp covered only by parity corpora (tools/passenger_teleport_corpus.py), which cannot run without vanilla data. The Game-level rider test exercised offset carry (sub-item 1) but not the rider keeping its angle to the vehicle through absolute and relative turns (sub-item 2).
What changed: extend teleport_rider_tests::a_vehicle_carries_its_riders_and_a_rider_leaves_its_vehicle with a second horse/villager pair: absolute turn 0->40 leaves rider at 70 (30-degree offset kept), relative turn to 50 shifts rider 70->80, and facing (turn_riders=false) leaves rider at 80. No runtime change; the teleport/passenger writers already implement Entity.teleportSameDimension/calculatePassengerTransition via vehicle_teleport::passenger_rotation.
Vanilla reference: 26.3 Entity.calculatePassengerTransition, PositionMoveRotation.calculateAbsolute, Entity.setXRot (pitch clamp).
Refs #9335
tools/test_enchanting_parity.py always built a fresh ItemStack for its offer
corpus, so EnchantmentMenu.slotsChanged's ItemStack.isEnchantable gate (the
enchantable component and an empty minecraft:enchantments) was never
exercised, and the oracle skipped the gate entirely. Fixture lines now carry
damage and a pre-existing enchantments entry; the oracle applies the gate and
the probe builds the same stack. The default corpus is unchanged and
--no-components reproduces it.
68992/68992 enchanting cases match.
A crystal's spin and bob start from a random draw. Which crystal gets which
draw, and a server's entity ids that seed a client's copies, follow the order
chunks load in, so crystals stood at different angles in each launch of a
frozen scene. Under HORIZON_STILL the start now comes from the position; the
draw is still made, and normal play is unchanged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The rain and thunder levels fade 0.01 a tick and stood still in a frozen
world, so a scene that wanted rain had to step the world about a hundred
ticks, and its mobs walked differently in each launch. Under HORIZON_STILL a
frozen world now holds both levels at their targets, as STILL already does
for a shader pack's smoothed values: /weather rain shows full rain with no
step. A running or stepping world, and normal play, fade them as before.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The hand's quads from a translucent sheet (stained glass, ice...) go after the opaque ones in
its buffer and draw after deferred with gbuffers_hand_water (or its fallbacks: gbuffers_hand,
textured_lit, textured, basic), blended, as translucent terrain does; through gbuffers_hand
they came out solid.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Reuse the admitted modern SNBT decode contract for merge and literal modify.
Add actual Game storage and command-oracle regressions for typed values and
no-change copy callbacks.
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
tools/test_use_item_on_parity.py never held an axe, hoe or shovel against a
block the minecraft:block_transformer registry names, so game.rs's
use_block_transformer was never exercised. It now compares log stripping,
copper scraping/waxing, copper doors and trapdoors, double copper chests and
soil tilling, which found two differences from vanilla:
- rooted dirt's hanging roots must use Block.popResourceFromFace, on the
clicked face with the face's own motion, and drop before the block changes;
- scraping or waxing a double copper chest must send a second block_change
game event and the same particle at the chest's other half.
The VanillaDispenserOracle stand-in player also swallowed the item use start
vibration LivingEntity.startUsingItem posts; it now posts it too.
5200/5200 cases match on the new corpus.
Gui.setScreen runs KeyMapping.releaseAll for every screen and
Minecraft.tick skips handleKeybinds while one is up, books, the sign
editor, dialogs and prompts included. Horizon keeps Screen::Playing under
those, and its release-all (track_toggle_screens) and the client input
gate (remote_handle_input) only knew the server dialog, while the book's
and sign's mouse arms eat both edges of a click: a dig held into a book
went on after the book closed, until the next click.
gameplay_has_keyboard now takes "a screen is over the world"
(screen_over_playing) in place of the dialog alone, and remote_handle_input
is not ready while one is up.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Adopt the bounded String input before duplicate removal in the two Mob NBT load paths. Keep the canonical retained state visible to real tag commands and saves, and pin cap, duplicate, scalar and live rewrite behavior against Java.
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
LocalPlayer.getRopeHoldPosition eases the lead's hand end from the view
to yRotO/xRotO by half the partial tick. Entity.turn moves yRotO and xRotO
by the mouse's delta too, and AbstractBoat.clampRotation moves yRotO with
the clamp, so only what the tick turned (a boat's deltaRotation) eases in.
Horizon's rope_rot_old was a once-a-tick snapshot the turn never moved:
while the mouse moved, the hand end lagged by half the turn since the
tick began and snapped back at the next tick, 20 times a second.
turn_player now moves rope_rot_old through the same Rotation::turn as the
view, and both boat clamps (the turn's and the tick's) move it with the
view. A test pins that a mouse turn leaves no gap, a boat turn eases, and
xRotO clamps at 90.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
`HappyGhast.addAdditionalSaveData` writes `still_timeout`, the timer
`still_tick` keeps in `counter`; `readAdditionalSaveData` reads it back and
a save without it is 0.
0, 7 and 10 through `mob_to_nbt` and `mob_from_nbt`: the Int vanilla writes,
the timer back on the mob, and `on_still_timeout` following it. Without the
writer the saved tag has no `still_timeout` (the test fails on `left: None,
right: Some(0)`).
`HappyGhast.addAdditionalSaveData` writes `still_timeout`, the
`serverStillTimeout` behind `staysStill`/`isOnStillTimeout`, and
`readAdditionalSaveData` reads it back:
protected void addAdditionalSaveData(ValueOutput out) {
super.addAdditionalSaveData(out);
out.putInt("still_timeout", this.serverStillTimeout);
}
Horizon keeps that timer in `Mob.extra.counter` (`happy_ghast::still_tick`
counts it down after the first 60 ticks, `player_above` refreshes it to 10)
but neither `mob_to_nbt` nor `mob_from_nbt` touched it, so a ghast saved
while it was held still came back with 0. For the ticks vanilla restores, a
dropped item resting on its back had no box to rest on
(`EntityGetter.getEntityCollisions` takes a happy ghast only while
`isOnStillTimeout`), and a rider had no control (`getControllingPassenger`
asks `isOnStillTimeout`).
ByteBufCodecs.holder in vanilla 26.3 reads a VarInt and, when it isn't 0,
looks the entry up with `byIdOrThrow(id - 1)`, which no negative id passes.
skip.rs's `holder` accepted any nonzero id, so a slot whose kinetic_weapon
sound holder was negative was kept as a raw component, and hashing it for an
inventory click computed `id - 1` on i32::MIN: a panic with overflow checks
on, a wrapped id otherwise.
ByteBufCodecs.holderSet reads the size plus one and builds
`new ArrayList(size)`, which refuses a negative size. `holder_set` computed
`n - 1` before checking the sign, the same overflow for i32::MIN.
Both readers now refuse a negative value with a protocol error, so every
component that reads a holder or a holder set gets the same boundary.
Valid direct and registry holders read as before (their vanilla hashes in
hash/raw.rs's tests are unchanged).
Fixes#11222
Co-Authored-By: Claude Opus 5.5 <[email protected]>
LivingEntityRenderer.isShaking is isFullyFrozen (DATA_TICKS_FROZEN at or
over 140), and setupRotations wobbles the body while it is. Mobs and
other players already shiver, but the window's own model, rebuilt each
tick by tick_player_model, never got the player's frozen ticks, and the
window ignored the DATA_TICKS_FROZEN its server sends it, so a player in
powder snow stood still in third person.
The own model takes ticks_frozen from the player's body, and own entity
data index 7 sets it on the window's player as LocalPlayer reads it.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Guard against negative scale values producing excessive shift
distances in CheckerboardBiomeSource::noise_biome by clamping
scale to a non-negative lower bound before adding the +2 offset.
This keeps the shift distance within a sane range and avoids
surprising behavior for out-of-range runtime scale inputs.
ServerGamePacketListenerImpl handles a client's packets in order: the
RELEASE_USE_ITEM a client sends from handleKeybinds comes before the move
LocalPlayer.tick sends in the same client tick, so the server shoots from
the position of the move it handled the tick before, one tick of strafe
behind the camera. Hub::process_inbox applies moves at once and hands a
PlayerAction to the game for after the pass; it held a queued move behind
a dig, UseItem, UseItemOn and Interact, not behind the release, the drops
or the stab. The move ran first, and the arrow left a tick ahead of the
camera: strafing left, it seemed to come from the left instead of the
right.
Every PlayerAction now holds the queued move for the next tick. A test
drains a release followed by a move and checks the game gets the release
alone with the guest's position and known movement unchanged.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Entity.removePassenger sets the leaving passenger's boardingCooldown to
60, but Player.removeVehicle overrides Entity.removeVehicle and zeroes it
right after super.removeVehicle(), so a player never waits to board again;
only mobs do. #8733 (from #8586, which missed that override) gave the host
and every guest the 60 ticks, checked by may_board before boarding a boat,
cart, cushion, ghast, llama, mount or nautilus: the delay the issue
reports.
The player's cooldown goes: the boarding_cooldown and riding_at_tick
fields of Game and Guest, their swap for a guest's turn, the per-tick
count-down and may_board at its eight call sites. Each interaction still
refuses a sneaking player, the other half of Entity.canRide. Mobs keep
extra.boarding_cooldown.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
16.666667 steps a 60 fps clock. STEP_MS is a finite number more than 0,
and the reply gives it back as given.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A Horizon pack's hot reload could rebuild the pack under the same name
mid-sequence, unseen by the scene check: it now waits for the reply. A
sequence doesn't start, or ends, while the level-loading screen shows,
which a new world keeps a moment after it is ready. The 30 s limit is on
the frames before the last: the last frame's PNG waits as screenshot does.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
screenshot-sequence FRAMES STEP_MS PATH, under HORIZON_STILL with the world
frozen, makes the running pack start over and draws FRAMES frames on a
scripted clock: frame n is n frames and (n - 1) x STEP_MS milliseconds in,
STEP_MS after the frame before (0 on frame 1). The last is written as
screenshot PATH writes it, its -hdr.png too under HDR output, and the reply
names the pack and the last frame's clock. The same build, GPU and driver
write the same bytes every run, for shaders/ packs, Horizon's own and none.
Sequence frames are submitted inline and waited for on the GPU, so each
reads the centre depth the one before left, or the sequence fails; a frame
the surface refused is drawn again. It refuses to start with far terrain,
TAA, frame generation, chunk fade or the shadow cache on, refuses commands
that would change the scene while it runs, and ends with an error when the
camera, world, settings, pack or window size change. cancel stops it, quit
still quits. Documented in agent-control.md's STILL section, shaders.md and
api/shaders.md.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Since smooth() keeps going through teleports, a pack remembers whether it
has drawn a frame of the world. A reset pack now forgets that too, as a
pack just loaded has seen none.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A pass that reads one of the pack's targets ([[post]] output) before the
pass that writes it got last frame's picture after a reset; a pack just
made reads it blank. The reset now drops the targets and their bind
groups, made again blank on the next frame; the pipelines stay.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Renderer::reset_shader_pack_history makes the next frame draw as a freshly
loaded pack's first, for either kind. A shaders/ pack's buffers, images,
storage buffers and shadow maps are made again when next drawn, its passes
get their declared uniform values back, and the frame uniforms, custom
uniforms and centre depth forget earlier frames; its programs stay. A
Horizon pack's post history goes black and its eased values, wetness and
frame count restart. Without a pack it does nothing. Nothing calls it yet.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A 60 fps clock steps 16.666667 ms a frame, which a whole number of
milliseconds can't give. FrameTiming::nth takes the step as f64.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
PackInputs::frame_timing gives the frame number, the seconds since frame 1
and the seconds since the frame before. Iris-format packs take it for
frameCounter, frameTimeCounter and frameTime and smooth wetness,
eyeBrightnessSmooth and centerDepthSmooth by its delta; custom uniforms
start over on its frame 1 and carry their state after. Horizon packs take
it for time, frame_index and delta_time, ease by it, and do not treat a
slow frame as a gap. Nothing sets it yet.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
LivingEntity.addAdditionalSaveData writes FallFlying as isFallFlying(), and
readAdditionalSaveData sets shared flag 7 from it, so a player who saves
mid-glide resumes the glide on the next load. Horizon wrote a fixed
FallFlying of 0 and never read it back: the player came back standing in
mid-air.
Level now keeps fall_flying, read from any numeric FallFlying tag (as
getBooleanOr does) and written from the guest's glide; playerdata's store
and apply carry it between the Guest and the file. The server's next
sync_player_state sends the 0x80 shared flag to the client, and
updateFallFlying's port ends the glide if it can't go on.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Entity.shouldBeSaved is true for every projectile in 26.3, and
WitherSkull adds `dangerous` on top of the hurting projectile's
fields. Horizon listed WitherSkull as not saved, so a skull still in
the air when its chunk unloaded was gone when the chunk came back,
and a blue skull came back as nothing.
Write and read the skull the same way as the fireballs: `dangerous`
as a byte (absent means a black skull), and no `Fire` since
WitherSkull.shouldBurn is false. The fire rule now keys on the
fireballs proper (is_item_fireball) instead of "not blast-immune",
which would have lit a saved skull.
Co-Authored-By: Grok <grok-4.7> <[email protected]>
Adds the Nord palette (polar night, snow storm, frost) as a launcher theme with its own dark and light tokens and sky art. Palette themes draw the square Blocks look: no glass, no rounded corners. The night tokens are square everywhere, and the Blocks status dots and controls follow. Blocks and the light glass look draw the baked sky, so resizing no longer rebakes the scene per frame and stays responsive. Settings gains the launcher-theme picker in the Blocks look. Nord now shows the sky instead of the scene's pale halo.
The first install on Windows now checks SHA256SUMS.sig (a Nightly's
SHA256SUMS.v2.sig, for the version its title names) against the release
key, then the archive against SHA256SUMS, as install.sh does. OpenSSL is
looked for on the PATH and in Git for Windows. Without OpenSSL 3 it says
so and keeps the GitHub SHA-256 check.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Post the existing damage event for accepted health-changing mob command hits so a same-tick teleport cannot move its origin. Preserve the existing duplicate marker and reinforcement handling; pin the real command/listener route against Java.
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
tools/test_block_removal_parity.py only ever mined as a survival player:
every mine case ran ServerPlayerGameMode.destroyBlock in SURVIVAL, so the
creative branch (Player.preventsBlockDrops, no mined statistic, no tool
wear) was never compared even though game.rs::break_block_as_player splits
on it. Half the mine cases now run in creative, with a tool that can destroy
blocks in creative (a sword and a mace cannot, which the creative-mining
predicate corpus already owns).
VanillaDispenserOracle's Miner gains a creative flag (isCreative and
hasInfiniteMaterials) and the real CREATIVE game mode; the game's dispense
fixture sets the same mode for the case. --survival-only reproduces the
corpus as it was before this change.
4000/4000 cases match on the new corpus.
The overlay formatted its lines and laid out its glyphs every frame. It now
paints at most 20 times a second and copies those vertices in between, and
repaints at once for another window size, GUI scale or font epoch.
- The refresh rate is read at most once a second: on Windows, winit calls
EnumDisplaySettingsExW each time. The Window line uses the surface size.
- Passes are timed only while the overlay lists their times, at most 10 frames
a second. gpu-passes and HORIZON_SHADER_PACK_TIMING keep every sample.
- The CPU name is read in the background when the window opens: on Windows it
starts reg.exe, which the first F3 frame waited for.
- DebugEntries::any_enabled and status no longer build a String per entry; the
game asks every frame, F3 closed or not.
- Closing the overlay stops the server thread computing its lines each tick.
- The Mem, Allocation rate and Allocated lines now show on Windows.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Horizon took shadowDistance, sunPathRotation, shadowIntervalSize,
shadowNearPlane and shadowFarPlane from a program's vertex shader too, and
read nothing of shadow.fsh but its formats when the pack has no shadow
pass. Iris 1.11.7, with probe packs of ours, ignored those five declared
only in final.vsh, gbuffers_textured.vsh or shadow.vsh (its shadow
projection, shadow camera and sun stayed at the defaults), and applied
shadowDistance and sunPathRotation declared only in shadow.fsh of a pack
whose shadow buffers nothing read.
Part of #10490
Co-Authored-By: Claude Opus 5.5 <[email protected]>
gbuffers_skybasic drew every part of the sky with the translucent blend.
Vanilla 26.3 draws the sky disc and the dark disc with RenderPipelines.SKY
(ColorTargetState.DEFAULT, no blending), the sunset fan translucent and
the stars with RenderPipelines.STARS (BlendFunction.OVERLAY: colour
SRC_ALPHA ONE, alpha ONE ZERO). Iris 1.11.7 drew them so with probe packs
of ours: the sky disc kept exactly what the program wrote, (0.3, 0.6,
0.9, 0.25) in buffers of four formats, and the stars added 0.25 x 0.5 to
the disc's 1.0 and left their own alpha 0.5. Without a
blend.gbuffers_skybasic of the pack's own, the disc and the void, and
the stars, now draw with pipelines of the same program that blend so,
keeping a blend.gbuffers_skybasic.<buffer> the pack sets (Iris blended
the disc into that buffer only).
Part of #10490
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A program writing a float vec4 into an integer colortex (RGBA32UI...) was
refused: wgpu has no float output into an integer target, so the whole
program, and every buffer it writes, was lost. Iris 1.11.7 drew such a
program with a probe pack of ours: its other buffers kept what it wrote,
and the integer buffer read the floats' bits back (GL leaves that value
undefined; these are NVIDIA 615.78.08's). The output now writes the
float's bits as the buffer's integer type.
Part of #10490
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Horizon always made its default noise texture 256 wide. Iris 1.11.7
made it the size a pack's `const int noiseTextureResolution` gives, when
declared in a fragment shader (composite, deferred, gbuffers, final, or
shadow.fsh with no shadow pass), and ignored it in a vertex shader, in
probe packs of ours. The setting is now read like a colortex format.
Part of #10490
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`.length()` of a runtime-sized array in a named buffer block made naga's
validator refuse the program: its GLSL frontend loads the array and takes
the length of the loaded value. After parsing, the length is now taken of
the array's pointer. Iris 1.11.7 ran such a program in a probe pack of
ours and read the buffer's length (bufferObject size / stride).
Part of #10490
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Horizon decided whether to run the shadow pass from the samplers of the
programs' vertex and fragment stages only. A pack whose only reader of
shadowtex0 or shadowcolor0 was a compute stage (prepare.csh alone, or a
composite.csh beside a composite.fsh that reads none) got an empty map
(1.0), while Iris 1.11.7 drew it for probe packs of ours (0.3956 depth,
0.251 shadowcolor0 at our noon scene). Compute stages now count as the
other stages do.
Part of #10490
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Horizon made an R11F_G11F_B10F custom image as RGBA16F, then refused
every program that declared it layout(r11f_g11f_b10f), since the shader's
storage format no longer matched the texture. Iris 1.11.7 stored into such
an image and read it back, with a probe pack of ours: (0.25, 1.5, 3.0)
read back as written, alpha 1, also filtered between texels. The image is
now Rg11b10Ufloat where the device stores into that format; elsewhere it
stays the RGBA16F stand-in, and the shader's image declares that format.
Part of #10490
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The default blend of gbuffers_skytextured added the sun's alpha over the
sky's (One, OneMinusSrcAlpha). Vanilla 26.3 draws the sun and moon with
RenderPipelines.CELESTIAL, BlendFunction.OVERLAY: colour (SRC_ALPHA, ONE),
alpha (ONE, ZERO). Iris 1.11.7 left the sun's own alpha (2.0) in an
RGBA32F buffer the sky had written 1.0 to, with a probe pack of ours;
Horizon left 1.0.
Part of #10490
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Thank you for adding Thai to the launcher in #8306. We were really
happy to see it. When we used it, though, many lines were hard to
understand for us as Thai players. The translation looks
machine-made, and many words have their everyday meaning instead of
their computer meaning. We rewrote it so that Thai players can use the
launcher comfortably. 731 of the 1225 strings changed. The others
were already good, so we kept them as they were.
Below is each word we changed, what it is now, and how a Thai reader
understands the old word. We hope this makes our reasons clear, even
if you don't read Thai:
- build: อาคาร / โครงสร้าง -> บิลด์
อาคาร means a building (a house, an office). โครงสร้าง means the
structure of something. "Native build" read as "the original
building". Thai developers and players say บิลด์, written as it
sounds.
- instance: ตัวอย่าง -> อินสแตนซ์
ตัวอย่าง means "example" or "sample", so "Instance" looked like a
demo copy of the game, not a separate game setup. อินสแตนซ์ is the
English word written in Thai, as Thai players say it.
- native (mod, build): ดั้งเดิม -> เนทีฟ
ดั้งเดิม means traditional or original. "Native mod" read as "the
original mod", which says nothing about compiled code. เนทีฟ is the
usual Thai programming word.
- launcher: ตัวเรียกใช้งาน / ตัวเปิด / (left in English) -> ลันเชอร์
The same thing had three names, so a player could not tell they
were one program. ลันเชอร์ is what Thai players call it.
- jar: ขวด / โหล -> ไฟล์ jar
ขวด is a bottle and โหล a glass jar for food. "Port gets the jars"
read as "Port gets the bottles". It is a file type, so ไฟล์ jar.
- Loader: รถตักดิน -> ตัวโหลด
รถตักดิน is a wheel loader, the construction vehicle.
- Client: ลูกค้า -> ไคลเอนต์
ลูกค้า is a shop customer. Here it means the game client.
- Running / run: วิ่ง -> กำลังทำงาน / รัน
วิ่ง is running on foot. "Running" read as "jogging". A program
that runs is ทำงาน (working) or รัน.
- Proxy: หนังสือมอบฉันทะ -> พร็อกซี
หนังสือมอบฉันทะ is a legal power-of-attorney letter.
- port: ท่าเรือ -> พอร์ต
ท่าเรือ is a harbour for ships. A network port is พอร์ต.
- cape: แหลม -> ผ้าคลุม
แหลม is a headland on a coast (or "sharp"). A Minecraft cape is a
cloak, ผ้าคลุม.
- cosmetics: เครื่องสำอาง -> คอสเมติก
เครื่องสำอาง is make-up (lipstick, powder). Thai gamers call
in-game items to wear คอสเมติก.
- seed: เมล็ดพันธุ์ -> seed
เมล็ดพันธุ์ is a plant seed for farming. Thai Minecraft players use
the English word for a world seed.
- Home (page): บ้าน -> หน้าแรก
บ้าน is a house. The Home page is the first page, หน้าแรก.
- Ban: บ้าน -> แบน
Ban was also translated as บ้าน ("house"), so the ban button said
"house". Thai players say แบน.
- founder (set): ผู้ก่อตั้ง -> ผู้เล่นรุ่นแรก
ผู้ก่อตั้ง is the person who founded a company, so "Free for
founders" read as "free for the people who started Horizon". The
shop page says the set is for "the players who were here first", so
it is ผู้เล่นรุ่นแรก, "early players". It is not "sponsors" either:
the set is free.
- player counts: คน -> ผู้เล่น
"{online} of {max} players" used คน (person). In a server list Thai
players expect ผู้เล่น (player), as the game itself says.
- Java Edition: รุ่นจาวา -> Java Edition
A product name, like Minecraft and Microsoft, which stay in
English. รุ่นจาวา ("the Java version") reads as a Java release.
- 78 strings still in English -> translated
They showed English in the middle of a Thai screen.
- Polite particle ครับ removed
ครับ is the spoken "sir" that ends a sentence when a man talks. In
UI text it sounds like a person talking, not a program.
We kept every {placeholder} and every middle dot of the English, and
we used one Thai word for each English term across the whole file.
The two of us, both native Thai speakers, read every changed line
(SamsVT and NotNiceKun). If anything still reads wrong,
please tell us and we will gladly fix it. Thank you for Horizon!
Narrate incompatible server list entries matching vanilla narration order: select -> incompatible -> version -> motd. Also remove the note about incompatible server status from docs/narrator.md known differences.
Closes#10317
Co-Authored-By: Antigravity <[email protected]>
macOS and Windows are supported platforms with dedicated documentation under docs/platform/. Replace the stale 'macOS and Windows are out of scope' sentence with a reference to docs/platform/.
Fixes#11145.
Co-Authored-By: Antigravity <[email protected]>
A villager's Brain.memories now keep golem_detected_recently, last_woken
and potential_job_site, and each memory's remaining ttl, through a save
and load (Brain.pack, MemoryMap.CODEC, ExpirableValue.codec). The brain
built at the first tick starts from the loaded memories.
last_worked_at_poi stays unsaved: the villager brain does not register it.
Co-Authored-By: Codex (gpt-6.1-terra) <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Use the shared mob home for spawn, movement restrictions and persistence.
Co-Authored-By: Codex (gpt-6.1-terra) <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Reject conflicting overlaps between paintings and item frames.
Co-Authored-By: Codex (gpt-6.1-terra) <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- The pad's own click hid the guide, and so did Alt or Super on their own. The
inventory rows now hide for the keyboard and mouse too.
- Rows that promised something the game refuses: Equip on what can't be swapped,
Throw on a Riptide trident out of water or about to break, Use on an empty or
fixed item frame, Trade on a baby villager.
- Slots: Put rows on a slot that refuses the carried stack or has no room, Put Down
for stacks with different components, Take over a result slot with another stack
in hand, and any rows on a crafter's output or Put rows on a furnace's.
- Rows drew over a panel that fills a small view.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
On Linux, Horizon asks ALSA for a 1024-frame fixed buffer to keep sound effects low-latency. When PipeWire is configured with min-quantum >= 1024 (common for Bluetooth audio or in VMs), the 1024-frame buffer repeatedly underruns with POLLERR and produces no sound.
Allow setting HORIZON_AUDIO_BUFFER=default or HORIZON_AUDIO_BUFFER=<frames> (e.g. 2048) to override the fixed buffer request and use a larger or system-default buffer. Document the environment variable in native-client.md.
Fixes#11082.
Co-Authored-By: Antigravity <[email protected]>
Now installer supports native to PrismLauncher json and correctly finds json, client.jar and other sources from minecraft, allowing it to install from portable installations without additional downloading from internet
DEFAULT is a Path object that formats with backslashes on Windows, while harness_args.py appends str(default). Pinning against str(DEFAULT) instead of the hardcoded POSIX string lets the test pass across all platforms.
Co-Authored-By: Antigravity <[email protected]>
Keep removed mob entries out of command enumeration and UUID/ID lookup before
the normal sweep, and prevent a discarded passenger from saving inside its
vehicle. Preserve existing death-time and health rules.
Validate frozen Game commands/API37/save assembly against real26.3 discard
and entity-manager behavior with both client and server JARs.
Co-Authored-By: Codex <[email protected]>
Replace saved NBT and item slots through the existing entity while preserving its live identity and runtime menu data. Keep normal decoding, dirty marking and update logging; verify real data commands, typed opaque fields, inventory removals and fresh different-type/chunk-loaded identities.
Co-Authored-By: Codex <[email protected]>
Review: hurts dealt after the creature tick (ordered TNT, beds and anchors,
falling blocks, conduits) still waited for the next tick. Explosions and
falling blocks now settle right after each hurt, before the push, and the
conduit pass after it runs. And the hurt's own events (its sound,
InfestedMobEffect.onMobHurt) are handled before the teleport, where the
enderman was hurt and from its random's next draws, as
LivingEntity.hurtServer runs them before Enderman.hurtServer teleports.
Tests: an_explosion_teleports_endermen_before_it_returns (11/12 left
pending without the explosion settle), an_infested_endermans_silverfish_
come_out_where_it_was_hurt (no silverfish when the outbox only joined the
teleport's events).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Reordering a long server list took many clicks on the icon's small arrows or
Shift+arrow presses. The press that selects a saved server now also holds it:
moving the pointer puts it on the saved server's row underneath, the others
close up, and the release saves the order. A press on the selected server
still joins, as in vanilla, and holds nothing.
Fixes#10060
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Horizon split a dying cube at the moment of death. Vanilla
AbstractCubeMob.remove runs the split when the entity is removed, which
LivingEntity.tickDeath does after the death animation (deathTime 20),
so the smaller variants appear about one second later.
Mod block entities can replace, clear and atomically take an owner-controlled batch of client update fields without saving it or changing retained contents and revisions. The lazy queue is empty for new, decoded and cloned entities; normal loaded-chunk and block broadcast paths remain unchanged.
Validated with 3 real entity lifecycle/concurrency/poison tests and 3 reusable save/chunk/two-viewer Hub tests. Strict all-target Clippy passes for horizon-client-render and horizon-client-lib. No new dependency, third-party implementation or resource files.
Co-authored-by: Codex <[email protected]>
A client mod (battle music, a combat HUD) couldn't hear its own player's
fights on a remote server: the attack and damage hooks run where Horizon is
the server. native_client_attack(target, player) fires when the attack packet
leaves; native_client_hurt(attacker, player) when the server's damage event
for this player arrives, with the causing entity it names and whether this
client knows it as a player.
Fixes#10064
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The oracle's stand-in player overrode LivingEntity.startUsingItem and only recorded the item, so
the real vanilla vibration never ran: ItemStack.causeUseVibration posts ITEM_INTERACT_START when
the stack's use effects ask for interaction vibrations, which every item's UseEffects.DEFAULT
does (DataComponents.COMMON_ITEM_COMPONENTS). Since #10272 Horizon posts it, so the harness
reported 284 false mismatches (4916/5200). It now calls causeUseVibration under the same guard
as vanilla: 5200/5200.
A first draft by DeepSeek V4.1 Flash, via pi, reviewed by Claude.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
An item's move takes getEntityCollisions: the entities whose
canBeCollidedWith(item) holds. AbstractMinecart overrides only
canCollideWith (for its own moves), so a cart keeps Entity's false and
an item dropped on it falls through; the item list chained the carts in.
Boats, living shulkers and a held happy ghast still hold items (#11091).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A client mod (timed or repeated screenshots, as fabrishot does) asks for the
next frame's screenshot through F2's own path: screenshots/, vanilla's file
name, the chat line. It returns the file it will write; an error without a
window, or while another screenshot waits for the frame, which it would
otherwise replace.
Part of #9606
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The comparison #1073 asks for, in the repository: FPS and 1% lows at render
distances 8, 16 and 32 for Horizon, vanilla 26.3 and vanilla with the common
performance mods, from the clean, all-windowed, idle-machine run already saved
in benchmarks/perf/, with the conditions, the startup/load/memory figures and
links to the full reports, the method and playhorizon.dev/benchmarks.
Co-Authored-By: OpenCode Agent <[email protected]>
Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
Enderman.hurtServer teleports inside the hurt (teleport(), or
repeatedlyTryToTeleport() for a projectile or a thrown potion). Mob::damage
has no world, so it only leaves the tries in counter2 and the enderman's
next AI step ran them. Game::settle_hurt_teleports runs them right after
/damage, and in the creature tick before the mobs tick and after the
systems' outbox: a command after /damage no longer finds it in place, and
under /tick freeze it teleports at all.
Command harness group "enderman command damage": 3/5 -> 5/5.
Fixes#11069
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A client mod (a command macro such as CommandKeys) can open the chat screen
with text pre-filled for the player to finish, as T and / open it. Nothing is
sent. An error with no player in a world or while another screen is open.
Part of #10120
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Take back the server list's move arrows as targets: they split Tab's stop on
the list, and the D-pad still couldn't land on them.
- A right stick on Z takes over in every direction, not only its pulled end.
- A pad's toggle-sneak doesn't count as holding, or a rebound key's release could
leave the player sneaking.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Keep entity actions and API 29 ahead of item fallback, reuse normal packet and crosshair selection, and let the first response consume with or without an acting-hand swing. Cover bounded native Mob entries with a pending fragment, generated dispatch and actual Game priority/filter tests; leave other living interaction tables explicit.
Co-Authored-By: Codex <[email protected]>
Keep ordinary wear, Unbreaking, durability criteria, stack removal and break handling in one path. Existing damage_held callers retain MainHand behavior; callers that model an offhand use may select OffHand without editing stack damage directly.
Validation: native prepared-resource Game test verifies offhand wear, break event48, unchanged main-hand stack, legacy event47 and creative immunity. Strict gameplay clippy, formatting and public-tree checks pass.
Co-Authored-By: Codex <[email protected]>
horizon-client-render forbids unsafe code, which the Windows ARM64 build
reported (IsWow64Process2 and sysctlbyname are FFI). The detection lives with
the other system probes in horizon_telemetry::system now.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: foudretbr's rebase onto main (a1ff75a4a) drops the stray `);` that broke `tests/mod_api_hooks.rs`, and the file-wide import reorder. The hook change is the same 9 files: with no mod loaded, every translucent screen background is still the vanilla gradient. The 10 hook tests and tests.test_stamp_native_hooks pass.
🤖 Agent: merged main so this builds again (its old base still called `mob_to_nbt` without the UUID lookup that #10544 fixed). Burst notes are unchanged: one group per packet kind, each naming the player who spent the most time; the 7 burst and event-timing tests pass on the merged head.
Cache conservative envelopes of every baked model alternative, invalidating
on mutable model access. Reject rear-origin falling blocks only when their
complete padded model is outside the posed camera's depth clip planes.
Depth-only clipping preserves TAA jitter and screen-edge geometry; shader
packs keep their original path.
Include geometry/cache regression tests and separate ordinary/TAA FPS and
image comparisons, with front/empty controls and an oversized model fixture.
Co-Authored-By: Codex GPT-6 <[email protected]>
🤖 Agent: README/docs-media only; keeps the Project status block and '### Java parity' table the site and tests read, so playhorizon.dev's parity number is unaffected.
Entity.collide adds getEntityCollisions: the boxes of the entities whose
canBeCollidedWith(mover) holds. Mob movement only had block collisions,
so a zombie or a dropped-on mob fell through a living shulker and
through a happy ghast a player holds still by standing on it (#10445).
Seen carries the happy ghast's still timeout and whether it carries
someone; the mob tick lists the slots that may block (shulkers, happy
ghasts) so other mobs pay nothing when there are none.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The README's pictures were from mid-October (launcher 0.2.30, the old
menus), so this retakes them and tidies the page around them.
- Pictures: Nightly 2026-11-05 (66f3981) windowed on the Mac mini's M4
Pro (Metal) with seed 8675309: a river valley with a village and a long
view, a cherry-grove sunset, a crimson forest with a piglin, survival
with the HUD, and two players over LAN; the launcher 0.2.47's Home (with
those screenshots in its instance) and Mods pages. WebP, 712 KB in all,
under docs/media/readme/. The five README-only images they replace are
removed; village/nether/underwater.webp stay (the launcher embeds them).
- Text: a short pitch, what works in the current release, a download
table with each platform's file and requirements (Windows x64 and Arm,
macOS Apple Silicon and Intel from 12, Linux glibc), the Microsoft
sign-in requirement, mods and shader packs (Java mods don't run;
modpacks import), hosting, agent control, build from source,
contributing, and Mojang's "NOT AN OFFICIAL MINECRAFT PRODUCT" line.
- Fixed claims: macOS Intel and Windows on Arm are out (not "from the next
Nightly"); the known-issues line points to docs/known-issues.md.
- Two badges instead of seven: the parity and gap badges the tests and
tools/parity_gaps.py keep in sync. "Project status", its gap block and
the "### Java parity: N%" table are unchanged, so the website still
reads its number and #build-from-source/#project-status still resolve.
Builds on #11040 by Web-mc1, who first noticed the launcher pictures were
outdated and retook them.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
packaging/server/amp/ holds a template for CubeCoders AMP's generic module.
Its update stage runs the egg's own install script (the release asset checked
against its SHA-256), the start runs the same horizon-start.sh, and its
settings are the egg's variables with the same defaults. Tests keep both in
step and check the console patterns against the lines the server logs.
Fixes#8238
Co-Authored-By: Claude Opus 5.5 <[email protected]>
MobEffectInstance.Details.MAP_CODEC uses optionalFieldOf("ambient", false)
and ("show_particles", true), so like amplifier 0 those defaults are not
written (the mob writer already does this). The harness gets a default
work dir, so tools/parity.py can run it with no arguments; the run-log
doc is dropped.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
tools/parity.py runs each harness with no arguments, and this one exited
asking for --work-dir; it now uses a fresh folder under .local.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Mob::voice already plays at the feet (Entity.playSound), stays quiet for a
Silent entity, uses getSoundVolume and HappyGhast's pitch 1.0, so the
special arm is not needed and a Silent happy ghast no longer sounds. The
test checks the Silent case too. The 238-line capture harness ran a Java
server, a native server and a client for 1200 ticks on every ai.rs
change; the resource test covers the position.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Each worker looks at the cancel flag before every chunk, not only before
every file, so Cancel stops within a chunk on large region files. A
canceled file is never swapped in by recreate_region_files.
- The writes of a region file reach the disk once, when the file is done,
instead of a sync_data after every chunk. On the ThinkPad (NVMe, ext4) the
per-chunk sync was half of a 1.21.1 world upgrade (187 s -> 89 s, one
thread); on macOS sync_data is F_FULLFSYNC and costs far more.
- The calling thread is worker 0 and helpers are named World Upgrader #N;
a helper that fails to start leaves its files to the others.
- Tag modifiers are no longer cloned per chunk; the recreate folder is made
once per file.
- Tests: the parallel/one-thread comparison also checks vanilla counts
computed from the fixture; a new test cancels from the first chunk with 4
workers and checks at most 4 chunks were looked at and that a canceled
recreate leaves the world unchanged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
compile() stopped handing out jobs once one pipeline was refused, so the
serial rebuild that follows compiled all the remaining programs cold.
Now every job runs and only the first refusal is recorded, so the
rebuild finds the driver cache warm. The test checks the jobs after a
refused one are still made.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Mods that speak QUIC (e4mc's relay) couldn't build: their dependencies must
already be in Horizon's lock. quinn joins horizon-mod-deps as an optional
dependency (feature quic: runtime-tokio + rustls-ring, so TLS stays on the
rustls + ring already in the lock). Nothing in the game builds it.
Fixes#9602
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The README and the hosting guide still said Intel Macs come "from the next
Nightly" and listed no Windows ARM64 launcher; both ship in every Nightly and
launcher release.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Auto already tries DirectX 12 before Vulkan on Windows ARM64, where Vulkan lost
the device (#7513, #6320 on Adreno), but it read the build's architecture: the
x64 game, which the x64 launcher installs on a Snapdragon PC, still started on
Vulkan. The order now follows the machine's architecture (IsWow64Process2), and
crash reports name the emulation ("x86_64 on aarch64"), on Windows and under
Rosetta 2 on macOS (sysctl.proc_translated).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Releasing Shift while R3 held sneak in hold mode dropped it for both, and so did
the other way round.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A small render distance (8 chunks on a phone or an integrated GPU) could only
add far terrain out to 64 chunks, the slider's first stop. The slider now stops
at 16, 24, 32, 48, 64, 96, 128, 192 and 256 before doubling as before.
Fixes#9694
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged #10312 (with main) in. The cache compares the whole Scoreboard twice per frame (displayed_ids and layout) and clones it on every change, so a server ticking a sidebar score every tick rebuilds and clones each frame; still cheaper than the old double sort, and tab_list parity passes on a union holding this head.
🤖 Agent: merged main in: #10328 (merged meanwhile) now caches the layouts of the tags your collect_name_tags keeps, and #10098's new hide_gui argument is passed in your early tab_list::visible check too, so F1 still hides the list on the skip path. Skipping a tag whose entity box (inflated 0.5) is off the frustum matches vanilla, where shouldRender gates the name tag with the entity. 194 selected harnesses run on a union holding this head: tab_list and hud pass, the 8 failures are identical without it.
tools/test_non_mob_saved_flags_parity.py needs a fresh --work-dir that
parity.py never passes, so every selected run would fail; the unit test
covers the four writers.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Minecarts and primed TNT now seat their riders from the riders map;
the branch's seated box is kept on that path.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: shelf mushroom stands in as brown_mushroom_block (solid, not the walk-through brown mushroom), explorer maps as filled_map; 26.2 refusal from #10477 still in place
The x64 macOS launcher (#1161) installed and ran the game, but native mod builds
stopped with "the launcher has none for macos x86_64": its managed toolchain
had no x86_64-apple-darwin host. Add Rust 1.99.1's official rustc, cargo and
rust-std for it, pinned by the SHA-256 static.rust-lang.org publishes; the
linker is the system's, as on Apple Silicon.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Prevent vertical letter-by-letter text stacking and control clipping
when running in narrow tiling or compact displays:
- Top bar switches to compact tab padding and hides long subtitle
- Home actions and subtabs adaptively scroll and wrap without overflowing
- Mods and Versions headers stack title and filters vertically
- Settings navigates via top scrollable pill bar on narrow displays
- About tiles and Account cards stack cleanly
- Setting rows wrap controls instead of shrinking labels to 0 width
Upstream's UUID bridge (#10304) now writes a mob anger target's UUID
through mob_to_nbt's uuid_of. The branch keeps its two additions on top:
a target read from the save that is not in the world keeps the loaded
UUID, and the loaded angry_at goes once the anger is over.
A mob only gets its UUID when it is first saved, so an anger target
not saved yet came out with no angry_at (wolves too). mob_tree_nbt now
gives a target that is still in the world its UUID first; the new
anger_at_a_mob_not_saved_yet_names_it test failed without that.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Mesa's hasvk Vulkan driver for Intel Haswell (Gen 7.5, vendor 0x8086) reports conformance version 0.0.0.0 and suffers from severe vertex buffer corruption and polygon tearing across 3D terrain (#9912).
- Check is_broken_haswell_vulkan in open_with_retry to skip non-conformant hasvk on Haswell and fall back cleanly to Mesa crocus OpenGL 4.6 Core Profile.
- Preserve Ivy Bridge (IVB) on Vulkan (#4059).
Horizon's update status (Up to date, Install) and the launcher's version with
Check now / Update sat under About, while the Updates section held only the
two auto-update switches. They now open Updates, in both looks: the Glass
tiles above the switches, the classic rows at the top of its Updates group.
About keeps the data folder (now a row like Logs), logs and the applications
menu.
Fixes#11035
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Each was painted by hand or hit-tested with no target, so a pad couldn't use it:
the recipe book's search box had no keyboard, a command's suggestions couldn't be
picked, a server couldn't be moved, Realms Configure's tabs had no bumpers, and
the HUD editor's footer ended its crosshair.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A capturing server source is not silent: it takes every sendSuccess and
sendFailure line and informs no operator, so run it that way.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The boat row said Ride while sneaking, in a boat or on a full one, which refuses
or opens a chest boat's chest. In play only moving the mouse took the pad out
of use, so the guide stayed up through keyboard and mouse-button play.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The integrated server matched the used item by id (minecraft:totem_of_undying)
and used it up before the client looked for it, so any other item with
death_protection showed a plain totem in the activation animation.
Vanilla LivingEntity.checkTotemDeathProtection takes the first hand whose
stack has DataComponents.DEATH_PROTECTION, keeps a copy before shrink(1), and
uses that copy for the stat, the used_totem trigger and the animation.
Part of #10587
ThrowableProjectile.tick runs on the client too: handleFirstTickBubbleColumn
before gravity, then applyEffectsFromBlocks' BubbleColumnBlock.entityInside
after the move, with Projectile's unclamped pushes (+0.06 inside, +0.1 at the
surface, -0.03 dragging down). The client's remote_projectile_tick had none of
it, so a pearl held in a stasis chamber on a server sank between the tracker's
updates and snapped back on each one.
Projectile::tick_remote adds both for ThrowableProjectiles, through a new
ProjectileWorld::bubble_column hook (bubble_column_at for the game's world).
The integrated and dedicated servers keep pushing their pearls in the
creature tick, so their projectiles are not pushed twice.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
They sat in the hook-point section, so the catalog test took them for hooks and hook_points_stay_hookable saw world_items!; now in the helpers' impl block of the same file (targets unchanged), the macro written out.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Their target goals call setUnseenMemoryTicks(300): the evoker's player
and villager goals, the illusioner's player, villager and iron golem
goals. Pillagers and vindicators keep the default 60.
Co-Authored-By: Codex (gpt-6.1-terra) <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The shelf mushroom is a solid shelf players stand and bounce on (ShelfMushroomBlock's shape); the brown
mushroom it stood in as has no collision, so a 26.2 client would fall through what the server stands on and
be pulled back. Its block now shows as brown_mushroom_block, whose collision holds the shelf's; the item stays
brown_mushroom. The new explorer maps are filled maps (MapItem), so they show as filled_map, which draws their
map_id, not as an empty map. v26_2_ids.rs regenerated with --substitutes. TooltipDisplay's writer no longer
allocates a list on every write.
Review follow-up for #10557 (Detimes).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: tooling only. When more than 20 harnesses newly fail at once, the audit now opens one issue instead of 421, and the open-issue lookup no longer stops at 200.
🤖 Agent: tooling only. ensure_work_dir now returns the folder argparse picks, so the --work-dir=PATH spelling no longer crashes four harnesses on sys.argv.index.
The test maps each free hook to its module; the two world generation hooks are free functions in horizon-world.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Four harnesses looked the directory up with sys.argv.index('--work-dir'), which raises
ValueError for the --work-dir=PATH spelling this PR keeps. ensure_work_dir now returns the
directory argparse will pick (the last one given) and they use that.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The 2026-11-06 audit tested a main commit that did not compile (mob_to_nbt arity in raids.rs) and opened
421 "Parity regression" issues, one per harness. Above 20 new failures the audit now opens one issue with
the list and the first compiler error, and keeps those harnesses' previous results so the next audit
compares them again. The open-issue lookup no longer stops at 200.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
One tick of a server player's own movement from a bot's input, played on a
copy of the body with the movement prediction's replay code, and the block
collision boxes in a box (Level.getBlockCollisions). Read-only; nothing runs
unless a mod calls them. Helper fragment player_bots, docs, unit tests.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- A book's page and signing title weren't fields, so a pad couldn't write one.
- A sign's keyboard closed on Enter after line 1, and nothing reopened it. Enter
now keeps it open, and A opens it again.
- A keyboard open over chat stayed over the death screen and took every button.
- B answered the death confirm during the second it's meant to wait.
- A guest who joined with a pad got P1's Controllers off in a new profile, so
that pad did nothing, on this join and every later one.
- Pad presses in another window could join a player.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Track MerchantResultSlot removal and quick-move counts, reset them at the achievement callback, and award through the actual trading player's stat API without a recipe ID.
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
The join test's bot reads packets with the pinned readers; the world
packets the adapter now writes in 26.2's layout are counted (chunks) or
skipped instead.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Chunk columns (states down with stand-ins, repacked at 26.2's 15-bit
global palette, block entity types down, light masks as longs),
light_update, level_particles (26.2 field order, one speed, Int count),
explode (no trailing sound flag, silent blasts name intentionally_empty),
move_entity_pos/_pos_rot/_rot, entity_position_sync and animate in
26.2's encodings, swing_animation as 26.2's animate.
Tests write each in 26.2's layout and read it back with translate's
26.2 readers, made pub(super) for that.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
While ids are renumbered down, the values kept as pinned bytes that hold items
(pot_decorations, container, use_remainder, sulfur_cube_content) are read again
and written with that version's ids, pot sides as 26.2's four item ids, and a
tag slot display as 26.2's bare tag id (a composite of item displays for a list
of items). Quirks::{pot_decorations_are_items, tag_slot_display_is_id} written
the other way; ids::quirks_written gives the quirks of the version written for.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The test server keeps every packet the hub hands to the game; a 26.2 bot
confirms its teleports by id alone, walks, starts and stops destroying the
block below in 26.2's action numbers, and swings with each hand.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
layout_26_2 mirrors translate's 26.2 readers for a ClientSession: spawn info game
types back to bytes, the advancement tab position back inside its display,
argument types renumbered down with a string stand-in for 26.3-only parsers,
26.3-only synchronized registries not sent, map decorations and stats in 26.2's
numbering. Enabled with layout_26_2::set_enabled.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A 26.2 client confirms a teleport by its id alone: the session follows the
player_position packets it is sent and the moves it reports, and fills in
26.3's position and rotation from the teleport it accepts. Its digging
actions past 0 are one up (26.3 inserted CHANGE_DESTROY_DIRECTION at 1), and
its main-hand swing becomes 26.3's punch (an off-hand swing has no packet).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A Game helper (Host::refresh_commands for trait mods): at the start of the
next server tick every mod's native_register_commands is asked again,
LanHost::native_commands and the hub's mod tree are rebuilt, and every
placed player is sent their tree again after native_player_commands_sent,
as PlayerList.sendPlayerPermissionLevel / Commands.sendCommands after
/reload. Hub::store_mod_commands keeps the new tree without sending it, so
each player gets one filtered tree. Servers whose mods never call it are
unchanged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Each entity's vertices are matched with last frame's by entity (Frame::entity_motion, now with
the GPU-posed start at each mark): last frame's pack buffer stays bound at set 1, binding 2, and
binding 3 gives each vertex its index there. at_velocity is this frame's view position less last
frame's (gbufferPreviousModelView joins the block when a pack reads it); a vertex with no match
reads zero, as before.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`tooltip_render_pre` (#10302) is labelled `RenderTooltipEvent.Pre`, so
`horizon-port-pack events` counts the 10 ATM10 mods that listen to it.
The "Next hooks" table in docs/native-hooks-neoforge.md listed
`player_tick_pre` and the client tick pair, which have now landed: the
section lists them with the rest.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Five command-oracle harnesses looked for a bare `--work-dir` and appended
their default anyway when the caller wrote `--work-dir=PATH`. argparse takes
the last value, so the caller's directory was silently replaced and the run
wrote its scratch output somewhere they did not ask for. They now share the
guard test_boat_leasher_parity.py already used, and tests/test_harness_args.py
pins both spellings.
🤖 Generated with Codebuff
Co-Authored-By: Codebuff <[email protected]>
ServerPlayer.swingAndResetAttackStrength: the guest's block, item, leash
knot and entity uses reset the attack ticker when their swing starts.
Co-Authored-By: Codex (gpt-6.1-terra) <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- An idle pad's trigger rests at -1, which counted as moved past 0.5, so touching
it took over from the pad in use.
- An analog trigger jumped to full at gilrs' 0.75, so a higher threshold did
nothing.
- gilrs drops `~` from SDL rows, so sticks on the nine Linux rows with one ran
backwards, the Switch 2 Pro among them.
- The 8BitDo Ultimate 2 without a row looked up when pushed down, with X and Y
crossed.
- Rumble reached the pad from an unfocused window, adaptive triggers stayed on
over books and signs, a stale lightbar stayed after splitscreen, and in_use
outlived an unplugged pad.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- FishingRodItem.use posts cast and retrieval vibrations using the stack's use effects.
- EnderEyeItem.use posts a projectile-shoot vibration.
Part of #9776.
Co-Authored-By: Codex (gpt-6.1-terra) <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ShowTradesToPlayer sets the main hand's drop chance to 0 when it shows
an offer (displayAsHeldItem) and back to 0.085 when it clears the hand
(clearHeldItem).
Co-Authored-By: Codex (gpt-6.1-terra) <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The hold shared the keyboard's slider_edit, so Start, a scroll, the mouse or a
Tab could leave some other slider held. The pad's own position also survived
Undo, so the next step jumped back near the undone value.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Add the native hook `entity_interact_specific` (NeoForge's `PlayerInteractEvent.EntityInteractSpecific`): a right-click on an entity hears the point clicked, measured from the entity's position, before `native_interact_entity`; a claim swings the hand and ends the click. Server-side.
Co-Authored-By: DeepSeek Pro via OpenRouter <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
player_pre_login and player_profile name those paths in their hook signatures; the generated
dispatch code compiled in the test crate needs them, as the existing horizon_client stubs.
Part of #9996.
A chunk's noise volume is made of whole cells, and InterpolatedSampler
filled it one cell at a time: per cell eight corner loads and four Z
lerps, per column two X lerps and a bounds-checked index, then a scalar
loop of stores and additions (fill_cell alone was 6.4% of the Overworld
proto phase's instructions). For a volume aligned to its cells,
fill_whole_cells now walks the four columns of one Z row of a cell from
the bottom of the volume to the top. The X lerps at a level of corners
are computed once (they are the top of one cell and the bottom of the
next: the same lerps of the same samples), the four columns' repeated
additions run as four lanes, and four Ys of the four columns are stored
as four vectors. Each value comes from the same operations on the same
operands in the same order as vanilla's InterpolatedFunction fillCell;
only the order between columns changes. The top cell may be cut short
(the 16x114x16 volumes); a volume that is not aligned, or whose top
corners would clamp, keeps the general path.
fill_noise_inner spent about ten instructions on each air block above
the aquifer's skip height. A downward run of non-positive densities that
stays above it is now skipped in one step: nothing was written for those
blocks and note_skipped only clears a flag, so one call covers the run.
Exact: chunkgen_bench --snapshot files (terrain and decorated) are
byte-identical to main in 16 cases: the Overworld at seeds 12345, -1,
987654321 and -7, the Nether at seeds 12345 and 42, the End at seeds
12345 and 7, sizes 3 to 12. A new test compares sample_volume and the
new path with a transcription of vanilla's sampleWithBlockStep and
fillCell, on an input with NaNs, infinities, signed zeros and
subnormals, for 4x8, 8x4, 4x4 and 8x8 cells, and checks which volumes
the new path takes.
Proto-phase instructions (callgrind, 64 chunks, one thread):
Overworld 5.970 G -> 5.615 G (-5.9%), the Nether 2.868 G -> 2.853 G
(-0.5%).
A first draft by GLM-5.3 and DeepSeek V4.1 Flash, via pi, reviewed and
reworked (the air-run skip).
Co-Authored-By: pi coding agent (DeepSeek-V4.1-Flash) <[email protected]>
Co-Authored-By: pi coding agent (GLM-5.3) <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The install side of the Launch API, experimental: what a build loads (the
new `content`), the folder and the on/off file of each kind under
--game-dir, when a change applies, native mods needing a rebuild, what to do
with a .hpack, and the checks before installing as mod-index.md specifies
them (where it comes from, size and SHA-256, the minisign key, revoked[] and
installed copies, the manifest). Listed in docs/api/README.md. Part of #10562.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Piglin.holdInOffHand equips the item with setItemSlotAndDropWhenKilled,
a guaranteed drop even without a player's kill, and makes the piglin
persistent unless the item is a gold ingot.
Co-Authored-By: Codex (gpt-6.1-terra) <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
LivingEntity.push(Entity) does nothing while the entity sleeps, and
doPush reaches the other entity through it. The mob snapshot carries
sleeping (a villager in bed, Fox.isSleeping) and push_entities skips the
push. The sleeper's own tick still pushes the others.
Co-Authored-By: Codex (gpt-6.1-terra) <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
BrushItem.onUseTick aims with getHitResultOnViewVector, so a mob between the
player and the block is the nearer hit and the use releases without a stroke.
tick_brush used the block-only pick and brushed through the mob.
`content` in horizon-client's reply, from the constants the build checks
content against: content mods' manifest api lines (APIS without the removed
WebAssembly 0.1), native mods' hooks level and that they need a rebuild, the
shader schema and the line it draws, the resource and data pack formats, the
modpack format and the index schema. A launcher offers only what works. The
generated reference shows the hooks level as a placeholder, since it rises
with every hook. Part of #10562.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
LivingEntity.stopUsingItem, reached when a use runs out or when the brush hits
no block, causes the ITEM_INTERACT_FINISH vibration. The brush and instrument
paths cleared the use directly instead, so only letting go of the button made a
sculk sensor hear them.
Merge main at 0873f24d2, retain its replacement shader screen, and route native selections and options through stable manifest IDs.
Co-Authored-By: GPT 6.1 Sol <[email protected]>
🤖 Agent: the pre-tick hook swapped each guest's whole state into Game every tick even with no mod loaded; I pushed a guard so `as_guest` runs only when `mod_api::mods()` isn't empty. The fragment passes tests.test_stamp_native_hooks.
🤖 Agent: verify failed because bake_bricolage.py imports fontTools before it reads its arguments, and CI has no fontTools. I pushed a change that checks the arguments first, so the new usage test passes without fontTools installed.
🤖 Agent: #10398 asked that a local build in a folder with a space still builds, so I pushed a fallback for GNU windres when the volume has no 8.3 names: outside CI it warns and builds without the icon instead of failing. With the taskbar-icon commit, `cargo check --target x86_64-pc-windows-gnu` passes on the desktop for horizon-client and horizon-client-input.
🤖 Agent: a failed region already restores its originals, so blocking the server start on any prune error took a server down for one corrupt chunk record. I pushed a change that starts it when no journal is left, with a supervisor test for it. Note that delete mode now writes regions one at a time with fsync, so a large prune takes longer than the old parallel rewrite. 169 horizon-server tests pass on Linux.
🤖 Agent: raids.rs now passes the same `persist.uuid_of` lookup persist.rs gives `mob_to_nbt`, and the four entity_nbt.rs test calls get `|_| None`, so main builds again after #10304 (verify green on this head).
🤖 Agent: translations only; %s placeholders kept and every file parses. The languages you flagged as uncertain (ba_ru, cv_cu, sah_sah...) stay worth a native speaker's look.
🤖 Agent: removes the old pack pages, their two slider kinds and four lang keys in all 87 files; nothing else read option_label_within, checked with clippy -D warnings on the stack over current main.
🤖 Agent: The non-constant texel offset becomes offset / textureSize(level 0) added to the coordinate: right for the base level, but at a sampled lower mip the shift is in base-level texels, not that level's (GL moves by texels of the level read). Fine for Arc's FXAA at level 0; worth a note if a pack hits it with textureLodOffset.
🤖 Agent: CAT_* macros come from biomes::CATEGORIES, so they pick up #10499's CAT_MOUNTAIN 17 and CAT_UNDERGROUND 18 with no second table to keep in sync; your test comparing every macro with constants() covers that.
🤖 Agent: The constant evaluator now refuses a non-finite F32 fold with Overflow, and only GLSL function bodies turn that into a run-time expression, so WGSL input still rejects 1.0/0.0 at compile time. Vendored naga's own 133 lib tests pass with the chain.
🤖 Agent: The integer-sampler helpers texelFetch the texel under the point with the coordinate clamped to the edge, which is what GL's nearest read of an integer texture gives; a bias argument is accepted and ignored, as GL does for these. Stack 3/6, checked with the rest of the chain on current main (clippy -D warnings, horizon-shaders and render tests).
LivingEntity.playBlockFallSound: the block 0.2 below the feet, at half
its volume and three quarters of its pitch, not for a silent mob or air.
Most mobs play it after their own fall sound, horses and llamas after
the damage, and a llama's damaging fall under six blocks plays it alone.
Co-Authored-By: Codex (gpt-6.1-terra) <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
as_guest swaps a player's whole state into Game: running it for every
player every tick with no mod to call cost the server for nothing.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
horizon-world cannot call horizon-client's dispatch, so the two generation hooks
call a NativeGenerateHooks table that entry.rs installs (as native_load_hooks
does for the loot hooks). mod_api re-exports NativeGenerate, NativeProtoChunk
and NativeLodColumn for the generated events. Without the table, and without
mods, both hooks do nothing.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Since #6253 the mod API generator needs a mod_api entry for every pending hook
fragment. Cancel flags get {cancel, out: cancel}, menu_input_closed collects its
slots, every other hook listens. The placeholder bodies are filled by
`cargo run -p xtask -- mod-api`.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Every frame each text display built a batch, emitted its styled glyphs and
transformed every vertex, and a display of width or height 0 (what plugins
summon for holograms) is noCulling, kept to the distance rule because its
transformation can put the text anywhere: 200 of them cost ~0.3 ms a frame
with none on screen (#4664).
Each display's local-space text quads are now kept, rebuilt when the text,
its style, opacity, background, shadow, see-through, the font epoch or the
GUI atlas revision change (obfuscated text every frame), and only
transformed per frame. A noCulling display is skipped when the box of its
quads, through this frame's full matrix, misses the frustum: it can't draw a
pixel. Displays with a size keep vanilla's box test; the view range stays.
min(150, 35) + 20 = 55, then one anger tick: 54. The test pinned the dropped
anger the earlier port left.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- NativeEffect (player_effects) and NativeInventoryKind (set_inventory) are pub and re-exported in
mod_api: the generated mod API refused both fragments.
- native_teleport_player's cross-level teleports fill LevelTeleport's new cause (hooks-move) with
"unknown"; player_teleport's docs name that case.
- lan_vehicles: the vehicle_moved_v2 call closure is FnMut.
- cargo fmt (game.rs mod lines in order).
#10398 asked that a local build in a folder with a space still builds: a
volume without 8.3 names gives windres no usable path, so outside CI
such a build warns and goes on without the icon, as before. CI and
every other case still fail without it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
On Peaceful, vanilla never lets a player be a mob's target
(`LivingEntity.canAttack` refuses a player there), so a mob that already
held one gives it up. A neutral mob also stops being angry and forgets
who hurt it (`NeutralMob.stopBeingAngry` calls `setLastHurtByMob(null)`).
Horizon dropped a player target only for creative and spectator players,
so an iron golem that had been angered kept attacking after the
difficulty was set to Peaceful.
Fixes#9759.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Fekd3xkgubE8VfXrALh2td
Co-Authored-By: Codex gpt-6.1-terra <[email protected]>
native_player_persistent_data(uuid) reads and
native_set_player_persistent_data(uuid, data) replaces the compound mods
keep on a player, as NeoForge's Entity.getPersistentData() and Bukkit's
PersistentDataContainer. It is saved in the player's file as
NeoForgeData, beside the keys Horizon keeps without modelling them, and
kept over a death and a level change; an empty compound takes the key
away. Nothing calls them without a mod.
Unproven: not yet compiled or probed live (build queue); the integration branch tests it.
native_item_dropped_v2(player, stack, cancel) is now cancellable, as
NeoForge's ItemTossEvent is: a canceled throw puts the stack back into
the player's inventory (Inventory.add, the held slot first); only what
does not fit is thrown, unheard, and native_item_dropped hears nothing.
The drop statistics and the swing still happen, as in NeoForge. Wanted
by the combat log, freeze and inventory-lock ports.
Unproven: this commit was neither compiled nor run (builds were stopped for
the integration test); `cargo run -p xtask -- mod-api` filled the dispatch.
The listen-only v2 drop hook before it was proven on a server.
Part of #10461.
native_projectile_world_border(dimension, entity, kind, pos, velocity,
collide) is heard each tick a thrown projectile (pearl, snowball, egg,
potion...) inside the world border would end its next step outside it,
before 26.3 stops it on the border like a wall (MC-310476). A mod may
change its position and velocity, or set `collide` false: that tick's ray
is then clipped without the border, so it flies through as before 26.3.
Arrows and tridents, which Horizon does not stop at the border, are not
heard. Static (no self), called from tick_projectiles.
Unproven: not compiled or run on a server on this branch (handed to the
hooks-all integration build).
Part of #10461.
native_trample(dimension, entity, kind, pos, fall_distance, cancel) is
heard when a player or mob that fell onto farmland is about to turn it to
dirt, once FarmlandBlock.fallOn's chance and checks have let it (NeoForge's
FarmlandTrampleEvent, Bukkit's PHYSICAL PlayerInteractEvent and
EntityInteractEvent). Canceling keeps the farmland and its crop; the fall
damage is taken either way. The landing rules ask through a new
BlockLandingWorld::trample_allowed (true by default), which the player's
and the mobs' landing worlds answer from a function the game sets once
(block_contact::TRAMPLE); there is no self.
Unproven: not compiled or run on a server on this branch (handed to the
hooks-all integration build).
Part of #10461.
native_player_respawn_position(player, name, dimension, pos, yaw, pitch)
is heard once per respawn, after the Respawn click and before the player
is placed (NeoForge's PlayerRespawnPositionEvent, Bukkit's
PlayerRespawnEvent.setRespawnLocation), with their respawn point: a bed
or anchor block's top, a forced point, else the world spawn. Left alone,
the point is resolved as before. A changed dimension or position places
the player exactly there, with no bed or anchor used and no free column
looked for; yaw and pitch set their look, which Hub::respawn now takes
instead of always 0, 0. An unknown dimension or a non-finite value
leaves that part unchanged.
Unproven: not compiled or run on a server on this branch (handed to the
hooks-all integration build).
Part of #10461.
native_player_teleport(player, name, cause, from_dimension, from,
to_dimension, to, relative, cancel) is heard before a server player's
teleport is sent (Bukkit's PlayerTeleportEvent, NeoForge's
EntityTeleportEvent): commands (/tp within a level and into another),
ender pearls, chorus fruit, the spectator teleport menu, a dismount and
other server moves of the player. A mod may move `to`, cancel (the
player and their body stay where they are), or set the look and motion
bits of the position packet's relative flags, so a pearl can keep the
player's speed (0x1f8, what 26.2 sent); the position bits are ignored.
The call sites name the cause in a thread-local (lan::hub::teleport_cause)
that Hub::teleport and Hub::teleport_keeping_motion read; a teleport
without a named cause (respawns, portals, the End gateway, vehicles
carrying riders, waking up, refused moves) is not heard. Command and
spectator teleports into another level carry their cause in
LevelTeleport. The hub reaches the hook through a function the game sets
once (lan::hub::PLAYER_TELEPORT); there is no self.
Unproven: not compiled or run on a server on this branch (handed to the
hooks-all integration build).
Part of #10461.
native_player_move(player, name, from, to, yaw, pitch, on_ground,
cancel) is heard for each position or look packet that changed either
and passed the server's checks (moved too quickly, moved wrongly, the
anticheat), before Hub::position_change applies it (Bukkit's
PlayerMoveEvent). Canceling sends the player back to `from`, as a
refused move does; a changed `to` puts them there and their client gets
a teleport. Riders, sleepers and moves while a teleport waits are not
heard. The hub reaches the hook through a function the game sets once
(lan::hub::PLAYER_MOVE), as the damage hooks do; there is no self.
Unproven: not compiled or run on a server on this branch (handed to the
hooks-all integration build).
Part of #10461.
The helper native_respawn_player(uuid) respawns a dead server player as
their client's PERFORM_RESPAWN does (Spigot's player.spigot().respawn()),
after the players' packets of the server tick, through the same
guest_respawn. Nothing calls it without a mod.
Unproven: not yet compiled or run on a server (builds stopped for the hooks-all integration).
The helper native_set_flight(uuid, allow, flying) lets a server player fly
in any game mode or not and sets whether they fly now, as Bukkit's
Player.setAllowFlight and setFlying: the guest keeps the mod's mayfly
(allow_flight) until their game mode changes or they respawn, their
abilities packet and the server's body carry it, and the flying check does
not kick them. Without a mod allow_flight stays false and every check is
the game mode's, as before.
Unproven: not yet compiled or run on a server (builds stopped for the hooks-all integration).
The helpers native_mount(entity, vehicle), native_dismount(entity) and
native_eject_passengers(vehicle) seat a server player or mob on a boat,
minecart or mob, take it off, or take every rider off, as /ride does with
its checks and native_entity_mount (Bukkit's addPassenger, leaveVehicle,
eject). /ride's mount checks move into ride_mount, which both use; the
command's answers are unchanged. Nothing calls the helpers without a mod.
Unproven: not yet compiled or run on a server (builds stopped for the hooks-all integration).
native_vehicle_moved_v2(player, dimension, vehicle, kind, from, to, cancel)
is heard right after native_vehicle_moved, with the same state swapped in,
and also for a mob a server player rides. A canceled move puts the vehicle
back at from with its riders, a changed to puts it there
(native_teleport_vehicle), as Bukkit's VehicleMoveEvent with a teleport.
v1 keeps its signature, firing point and vehicles. Without mods nothing
moves the vehicle.
Unproven: not yet compiled or run on a server (builds stopped for the hooks-all integration).
The helper native_teleport_vehicle(dimension, id, pos) teleports a boat,
minecart or ridden mob of the level in place with its riders, as /tp does
a vehicle (Bukkit's Vehicle.teleport): the riders stay seated and a server
player riding it is told, as their client drives it. Only where nobody's
state is swapped in. Nothing calls it without a mod.
Unproven: not yet compiled or run on a server (builds stopped for the hooks-all integration).
The helper native_teleport_player(uuid, dimension, pos, yaw, pitch)
teleports a server player as /tp does (Bukkit's Player.teleport): at once
when they are the acting player or nobody's state is swapped in, else
after the players' packets of the server tick, so a port no longer runs
execute in .. run tp from the console, which waits for the next tick and
does nothing inside a player's hook. Into another level it is a command's
level teleport. Nothing calls it without a mod.
Unproven: not yet compiled or run on a server (builds stopped for the hooks-all integration).
The helper native_resend_commands(player) sends a player their command
tree again, with native_player_commands_sent, its v2 and the permission
nodes of the mods' commands asked anew, as Bukkit's Player.updateCommands:
after a permissions mod changed what they may use. The tree goes out in
the next server tick, with no player's state swapped in, so the helper
works from any server hook, the player's own included. Nothing calls it
without a mod.
Unproven: not yet built or run (handed to the hooks-all integration branch for the build and proof).
native_player_command_level(player, name, command, level) is heard in
Game::guest_command after native_player_command, before the server checks
the command: level comes in as the player's own and becomes the level this
one command runs at (GuestCommand.permission), so a permissions mod can
grant /gamemode to a player of level 0 or refuse a command to an operator,
as Paper's VanillaCommandWrapper does with minecraft.command nodes. A mod
command with a permission node falls back to this level. Unchanged, the
command runs at the player's level, as before.
Unproven: not yet built or run (handed to the hooks-all integration branch for the build and proof).
NativeCommand::new(usage).permission(node) gives a mod's command a node, as
Bukkit's plugin.yml permission: does. The server then asks
native_has_permission(player, node, level), the command's level as the
default, both before it puts the command in a player's command tree and
before a player's typed command reaches native_perform_command, so a
permissions mod can grant it below that level or refuse it to an operator.
Without a permissions mod the level decides, as for a command without a
node; a dedicated server's console runs every registered command as before.
Unproven: not yet built or run (handed to the hooks-all integration branch for the build and proof).
native_player_commands_sent_v2(player, name, names, names_hidden) is heard
right after native_player_commands_sent, at the same times, with the root
commands the player's level hides (those a level 4 operator would get) in
names_hidden. A root a mod moves into names is sent with its subtree, as
for an operator of its level, so a command a permissions mod grants
completes and highlights in the player's chat box (Paper's
PlayerCommandSendEvent after a passed permission check). v1 keeps its
signature and firing point. Without mods nothing is granted and the tree
is unchanged.
Unproven: not yet built or run (handed to the hooks-all integration branch for the build and proof).
The hook native_permission_check(player, node, default_level, result) lets a
permissions mod (a LuckPerms port) say whether a player has a permission
node; the first mod to set result decides. The helper
native_has_permission(player, node, default_level) asks it, for every mod
and for the server, and falls back to the player's operator level against
default_level when no mod answers, as Bukkit's Permissible.hasPermission and
NeoForge's PermissionAPI.getPermission do. Without a permissions mod every
answer is the operator level, so nothing changes for unmodded servers.
Unproven: not yet built or run (handed to the hooks-all integration branch for the build and proof).
CI has no fontTools: the import failed before main() could print the
usage, so the new test saw a traceback (verify failed).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Use the entity position for the special ambient caller and cover adult and baby natural events through the real Mob tick and recording route.
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
parity-main: natural_spawn first fails at f6a9444de (#10416), of 182 merges
since 66f3981f5. The spawn start heights it produces differ from vanilla 26.3
by up to tens of blocks (chunk/10 monster: vanilla y 99, Horizon 62), so
players would see mobs spawn at the wrong heights. Reverted until the change
passes the natural_spawn harness.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Written by hand from a scratch resolution seeded with this lock: rusqlite,
libsqlite3-sys, fallible-iterator, fallible-streaming-iterator, hashlink,
vcpkg, yaml-rust2, arraydeque, gif, weezl, color_quant, snap. Existing
entries keep their versions; chrono gains serde and mysql_common gains chrono.
To confirm: cargo metadata --locked must not want to change it.
#10304 gave mob_to_nbt a fifth argument after the raid observation's call to it
was written, and the two merged without a conflict.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Main added enderman_hurt_teleports after the block contact damage; this
branch drops that call's world argument. Both kept.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The bundle's fragments carry no mod_api entry: session A adds them in one
step (add_mod_api.py, which takes a cancel: &mut bool parameter for the
cancel contract). So interact_entity_v2's claimed and
right_click_block_v2's canceled are named cancel; they act as before.
Not built yet.
native_living_incoming_damage_v3(victim, victim_uuid, kind, attacker,
attacker_uuid, direct, damage_type, amount, cancel) fires everywhere v1
fires (health::HURT_SERVER), right after it with the amount v1 left and
the same cancel flag. It adds a player victim's UUID, the causing entity
where v1 leaves 0 (a mob's attacker, a player's melee, the synchronous
attack in progress as living_knockback_v2 falls back to), a player
attacker's UUID and the direct entity. Players are noted by entity id as
each guest's turn starts (game/native_damage.rs, only while a native mod
is loaded), since the hit carries ids only and no Game.
Not done here, as the plan asked: a &mut self host (the victim is
borrowed out of the game inside Mob/Player/Dragon hurtServer, behind the
HURT_SERVER fn pointer) and Bukkit's modifier breakdown (the reductions
run after the hook in horizon-world's living_hurt and Mob::hurt_server).
Sentinel (attacker matched within 500 ms), Wanted, HuskSync and
CombatLog (an id-to-player map each tick) need the names.
Not built yet.
native_kick(player, reason) disconnects a player with a Text reason in its
colours and translations (Adventure's Player.kick(Component)), through
Hub::kick as /kick does, so the log line and native_player_kicked follow.
It is queued and runs at the tick's end with the mod messages
(flush_native_net), so it works from any server hook, the kicked player's
own included; today a port sends a raw Disconnect and then runs /kick with
one plain line.
Part of #10461.
native_resource_pack_status_v2(player, name, pack, status) is an additive
version of native_resource_pack_status with the answered pack's id
(Bukkit's PlayerResourcePackStatusEvent.getID), so a mod's own pack's
answers are told apart from the server's. In play it is heard just before
v1, which keeps its signature and firing point. The answers to the
server's own pack given during configuration, which the connection thread
handled alone, are kept (lan::Configured::pack_answers) and heard once the
player has logged in, right after native_player_logged_in. The connection
answers the packets exactly as before.
Part of #10461.
rusqlite 0.41 (bundled + cache, no default features) reads SQLite files such
as a Bukkit plugin's .db; yaml-rust2 0.13 (no default features, safe Rust)
reads and writes config.yml; gif 0.14 and snap 1 are small and pure Rust;
mysql gains chrono for DATETIME columns. All optional, nothing builds them.
Cargo.lock not yet updated.
tick_server_spans calls native_server_tick_timed with the pass's wall and CPU time and merged spans (TickSpan, re-exported from spans::Span) before handing them to the report. Spans are switched through tick_profile::want_spans by who wants them (HORIZON_SPIKES, horizon-report, a mod), so a mod and a report no longer turn each other's spans off. tick_profile keeps a mod's mob cost sums apart from the report's (two bits, two maps), so both can sample at once; calibration stays the report's. Fragment server_tick_timed.
While a mod turns counting on, the network threads keep each framed and read play packet's id and body size in the connection's Outbox (io_pool Conn::frame and Conn::read, behind one relaxed load of a static switch, capped at 65536 each way); the server tick's end hands them to native_server_packets per player and way. Off (the default) nothing is kept and the hook never fires. Fragment server_packets.
native_hurt_entity(dimension, target, amount, damage_type, attacker)
hurts a mob or a player of the level as LivingEntity.hurtServer with
damageSources().mobAttack(attacker) does, the way /damage <target>
<amount> <type> by <attacker> goes: cmd_damage's hurt is moved unchanged
into damage_target, which both call. Returns whether it hurt; false for
another dimension, a target that is neither a mob nor a player of the
level, a missing attacker or a non-finite amount.
Sentinel's own damage (damage set, workaround damage, lightning weapon)
today runs a damage command by UUID.
Not built yet.
native_send_resource_pack(player, id, url, sha1, prompt, required) sends
ClientboundResourcePackPushPacket with the mod's own pack id (Bukkit's
Player.addResourcePack), checked against the packet's limits (hash at most
40, URL at most 32767 characters); native_pop_resource_pack(player, id)
sends ClientboundResourcePackPopPacket for one pack or, with None, all
(removeResourcePack). Both go through native_send. A decline still kicks
only when the server's own pack is required, as vanilla's
handleResourcePackResponse does.
Part of #10461.
native_player_register_channels(player, name, channels, register) hears
the names a client registers (minecraft:register) or unregisters
(minecraft:unregister), NUL-separated as Bukkit's and Fabric's clients send
them; vanilla reads neither (Bukkit's PlayerRegisterChannelEvent). What a
client registered during configuration, which the connection thread
dropped, is kept (lan::Configured on Joined) and heard once right after
native_player_logged_in; a message in play is heard as it is handled, with
the player's state swapped in, and then goes on as before. The helper
native_player_channels(player) lists a guest's channels
(Player.getListeningPluginChannels). At most 128 names of at most 256
bytes are kept per client.
Part of #10461.
names.rs: #10328 landed on main; its NameTagLayouts frame now wraps this
PR's frustum-culled collect_name_tags, both test sets kept.
scoreboard.rs: #10098 added the hide_gui argument to tab_list::visible;
the early visibility check here passes !self.ui.hide_gui too.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
native_block_redstone(dimension, pos, old, new) is asked, as Bukkit's
BlockRedstoneEvent, when a button is pressed (hand or arrow) or let go by
its release tick, a lever is pulled, or a pressure plate's weight check
changes its signal. A mod may change new; one equal to old keeps the
component as it is, and a held button or plate is asked again at its
next scheduled check. The seam is LevelAccess::redstone_change in
horizon-world (default: leaves the signal), which GameLevel forwards on
an authoritative level. Wire, torches, diodes, observers and the other
sources are not heard yet.
CoreSMPEssentials part B holds its power rods' component on without the
2-tick re-set that leaks a 1-tick off pulse. Test (not run):
a_level_can_keep_a_lever_as_it_is.
Not built yet.
Bukkit's Player.setCompassTarget: Hub.compass_targets by UUID while the player is on; default_spawn sends the target in place of the world spawn wherever it is sent (join, respawn, level change). Without a target the packet is unchanged. Helper-only fragment compass_target.
Bukkit's BossBar.addPlayer/removePlayer: a NativeBossBar (mod_api) by player UUID in BossState, offered to that viewer by boss_events_for, so publish_boss_bars sends Add/Update/Remove as for the world's bars. Unmodded servers keep an empty map. Helper-only fragment boss_bar.
Vanilla's ServerLevel.sendParticles(player, ...) and Bukkit's Player.spawnParticle: within 32 blocks, 512 with force (overrideLimiter). Replaces the hand-built LevelParticles (dust id hard-coded) of plugin ports; same options and checks as GameLevel::native_send_particles. Helper-only fragment send_particles_to.
Bukkit's Player.playSound for plugin ports, which sent a raw ClientboundSoundPacket through native_send. Queued in game/native_player_fx.rs and sent at the tick's end with the hub back, so it works from any server hook; helper-only fragment play_sound_to.
Sentinel's native attacks (its default) call Mob.doHurtTarget for the
body. Horizon's do_hurt_target needs the goal's world, and its player is
whichever guest the mob sees, so the port injected a goal and wrote
LanHost.mob_players to bind the body to its target. native_mob_attack
runs do_hurt_target from any server hook with the target guest's view as
the mob's player (a new lan_guest_view, the single-guest form of
lan_guest_views), then hands the hits on that player to the guest and the
rest to handle_mob_events, as a mob's tick does. Helper only: unmodded
servers never call it. Part of #10461.
The Team review (kacpherek) on #10382: groups filled in arrival order
under a 32 cap hid a late UseItemOn burst behind other players' moves;
count > 1 dropped 69 players sending one 6-9 ms use each; and one slow
event hid the rest of its group. Groups are now one per packet kind (a
bounded set) with the time of their short events only (the slow ones
are noted on their own), and each names the player who spent the most
of it: "213 x UseItemOn under 10 ms each; most from Steve: 140 of
them, 700.0 ms".
Co-Authored-By: Claude Opus 5.5 <[email protected]>
native_remove_entity(dimension, id) also removes projectiles (arrows,
tridents, snowballs, eggs, pearls, fireballs, wind charges, llama spit,
shulker bullets, firework rockets) as /kill does (kill_target), and a
server player's fishing bobber, as Entity.discard: the bobber of the
player in place, or outside a guest's turn any player's of the level
(remove_server_hook in fishing.rs). Displays, markers and interactions go
as before. Sentinel's arrow cleanup and CoreSMPEssentials part B's power
rods need it.
Not built yet.
Citizens' Navigator and Sentinel walk their bodies to points and
entities, stop them, ask whether they still walk and aim their heads.
Horizon offered this only to a goal (Mob::nav_move_to with the goal's
world), so ports injected a goal that read intents from a global map, or
moved bodies in straight /tp steps through walls. native_mob_navigate,
native_mob_follow, native_mob_stop, native_mob_navigating and
native_mob_look_at call the mob's own navigation and look control
(PathNavigation.moveTo, stop, isInProgress, LookControl.setLookAt) from
any server hook, for a mob of any loaded level, with the world its goals
see. Helpers only: unmodded servers never call them. Part of #10461.
native_move_entity turns only the entities Horizon keeps without
simulating them, and native_teleport_mob takes no rotation, so NPC ports
turned their bodies with execute .. rotate or tp .. facing every few
ticks. The helper is /rotate (Entity.forceSetRotation) for an entity of
any loaded level, with an optional head yaw (setYHeadRot) for a mob.
Helper only: unmodded servers never call it. Part of #10461.
native_entity_data_to(viewer, entity, values) is a typed shortcut of
native_server_packet for ClientboundSetEntityDataPacket: the entries one
guest is about to get for one entity, changeable for that viewer alone, so
a mod can show one player glowing to one viewer without touching the
server's own flags (TAB's and Essentials' per-viewer glow, which resend
their own packets on a timer today). Heard just before native_server_packet
for the same packet. Switched on by native_watch_entity_data, which decodes
that packet only. EntityDataValue is horizon_protocol's entity_data::Value.
Part of #10461.
native_fishing_hook_landed(player, hook, dimension, block, face) hears a
server player's flying fishing bobber meeting a block on its way, and
each tick it lies on one (on the ground as the tick began or ends: the
block under its centre, its top), as Bukkit's ProjectileHitEvent for a
FishHook. It is heard at the end of FishingHook.tick with the bobber back
in place, so a mod can remove it there. hook_block_hit keeps its result;
a sibling hook_block_hit_at also gives the block and face.
CoreSMPEssentials part B starts its power rods from the plate, button or
lever the bobber lands on; native_projectile_impact leaves bobbers out.
Not built yet.
NativeLevelStats counts a level's loaded and forced chunks, block entities and ticking ones (BlockEntities::ticking_len), and its entities (players included) and block entities by type. The entity list reuses the selector walk (level_candidates, now pub(in crate::game)) with a parked level swapped in as /execute in does; block entity types come from the block (lan::chunks::block_entity_type), which the enum's variants cannot tell apart, so this replaces the planned BlockEntity::type_id(). Fragment level_stats.
A stage plugin keeps its actors by UUID and moves, changes or removes them
in whatever level they are in; Horizon's entity handles reach the level in
place only, so ports ran tp/data merge entity by UUID on the console.
native_entity_by_uuid finds the entity (players included) in any loaded
level and returns its dimension, id and type; native_merge_entity_nbt is
native_entity_merge_nbt for a named loaded level. Helpers only: unmodded
servers never call them. Part of #10461.
native_lod_column(dimension, column, seed, flat, column_out) is asked
for each column GeneratorWorker::lod_grid, surface_sample and
surface_column sample. A mod whose native_chunk_generate builds that
chunk answers the column's blocks (and its biome) with
column_out.replace, and the far terrain shows them, with the top, the
water over it and the spans derived as vanilla's sample derives them,
instead of vanilla's noise until the chunk generates.
Vanilla's far sample is computed first and only replaced when a mod
answers: without a mod, one empty call per column and the same columns
(#105).
native_player_fish_v2(player, name, state, hand, cancel) is
native_player_fish with the rod's hand: the hand the cast or reel-in uses
(Bukkit's PlayerFishEvent.getHand), and for a bite the hand holding the
rod, main hand first as FishingHook.shouldStopFishing looks. Asked right
after the unchanged v1 hook with the same cancel flag. CoreSMPEssentials
part B sees off-hand stasis and power rod casts.
Not built yet.
native_server_packet(player, packet, cancel) hears each play packet the
server is about to send one guest, decoded, a broadcast once for each
player, where PlayerSender::send hands it over (ProtocolLib's and
PacketEvents' outgoing listeners; NeoForge and Bukkit have no event).
A changed packet is encoded again, a canceled one is not sent. Nothing is
decoded or copied until a mod calls the helper native_watch_server_packets:
until then each packet costs one relaxed atomic load. The hook is a free
function the game sets once (lan::conn::SERVER_PACKET), as player_move is.
Part of #10461.
Both helpers were refused for any level but the one in place, so a mod
spawning from native_server_tick (where no level is defined) or into
the Nether from the Overworld's tick could not, and Stage-style ports ran
`execute in <dimension> run summon` instead. The named level is now put
in place for the call and put back afterwards, through the same
enter_command_level that `execute in` uses (npc_in_level in the new
native_npcs.rs). A level not loaded is still refused. Unmodded servers
never call these helpers, so nothing changes for them. Part of #10461.
native_right_click_block_v2(pos, face, hand, canceled) is
native_right_click_block with the hand that clicks (NeoForge's
PlayerInteractEvent.RightClickBlock.getHand), asked right after the
unchanged v1 hook with the same flag, once per hand tried. A cancel does
what a v1 cancel does. CoreWG's guard refuses an off-hand flint and steel
or bone meal; today it sees the main hand only.
Not built yet.
Keeps both test modules (mixed_size_tests, undrawn_terrain_tests) and
has_terrain with #10502's doc and undrawn flag after sizes_differ.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
c843c1aa0 (the loot-pickup save test) calls mob_to_nbt with the old four
arguments; the bridge's fifth, uuid_of, is |_| None there like the other tests.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
native_tick_times() returns the last 100 tick durations at vanilla's getTickTimesNanos indices (tick n at n % 100); native_tps() the ticks a second over one, five and fifteen minutes (Bukkit's getTPS), measured between recorded tick ends in FixedStep::record. Helpers only, fragment tick_times.
native_interact_entity_v2(entity, kind, hand, stack, claimed) is
native_interact_entity with the hand that clicks (NeoForge's
PlayerInteractEvent.EntityInteract.getHand), asked right after the
unchanged v1 hook with the same claimed flag: a click v1 claimed is not
asked again. Once per hand vanilla tries, so a mod claiming the main hand
hears one event per click (Citizens' click events and commandtrait
hands, CoreWG, CoreSMPEssentials part B).
Not built yet.
native_chunk_generate(dimension, chunk, seed, flat, chunk_out,
generate) is asked on the generation workers for every chunk, after its
biomes and structure starts and before the noise (Bukkit's
ChunkGenerator.generateNoise). A mod sets the chunk's blocks through
NativeProtoChunk::set_block and answers NativeGenerate::Replaced with
the shouldGenerate* flags: vanilla then runs only the surface rule, the
carvers (on a superflat level too), the structures and the features it
asks for. set_biomes and clear_structures work with either answer.
Left at Vanilla, nothing changes: the hook is one empty call per chunk
and every vanilla step runs as before. Replaces the cancellable HEAD on
VanillaTerrain::proto that generated the vanilla chunk and overwrote it,
and the coregen_glue module (#104).
The world's seed as /seed prints it (Bukkit's World.getSeed), for a
generator mod's preview that must draw the terrain the chunks get.
None off a server, where only a hash of the seed is known. No
behaviour change. Part of the world generation bundle (#98).
Main checked bubble columns by name (6bd507f89); this branch reads the
block's exported liquid flag, which covers the same three blocks and any
mod block marked liquid, so the name list and its test go.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Read Git's NUL-delimited UTF-8 output without newline conversion, and
include deleted source paths when files move between check scopes.
Keep pr_ready's line accounting consistent with parity selection.
Fixes#10535
Co-Authored-By: Codex (GPT-6) <[email protected]>
Most NBT strings in a saved chunk (names, ids, compound keys) are ASCII.
The reader decoded modified UTF-8 one byte at a time and the disk writer
put every unit through its own limit check. Now an all-ASCII string is
widened in one pass when read, and a string whose units are all one byte
(1 to 127) is written in one extend when it fits under the byte limit;
anything else takes the old per-unit loop, so the bytes and the error
offset are unchanged. A new test checks ASCII, zero, mixed and non-ASCII
strings against a per-unit reference and the limit's error offset.
While the dragon fight's world fog leaves out the End's sky, Iris 1.11.7 still
runs a pack's gbuffers_skybasic (render stage SKY, vertex colour fogColor), and
the gl_Vertex values our probe packs read there (render distances 12 and 8) fit
a fan around the horizon: from (0, -16, 0) under the eye to eight rim points 16
above it, every 45 degrees, at the render distance. Horizon ran no sky program
in the End, so packs that paint their sky there left that band empty.
Frame carries the boss bar's world fog; PackSky draws only the fan then, in
the fog colour.
Part of #10491
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The crossbow hit, trident hit and trident hit_ground sounds come from the same
Entity.playSound as the arrow hit (AbstractArrow.onHitBlock / onHitEntity,
ThrownTrident.onHitEntity), so they take getSoundSource(), NEUTRAL, too; the
name-based guess put them on PLAYERS.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A saved chunk's zlib stream was inflated by a new Decompress, 8 KiB at a
time into a stack block, each block then copied onto the output, which
grew by doubling from empty. Now each thread keeps one inflater, reset
for every chunk, and a whole stream that fits is inflated straight into
the output, reserved at about eight times the stream (a chunk's usual
ratio) and never past one byte over the limit. Any stream that fails
(damaged, cut short, over the limit) is decoded again the old way, so the
error and what the output keeps are exactly as before. A new test
compares both on whole, cut and damaged streams around every limit.
A failed region restores its originals before the error comes back, so
the world is whole: blocking the start then took a server down for a
corrupt chunk record. Only a recovery that cannot finish stops it now.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
DragonSittingScanningPhase.doServerTick computes Math.acos(dot) * 180.0F / (float) Math.PI
and Mth.atan2(..) * 180.0F / (float) Math.PI in double, not a multiply by the float
RAD_TO_DEG constant. Also moves the Silent test doc comment back onto its test.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The driver compiles each program of a shaders/ pack as its pipeline is made,
and a pack's pipelines were made one at a time while the pack was laid out:
Metal's compiler service sat on one core for 5 to 50 s on a big pack (BSL,
Complementary, Photon) with four others idle.
The pack is now laid out first, its pipelines recorded as jobs whose pipeline
goes in a slot of its pass, then made all at once on up to 8 threads (wgpu
keeps error scopes per thread, so each is still checked). When wgpu or the
driver refuses one, the pack is built again a program at a time, as before,
so that program is skipped with its note exactly as it was.
Cold loads on an M5 (each pack's first load, nothing in the Metal cache),
pipeline creation: Bliss 6.3 s -> 1.8 s, Solas 5.2 s -> 1.6 s, Complementary
Unbound 9.0 s -> 2.3 s.
Loading a shaders/ pack made its pipelines on the main thread, and the driver
compiles each program there: the game froze for 5 to 50 s on a big pack
(Complementary Reimagined, Photon), with no frame drawn.
The renderer now builds the pack on a "Shader pack" thread (load_shader_pack)
with its own handles to the device and the inputs, and the game keeps drawing
without a pack meanwhile; each frame poll_shader_pack checks the thread and
runs the pack once it is built, logging its programs and notes as before. A
pack the thread refuses or panics on is turned off as before. Picking another
pack, or none, drops one that is still building.
A NativeChunkSnapshot of a loaded chunk of an open level (block states,
WORLD_SURFACE heights, noise biomes, the fuzzy biome zoom's cell) that
shares the chunk's sections copy-on-write and can be read on another
thread, with native_block_map_color, native_biome_name and
native_biome_tint to draw it. Replaces the engine internals a web map
reads today. No behaviour change. Part of #10461.
AngerManagement.increaseAnger adds angerByUuid's anger for a new suspect
(removeInt, uncapped). The mob layer knows no UUIDs, so convertFromUuids' port
dropped the saved anger of a suspect that was angered again first (a reloaded
warden hearing the player within its first anger tick); it is added there now.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Direction.LEGACY_ID_CODEC reads a byte through from3DDataValue, which wraps it
(abs(b % 6)), and getIntOr("Steps", 0) keeps any int; the read clamped Steps to
0 and passed an out-of-range Dir through.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A boat's or minecart's box query (boat_contacts, boat_obstacles,
cart_entity_collisions) asked every mob of the level for its box, and
Mob::bounds reads the mob's whole size (dims: sleeping, baby, species, scale)
across several cache lines. While boats and carts tick, the widest half width
and tallest height of the mobs (BoxReach) are worked out once, and a mob whose
position is farther than that from the query is skipped without its box. The
reach is worked out again when a mob joins or leaves, and nothing a vehicle
does resizes a mob, so the same mobs are found.
native_attribute(uuid, attribute) gives an attribute's base value and its
value with every modifier (Bukkit's AttributeInstance.getBaseValue and
getValue); native_set_attribute_base sets the base as
/attribute .. base set does; native_set_attributes loads a saved
attributes list onto the player as vanilla's AttributeMap.load reads a
player's file. A lower max_health caps the health, and the client gets
UpdateAttributes from the tick's sync. Nothing calls them without a mod.
Unproven: not yet compiled or probed live (build queue); the integration branch tests it.
native_effects(uuid) reads a player's effects as NativeEffect { id,
amplifier, duration, ambient, particles }; native_add_effect gives one as
vanilla's LivingEntity.addEffect (Bukkit's addPotionEffect), so a weaker
or shorter one waits under it; native_remove_effect takes one off as
/effect clear does; native_set_effects replaces them all, what a mod
restoring a saved player needs instead of writing PlayerEffects into the
hub's guests. The client gets UpdateMobEffect with the ambient and
particle flags from the tick's sync. Horizon keeps no hidden-icon flag on
an effect, so none is offered. Nothing calls them without a mod.
Unproven: not yet compiled or probed live (build queue); the integration branch tests it.
native_player_food(uuid) reads a player's food level, saturation and
exhaustion; native_set_player_status(uuid, health, food, xp) sets each
that is given, as Bukkit's setHealth, setFoodLevel, setSaturation,
setExhaustion, setLevel, setExp and setTotalExperience. The write goes
through the player's turn as /kill and /effect reach a player
(for_player), so their client gets SetHealth and SetExperience with the
tick's sync. Health is capped at the maximum; 0 or less kills as /kill
does, with "<player> died" (Bukkit's setHealth(0), LivingEntity.die with
the generic source). The mod's food is not heard as the player's own
change (native_food_level_changed) outside their turn. Nothing calls it
without a mod.
Unproven: not yet compiled or probed live (build queue); the integration branch tests it.
The helper native_player_compound(uuid) gives a player's compound as the
server would save it now (players/data/<uuid>.dat, playerdata::player_tag),
vanilla's ServerPlayer.saveWithoutId: what a mod keeping players in step
across servers (a HuskSync port) stores, instead of calling player_tag on
the hub's guests itself. None inside a player's turn, where their live
state is out of their record, and for a player not on. The new module
native_player_state.rs holds this group's helpers. Nothing calls it
without a mod.
Unproven: not yet compiled or probed live (build queue); the integration branch tests it.
What a Horizon server sends a 26.2 client (accept_older_clients) now carries 26.2's
registry ids: tags, block states, entity, block entity, block, attribute, sound and
particle ids, and items and components through the existing Way::Down writers, which
now leave out components the client lacks. A creative slot or container click from the
client is read with its ids up.
generate_registry_id_adapter.py --substitutes writes, from both versions' data
reports, a stand-in for each 26.3 entry 26.2 lacks (poplar as birch, wool and concrete
slabs and stairs as stone ones of the same shape, ...); what has none (the cushion
entity) isn't sent. v26_2_ids.rs is regenerated with it.
Part of #10404.
🤖 Agent: a 26.2 client is now refused in configuration before any untranslated registry data, tags or play packets (bebf670); 26.3 path byte-for-byte unchanged with the option off
native_profile_by_name(name) gives the id and name, and
native_profile_by_uuid(uuid) the name, of a player who is on, else one
in a dedicated server's profile cache (usercache.json): vanilla's
GameProfileCache, Bukkit's getOfflinePlayer without its network
lookup. They ask no profile service and make up no offline player;
UserCache::find_by_name reads without changing the cache. native_player_head
names a player who is off from the cache too. Ports parse
usercache.json or ask Mojang themselves today.
A proxy that balances players between servers has to know, before it sends
one, how many players a server has and whether a login would be refused as
full. transfer-status answers with counts only: the players placed, held and
expected, the hand-overs under way, the places the player limit counts, the
limit, and whether the server is full. It names no player, changes nothing
and writes nothing to the log, since a proxy asks it often. Like the player-*
requests, it is refused while [transfer] is off.
Refs #7484
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Both tools above used to crash, or write into a flag-named path, instead of
asking for what they need. These tests run them in an empty folder and check
that they exit non-zero with a usage line and write nothing.
🤖 Generated with Codebuff
Co-Authored-By: Codebuff <[email protected]>
Both arguments were read unguarded, so running it with none, or with one, was an
IndexError rather than the command its docstring shows. Print the usage line.
🤖 Generated with Codebuff
Co-Authored-By: Codebuff <[email protected]>
The script read sys.argv[1] straight away, so a missing argument ended in an
IndexError traceback, and a mistyped flag created a directory named --help and
filled it with 66 MB of fixtures. Print the usage instead: a flag is not a path.
🤖 Generated with Codebuff
Co-Authored-By: Codebuff <[email protected]>
frame::contents made a new ZlibEncoder for every packet above the
compression threshold, which allocates and clears zlib's window and hash
tables (some 300 KiB) each time. A thread-local Compress, reset before each
packet and driven with the encoder's own calls (the input unflushed, then
finish), writes the same bytes. A new test checks them against a fresh
encoder for bodies from 256 bytes to 300 KiB, in both orders.
GameRenderer.renderLevel gives LevelRenderer.render shouldRenderSky false
while a boss bar makes world fog (the Ender Dragon's), so the frame is only
cleared to the fog colour. Horizon drew the End's sky box behind the island
during the fight. fog::sky_pass_skipped now holds vanilla's conditions:
LevelRenderer.addSkyPass's (lava, powder snow, blindness or darkness) and
the boss fog.
Part of #10491
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The description at the bottom of the page stayed the same whichever value was
picked, on the Gameplay Guide, Button Glyphs, On-screen Keyboard and Lightbar.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
native_set_inventory_slot(uuid, kind, slot, expected, new) puts a stack
in any slot of a player's inventory or ender chest, armour and off hand
included, if it still holds exactly `expected`, and returns what was
there (Bukkit's setItem). native_take_stacks(uuid, slots) takes exact
stacks out of the given slots, all or none. Both are checked like
native_move_stack and work live or on the player's file. Wanted by the
InvSee, AdminKit and Wanted ports (armour slots, splitting, stakes);
Wanted edits LanHost guests directly for this today.
Unproven: not yet built or run on a server (handed to the hooks-all integration test).
Part of #10461.
native_set_inventory(uuid, kind, slots, selected) replaces a player's 41
inventory slots or 27 ender chest slots (Bukkit's
PlayerInventory.setContents), live or in their file like
native_inventory. For the inventory it sets the held hotbar slot, empties
the cursor and the 2x2 crafting grid, and sends a player on the server the
whole inventory at once. The HuskSync port writes Guest fields directly
for this today.
Unproven: not yet built or run on a server (handed to the hooks-all integration test).
Part of #10461.
native_block_place(player, name, stack, pos, state, cancel) is heard
where BlockItem.place has found the position and the state and checked
survival, before anything is set (Bukkit's BlockPlaceEvent, NeoForge's
BlockEvent.EntityPlaceEvent). A canceled placement is a failed one: no
block, no use, no swing, and the client gets its blocks and inventory
back. The position and state come from the existing dry run
(Behaviours::placement_state). Ports cancel placements today by claiming
native_use_item_on, which also stops doors, chests and the item's other
uses.
Unproven: not yet built or run on a server (handed to the hooks-all integration test).
Part of #10461.
native_inventory_drag(player, name, window, kind, slots, cancel) hears a
guest's quick craft in their own inventory or a vanilla menu once, at its
end, with the kind and the window slots the client dragged over (Bukkit's
InventoryDragEvent). A canceled drag's end is handled like a canceled
click: the drag ends empty and the client's prediction is answered with
the server's slots and cursor.
Unproven: not yet built or run on a server (handed to the hooks-all integration test).
Part of #10461.
native_inventory_click(player, name, window, slot, button, mode, cancel)
hears a guest's click in their own inventory or a vanilla menu they have
open before any handler applies it (Bukkit's InventoryClickEvent). A
canceled click becomes a drag start of no type, which every menu takes as
resetQuickCraft and nothing more, so nothing moves and the client's
predicted slots and cursor are answered with the server's, as for any
click the server refused. Drags and a mod's own menus are not heard.
Wanted by the HuskSync and Essentials ports (data lock, freeze).
Unproven: not yet built or run on a server (handed to the hooks-all integration test).
Part of #10461.
native_player_launch_projectile_v2(player, name, stack, cancel) is heard
where ProjectileItem.use creates a snowball, egg, ender pearl or a mod's
thrown item, before the projectile exists (Bukkit's ProjectileLaunchEvent
for a player). A canceled throw is a failed use: nothing is thrown, used
or put on cooldown, v1 hears nothing, and the guest's client, which took
the item at once, gets its inventory back. Wanted by the HuskSync port's
data lock (no ender pearls while locked).
Unproven: not yet built or run on a server (handed to the hooks-all integration test).
Part of #10461.
position_riders finds every vehicle's riders in one pass over the mobs instead
of one per boat, minecart and primed TNT. A boat tests a mob's box before
anything else, and works out the scoreboard teams only when something touches
it, instead of once per boat a tick.
HORIZON_TRACE=FILE, horizon-ctl trace FILE [SECONDS] and horizon-trace on a
dedicated server's console write each span of every thread as a Chrome trace
event, so what ran beside what, and what a thread waited for, can be read off
in ui.perfetto.dev. A thread that records spans but is never taken from (a far
terrain or lighting worker while spans are on) now drops them past 16k instead
of growing for as long as spans stay on.
Load and save Evoker and Illusioner SpellTicks using the counter already consumed by casting goals and server AI. Cover the real codec and actual Game tick path.
Co-Authored-By: Codex <[email protected]>
native_player_textures(uuid) gives the textures property (value and
signature) of a player who is on, as their player list entry carries
it (Paper's Player.getPlayerProfile). native_player_head(uuid) gives a
minecraft:player_head whose profile holds the player's id, and their
name and skin when they are on (Bukkit's SkullMeta.setOwningPlayer).
Ports look the current skin up at the session server today, or build
heads with a name-only profile.
native_player_pre_login(player, name, address, defer) runs on the
connection's own thread once the session service or the proxy has named
the player and before the server's own checks (Bukkit's
AsyncPlayerPreLoginEvent). A mod that needs time, a permissions or
inventory load, or may refuse, pushes a Receiver<Result<(), Text>> onto
defer and answers from any thread: Ok lets the player on, Err refuses
them with the reason. The login waits for every answer, 20 s at most in
all; a missing or dropped answer refuses with a plain Disconnected.
Today such loads start in native_player_login, where a database failure
cannot refuse the player.
native_set_profile_property(player, name, value) sets or removes a
property of an online player's profile (Paper's Player.setPlayerProfile);
native_refresh_player(player) shows them again at the end of their
level's pass: everyone who sees them gets their list entry anew and
their entity again, and the player is respawned in place into the same
level with all data kept (Paper's hidePlayer/showPlayer plus the
respawn), so their own client draws a new skin too. Both work from any
server hook, native_server_tick included. They replace the raw HEAD
injection on Game::flush_native_net that edits Guest.properties and
sends the player info itself.
native_player_profile(player, name, properties) runs on the
connection's own thread once the server has let a player in and before
it sends LoginFinished. A mod may change, add or remove the profile's
properties (a skin's textures): the player's own client and every
player list get the new ones (Velocity's GameProfileRequestEvent,
Paper's PlayerProfile at AsyncPlayerPreLoginEvent). It replaces the raw
HEAD injections on lan::conn::login_and_configure and
Game::lan_guest_joined that skin and Floodgate ports use today.
🤖 Agent: is_prepared now answers from a 5 s per-version cache (21.4 µs to 0.21 µs per call); need_prepared and the CLI still run the full per-file check, so an empty JSON is caught before launch.
The helper native_player_permission(player) gives the permission level of a
player on the server or joining it (0 to 4, ops.json's for an operator,
PlayerList.getProfilePermissions). It works inside native_player_login for the
player that hook hears, whose level the hub set on admitting them, so a login
gate that lets operators through needs no ops.json of its own. The hook's
contract is unchanged.
The helper native_send_custom_payload_to_player(player, channel, data) queues
a ClientboundCustomPayloadPacket on any namespace:path channel outside
horizon: (at most ClientboundCustomPayloadPacket's 1 MiB) for one player's
client, vanilla or Horizon, sent at the tick's end with the mod messages, so
it works from any server hook. Bukkit's Player.sendPluginMessage.
native_custom_payload_from_client(player, channel, data) hears a guest's
ServerboundCustomPayloadPacket in play on any channel that isn't Horizon's own
(horizon:mod_net, horizon:quiet_commands, horizon:lod), which the server drops
today. Like mod messages it waits for the tick's end, with the hub back, so
every helper works; it is heard from a dead player too, as vanilla's
handleCustomPayload doesn't look at health. Horizon's own channels are handled
as before. The client side is #6774.
With proxy forwarding on (forwarding.secret, offline mode, not the integrated
server's own player) the handshake's host is read with Short.MAX_VALUE as its
limit, as Paper does behind a proxy, so a proxy's data after the host (a
Floodgate player's, often longer than 255 characters) no longer drops the
login. Without forwarding the limit stays vanilla's 255; it is the only string
read with that limit.
native_handshake(address, host, intent, cancel, reason) is a free function in
horizon_client_lib::lan::conn, called on the connection's own thread for each
network connection's handshake, before the server acts on the intent. A mod may
rewrite the host (a proxy's data after it, Floodgate's, included) or refuse the
connection: a login or transfer is disconnected with the reason, a status ping
is closed unanswered. Wired through NativeClientHooks like player_chat. Nothing
changes without a mod.
Only packet ids are translated for an older client so far (#10404): the
registry data, tags (registry ids) and play packets (chunks, light, login,
player_action numbering...) would reach a real 26.2 client in 26.3 ids and
layouts. With accept_older_clients on, such a client now logs in and is
told so in configuration, right after the known packs and before any of
them. NetConfig::older_clients_play (tests only, not in server.toml) keeps
the play path reachable for the id renumbering test until the later parts
translate the contents.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
#10389 made is_prepared parse research-lock.json and the manifest, list the
prepared folder and stat every table on each call. Pages call it while drawing
(instance rows, the Play menu, can_start), so every frame did file I/O.
is_prepared now keeps its answer per version folder for 5 s and drops it when
the launcher installs, removes, reuses resources or a job exits.
check_prepared runs the full check and refreshes the answer; starting the game
or a server (need_prepared), the CLI and the post-prepare auto-play use it, so
an empty JSON is still caught before a launch.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
native_creative_slot_v2(player, name, slot, stack, cancel) is an
additive version of native_creative_slot with the whole ItemStack put in
the slot (none for an emptied one), components included. It is heard
just before v1, which keeps its signature and firing point and hears
nothing of a packet canceled here.
Part of #10461.
native_item_dropped_v2(player, stack) is an additive version of
native_item_dropped with the whole ItemStack thrown, components included
(enchantments, unbreakable, attribute modifiers...), heard just before
v1, which keeps its signature and firing point.
Part of #10461.
native_item_picked_up_v2(player, stack, thrower, position, canceled) is
an additive version of native_item_picked_up: the whole ItemStack with
its components, the item entity's thrower UUID (none for block and mob
drops) and its position. It is heard just before v1, which keeps its
signature and firing point and hears nothing of a pickup canceled here.
Part of #10461.
native_player_spawn_location(player, name, dimension, position,
rotation) is heard once per join after the saved data is loaded (or a
newcomer's spawn found) and before the level is chosen and the world is
sent (Spigot's PlayerSpawnLocationEvent). A mod that changes them places
the player there, in that level, with no teleport afterwards. An unknown
dimension or a non-finite value leaves that part unchanged.
Part of #10461.
native_player_hurt_by_player(attacker, attacker_entity, victim,
victim_entity, source, damage_type, amount, canceled) names both players
by UUID and network entity id, and how the hit came: melee, sweep, or the
projectile's entity type (arrow, spectral arrow, snowball, egg, a mod's
thrown type). It is heard where native_living_incoming_damage is, at the
victim's hurtServer past its guards (pvp rule and teams allow it, victim
not invulnerable or dead), and just before it: changing the amount
reaches v1, canceling skips the hurt and v1 alike. The PvP call sites
name the hit in a thread-local scope that hurt_server reads.
A snowball or egg does zero damage, where vanilla's Player.hurtServer
stops: it is heard there with amount 0 and nothing the mods set applies.
Thrown tridents hurt no players in Horizon yet and are not heard.
Part of #10461.
native_player_logging_out(player, name) is heard when a placed player's
connection is gone (lost, kicked, quit) but before their menu closes and
they are saved and removed (Bukkit's PlayerQuitEvent): they are still in
their level and listed in native_server_players. No state is swapped in;
the hub is back in self.lan for the call, so the UUID helpers and
native_run_command reach them. A death a mod causes there (`kill <name>`)
is died at once (death message, drops, native_player_died), since the
leaving player gets no further tick to die in; they are then saved dead.
Not heard when the server stops, where native_player_logged_out already
tells with them still listed. Unmodded servers are unchanged: the death
step only runs when the hook took the player from alive to zero health.
Part of #10461.
🤖 Agent: Entity.checkFallDamage subtracts (float)ya into the double, which the f64::from(.. as f32) step reproduces, and Entity.load reads fall_distance back. Main merged in again after #10422 landed (GitHub read the criss-cross bases as a conflict); the diff to main is this PR's own 6 files.
🤖 Agent: `camera_list` is now collected into a Vec on every frame, even with no pack, where the old iterator was lazy; it's one small allocation per frame. The shadow-only set and the spawner's `shadow_pass_drew` scan (linear in shadow_visible per spawner) run only under a pack whose shadow draws entities.
A pack (or the dimension folder it uses) with no gbuffers program for terrain
had the game draw terrain itself, into colortex0 and depth. Iris 1.11.7 draws
none: with probe packs of ours having no gbuffers programs, at the root or in
world-1 below a root that has them, colortex0 held only the sky (or fog
colour), entities and particles, depthtex0 had no terrain, and colortex1 to 7
none either; water and lava were missing too.
When no program of gbuffers_terrain's chain (nor _solid or _cutout) is in the
folder, the pack takes solid and cutout terrain from the game and draws
nothing; the same for translucent terrain and gbuffers_water's chain. A
program that is there but fails to load still leaves terrain to the game.
Fixes#10455
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`biome_category` read 0 (CAT_NONE) in lush caves and the other cave biomes,
where Iris 1.11.7 reads 5 (CAT_PLAINS), and differed in 12 more vanilla
biomes. A probe pack of ours read Iris's value in each of vanilla 26.3's 67
biomes (`fillbiome` around the camera): they fit vanilla's `is_*` biome tags
(BiomeTags; `is_savanna` unused), then desert, swamps, mushroom fields and
snowy plains by name, NONE for the void, and PLAINS for the rest. Mountain
biomes read 17, which Iris names CAT_MOUNTAIN; CAT_UNDERGROUND is 18.
Biomes vanilla doesn't have keep the old guess from words of their name.
Fixes#10453
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Left and right changed a slider as soon as the focus passed over it, so moving
through a settings page could change a value. A now takes hold of it, left and
right move it, A or B lets go and up or down moves on, with a white outline
while held.
The gameplay guide's on/off switch becomes Full, Basic and Off. Basic keeps the
right column. A file saved with the old switch keeps its meaning.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Use the existing species maximum after shared Mob tick work, preserving the normal underwater countdown. Cover the real full-tick path and actual Game commands against fresh Java.
Co-Authored-By: Codex <[email protected]>
🤖 Agent: Mob.addAdditionalSaveData always writes canPickUpLoot(), so moving it out of the put-if-absent defaults is the fix (Allay and sulfur cube overrides now win over a loaded byte). Main merged in after #10393 landed; clippy and the entity_nbt tests pass on the merge.
🤖 Agent: piglin_replace follows Piglin.canReplaceCurrentItem in order (PREVENT_ARMOR_CHANGE first, then wanted/unwanted, then Mob.canReplaceCurrentItem), with getPreferredWeaponType null for babies.
🤖 Agent: `is_prepared` now reads the manifest and stats every top-level JSON on each call, and the launcher calls it from render paths (glass_home, glass_palette, can_start): a few hundred µs per frame at most, but a cache keyed on the folder would remove it. I dropped the unused `is_json_file`; launcher clippy and backend tests pass.
🤖 Agent: checked on Linux with mingw: the build script now compiles `main_res.o` for x86_64-pc-windows-gnu and passes it to the link, which the host-gated `#[cfg(windows)]` skipped. I kept the warn-only fallback outside CI for windres paths with a space (#10398).
🤖 Agent: causeExtraKnockback's non-living branch is Entity.push (no knockback resistance, no halving), as strike_boat does; the guest path still checks the attack range before skipping look_at_entity. We merged #10434 into this branch: both extend the boat fixture and harness.
🤖 Agent: for one-digit mantissas {:.1e} picks the closest two-digit decimal, which is what Java's Float/Double.toString does (Float.MIN_VALUE prints 1.4E-45). We switched the harness to the shared release world_probe: debug at CARGO_BUILD_JOBS=1 would have compiled horizon-world again in CI.
🤖 Agent: RealmsPdpScreen shows the trial description (and a Try for free button) only when trialAvailable; Horizon does not ask for it, so we dropped the description and kept the title with Buy, vanilla's no-trial layout. Titles are drawn plain where vanilla underlines/bolds them: small follow-up.
🤖 Agent: start() matches NeutralMob.stopBeingAngry plus IronGolem.startPersistentAngerTimer (nextInt(381) + 400 = rangeOfSeconds(20, 39)) at target priority 4. We moved the creeper test's doc comment back onto its own test: the new test had been inserted between them.
🤖 Agent: the tier fetch and both PUTs hit the same paths as RealmsClient.getRealmTierConfiguration/updateRenderDistance/updateSimulationDistance, and a reply missing either range is an error as in vanilla; nothing beyond what the vanilla settings tab offers.
🤖 Agent: only Tags move to the new id, as ConversionType.convertCommon copies them; the old raw compound (UUID, species data) is left behind, which the save/reload test checks.
🤖 Agent: hook_block_hit now runs on the client before moving, as FishingHook.checkCollision does on both sides, and gravity is f64::from(0.03f32) (getDefaultGravity is a float).
🤖 Agent: matches Pig/Strider.getControllingPassenger (saddled, first passenger a player holding the right stick in either hand); the server side reads the same rule through moved_by_client, so it no longer simulates the mount twice.
🤖 Agent: component_bool matches NbtOps.getBooleanValue (doubleValue != 0, so 0.5f and NaN are true, Short 256 too), not NumericTag.asBoolean's byteValue; that is the Hypixel glint_override report.
🤖 Agent: LivingEntity.aiStep resets fall distance for slow_falling or levitation before travel, NoAI included; dropping the later travelInAir-style reset is right, it erased the current tick's descent.
🤖 Agent: packet_utf8 differs from from_utf8_lossy only on ED A0..BF (encoded surrogates), which Java consumes as one replacement; the length limits still use the encoded bytes first and UTF-16 units after, as Utf8String.read does.
🤖 Agent: reading the bytes once and parsing them removes the exists()-then-load race too; only NotFound starts an empty list, so a directory or a locked ops.json now stops the stopped-server command instead of being overwritten.
🤖 Agent: fangs_allied follows Entity.isAlliedTo both ways (AbstractIllager tag only when both are teamless, Evoker's vex root-owner rule); guests on the evoker's team are still bitten, as before, which is a follow-up rather than a regression.
🤖 Agent: follows LivingEntity.onEquipItem (same item+components or firstTick: nothing; EQUIPPABLE on the new stack picks equip over unequip) and data merge keeps first_tick. We removed the docs/parity run-log file.
🤖 Agent: all nine families are Monster subclasses (entity.hostile.* is Monster's shared hurt/death/fall set), so HOSTILE is right; the list scan is 34 short strings per sound, negligible. We removed the docs/parity run-log file.
🤖 Agent: DataComponents.DAMAGE goes through Codec.INT (Number.intValue: truncation, Long keeps its low 32 bits), and dynamic::as_int already does that. We removed the docs/parity run-log file.
🤖 Agent: LootCommand.dropFishingLoot's no-tool endpoint passes ItemStack.EMPTY, and the code only reads tool_stack when a tool, mainhand or offhand was given, so an omitted tool stays empty. We removed the docs/parity run-log file.
🤖 Agent: Pig BABY_DIMENSIONS has PASSENGER (0, 0.5, 0), so the one match arm is right. We removed docs/parity/baby-pig-passenger-attachment.md: run logs and hashes belong in the PR description, not in docs/.
head_skin decoded the textures property (base64, JSON, URL checks) and hashed
the skin location (SHA-1) for every head on every frame, before asking the
atlas whether the skin was resident; an armor stand's worn head also turned
its profile NBT into JSON each frame. With 60 stands wearing textured heads
and 36 heads in the inventory that was ~9 us a head a frame (#4664: a server
full of menu heads, CPU-bound with the inventory open).
The pure part (profile -> textures -> skin location, failures included) is
now kept in a bounded cache beside the resolver; worn heads reuse the JSON of
an unchanged profile. The download state, atlas residency, drawn tracking and
eviction re-uploads still run per draw.
`Game::shoot_crossbow` set the arrow's weapon and Piercing count but never
ran the weapon's `projectile_spawned` ignite, which only the bow path
applied. In vanilla, `AbstractArrow`'s constructor runs
`EnchantmentHelper.onProjectileSpawned` for every arrow built with a
weapon, bow or crossbow, so a Flame crossbow shoots burning arrows that
prime TNT and set entities on fire.
The bow's `bow_projectile_ignite` becomes `ignite_spawned_arrow(weapon,
arrow)`, shared by both weapons; the crossbow ignites each arrow of its
volley after Piercing, as the constructor does. The port map rows follow
the rename, and a resource-backed regression checks no Flame, Flame and
Flame with Multishot.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
🤖 Agent: confirmed in Blocks.<clinit>: water, lava and bubble_column are the only three blocks with .liquid(), so the name list is complete for Block.isPossibleToRespawnInThis.
🤖 Agent: `tick_blocks` starts a fresh `laps` after the scheduled ticks, so the stretch between `tb_entity_inside` and `tb_broadcast` shows only through `random_ticks`, `tb_scheduled` and `tb_fluids`. The counters and laps cost one relaxed load each when spans are off.
🤖 Agent: HappyGhast.getHurtSound/getDeathSound pick GHASTLING_* by isBaby, and happy_ghast::sounds already does exactly that, so the new voice_sounds arm is the whole fix.
🤖 Agent: checked against 26.3: `ChunkAccess.getHeight` returns `getFirstAvailable - 1` and `getRandomPosWithin` adds the 1 back, so topEmptyY is Horizon's `world_surface` itself. The old +1 made the bound 130 for a column solid to y 63. run-parity's natural_spawn harness checks the draws end to end.
🤖 Agent: rowing_time now gates on paddle_controller like AbstractBoat.getPaddleState (flag AND a controlling passenger) and clamps a as Mth.clampedLerp does; #10386 carries this branch merged, queued after it.
🤖 Agent: the `[false; 256]` table relies on a local palette never holding more than 256 entries (global palettes keep the per-block path). The test covers 1, 2, 256 and 300 states, a stale palette entry, and the LCG draws against an independent oracle.
🤖 Agent: matches Entity.saveWithoutId: the orb's command NBT now reads its registered uuid_for(id) instead of a fresh random UUID per read; queued first of the 10393/10422/10458 stack (the other two carry this branch merged).
🤖 Agent: `native_bake_layer` re-reads entity-models.json on every call for a vanilla layer no mob has (the doc says so), so a mod that bakes per frame would hitch; caching that document is the natural next step. The humanoid-against-zombie#main bake test is the right oracle, and the fragment stamps cleanly (tests.test_stamp_native_hooks).
🤖 Agent: the early return for an unchanged size stays ahead of the new `device.poll(wait)`, so the GPU sync happens once per real size change. A drag-resize pays it per new size, a fair price for not holding both sets of buffers.
🤖 Agent: `world_seen` never resets inside a `Custom`, so the restart on entering the Nether relies on the pack being rebuilt for each dimension. If `PackPost` is ever kept across dimensions, it will need resetting there.
🤖 Agent: only `Source::Color` gets the linear-mip sampler; shadowcolor, custom textures and the lightmap keep `Linear` (nearest level), which matches what was measured. Buffers without mipmaps read the same as before.
🤖 Agent: the 256ths weights are a fit to one NVIDIA GPU but now apply on every GPU, so an AMD or Intel reading of Iris may move them later. The cost is two floors and a multiply per filtered shadow read.
🤖 Agent: the spectator rule keys on `pack_ids.is_some()`, so a native pack's shadow keeps the first-person model, as the test pins. The other game modes stay as before, since only spectator was measured.
🤖 Agent: the +1/32 bias moves only places within one float step under a 32768th line, so #9839's water tops (29094.34) and 13/15 offsets (28398.93) still floor as before, and the new test case pins the 1, 0.5 and 9.5 edges.
🤖 Agent: with the near and far planes dropped, the shadow pass now draws every section inside the box's sides out to its reach, so a pack with tight planes draws more shadow terrain than before. Worth watching in the cave perf numbers. #10471 conflicts with this in gfx.rs; I'll merge main into it once this lands.
Both extend the boat authority fixture and harness: keep the paddle and
attack cases, both Case fields and both docstring paragraphs. The harness
keeps main's fast-profile build with #10434's --resources option.
Queued after #10434, so either landing first leaves the other mergeable.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
All three add a command_exec fixture group at the same lines and a test
module at the end of entity_nbt.rs; keep every group, flag, dispatch block
and module. Queued in order so whichever lands first leaves the others
mergeable.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Both add a command_exec fixture group at the same lines; keep both groups,
their isolation sets, flags and dispatch blocks. Queued in order so either
landing first leaves the other mergeable.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A debug world_probe at one job compiles horizon-world from scratch beside
the release probe every other horizon-world harness uses.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Horizon does not ask trialAvailabilityTask. Vanilla shows the trial
description (and offers the trial in the footer) only when one is
available; otherwise the header is the title alone and the footer offers
Buy, which is what this screen shows.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The PR description already holds the vanilla reference and the measured
results; docs/ keeps player and contributor documentation only.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The PR description already holds the vanilla reference and the measured
results; docs/ keeps player and contributor documentation only.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The PR description already holds the vanilla reference and the measured
results; docs/ keeps player and contributor documentation only.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The PR description already holds the vanilla reference and the measured
results; docs/ keeps player and contributor documentation only.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The PR description already holds the vanilla reference and the measured
results; docs/ keeps player and contributor documentation only.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The GNU windres the launcher's native mod builds use can't take a path with a
space, so a local build there must go on without the icon (#10398 asked to keep
it); CI still fails without it. One Windows block for the stack and the icon.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
native_log_line(level, thread, message, line) hears each message
written to logs/latest.log, on the log's writer thread after the file
has it, so the thread that logged never waits on a mod and a Discord
console bridge needs no tailing of latest.log. line is the text as the
file got it. The logger calls it through logging::set_line_hook,
installed before the log opens; trait mods are reached through
NativeClientHooks. A callback that panics is not called again, so its
own panic line can't loop.
Refs #10461
native_console_line(source, line, cancel, replies) hears each line the
dedicated server's console is about to run, before vanilla's commands,
the dedicated-only ones and mods' registered ones: source is console
(typed on the console, or sent by horizon-server as the console) or
rcon. Set cancel and it does not run; replies are logged as the
console's feedback, so they also reach the RCON or horizon-server
caller. Until now native_perform_console_command only heard commands
a mod registered, so an audit mod could not see ordinary console
commands (Bukkit's ServerCommandEvent / RemoteServerCommandEvent).
Refs #10461
A mod limits one player's view distance below the server's: only that
player is told its radius (SetChunkCacheRadius) and is sent and tracks
chunks within it; native_player_view_distance reads it. The guest's
distance is ChunkMap.getPlayerViewDistance under the limit; with no limit
(every unmodded server) it is exactly as before. Part of #10461.
A level's world border bounds as the server enforces it, for a web map's
border line and a pre-generator's border selection. Vanilla 26.3 keeps a
border per level (ServerLevel.getWorldBorder, each level's
data/minecraft/world_border.dat); Horizon keeps one for every level, so
the helper answers the same bounds for each level until that is fixed.
No behaviour change. Part of #10461.
native_force_chunk only reaches the level in place, so a mod could not hold
a chunk of the Nether or the End from a server-wide hook, and a level not
open yet never ticked: a pre-generator for those levels never advanced.
native_force_chunk_in takes the dimension; a level held this way opens at
the next server tick and ticks while held. Unmodded servers hold no mod
tickets, so nothing changes for them. Part of #10461.
native_player_tick_pre() runs once a tick for every living player in the
world, at the top of their ServerPlayer.tick, before their digging, item
use and cooldowns. native_player_tick (API 31) already ends the same tick.
It fires in tick_guests after the checks that skip a dead player and one on
a server trip, so the pair brackets the same players. Single player's own
player is a guest of its internal server.
The fragment player_tick_pre.json labels it PlayerTickEvent.Pre (side
server, not cancellable) for horizon-port-pack events. 23 of ATM10's mods
listen to it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
SDL rows match a firmware version exactly, and gilrs' copy has no row for
045e:0b13 at 0x0520. The pad then falls back to gilrs' default mapping: its
right stick is on ABS_Z/ABS_RZ, which nothing read, its triggers on
ABS_GAS/ABS_BRAKE, which that mapping doesn't name, and X and Y stay crossed
because it lists C and Z.
Recognise that layout from what the pad reports, so a new firmware works
without a row.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Terrain's `mc_Entity.y` was -1 on every block but water and lava, and -1 on
those too when the pack has no block.properties. Iris 1.11.7, with probe packs
of ours, reads 1 on water and lava and 0 on every other block, whether
block.properties maps it, leaves it out, or the pack has no such file.
The sprite layers' table now starts each layer at (-1, 0), with y 1 on the
fluid sprites, before block.properties is read.
Fixes#10452
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A composite program writing two colortex buffers was skipped whenever their
`size.buffer` lines differed, even when one relative size and one pixel size
came to the same size (`0.25 0.25` and `320 180` at 1280x720). Iris 1.11.7
draws it (a probe pack of ours: texel (100, 50) of both reads 100.5, 50.5 and
texcoord 0.314, 0.281).
Sizes of one kind that differ still skip the program as the pack loads. A mix
of the two kinds is kept, and the pass is skipped only at render sizes where
the buffers it writes resolve to different sizes.
Fixes#10454
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A shader pack's terrain gl_Vertex floors each vertex's place in its
section to a 32768th. Plants whose random offset is 9/15 of a block came
out a 32768th under a half block, where our probe packs read Iris
1.11.7's exactly on it: we guess, untested, that our float sums put them
a hair under the line. A place up to 1/32 of a 32768th under a grid line
is now on it.
Part of #8464
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A pack's texture() on shadowtex0/1 through a plain sampler blended the
four texels with the exact fraction. Iris 1.11.7's filtered reads on our
NVIDIA GPU fit weights in whole 256ths: the fraction rounded half up on
each axis, the far corner's weight their product over 256 rounded half
up, the others the rest of each axis. #8464's lit probe then differs
from Iris's at no pixel of ow_ground_noon.
Part of #8464
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A shader pack's shadow pass drew the block entities the camera's view
had built: a chest beside the view, in a section whose terrain the
shadow pass draws, was left out, and a spawner's mob out of the view
and out of those sections went in. Our probe packs' Iris 1.11.7 shadow
maps hold a chest wherever the shadow pass draws its section's terrain
and nothing of the one spawner in our scenes, which lies outside them.
The block entities of the sections the shadow pass drew last frame are
now added hidden from the camera, and a spawner's mob that is out of
the view and out of those sections is left out.
Part of #8464
Co-Authored-By: Claude Opus 5.5 <[email protected]>
In first person, a shader pack's shadow pass drew the spectator's own
model (its head) at the camera. Our probe packs' readings of Iris
1.11.7's shadow map in spectator mode hold the terrain below the camera
there.
Part of #8464
Co-Authored-By: Claude Opus 5.5 <[email protected]>
37 of ATM10's mods listen to the parent ModConfigEvent, which hears the
three events NeoForge's config page documents under it: Loading, Reloading
and Unloading. config_loading, config_reloading and config_unloading answer
them, so the parent now counts as hooked rather than as another phase's:
93 of 326 names, up from 92.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A server.toml option, [server] accept_older_clients (horizon-accept-older-clients), off
by default. On, a login handshake stating 776 opens an adapter::ClientSession on the
connection instead of being refused, and a 26.2 status ping is told its own protocol.
The session follows the connection's states as Session does, mirrored, and renumbers
packet ids both ways with the inverse of the generated 26.2 tables, in the blocking
login path and in the io_pool play path. Off, nothing changes.
Part of #10404.
`RealmsSelectWorldTemplateScreen.Entry`'s 15x15 `icon/link` and
`icon/video_link` buttons beside each row's version, opening the
template's page or trailer; the drawn rectangles are kept for the click.
Co-Authored-By: ZCode <[email protected]>
Adopt and project the existing fall counter, including Java's per-movement float narrowing before double accumulation. Preserve travel and landing-call order, and cover the real tick/save route plus decoded saves without raw data.
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
🤖 Agent: staged files now get the current time, so a Nightly's source unpacked with its commit's older mtimes can no longer pass for already-compiled crates (Windows/macOS fs::copy kept them); one-time stage rewrite for old stages.
A custom uniform's smooth() restarted on every frame whose camera biome was
unknown, so after each teleport it started again, and it started from the
unknown frame's values (biome -1, biome_category 0, temperature 0): the
frame that restarted it also stored them, and the first frame that knew the
biome went on from there.
Now smooth() restarts only until the first frame that knows the camera's
biome, that frame included, and then keeps going for as long as the pack
runs. With a probe pack of ours, Iris 1.11.7 kept smoothing across
teleports within a dimension and started again when the player entered
the Nether.
Fixes#10450
Part of #9307
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A shader pack's colortex read between two mipmap levels (textureLod at a
fractional level) took the nearest level: the buffers' sampler had the
default, nearest, mipmap filter. In Iris 1.11.7 our probe packs read every
colortex, in composite and in final, as the two nearest levels mixed. The
buffers now get a sampler of their own that blends them; shadowcolor and
the other textures keep theirs (not measured).
Fixes#10451
Part of #9307
Co-Authored-By: Claude Opus 5.5 <[email protected]>
NeoForge's documentation describes one event under two names. 1.21.1's
"Registering Additional Models" (ModelEvent.RegisterAdditional, one
ModelResourceLocation per model) is "Registering Standalone Models"
(ModelEvent.RegisterStandalone, a key and a baker per model) from 1.21.5 on.
Both versions use the same sentence, and the example method is still
registerAdditional. standalone_models (API 151) answers it, so the 47 ATM10
mods that listen to it now count as hooked: 92 of 326 names, up from 91.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A Nightly's source keeps its commit's mtimes when unpacked, and fs::copy
keeps them on Windows and macOS, so a changed engine file reached
native/stage/ older than the previous build and Cargo reused the old
horizon-core. sync now sets each written file's mtime to now; a stage
written before this is rebuilt once.
Fixes#10421
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The screen behind Add Realm is 26.3's `RealmsPdpScreen`: the header's
logo, the trial header, then the friends, minigames and private-server
entries with their 80x45 pictures and 182-wide text, scrolling between
the header and the footer, whose buttons are the PDP's own title (the
buy link) and Back.
Its old strings were dead: `mco.title` never existed, and 26.3's
deprecated.json removes `mco.selectServer.popup` and
`mco.selectServer.buy`, so the screen drew raw keys.
Co-Authored-By: ZCode <[email protected]>
Keep the command-block /say LAN regression and upstream legacy PvP checks,
with both registrations intact. Preserve upstream function bodies and the
original say regression unchanged.
Validation: Python syntax/AST and registrations checked; public-tree check
passed; Python unittest suite passed (41 skipped). pr_ready could not start
Rust checks because Cargo is unavailable in this restored environment.
Co-Authored-By: Codex <[email protected]>
🤖 Agent: options.txt is vanilla's key list again; the emote key is saved as emoteKey in horizon-extras.txt and an old key_key.emote line is migrated, options parity passed on the desktop.
Our probe pack's Iris 1.11.7 shadow maps hold the terrain at every depth we
put it along the light (from 535 blocks beyond the shadow camera to about 120
before it), whatever shadowNearPlane and shadowFarPlane the pack sets, or
none. Horizon dropped the sections outside the shadow projection's near and
far planes; since #9768 those are the pack's own planes with no offset, so
in our probe's cave scene with the planes at 0.05 and 256 the surface toward
the light was missing from the shadow map. A shaders/ pack's shadow pass now
culls by the box's sides only; Horizon's own packs keep their planes.
Fixes#10403
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A NeoForge mod builds its entity models in code (createBodyLayer: a
MeshDefinition of CubeListBuilder boxes at PartPoses, a LayerDefinition),
registers them in EntityRenderersEvent.RegisterLayerDefinitions, bakes them
with EntityModelSet.bakeLayer and draws the ModelPart tree it posed in
setupAnim with ModelPart.render. Native mods get the same:
- the builders and ModelPart, ported from vanilla (mobs/model_layers.rs):
box UV from texOffs, mirror, deformation, texture scale and visible faces
as ModelPart.Cube bakes them; poses, reset_pose, get_child;
- native_layer_definitions, a collect hook asked once at start, keyed by
ModelLayerLocation ids in the mod's own namespace;
- native_bake_layer: a registered layer, or vanilla's by its
entity-models.json name (minecraft:zombie#main);
- native_draw_model_part: ModelPart.render under a mob's or block entity's
frame, textured from an atlas sprite.
The humanoid mesh built with these builders bakes to exactly vanilla's
minecraft:zombie#main (an ignored test against the prepared resources).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: the kept buffer is cleared at the end of every tick, so no villager Arc<villager::Seen> outlives the tick, and #[inline] on Mob::seen only changes where each Seen is written (in its slot instead of a stack copy): same values, same order. creature_prepare 1.2 → 0.8 ms in both A/B rounds.
Options parity regression from #10092: key.emote was in the vanilla key list, so options.txt
gained a key_key.emote line and every key after it shifted. It is now a Horizon-only mapping
(HORIZON_MAPPINGS): bound in Controls and translated like the others, saved as emoteKey in
horizon-extras.txt. A key_key.emote line written by the broken build is read once and dropped.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Brings the branch up to date and re-runs CI: verify failed once in
horizon-relay's slow_first_lines_and_too_many_connections_are_closed (a
timing test, 5.6 s against its 3 s on a loaded runner), which this
change doesn't touch.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
🤖 Agent: a mob that did not tick still gets its last position refreshed (no step), so one moved by a command while out of range doesn't count a walk or a landing once it ticks again; damage and mount events stay for every mob since vanilla raises them from hurt/startRiding. Proven on vanilla 26.3 with ore_probe2.py (settled pig/item stop lighting redstone ore once the player is 6 chunks away).
Rebuilding a changed or new section's random-tick candidate mask asked
whether the block state ticks randomly for each of its 4,096 blocks. A local
palette holds a handful of states: each entry is now classified once and the
mask built from the decoded palette indices. Single-value and global
palettes keep the previous path. Same mask, same draws.
Write CanPickUpLoot on every save through the existing virtual getter, preserving the Allay and sulfur-cube overrides instead of retaining stale raw bytes. Cover decoded and mutated models plus frozen actual command reads.
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
NaturalSpawner.getRandomPosWithin draws Y in [minY, getHeight(WORLD_SURFACE)
+ 1], and ChunkAccess.getHeight is getFirstAvailable - 1: the top is the
first empty block. Horizon's world_surface already returns that block and
the spawner still added one, so every start could land one block higher
than vanilla's, in the air above the surface, where vanilla's draw never
goes; nextInt's bound was one larger too, so the draws differed from
vanilla's from there on.
🤖 Agent: idle entities (outside the simulation-level entity-ticking range) no longer run sendChanges, as in vanilla's ChunkMap.tick, so their 60-tick MoveEntityPos stops; a bare state (no data) is never paired, and the idle check follows the tracker's viewers (placed or held). tick_lan −1.66 ms (−24 %) in 3 A/B pairs.
🤖 Agent: queued. Every tracked entity now records what it rides, so any passenger (item/block display included) hides the player's tag as vanilla's isVehicle does; one risk checked: a removed vehicle leaves a stale id in passengers' vehicle, harmless since tags look up live entities.
🤖 Agent: the gate is the set of mobs the loop actually ticked (Creatures::ticked), not a second ticking-area test, so a mob that walks out of range during its own tick still finishes its pickups as in vanilla; proven against a vanilla 26.3 server (pickup_probe.py: only the ticking villager and zombie take their item).
Vanilla's LivingEntityRenderer.shouldShowName hides a name tag for any
entity with a passenger (`!entity.isVehicle()`). The client only counted
mobs, text displays, armor stands and interactions as passengers, so a
server that hangs its own name over a player with an item display, a
block display or any other entity still got the player's tag drawn
under it: two names (#5666).
Every tracked entity now records the vehicle SetPassengers puts it on,
and the name tags read that, whatever the passenger's type. The typed
lists stay for placing the riders.
The rest of #5666 was fixed already on main: the sword drawn in the
off hand by #5111 (each remote hand renders its own stack), the double
tag of custom-named players by #8457, and text display and armor stand
riders by #9754.
Fixes#5666
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A pack with large buffers (SEUS PTGI at 3440x1440) held both sets at once while resizing and ran
out of video memory going back to fullscreen.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A slow tick named spawn_natural, tick_blocks and tick_creature_entities but
nothing inside them. With 100 bots they take ~11, ~21 and ~10 ms a tick, and
an index for spawn_selection's nearest-player shortlist saved nothing: the
time was elsewhere. Their steps now have laps (sn_*, tb_*, tc_*), and the
natural-spawn pass counts its attempts and spawns (sa_attempts/sa_spawns,
one note a tick). They show the attempt loop itself: ~18,850 attempts a tick
for ~3 spawns (#3910). Only when HORIZON_SPIKES is set.
Steps, landings, splashes, flaps and stepping on redstone ore come from
an entity's own tick in vanilla (Entity.move, applyEffectsFromBlocks
from LivingEntity.aiStep and ItemEntity.tick), and ServerLevel.tick
ticks only the entities in entity-ticking chunks. Here every loaded mob
and dropped item on the ground stepped on the block under it each tick:
a pig or an item that had settled on redstone ore kept lighting it after
the player went six chunks away.
Those events are now for the mobs tick_creature_entities ticked (and
those standing in the ticking area, as the sculk fixture posts before
the mobs tick) and the items in the ticking area. A mob that did not
tick keeps where it is as its last position, without a step. Damage and
mounting are raised by hurt and startRiding, so they stay for every mob.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Reuse the existing identity getter rather than generating a UUID per read. Cover repeated saved reads, absent raw input and stale raw UUIDs through the actual Game getter and command fixture.
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
RealmsSettingsTab's two distance sliders, fed by
getRealmTierConfiguration (asked until it answers, as
tierConfigurationFetched does) and saved with updateRenderDistance /
updateSimulationDistance only when a value changed, alongside
saveSettings' existing name, description and region update.
Co-Authored-By: ZCode <[email protected]>
tick_creature_entities snapshots every loaded mob (`Seen`, ~400 bytes)
before the loop and again after each mob's tick. Mob::seen lives in the
client lib and was not inlined into the game crate, so each one was
built on the stack and copied into its slot, and the snapshot's
megabytes were allocated and freed every tick.
Mob::seen is #[inline] now, and the emptied buffer stays in Creatures
for the next tick. The snapshot holds the same values.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Part of #9998.
Startup parsed entity-models.json (2.4 MB) into a serde_json::Value six
times: for the handoff's GameCpuResources, MobTypes, the player type, both
player arm models and the spin-attack model. The rest worker did five of
those one after another. Nothing in the document changes during a loader
generation, and a pack change starts a new one.
load_resources now parses it once on its own step beside load_core and
shares it as an Arc<EntityModelDocument> with every model loader. A
missing or malformed document leaves each caller on its old path, so
errors and fallbacks are unchanged; a dedicated server draws no models and
skips the parse.
Callgrind, the whole resource load in the game (to "resources loaded"):
21.576 G -> 20.631 G instructions (-4.38%); the entity-model path alone
1.594 G -> 0.349 G (-78.1%). A new test builds MobTypes from a shared
document and from its own parse and compares every type, model and layer.
A first draft by DeepSeek V4.1 Flash (pi coding agent), reviewed by Claude.
Co-Authored-By: pi coding agent (DeepSeek-V4.1-Flash) <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Fekd3xkgubE8VfXrALh2td
light_chunk filled its 48x48xheight region from every non-air section by
unpacking 4,096 state ids into a scratch array, then reading each one
back, looking up LightTables::info and storing 2 of its bytes. That round
trip was 35% of a light job, and the flood fill only ever reads those 2
bytes.
PalettedContainer::unpack_ids_mapped decodes the packed storage through a
lookup table in one pass (out[i] = map[id of entry i]), with a fast path
for 4 bits per entry, a linear palette's usual width. light.rs's new
states_of maps a section's palette to the packed light info once (at most
256 entries), then copies the region's 16-wide rows from the staged
values. A section whose palette emits light stages full infos and seeds
block light and the queue per cell, in the same order as before. Air now
writes info[0], which is 0, the value the region already holds (asserted
in debug builds).
Callgrind, gen_light_bench --seed 1234 --radius 4 --threads 1 (81 lit
columns): the light job 1.991 G -> 1.260 G instructions (-36.7%), the
whole run -1.15%. The light job is 5-7% of the workers' CPU, so this is
about 2% of the chunk pipeline.
Exact: gen_light_bench's block and light checksum is identical for three
seeds and centres at 1 and 4 threads, light_chunk_matches_old_algorithm
(an independent light implementation over random 3x3 regions) passes
unchanged, and the new section_states_match_the_unpacked_ids compares
states_of with a walk over unpacked state ids for one value, 4 to 8 bits
and the global palette, with and without light sources.
A first draft by DeepSeek V4.1 Flash (pi coding agent), reviewed by Claude.
Co-Authored-By: pi coding agent (DeepSeek-V4.1-Flash) <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Fekd3xkgubE8VfXrALh2td
A killed exporter left an empty gameplay-facts.json (or another table) behind.
The preparer only checked that the path exists, so "prepare again" kept the
empty file, and the launcher called the folder prepared and started the game
on it. Trust an earlier export only when it still parses, and treat a folder
with an empty top-level JSON table as not prepared so Play prepares it again.
Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
Part of #9996.
BlockSection::set and BlockSection::fill_run are 7.0% of the proto phase
in the overworld and 9.4% in the Nether (callgrind on generate_proto).
Each write located its packed word twice, and fill_run rebuilt the word
mask for every block.
PackedStorage now hands out a Slot (word and bit shift) that read_at and
write_at take, so set locates the entry once; the palette-growth branch,
which re-packs the words, looks it up again. A run in one column moves
256 entries per Y, so while a word holds a whole number of those steps
(every width generation produces) the bit shift stays the same along the
run and WordRun writes each block with one load, mask and store. Widths
that do not divide the stride go one set per block, with the same palette
edits in the same order.
Live palette slots hold distinct states (sections loaded from disk are
rebuilt through set), so "already this state" is old_id == id and "was
air" is old_id == air_id, both resolved once per call. set on a uniform
section has its own small path.
Palettes, their order, the free list, counts, the chosen width and the
packed words are unchanged: the 8 chunkgen_bench snapshot files (terrain
and decorated; overworld seeds 12345 and -1, the Nether, the End) are
byte-identical to main, and a new unit test replays random set/fill_run
sequences over every palette width against main's fill_run.
Proto-phase instructions: overworld 6.497 G -> 6.340 G (-2.41%), Nether
2.974 G -> 2.859 G (-3.84%).
A first draft by DeepSeek V4.1 Flash (pi coding agent), reviewed by Claude.
Co-Authored-By: pi coding agent (DeepSeek-V4.1-Flash) <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Fekd3xkgubE8VfXrALh2td
The not-installed Zoom row pushed Page::Mods from Extras, which
every_setting_and_page_is_reached_from_one_place refuses: Options > Mods
already has the one way in. It is now a note that says where to install
Zoom; once the mod is built in the row is still its switch and Settings
button.
Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
🤖 Agent: Checked on the desktop against main 9f8f6842d (merges cleanly, lock passes --locked). All 19 new lock packages are permissive (MIT/Apache/BSD-3/ISC-style webpki/Zlib/BSL option on ryu); the only build scripts (mysql, mysql_common) just pass target env vars, no downloads; TLS is rustls+ring only, flate2 uses miniz_oxide, no openssl/native-tls/-sys/cmake. cargo tree for horizon-client (default, --no-default-features, --target all) is identical to main, so players' game and native-mod compile are unchanged; the launcher's cold cargo fetch gains 21 crates / +2.5 MB once (noise in wall time). Note for later: webpki 0.22 and rustls-pemfile are unmaintained upstream; worth moving to mysql 28 when it no longer rewrites the lock.
serde's "expected value at line 1 column 1" says nothing about which of the
prepared folder's tables is damaged, so a player with a killed export only saw
a crash report. Read the core tables through horizon_world::json_file, which
prefixes the path, and have the startup check name a top-level table left
empty before the window opens.
Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
While Tab is held every frame sorted every entry twice (once for the faces,
once for the rows), allocated team keys inside the comparator, rebuilt and
measured every row's text, split the header and footer, and pruned the
health states with a scan per row (#10108). TabList now keeps the sorted
order and the laid-out rows, rebuilt only when their inputs differ (players,
scoreboard, header/footer, width, font and language); the faces reuse the
order; sort keys are borrowed; the health states go by a uuid set. Hearts
still animate every frame.
Mob.aiStep's looting, the villagers' item sensor and the fox, dolphin,
panda, piglin, allay and sulfur cube item passes run after the mob loop
over every loaded mob. In vanilla they are part of the mob's own tick,
and ServerLevel.tick ticks only the entities in inEntityTickingRange: a
villager six chunks from the nearest player, loaded but outside the
simulation distance, took the bread at its feet here and not there.
The loop records the mobs whose tick ran (Creatures::ticked), and those
passes leave the others alone.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
In adventure mode, vanilla allows ItemStack.useOn if the item's can_place_on component matches the clicked block. Previously, use_map_on_banner checked only !self.may_build() and returned early without checking can_place_on.
This honours can_place_on_in_adventure on the banner's block position, allowing adventure mode players with matching maps to toggle banner markers.
Co-Authored-By: Antigravity <[email protected]>
🤖 Agent: each mob footstep scanned every mob three times (two id lookups and the warden/allay listener loop); now an event with no sculk block in reach and no warden or allay within 24 blocks skips the context and the listeners. Listeners give up past 16 blocks before any side effect, so vibrations are unchanged.
Override only ArrowSound::HitGround through Audio::play_on. Add an actual Game/Java block-hit capture using the existing headless client, Studio recorder and official sound codec.
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
`Warden.addAdditionalSaveData` stores an `AngerManagement`: a `suspects` list of
{uuid, anger} pairs, and nothing else about the anger. Horizon wrote an empty
`anger` compound and read nothing back, so a reloaded warden was calm.
`AngerManagement` now keeps the suspects that are not in the level by UUID next
to the live ones, both calming by one an anger tick, and looks the UUIDs up
again every two anger ticks through the game layer's map
(`Game::who_by_uuid`, reached from the mob layer through `MobWorld`'s new
`WhoLookup` seam). The chunk save names a suspect by its UUID, minting one for
a loaded suspect that has none yet.
A save can now name an entity it only knows as a `Who`: `mob_to_nbt` takes a
`uuid_of` resolver, `who_uuid` writes a `Who` as a UUID and `who_by_uuid` turns a
stored UUID back into the `Who` of a loaded player or mob. `EntityMemory::uuid_of`
supplies the id -> UUID map the game layer keeps, and the game layer's `mob_by_uuid`
now resolves through the same seam.
The neutral mobs' `angry_at` is the first writer: anger at another mob now saves
that mob's UUID instead of nothing. The warden's suspect list and the shulker
bullet's target are the two follow-ups this bridge unblocks; neither is written
here.
🤖 Agent: launcher-only fix. Maps the service's 403 use_checkout / not_offered_here to their own messages (86 tables), and drops one_click after use_checkout so the Shop opens the free checkout. Risk: none on the hot path; only the claim's error branch changes. Launcher tests 719 passed on the desktop.
🤖 Agent: villager_pickups tested every loaded villager against every dropped item, looking up tags before the reach: that was 12.8 of 23 ms of the mob phase in the load test (22.7 → 8.1 ms after this). The new ItemScan keeps the full scan's order and falls back to it if the item list changes under it.
🤖 Agent: merging. Every knock_back caller outside the legacy rules passes legacy = false, so the 26.3 path is unchanged. 1.8's air lift and the unhalved sprint push only run under the hosted-world switch.
The cosmetics service answers POST /v1/claim with 403 use_checkout where
Paddle sells (the set goes through its free checkout there) and 403
not_offered_here where the claim is left out. The launcher mapped neither,
so a player saw "Horizon's cosmetics service had a problem" (Discord bug
thread, 2026-11-05).
- Both codes are the Shop's own refusals now, each with its own words, in
all 86 lang tables.
- After use_checkout the saved answer drops one_click, so the Wardrobe stops
offering "Claim for free" and the Shop's button opens the free checkout on
playhorizon.dev.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merging. [combat] is only written when legacy is on, and unknown keys are rejected. The game reads it in dedicated.rs, and a bad section logs and keeps 26.3's rules. The test now looks for the section line, not the template's '# [combat]' comment, which made verify fail on the first head.
🤖 Agent: merging. legacy_rules() is remote.is_none() && combatRules, so a window on any server keeps 26.3's cooldown and damage. Measured over LAN: 1.8 rules 7 and 7 (diamond axe, charged then right after a swap), 26.3 9 and 3.6.
Every frame built, styled and measured the name tag of every player within
64 blocks, drawn or not: with 150 players and the camera facing the sky that
was ~0.74 ms a frame (#4664). A tag is now built only for an entity vanilla
would render, its culling box grown by 0.5 in the frame's frustum
(EntityRenderer.shouldRender), checked before any text work; armor stands
likewise. Status and native snapshot queries still see every tag.
other_players finds each player's entity through one id->index map instead
of scanning every entity per player (the first match, as iter().find).
🤖 Agent: merging. The 1.9 hit sounds and particles are filtered on receipt (remote.rs, lan_particles.rs), so nothing sent changes. Hurt sounds and crit particles stay, and 1.8 hit sounds are off by default.
Remove the blanket trait-mod presence bypass while preserving shader-pack guards. Verify the projected shadow in a game with an active native camera hook.
Co-Authored-By: Codex GPT-6 <[email protected]>
🤖 Agent: merging. Legacy only moves the hand's matrices, and only for items with vanilla's flat display (pack-moved and post-1.8 items keep 26.3's). It's off by default, and the renderer is the only reader of the equip state it changes. On scripted fights against a vanilla server, #10117's Modern-vs-Legacy packet comparison matched on every scenario that is stable between two identical clients.
Zoom moved from the built-in extras to Horizon's official native mod
(#2030), so the Extras page lost its Zoom setting. Players looking for it
there, including the macOS report in #8713, found nothing.
The Extras page now keeps a Zoom row: with the mod built in it is the mod's
own on/off switch plus a Settings button that opens its page; without the
mod it opens the Mods page where it is installed. The cause is not
macOS-specific: every platform lost the row when Zoom moved.
Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
The shader screen (shader_ui) draws the pack list and every pack's settings,
so the options screen's own rows for them go: the Shader Packs and Shader
Options button lists, a Horizon pack's option pages, their actions and the two
pack slider kinds, with the label fitter only they used and the four lang keys
only they read.
A button that pushes the Shader Packs page (as one on the Options screen would) listed the packs but left the picked pack's settings unread until a pack was picked again. Every way into the screen now reads them with the list.
A mob near some dropped item tested every item on the level (2,000 in
the load test: 1.5 ms a tick). ItemAreas now keeps the items of each
cell, and ItemScan walks the list in its order from one item in a met
cell to the next, testing what the scan tested; it keeps up with the
items the walk removes, and tests every item from there on if the list
changes any other way. The villagers' pickups walk the same way.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Shader Packs... opens a screen in the frame of the graphics and controller
settings (settings_shell) instead of two vanilla button lists. Its first page
lists the installed packs (a tick box on the one in use; picking it again
turns shaders off), the pack folder, and Horizon's own Shadow Cache under
"Built-in settings". Under the picked pack's name, the page list holds the
pack's own screens (a shaders/ pack's screen.NAMEs, a Horizon pack's
[[pages]]), as "Pack settings": switches, sliders, choices, profiles and links
to deeper screens, with the pack's own descriptions in the strip. The search
finds an option on any of the pack's screens. A shaders/ pack's changes apply
with Apply or when the screen closes; picking another pack keeps the last
one's changes first.
MinecartHopper.tick has no transfer cooldown in 26.3: it calls tryConsumeItems every tick, and consumedItemThisFrame caps a tick at one item even when makeStepAlongTrack tries too. The reported pre-#2250 whole-stack pickup is gone; add the regression test that catches it coming back, and cite the per-frame cap next to hopper_cart_suck.
Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
Floor floating Health values before short narrowing and preserve integer narrowing. Add an actual decode-to-save regression and an isolated Game/Java command fixture.
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
The launcher traced a compile error to a mod only inside the helper function itself. horizon-mixins
wraps each helper in a block that calls it, `{ fn helper(…) { … } helper(…) }`. When Horizon
changes a type the helper's signature mentions (flair 1.0.0: `extra` became five-element tuples),
the error lands on that generated call, so no mod was named and the whole build failed instead of
leaving that mod out. A block holding only the helper and its call now counts as the helper's.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Cull conservative bounds of flame billboards before light lookup and vertex expansion. Keep shader/mod/TAA paths and unsupported dimensions, with emitted-geometry and edge-image coverage plus paired FPS samples.
Co-Authored-By: Codex GPT-6 <[email protected]>
HORIZON_SPIKES named almost nothing inside bench's prepare phase: with 150
players on a server and the camera facing the sky, prepare took ~0.43 ms a
frame and its spans accounted for ~0.005 ms. Each step between the fixed
ticks and the renderer now has a lap (pr_hud, pr_entities, pr_lights,
pr_frame, ...), dynamic lights two finer ones. They showed the cost: name
tags (eg_name_tags) built every frame for every player, drawn or not (#4664).
A resident column the server sends again was always relit here, for a
Horizon server's bulk edits whose packet carries the light from before
its relight. A proxy's next server resends its whole world at the same
coordinates, so every column queued light jobs for itself and its eight
neighbours, which a small CPU could not work through.
Relight a resent column only when the server announced Horizon's quiet
commands, the same feature flag Horizon servers send in configuration;
any other server's packet light is final, as vanilla applies it. A
reconfiguration resets the announcement until the new server sends its
own.
Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
From review, each measured against vanilla:
- DragonHoverPhase.getFlySpeed is 1, not the default 0.6: a hovering dragon thrown upwards
climbs back at vanilla's rate (new harness case, 12 commands off before).
- wrap / wrap64 are Mth.wrapDegrees: Java's % then one step of 360. rem_euclid lost the low
bits of a negative angle (-10.123456 became -10.123444) every tick (unit test).
- The parts' placement, the strafe aim and the flap timing use Mth.sin / Mth.cos and vanilla's
float casts.
- DragonSittingScanningPhase turns only while the player is more than 10 degrees off the
heading, with Mth.atan2 and vanilla's degree constant; dist is clamped in both divisions.
- A dragon's parts start at the origin, as vanilla's part entities do until the first aiStep,
so its first scanning turn aims from there.
The command execution harness gets "dragon scanning turn" (a dragon facing a survival player and
one turned away): 6/12 before, 12/12 now. All dragon and End harnesses match; the whole command
execution harness: 15832/15832.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ShulkerBullet.addAdditionalSaveData writes `Target` (the UUID it homes on, only
while it has one), `Dir` (`Direction.LEGACY_ID_CODEC`, the step it is on, absent
until it picks one), `Steps` and the per-axis target deltas `TXD`/`TYD`/`TZD`,
over `Projectile`'s owner and LeftOwner; readAdditionalSaveData reads them back
and keeps `Target` as an `EntityReference` it re-resolves every tick.
Horizon's `saved_with_chunk` turned a bullet with a target away - which is every
bullet a shulker fires - and `spit_or_bullet_to_nbt` refused it too, so the chunk
save dropped the bullet, and nothing read a step or a delta back.
The writer writes those keys now, the reader reads `Dir`, `Steps` and the deltas
into the kind, and `Target` goes through the UUID bridge: `Game::save_who_uuid`
answers it with `who_uuid` over `EntityMemory::uuid_of` (nothing for a mob the
level has no UUID for, never a made-up one), and `Game::bullet_target` resolves
the stored UUID with `Game::who_by_uuid`, which names the player or a mob that is
loaded. A bullet whose target is not loaded comes back with none and falls, as
`ShulkerBullet.tick` does with a target it cannot find, rather than homing on
whatever its old id means now.
A loaded bullet's own random stream is seeded from where it is, since
`Entity.random` is not saved: the same stand-in `Projectile::new` gives an
arrow's.
A save can now name an entity it only knows as a `Who`: `mob_to_nbt` takes a
`uuid_of` resolver, `who_uuid` writes a `Who` as a UUID and `who_by_uuid` turns a
stored UUID back into the `Who` of a loaded player or mob. `EntityMemory::uuid_of`
supplies the id -> UUID map the game layer keeps, and the game layer's `mob_by_uuid`
now resolves through the same seam.
The neutral mobs' `angry_at` is the first writer: anger at another mob now saves
that mob's UUID instead of nothing. The warden's suspect list and the shulker
bullet's target are the two follow-ups this bridge unblocks; neither is written
here.
🤖 Agent: merging the mod hot-load design note, docs only. The point to check is the native measurement it rests on: a one-line gameplay change rebuilds in 570 s against 812 s cold, and mold doesn't help, which is why the run-time switch came before background builds.
🤖 Agent: merging live switching of content mods. The point to check is the server's world tables: with no data pack they used to stay on the startup resources, so a switched mod's data never applied. After a live switch they are now built from the new root (data_switched), checked in game by a mod function that loads, unloads and loads again.
GLSL asks a texel offset to be a constant expression; GL drivers also take one a
function receives as a `const in ivec2` parameter (Arc's FXAA samples its neighbours
that way, so its final pass was skipped in every dimension). Such an offset now moves
the coordinate by that many texels of the base level; a constant one stays an offset.
The biome categories and precipitations custom uniforms compare with are defined for the
GLSL as well, with the same values: Sildur's Enhanced Default reads
`biome_category == CAT_THE_END` in gbuffers_skytextured, which failed on the unknown
name. BIOME_* still needs the world's registry and stays in custom uniforms.
Three things GL drivers take that naga's GLSL front end refused:
- an empty declaration (`;`) at global scope in any version, as a macro that writes a
function leaves after it (DrDestens: 56 programs skipped);
- a precision qualifier on a function parameter (`float rand(highp vec2 p)`,
TrailerShaders: final and composite);
- a constant expression in a function whose value is infinite or NaN
(`1.0 / toeX` with toeX 0, Epoch's tonemap at its defaults): it is computed at run
time, as GL does, instead of folded into a literal that can't hold it. WGSL keeps
refusing it.
GLSL lets a pack sample a usampler2D or isampler2D with texture(); GL drivers read an
integer texture with nearest filtering, so it is the texel the point falls in. naga
rejects sampling an integer image, and Mello, Voyager, Exposa and Reverie lost the
programs that read their integer colortex buffers that way (Mello 20, Voyager 42, Exposa
48). Those calls now go to helpers that texelFetch the texel under the point, clamped to
the edge, at level 0 (textureLod: the level it names).
`const vec4 s = vec4(scale[0].x, ...)` over a constant array read the index and the
array from the function's arena while folding in the module's: naga panicked (index out
of bounds) and Ebin Resurrected failed to load. Both now come from the global arena.
A panic in one pack no longer stops the run: it is caught, and each pack ends with a
SUMMARY line of the programs skipped in each dimension. A pack without shaders.properties
keys is read as one (every key at its default), not a failure.
`mat2(v)` with a vec4 `v` is the matrix with columns v.xy and v.zw in GLSL, as when the
components come as several arguments. naga's GLSL front end made each column the whole
vector, which failed validation ("Composing 0's component type is not expected"):
Bloop's water normals (prepare1) were skipped.
Packs compare block ids with Distant Horizons' block materials outside any
DISTANT_HORIZONS guard (Exposa: `blockID == DH_BLOCK_LEAVES`), so Iris defines them in
its standard macros whether the mod is there or not. Exposa's composite and composite1
failed on the unknown names; DH_BLOCK_UNKNOWN to DH_BLOCK_ILLUMINATED are now 0 to 15,
the mod's EDhApiBlockMaterial numbers.
GLSL asks a fragment input of integer type to be `flat`, but since 4.30 only the
fragment stage's interpolation qualifier counts: a vertex stage may write
`out uint frag_worldTN;` and GL drivers link it to `flat in uint`. naga left such an
output without interpolation and refused it (Alpha Piscium: 111 programs). An integer
vertex output or fragment input with no qualifier is now flat.
`colors2_colorspaces_ ## a ## _to_ ## b ##(x)` (Alpha Piscium) pastes the name's last
part to its `(`. The preprocessor made one token of `..._6_to_8(`, which named no macro,
so the call to the function-like macro `colors2_colorspaces_6_to_8` was left for the
compiler as an unknown function. As GL drivers' preprocessors do, the paste now stays the
tokens it is made of, and the name is rescanned with its `(`.
Retain bounded local text quads while updating camera transforms, lighting and cosmetic marks every frame. Invalidate on text/font/atlas/bidi changes and bypass animated or inline content.
Co-Authored-By: Codex GPT-6 <[email protected]>
🤖 Agent: the request log reads the player's name before the request runs, so a release or drop that removes the guest still logs who it was. The e2e this PR adds ran on this exact stack against current main: 31/31 over real control sockets with horizon-bots.
🤖 Agent: pushed 4020e2537 for martipops' review. It refuses a rotation that overflows f32 (1e39 became inf/NaN after the cast), refuses a named position when the record still has a RootVehicle to restore (the vehicle would undo it), keeps recentered on retries, and documents that velocity is the server's body only. Tests added for each; union-tested on the desktop.
🤖 Agent: the commit point holds. place_arrival writes the .dat with write_dat_durable plus the advancements and stats with write_durable, each a Result, before ok, and the player stays held on any error. Retries after the commit answer through placed_arrival. martipops' two reviews are answered point by point in the comment above. server_transfer_e2e passed on the final stack (31/31).
🤖 Agent: with [transfer] off, no login is expected, so held stays false and watches() equals here(): every here→watches swap in hub.rs and the loading_chunks refactor are no-ops for ordinary servers. With it on, a held player is a viewer but not tracked, and gets list changes through broadcast_watching. Union-tested with main and the stack on the desktop, plus server_transfer_e2e (31/31).
tick_mob_sculk_events posts every loaded mob's footsteps, landings and
splashes. Each event looked its mob up by a scan of every mob (for the
context and the supporting block) and asked every mob whether it was a
warden or an allay, so a server with thousands of mobs paid three scans
of them for each footstep near no listener.
Now, as GameEventDispatcher only visits the listeners around the event:
while the mobs' events are posted, mobs are found by id through a slot
map, the wardens and allays are listed once with their positions, and
an event with no sculk block in reach and no warden or allay within 24
blocks skips the context and the listeners. A mob that stands as it
stood with no step due (every mob outside the simulation distance)
leaves at once, and a mob riding nothing, as before, skips the mount
bookkeeping. Mods still get every event (native_game_event).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
RealmsResetWorldScreen's tiles, RealmsSelectWorldTemplateScreen's paged
list, the local-world picker and RealmsUploadScreen with the tar.gz
packer and the upload body. Follows the 26.3 bytecode of those screens,
client.worldupload.RealmsWorldUpload/RealmsUploadWorldPacker/FileUpload
and RealmsClient.fetchWorldTemplates/resetWorldWithTemplate/
requestUploadInfo/putIntoMinigameMode.
Co-Authored-By: ZCode <[email protected]>
Native rebuild times measured on the desktop, live switch times in game
on the ThinkPad and the content root on the desktop. Option A as built:
trait mods switch at run time, RenderHooks and the launcher's build-all
come next. The browser's buttons and index fetch as implemented.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
blocks_and_items_declared_in_code_get_ids expected a changed declaration
to rewrite the same root's stamp; each set now has its own root, so it
checks the new root and that the old one is left as it was. Found by
running the content-root tests on real prepared resources.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
villager_pickups tested every loaded villager against every dropped item,
looking the item up in the villager food table and tags before the reach:
on a server with hundreds of villagers and thousands of items that was
most of the mob phase. The reach comes first now, and a villager with no
item in its cells skips the scan (ItemAreas, as the looting passes do).
Mobs outside the simulation distance only check despawning, which reads
the nearest player and the difficulty: they no longer copy the player
view they see. A ticking mob borrows its guest's view for its tick (the
tick reads it through &MobWorld) instead of cloning it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: unknown states now read as air at the three places they enter (chunk palettes, block updates, BlockFacts::get), and the palette scan is skipped when nothing is unknown, so a vanilla-width chunk costs one maybe_has over its palette. Vanilla would disconnect on such an id (MissingPaletteEntryException) instead; air matches what Horizon already did for a wider host, so I took it. Union-tested on the desktop (clippy, wide_states, horizon-items).
🤖 Agent: removing JAVA_TOOL_OPTIONS, _JAVA_OPTIONS and JDK_JAVA_OPTIONS in java::command reaches every exporter JVM and javac but not the game's own launch, so a player's global -XX:+UseZGC still applies in play and only stops clashing with BATCH_OPTIONS' SerialGC during preparation. Built and tested in a union on the desktop (clippy, horizon-prepare tests).
Answers martipops' review of #8735:
- A rotation finite as f64 but past f32::MAX became an infinity (and a NaN pitch) after the
cast; it is refused now, like any rotation that is not a number.
- A record with a vehicle still to restore (RootVehicle) seats the player back on it at the
vehicle's own place, which undid a position the request named; the two are refused together.
- A retried import of the same hand-over, or a retry after the commit, answers the same
`recentered` as the first reply (kept on the guest until it is placed).
- docs/server.md says the velocity is the server's only: the arrival's teleport carries no
motion, as a vanilla login's does not.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ChunkMap.tick runs ServerEntity.sendChanges only for an entity that changed
section, needs sync, or is in DistanceManager.inEntityTickingRange. Horizon
ran it for every tracked entity, so an idle one sent its position every 60
ticks and advanced its update clock. EntityState::idle now carries the
range (the simulation level, Levels::in_entity_ticking_range), and the
tracker skips sendChanges for an idle entity that stayed in its section and
needs no sync; a lead change still goes out at once (Leashable).
The game then stops describing such entities in full every tick: an idle
mob or item that was in the tracker's last pass at the same place, carries
no passengers, and is already tracked by every client within its tracking
range is described bare (where it is, none of its data). The tracker never
reads its data and never pairs a bare state, so every pairing still sends
the entity as it is (game/lan_idle.rs).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
draw_tab_list ran every frame and, through remote_tab_players, cloned every
listed player and formatted two object-skin keys for each before it knew
whether the list was shown; update_remote_skins then searched every entity
once per level player and cloned the whole skin map. With 150 players and
none drawn a frame still cost ~0.4 ms more than with none (#4664).
- The list's visibility comes from the listed count; the players are built
only while it is shown or when it opens/closes (narration).
- The id:/name: object skins publish only when a player, its skin or the
atlas's object skins changed (a fingerprint and an atlas revision), so
inline faces keep the server's skin as before.
- update_remote_skins finds players and mannequins through one id->index map
and moves the resolved skins in instead of cloning them.
The Players tab's second button now opens RealmsInviteCodesScreen: the count in the
title, the subtitle, a row per code with its expiration (or Never), Copy / Edit / Delete
per row, and Create. RealmsEditInviteCodeScreen edits one code: its active flag, the
seven InviteCodeExpiration choices (a click on the slider track picks the nearest — this
screen has no drag event yet), Copy to the clipboard (a new Outcome::Copy the title
turns into Action::CopyText), Delete, Done and Back.
RealmsClient gains inviteCodes (GET api/v1.0/invite/codes?realmId={id}),
createInviteCode (POST .../invite/codes), updateInviteCode (POST .../update) and
deleteInviteCode (DELETE .../delete/{code}), all on the invite-code service with the
token in x-mojangauth, with InviteCode's DTO. The existing redeemInviteCode now goes
through the same helper. An existing expiration reads back as the closest of the seven
choices (InviteCodeExpiration.closestTo).
Co-Authored-By: ZCode <[email protected]>
🤖 Agent: merging. The default --clients vanilla,horizon run is unchanged, and so is main's one-client capture (now 'one or two of'). horizon-legacy only adds a horizon-extras.txt in its own scratch home. On the desktop, Modern vs Legacy matched on 14 of 16 scenarios; eat and dig_teleport also differed between two identical Modern clients there.
🤖 Agent: merging. Pictures are now sized to the window × scale and capped by the atlas limit (the wgpu adapters' min on Linux, 16384 on Metal/DX), instead of a fixed 3840. RSS stays flat at 173 MiB for 5 min on a real GPU. One thing to watch: the Linux probe opens its own Vulkan/GL instance at startup on a background thread, and until it answers pictures cap at 8192.
With a stitched colour atlas (sprites at their own sizes), the normal and
specular atlases were still packed as a grid by PackAtlas::material, so a
block's material texels sat under another sprite's colour (#9570). The
companion atlases now reuse the colour atlas's size, rectangles and padding;
each map is nearest-sampled from its single level into its sprite's
rectangle (an fs_material entry point), and animated sprites copy into the
same rectangles. The grid path is unchanged.
EnderDragon's constructor sets EnderDragonPhase.HOVERING; only EnderDragonFight.createNewDragon
sets the holding pattern. Dragon::new started every dragon holding its pattern, so a dragon from
/summon, a spawn egg or a save without DragonPhase flew off. It now starts hovering, and
create_new_dragon sets the holding pattern.
Hovering had never run on the server, and the flight step diverged there: the target is a hair
away, where Vec3.normalize returns zero under 1.0E-5 and vanilla's Mth.sin/cos table and
Mth.atan2 turn the dragon differently from the exact functions. The step now uses them, the
doubles of moveRelative's input vector and vanilla's 0.91F and 0.8F as doubles.
The command execution harness gets "summoned dragon hovers" (phase and position every 20 ticks
over 120): 0/13 before, 25/25 now. The dragon harnesses (server dragon, contacts, NoAI, not the
fight's, death outside the End, death roar, forced End, spawn eggs in fluids, dispensers) match.
Fixes#10285
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Use conservative projected-shadow bounds, retaining shadows whose casters are outside the view. Keep shader packs and native mods on the original path. Add resource-backed bounds coverage and measured FPS results.
Co-Authored-By: Codex GPT-6 <[email protected]>
In the glass theme account settings (skin_panel), long translated option
labels such as French 'Bras classiques' protruded outside the segmented
control pill due to an inherited ml(40px) margin and cross-axis stretching.
Additionally, the multi-sentence caption wrapped into a 10-line block inside
the narrow column, clipping off the bottom on 1080p displays.
- Remove the ml(40px) margin in glass theme mode.
- Set items_start() on the control column so segments size to natural width.
- Adjust segments option padding to 12px with flex_none and justify_center.
- Reorganize skin_panel so the caption spans full width across the bottom.
- Adjust Microsoft card width to 360px and reduce vertical padding in glass_who_plays.
Co-Authored-By: Antigravity <[email protected]>
Join a Friend opens from the title screen's Friends button and from the
server list. Both exits always went to the server list, so the title's
entry landed the player on a list they never opened (#9190, item 10).
The page now remembers whether the title opened it.
#10236 capped screenshots and the background at a fixed 3840 px, so an
8K screen lost detail. Pictures are now made at what the screen shows:
- viewer and background: the window's longer side x its scale factor
(7680 px on an 8K screen), tiles 360 points x the scale factor, in
256 px steps so a resize doesn't remake them every frame;
- never above the biggest texture GPUI's atlas holds: on Linux the
smallest max_texture_dimension_2d over wgpu's Vulkan/GL adapters
(asked once in the background, 8192 until known), elsewhere the fixed
16384 of the Metal and DirectX atlases. A larger picture would make
the atlas add a max-size texture on every frame (zed#66595).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A save can now name an entity it only knows as a `Who`: `mob_to_nbt` takes a
`uuid_of` resolver, `who_uuid` writes a `Who` as a UUID and `who_by_uuid` turns a
stored UUID back into the `Who` of a loaded player or mob. `EntityMemory::uuid_of`
supplies the id -> UUID map the game layer keeps, and the game layer's `mob_by_uuid`
now resolves through the same seam.
The neutral mobs' `angry_at` is the first writer: anger at another mob now saves
that mob's UUID instead of nothing. The warden's suspect list and the shulker
bullet's target are the two follow-ups this bridge unblocks; neither is written
here.
In play the HUD's top corners name the buttons for what the crosshair is on, what's held and how the player is
moving. In an inventory the bottom corners do: plain face buttons at the bottom right, everything else at the
bottom left, by what the crosshair is on and what's carried. Rows follow bindings and step around what was drawn.
The mouse or keyboard taking over, a prompt, F1, F3 and the setting hide them.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Under Linux, executing a freshly copied test fixture can fail with ETXTBSY (os error 26) if another test thread forked while the file was being written. Copy the fixture via a temporary file before renaming it into place, and retry exec/spawn if ETXTBSY occurs.
Fixes#10271
With the legacy rules on, a player's knockback follows 1.8.9's
EntityLivingBase.knockBack: the motion halves and the 0.4 push lifts in the
air too (26.3 lifts only from the ground), and knockback resistance is a
chance to take none instead of a cut. The sprint and Knockback push is added
on top as attackTargetEntityWithCurrentItem's addVelocity (0.1 up) instead
of going through knockback, for players and mobs alike.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
horizon-server accepts and keeps a [combat] section (off by default, left out
of the file while off); the dedicated game reads it itself, like
[anticheat], and turns on the legacy rules a hosted world's PvP page switch
gives. A wrong section is logged and fights keep 26.3's rules.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Under Combat feel Legacy the client draws no sweep_attack or
damage_indicator particles a server sends (1.8.9 had neither), and with
'1.8 hit sounds' plays none of the entity.player.attack.* sounds 1.9
added. Hurt sounds, crits and enchanted hits stay. Only what this client
shows changes; nothing is sent.
1.8.9 drew no red tint over armour (LayerArmorBase.shouldCombineTextures
is false, so setBrightness skips it) and the body's flash is red at 0.3,
as 26.3's: both already match.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Combat feel: Modern (default) or Legacy (1.8.9). Legacy changes only the
picture: a flat item in first person takes 1.8.9's ItemRenderer pose
(swing, eating and drinking, bow pull) through 26.3's own display
transform, a sword is held up while use is held (1.8.9's block pose,
swinging as 1.7.10 did), the main hand no longer dips after each hit,
and the attack indicator hides unless the player keeps it. The clicks
and every packet stay 26.3's. Poses 1.8.9 lacked (crossbow, trident,
spear, brush) and resource packs' own displays stay modern.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A PvP page switch, off by default: single player and LAN worlds resolve
melee hits by 1.8.9's rules (EntityPlayer.attackTargetEntityWithCurrentItem):
no attack cooldown and no sweep, 1.8.9 weapon damage (ItemSword 4 / ItemAxe 3 /
ItemPickaxe 2 / ItemSpade 1 plus ToolMaterial), Strength x(1 + 1.3 per level)
and Weakness -0.5 per level, Sharpness 1.25 per level, and critical hits that
allow sprinting and need no charge. Only a game that is the server uses them;
a window playing on a server never does, so what it sends stays the same.
The window passes the switch to its server thread each frame, like the
distances. lan_e2e's legacy-pvp check measures two diamond axe hits 12 ticks
apart: 7 and 7 under the rules, 9 then a cooldown-cut hit without.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merging. These are #10079's commits merged with main and nothing else; #10079 had landed in the already-merged pvp/hud-layout. The editor opens only from a world and is the only screen that writes the layout.
🤖 Agent: verified on the desktop: horizon-mods 36 tests, launcher 589 tests, clippy -D warnings clean. broken_on is read only from the registry index (the publish job adds it from horizon-mods' daily native-compat run). The warning shows only when the entry's Horizon is the commit the mod would build on, so a mod pinned elsewhere doesn't warn.
🤖 Agent: the harness itself: 16 scenarios, real vanilla 26.3 client vs Horizon, per tick and field; on main 11/16 match, with the series + #9918 16/16. Named-only and solo in parity_hosts (needs a virtual screen, ~15 min). Merge of main resolved a game.rs mod-list conflict (tick_profile + tick_script).
🤖 Agent: wire stream harness (#10082) on main: number keys' SetCarriedItem left a tick early (hotbar_swap, 15 ticks; dig_teleport) and a key tapped with a swing at the sky sent the slot before the Punch; with this change every tick matches vanilla. Head = cc7200cd7a's +35 −43 plus merges of #10083, #10085 and main (conflicts in remote.rs resolved to the tree the harness after-run built and passed).
🤖 Agent: verified end to end on the desktop: with 30 registry mods on Nightly 2026-11-05, main's launcher fails the whole build (eatinganimation E_TARGET_NOT_FOUND), and this one builds 22 and leaves out 8 with their reasons. MIXINS_REV moves to 5a64af9 (horizon-mixins#27, #28). The risk to watch: a compile failure costs one full recompile per round, capped at 3.
Stereo music and records were downmixed to mono and panned by the camera, so turning away hard-panned them and distance attenuation could silence them. OpenAL Soft plays a multi-channel buffer without spatialization or distance attenuation (AL_SOFT_source_spatialize auto), as vanilla's HRTF-off context does.
Keep the decoded channel count through the cache and mixer; mono buffers keep the existing panning and linear attenuation. Add a mixer test that stereo frames stay direct and unattenuated at any camera angle or distance.
Fixes#10037
Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
Vanilla's Math.sin is HotSpot's intrinsic, which rounds one of the drop
test's throws a bit lower on AArch64 than on x86-64; a vanilla 26.3 run
on an M-series Mac gives the AArch64 value. The install.sh fixture only
holds the Linux x86-64 archive and ran the cases with /usr/bin's
OpenSSL, LibreSSL on macOS: a fake uname picks that archive, and the
cases get the OpenSSL 3 the script checked for.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Keep spectral duration in the loaded arrow state and always write its integer
value, including the default 200. Cover the live command route and load/save
without raw input while retaining broader type and display diagnostics.
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
🤖 Agent: inventory in the wire stream harness: a shift-click's changed slots went out 10, 37 where vanilla's Int2ObjectOpenHashMap iterates 37, 10; now byte-identical, unit test pinned to the captured order.
🤖 Agent: bridge in the wire stream harness: 19 of 19 UseItemOn cursors differed from vanilla by ~6e-5 on main (double trig in the pick ray), all bit-identical with Mth's float table. Hot path cost: one table lookup per frame instead of libm sin/cos.
🤖 Agent: read against LocalPlayer.sendChanges and AbstractBoat.tick (rider look, vehicle, then sprint; paddles first). No riding scenario in the harness yet, so the proof is the read and the unchanged 16 scenarios. Land before #10084, which merges this branch.
🤖 Agent: verified with the wire stream harness (#10082): on main, dig_teleport answered a teleport mid-dig with a new START (sequence 34) instead of vanilla's ABORT; with the series every tick matches. pr_ready clean except the desktop nix-shell's uname-less install.sh test.
The held slot leaves remote_send_state (this PR); the boat's paddles go first and a
passenger sends its look, vehicle, then sprint (#10085).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A loaded desktop's vanilla client failed to log in; rerunning Horizon alone and
comparing with the earlier vanilla capture crashed on the missing client.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
From review:
- A perched dragon shrugs off a direct arrow, trident or wind charge given as `by`
(AbstractDragonSittingPhase.onHurt is 0, under 0.01): the command is refused.
- dealDefaultKnockback: a counted hit of a type not in #no_knockback pushes the dragon away from
the command's source position (`at`, else the direct entity), or in no direction without one.
- credit_dragon_kill switches to the credited guest whenever they are not the one acting, as the
mob kill credit does, so a guest's command that credits another guest awards that guest.
The harness scenario summons its dragons holding their pattern (DragonPhase 0): vanilla summons a
hovering dragon, which is sitting, and Horizon one holding its pattern; that gap is separate.
The intermediate build: 36/39; now 39/39.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: the keyboard path is a mechanical move from keys.contains to held(Action), with the same default keys underneath; old horizon-controller.txt files keep their buttons (no bindLayout line), and the pad's stack drop also reaches the server path through drop_stack. Union pr_ready green.
🤖 Agent: the check is SocketAddr::from_pathname, and the test compares it with a real bind at every length 1-150, so it follows the OS limit (104 on macOS, 108 on Linux) without a hardcoded number.
🤖 Agent: SOCKS goes through ureq's own socks-proxy feature (socks 0.3.4, MIT/Apache), and the loopback SOCKS5 test proves the request reaches an .invalid host only through the proxy. Nothing changes for players without a proxy set.
🤖 Agent: matches 26.3: OptionsList inherits AbstractScrollArea.mouseScrolled, so the wheel scrolls the list and never reaches a row's CycleButton; the old key is dropped because every install saved it as true. Main merged in, union pr_ready green.
Client mods that bake from the world in bulk (a minimap's tiles, a world
map export) had to clone `game.world.chunks` and read `world_biomes!`,
which pins them to one Horizon commit. Four stable helpers replace that:
- `native_client_chunks(from, to)` shares the client world's loaded
chunks of an area as `NativeClientChunks` (Send + Sync): block state,
biome id, loaded, each column's top non-empty section, build height.
- `native_client_chunks_changed(&snapshot)` names the chunks that differ
from the snapshot now. A held chunk is copied before any edit, so the
same allocation means the same chunk.
- `native_client_biomes()` gives the biome table the terrain is drawn
with (`NativeClientBiomes`): names and tints by `NativeBiomeId`.
- `native_client_biomes_changed(&biomes)` says when that table was
replaced (a mod's seasons, a resource reload, another world).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A connection reported its world while still connecting, before a failed
login could drop it, and a resource-pack reload's loading screen still
counted as playing.
Co-Authored-By: Claude Opus 5.5 <[邮箱]>
Round-two review finding: a long invite's identity kept only the host's
addresses when it had any. Two friends on other networks behind the
same private address and the fixed direct port (25580), each reached
through a relay, shared one NativeWorldId. The relay's address and
nameplate now stay beside the addresses; neither secret takes part.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Review findings on the session state helpers:
- NativeClientState.screen read remote_loading(), which leaves out the
portal loading screens because they draw no panorama blur, so a
minimap drew over them. A connection's screen is now one predicate,
remote_screen_up(), which remote_overlay draws by too.
- Every long friend invite showed as the same "Friend's world", so two
friends' worlds shared one NativeWorldId and a mod's per-world data.
A friend's world is now friend: and the invite's public part (the
host's addresses, else its relay nameplate), never its secret.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A minimap or waypoint mod can now read what it needs through stable
helpers instead of Game's fields, on a server's client and in single
player alike:
- native_client_state() gains screen (NativeScreenKind: playing, chat,
death or another screen), debug_screen (F3), world (NativeWorldId:
single player's save folder or the server's address, for per-world
data) and sleeping_pos (the bed, LivingEntity.getSleepingPos).
- native_biome(pos): the biome at a block as Level.getBiome picks it,
through BiomeManager's zoom.
- native_data_dir(mod_id) joins the helper catalog.
The window's connection remembers the save it joined through memory,
its own server thread's or P1's for a splitscreen seat, so the world
identity needs no mixin on open_integrated. A friend's world is named
as players see it, never by its invite secret.
Helper-only API fragment session_state.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Aligning every tick after the teleport on its acceptance moved the script's own
packets (the button release, the 20-tick position) by the server's lag: on the
desktop vanilla got the answer at tick 26, Horizon at 29, and both clients'
packets were otherwise identical tick for tick.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: harness and examples-only probe, nothing players run. New head: CI's parallel shards raced on one schematic .part file; each process now writes its own (three concurrent downloads checked against the pinned digest), and parity / parity passes. Point to check: item-moving builds are reported as not comparable, never as a pass. Result: 111/111 verified contraptions match vanilla.
The second end-to-end build of 30 registry mods on 6475f62 stopped on
errors inside carryblock's, distant_horizons', smartf3's and treefall's
support modules (made before the crate split), which named no mod: only
injected helpers were traced. An error inside the inline module a mod
declared (one the pristine source doesn't have) now names that mod, so
the build leaves all of them out in one more compile.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A windowless game with a resident chunk, a jukebox at (0, 64, 0) and a disc put
in through use_jukebox: 41 ticks of the jukebox tick post the event at ticks 0,
20 and 40 and nothing else, and a jukebox that is never played posts none.
JukeboxSongPlayer.tick posts GameEvent.JUKEBOX_PLAY at the block every
PLAY_EVENT_INTERVAL_TICKS (20) ticks of a song, right before the music
particles:
37: invokevirtual #97 // Method shouldEmitJukeboxPlayingEvent:()Z
44: getstatic #101 // Field GameEvent.JUKEBOX_PLAY
56: invokeinterface // LevelAccessor.gameEvent:(Holder,BlockPos,Context)
66: invokestatic #104 // Method spawnMusicParticles:(LevelAccessor,BlockPos;)V
Horizon drew the note particle in the same twenty-tick branch and posted
nothing, so a sculk sensor beside a playing jukebox never clicked and an allay
never heard the song, although the stop half (JUKEBOX_STOP_PLAY with level
event 1011) was already posted when the jukebox went away.
cmd_damage had no arm for them, so both said "Target is invulnerable".
EnderDragon.hurtServer is hurt(body, ...): a quarter plus up to 1; reallyHurt runs only with a
player behind the source or an #is_explosion type and refuses fire, and hurt succeeds either
way unless the dragon is dying or the hit is under 0.01. EndCrystal.hurtServer refuses an
Invulnerable crystal (unless generic_kill, out_of_world or a creative player), fire and the
dragon's own damage; else it is removed, and blows up unless the damage is an explosion.
The command execution harness gets a "dragon command damage" group: 15/31 before, 31/31 after.
Fixes#10265
Co-Authored-By: Claude Opus 5.5 <[email protected]>
CI runs the three shards at once on an empty .local/lra-catalog: two of them
downloaded the same schematic into one `.part` name and one rename found it
gone. The part file now carries the process id.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: test-only oracle (mods/horizon/src/test). Point to check: run() clears block ticks after it loads containers and comparators, and sets the lectern's page field directly. Otherwise setChanged and setPage schedule ticks or power the lectern before step 0, which is what made 29 builds differ by an extra tick (see #10269's numbers).
canSeeSkyFromBelowWater stops when a block dampens light and is not liquid(). Kelp and seagrass hold water, so guardians under them were still rolling the 1-in-20 and spawned less often.
Co-Authored-By: Grok <grok-4.7> <[email protected]>
Co-authored-by: Cursor <[email protected]>
emptyContents destroys the block only when it is not liquid(). A bubble column is liquid and not a LiquidBlock, so breaking it first played destroy particles and a sculk vibration.
Co-Authored-By: Grok <grok-4.7> <[email protected]>
Co-authored-by: Cursor <[email protected]>
A windowless game from the prepared resources, a bow in hand: the two events the
use posts are read back from the game's own event recorder, at the player's feet
(10, 64, -4). A spear, whose use effects set interact_vibrations false, posts
neither.
LivingEntity.startUsingItem ends with
this.useItem.causeUseVibration(this, GameEvent.ITEM_INTERACT_START), and
ItemStack.causeUseVibration posts only when the stack's use effects ask for
interaction vibrations - the same gate and the same position stopUsingItem
already used for ITEM_INTERACT_FINISH. Horizon posted only the finish event, so
a sculk sensor beside a player drawing a bow never clicked and an allay never
heard the use start.
use_vibration takes the event now, and start_using_item calls it where vanilla
posts it: after the use is set up, before the kinetic weapon's map (which this
change leaves alone).
🤖 Agent: data and reader only, nothing players run. The catalogue is WTFPL from maybeachromium (LICENSE beside it). Point to check: tools/lra_schem.py is the one parser of untrusted downloaded schematics. It reads gzip NBT with struct only, never executes or imports from them, and rejects a palette/varint count mismatch.
stamp_native_hooks.py refuses a fragment that lists a guarded file
(native_hooks.rs is the generated source it stamps itself), so the fragment
declared an invalid comment path and the repo-wide stamp check failed.
A server with mods or states the client does not have can send block states outside our registry. Instead of panicking with an out of bounds slice access when indexing BlockFacts or unpacking palettes, map unknown states to air.
🤖 Agent: Wardrobe layer, retargeted to main after #10144. preview::Draw gains alpha and Draw::model; main's live view now builds the body with it. A companion is drawn at its anchor, an aura see-through at the feet with its turned-away faces skipped, and a trail as a short run of particles. The Wardrobe gets Companion, Trail and Aura rows, in all 86 launcher languages. Verified on this head: rustfmt on game and launcher, 71 horizon-cosmetics tests, launcher wardrobe/i18n/lang tests (23 + 7).
🤖 Agent: render layer, retargeted to main after #10143. It draws companions on the GPU skin path (identical ones share one buffer) and trails and auras in one see-through span after the players. Nothing is drawn beyond 48 blocks or outside the view. Measured in-game with HORIZON_COSMETICS_LOCAL: +0.6 ms prepare at 16 wearers and +1.1 to 2.4 ms at 64, mostly the auras built on the CPU; the follow-up is #10251. Verified on this head: horizon-client clippy -D warnings.
🤖 Agent: settings layer, retargeted to main after #10142. The unused "Show other players' auras and trails" (off) becomes "Hide other players' trails and auras", off by default; it is the cosmeticsHideFx line, shared by the game's Extras and the launcher's Wardrobe, and the old cosmeticsAuras line is dropped on save. Translated into the game's 75 languages and the launcher's 86. Verified on this head: clippy -D warnings on game and launcher.
🤖 Agent: merging the native on/off switch. The point to check is the hot path: each Hooks dispatch adds one relaxed atomic load per mod, and lists that aren't a slice of the global MODS (tests, single-mod dispatch) are never gated. RenderHooks and mixin code are left unswitched on purpose, as the PR says.
🤖 Agent: horizon-report format 2 (mob costs, level spans, tick_lan/sculk sub-spans, workers, network, overhead, stop/status). verify green on eb55d13; live on the SMP since 21:08Z as prof2 at 0.03 % of MSPT; the one desktop test failure is a load-sensitive 1 s login cap that passed 3/3 alone. Only spans and counters added, no gameplay change.
🤖 Agent: runtime layer, retargeted to main after #10140. follow.rs (frame-rate independent lag, snap after a teleport, kept out of the crosshair cone and clear of the head) and trail.rs (spawns by distance walked, 24 particles per player and 512 in all) are pure horizon-cosmetics code, plus the fx_bench example. Nothing in the game calls them yet. Verified on this head: clippy -D warnings and 68 horizon-cosmetics tests.
🤖 Agent: any GUI sprite with an animation section now packs its first frame and animates, so the 16x64 music_notes strip no longer squashes into one slot; load-time only.
check already refused a jar whose data folders are named as before 1.21; carry and report did
not, so a 1.20 pack's mods were carried and counted as partial while 26.3 read none of their
recipes, loot tables, tags or structures (Prominence II: 149 of 308 "come over", 28,948 recipes
and 8,784 loot tables in recipes/ and loot_tables/).
old_layout moves to carry and reads every folder 1.21 renamed (structures/, advancements/,
tags/blocks/, ...), by the larger share: a 1.20 jar may hold a misnamed singular tag folder and
a 1.21 jar a stale plural copy. carry writes old_layout to carry-report.json and carries
nothing; the report's row says why, is not one Port can carry over, and the launcher does not
count it as portable.
Fixes#10249
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: sulfur cube blast is TNT-interaction only with mobGriefing (NONE without), ENTITY_INTERACT sourced by the cube, tnt.disabled overlay and used stat.
🤖 Agent: rider body follows the vehicle kind as in vanilla (boat yaw, solveBodyRot's 85° clamp for living vehicles, on-foot turn in minecarts); portal E guard only while inside the portal this tick.
🤖 Agent: format layer of the companions/trails/auras stack, rebased onto the emotes: Slot gets Companion, Trail and Aura next to Emote, and Item::parse checks both kinds of block. Item::animate also builds its animated-part table once at parse instead of searching by name every frame, which saves about 0.3 ms a frame at 64 wearers (profiled). Verified on this head: clippy -D warnings on game and launcher, 60 horizon-cosmetics tests; nothing draws the new slots yet.
The end-to-end build of 30 registry mods on 6475f62 stopped on
eatinganimation's `held-frame` helper (E0063, ItemRenderState's new fields)
without naming it: the staged item_render.rs carries the helper with a
trailing comma rustfmt added, so its tokens no longer equal the plan's
helper_source. A helper is now also the function of its name in the staged
file, when the pristine source has no function of that name; two mods'
helpers of one name still name neither.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The switch and remove buttons run on the window's thread: while an
automatic update holds the game folder they now say "Wait for the current
operation to finish." instead of freezing the launcher until its downloads
end. auto::run takes the lock after the registry index arrives.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
redstone_catalog_fixture builds the same contraptions in a map-backed
horizon-world level, clicks the same levers and buttons
(Behaviours::use_without_item) and runs tick_scheduled, run_block_events
and tick_block_entities, printing what VanillaRedstoneCatalogOracle prints.
tools/test_redstone_catalog_parity.py reads the catalogue, downloads the
archive's schematics into .local/lra-catalog/ (pinned to da4dd51, one
sha256 over all 188), turns each entry into a case (switches off except
those the catalogue held on, a neighbour update on every block, then input
steps: every vector up to 8 bits, random ones beyond, random changes for
stateful builds) and compares both sides step by step. verified entries
must match; partial and unverified ones are reported. Entries whose inputs
are analog levels, lecterns or bare wire are listed as unsupported for
now. Three shards; --full runs larger sweeps and every vector up to 16
bits.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
VanillaRedstoneCatalogOracle builds each contraption (block states and
block entities the harness reads from its schematic) in the overworld of a
real flat-world server, one contraption per server tick. Comparators keep
their saved output, containers and lecterns get real items (set silently:
no comparator ticks, no lectern pulse), and the comparator level of every
block entity is printed for the Horizon probe. The game clock keeps
running between contraptions (LevelTicks orders chunks by trigger time);
the probe starts from the time printed.
Levers and buttons are driven with useWithoutItem, and each tick runs what
ServerLevel.tick runs for blocks: scheduled block and fluid ticks, block
events and block entities, so pistons move. After each tick it prints the
outputs that changed, and at the end of each step the tick it ended on,
whether the level went quiet and a hash of the contraption's box. A tick
budget per contraption (the probe counts the same) bounds machines that
never settle; containers at or next to hoppers, droppers, dispensers and
crafters are watched every tick, and a changed level marks the run as not
comparable (the probe does not move items). HORIZON_CATALOG_TRACE=1 adds
every block tick and every block at a step's end.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
tools/lra-catalog/entries.jsonl is maybeachromium's catalogue of the Logical
Redstone Archive (#10103): input and output positions, encodings and checks
for every entry, one entry per line. They shared it under the WTFPL
(LICENSE). The archive's schematics have no licence and stay out of the
repository.
tools/lra_schem.py reads Sponge schematics (versions 1 to 3), written from
the format's specification.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Cull::supported previously gated early-depth occlusion culling to Vulkan and Metal. Enable wgpu::Backend::Gl when the device supports SHADER_EARLY_DEPTH_TEST alongside indirect first instance and storage buffer writes.
SulfurCube.tickFuse explodes with ExplosionInteraction.TNT only when
mobGriefing is on (NONE otherwise), and equipItem's gameEvent has the
cube, not the player, as its source. Drop tests that checked constants.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The three vanilla dimensions, built from the shipped data, must answer what
their noise settings say: the overworld and the nether add creature packs at
generation, the End does not. This is the value `spawn_generation_creatures`
now returns on, read through `VanillaTerrain`, so it covers the whole path from
`end.json` to the gate instead of just the JSON read.
With the setting read as a hardcoded false (the pre-change behaviour, where only
`Dim::End` decided) it fails as
`assertion left == right failed: disable_mob_generation of minecraft:the_end —
left: false, right: true`.
`NoiseBasedChunkGenerator.spawnOriginalMobs` reads
`NoiseGeneratorSettings.disableMobGeneration` and places nothing when it is
set. The shipped presets are explicit about it: `end` sets it true, the other
six set it false. Horizon never read the field and instead skipped the pass for
`Dim::End` alone, so a datapack dimension that used the End's noise settings -
or any preset asking for no creature packs - still got them, and the reason the
End was empty lived in a dimension check rather than in the data.
The setting now reaches the client seam: `Generator`'s settings carry it, the
pipeline trait exposes it next to `sea_level`, `VanillaTerrain` answers it, and
`spawn_generation_creatures` returns before doing any work when it is set. The
existing `Dim::End` check stays as well, so a preset that fails to load cannot
start filling the End; for the shipped data the two agree, since `end.json` is
the only preset that sets the flag.
Vanilla's Camel.canAddPassenger accepts a rider while two or fewer ride,
with no baby check. Drop two tests that only checked their own inputs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A boat sets its passenger's body to its yaw (AbstractBoat.clampRotation);
a living vehicle's rider is drawn from the vehicle's body with the head at
most 85 degrees off and a fifth of the offset past 50
(LivingEntityRenderer.solveBodyRot). Minecarts and cushions keep the
on-foot turn, as vanilla's LivingEntity.tick does. The inventory key is
held back only while the last tick found the player in the portal, not
during the effect's fade-out.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
anvil_update fires where AnvilMenu.createResult ends, once vanilla has
worked out the result, while the left slot holds an item: on the host for
each player's menu (single player's server thread, a LAN or dedicated
host), before prepare_anvil, and in the window's own menu. A mod is told
the inputs, the rename box's text and vanilla's result. It may change the
output, the level cost and the material cost, or cancel, which leaves no
result (NeoForge's CommonHooks.onAnvilUpdate).
The host asks again after a rename that leaves the result as it was, as
setItemName runs createResult. The window's click prediction starts from
the station's cost and material cost, so a result a mod made can be taken
there as on the server. The window names the player as the server knows
them.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Part of #9996.
NoiseBasedAquifer::compute_substance is 4.7% of the overworld's proto
phase by itself. A sixth of that went to rebuilding the 12 grid cells
around a block whenever its aquifer cell changed: every location was
unpacked again from its packed long, and the neighbourhood was copied
as a 288-byte Option. The fill walks each column from the top down, so
a rebuild usually follows the cell just above.
Each drawn location is now also kept as the coordinates BlockPos.getX,
getY and getZ read back from it, and the 12 cells are arrays of grid
index, coordinates and horizontal distance to the column. One cell
down, the 8 cells at the old anchor's y and the y below move up two
slots and only the 4 new ones are loaded, in vanilla's loop order. A
cached fluid status is read inline; computing a missing one is out of
line. Default air, water and lava are classified by their IDs before
any block name is read.
Every block, pending fluid update, error and cached cell is the same as
before: the same operations in the same order, the same positional
random draws for the same cells. A test runs the previous search
against the new one over whole columns, scrambled blocks and positions
outside the grid, and the snapshots match byte for byte.
A first draft by DeepSeek V4.1 Flash, via pi, was reviewed and
reworked.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WZzq8LUjYucp4kaN6UfSg7
The ledger named the four renderer overrides but not the method they override;
it now says the box is the frame's, grown by 0.5, and points at the dropped
item's own copy of it.
An item sliding from x = 0 to 1 in camera space: the box is -0.625..0.625 at the
start of the frame, -0.125..1.125 halfway through and 0.375..1.625 at its end,
where the old code had the last on every frame. A falling item's box floor is
65.0 halfway down a 66 -> 65 drop instead of the tick's 64.5.
EntityRenderer.shouldRender tests the frustum against
getBoundingBoxForCulling(entity, partialTick), which EntityRenderer leaves as
Entity.getInterpolatedBoundingBox(partialTick) - ItemEntity overrides neither -
grown by 0.5 all round.
The dropped-item pass built that box from the tick's own position, so for the
frames of a tick in which an item crossed the frustum edge it was culled at the
cell it was about to reach: an item sliding from x = 0 to 1 had its box at
0.375..1.625 (camera space) on every frame, where at the start of the frame it
stands at -0.625..0.625.
dropped_item_cull_box(previous, position, alpha, camera) is that box now, the
interpolated position grown by the item's 0.125 half-width and 0.5, with the
0.25 height below and 0.75 above.
🤖 Agent: merging the in-game mod browser. The point to check is that install never trusts the download: each file goes through horizon_mods::index::check_file (size, SHA-256, signature, manifest against the index) before it reaches horizon-mods/, and revoked files are refused. Tests pin both cases with a real .hmod.
The Wardrobe's picture (horizon_cosmetics::preview, its live view
included) draws a companion at its anchor, an aura see-through at the
feet and a trail as a short run of particles behind; the Wardrobe gets a
row for each slot.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A companion is posed on no bone: it follows its anchor with its lag,
keeps clear of the head and, for the player's own in first person, out of
the middle of the view. Trails spawn by distance walked and are drawn as
camera-facing particles; auras blend at the feet with their back faces
dropped. Trails and auras of every player go in one see-through span after
the players. Others' follow "Other players' cosmetics" and "Hide other
players' trails and auras". Nothing is an entity or reaches a server.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The setting that was "Show other players' auras and trails" (off, for
items that didn't exist) becomes a hide switch, off by default: others'
trails and auras show like their other cosmetics unless the player hides
them. In the game's Horizon settings and the launcher's Wardrobe, the same
cosmeticsHideFx line of horizon-extras.txt; the old cosmeticsAuras line is
dropped on save.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
follow.rs: a frame-rate independent lag toward the anchor, a snap after a
teleport, and the two rules that keep a companion out of the way (out of
a cone round a first-person crosshair, clear of the head). trail.rs:
particles spawned by distance walked only, placed from their age alone,
24 per player and 512 in all. fx_bench measures their CPU cost for 1, 16
and 64 wearers.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Three new slots that ride no bone: a companion floats beside the player,
a trail leaves particles where they walk, an aura glows faintly at their
feet or round the body. Their blocks, limits and checks live in fx.rs;
item.rs reads them, refuses a bone on them and measures their extent.
Nothing draws them yet.
Item::animate no longer looks its parts up by name every frame (a tree
search and string compares per animation, for every wearer): the animated
parts are found once, when the item is read. At 64 wearers in view that
was 0.3 ms a frame on an i7-7500U.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A mob walking 0.9 -> 1.1 (eye 1.62) reads the cell at 0 at the start of the frame
and 1 from the boundary on, where the old probe read the tick's cell 1 on every
frame; a falling one reads 71.62 then 70.62. An arrow falling 64 -> 65 reads
64.13, 64.63 and 65.0, so the cell at 65 only at the end of the frame, where the
old probe read 65.0 throughout.
EntityRenderer.getPackedLightCoords takes BlockPos.containing of
Entity.getLightProbePosition(partialTick), and Entity leaves that method as
getEyePosition: getEyeHeight() over getPosition(partialTick), which is
Mth.lerp(partialTick, xo, x) on each axis.
The mob pass probed floor(position) with position.y + eye_height(), and the
projectile pass the same on its own position: the tick's own position, so a mob
walking from x = 0.9 to 1.1 was drawn lit by the cell at 1 for every frame of the
tick, and a falling one by the cell it was about to reach, instead of the cell it
was drawn in.
Mob::light_probe(partial) and Projectile::light_probe_y(partial) now give the
probe of the frame, through mobs::living::eye_probe_cell (Mth.lerp plus the eye
height, floored per axis).
Old/new for a mob walking 0.9 -> 1.1 (eye 1.62): cell x = 1 on every frame before,
x = 0 at alpha 0 and 1 from the boundary on. For an arrow falling 64 -> 65: 65.0
before, 64.13 at alpha 0 and 64.63 at alpha 0.5, the cell at 65 only at alpha 1.
Held mappings are named by an Action rather than read by key, per device, and more actions can be bound: hotbar
slots, fullscreen, toolbars, social and quick actions. Held drop repeats and throws the stack. Readers of the
movement keys see the left stick, and a bumper steps the hotbar through the wheel's gates. Prompts over gameplay
are pad-driven and keep what's behind them, the keyboard opens on every field, and focus follows its widget.
Buttons and the crosshair draw from Horizon's own art, one file per pad family, instead of text labels.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merging. The screenshot tiles, viewer and background no longer go through GPUI's never-evicted img(path) cache. Two 32k×16k screenshots on Home went from an 8.7 GiB peak to 277 MiB on the desktop. One risk to watch: the 2 s grace lets the budget overflow while more than 256 MiB of tiles is on screen at once (about 280 tiles with Show all), which is bounded by the list itself.
Screenshot tiles (Overview's three newest, the Screenshots tab), the
viewer and Home's background went through gpui::img(path): GPUI decodes
the whole picture and keeps it, and its sprite-atlas copy, until the
launcher quits. One 32768x16384 screenshot is 2 GiB decoded; with two
of them Home alone reached 8.7 GiB (24 GiB reported on Linux).
- pictures (lib): PNGs are read a row at a time and box-filtered down to
the drawn size, never held whole; other formats decode whole only
under 256 MiB. Lru keeps made pictures under a byte budget, sparing
those drawn in the last 2 s.
- picture_cache: makes them off the UI thread (3 at once), 640 px for
tiles, 3840 px for the viewer and background, 256 MiB budget; evicted
images are dropped from the window's sprite atlas.
- glass_shot_blur's blurred copies go through it too, from a shrunk read
instead of image::open.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The row said the probe was drawing-only and skipped; it is what selects the block
an armour stand is drawn with, so it now points at the code that walks it.
Four cases over the jar's values: a full stand's eye at 65.7775 (cell 65, where
the old base + 0.5 read 64), a small stand at 64.2 (65.1875, cell 65, where the
old read 64), a marker's box scanned for a 15 at (0, 65, 0) and falling back to
the first cell walked (0, 64, 0), y walked before z, the cell above the box left
out, and the marker branch ignoring the partial tick; and a burning stand's block
light at 15 with the sky light left as the probe cell's.
EntityRenderer.getPackedLightCoords reads the block Entity.getLightProbePosition
is in, and ArmorStand overrides that method:
- a marker walks BlockPos.betweenClosed of getDimensionsMarker(false) - the box it
would have without the marker flag, 0.5 by 1.975, or 0.25 by 0.9875 when small -
from position() with no partial tick, and returns the first cell whose
max(block, sky) brightness is 15, else the brightest, ties going to the first
cell BlockPos walks (x fastest, then y, then z);
- every other stand is Entity.getLightProbePosition, the eye: eyeHeight 1.7775 for
a full stand and 0.9875 for a small one (BABY_DIMENSIONS), at the frame's
interpolated position.
Horizon read floor(base + 0.5) for every stand, so a stand standing on a lit floor
in a dark room was drawn with the floor's light: a full stand on y = 64 read the
cell at 64 where its eye at 65.7775 is in the cell at 65, and a hologram marker on
a dark ceiling was lit from its feet's block instead of the brightest cell of the
box it would have without its flag.
EntityRenderer.getBlockLightLevel also puts a stand that isOnFire at 15 block
light, which was not read at all.
On a smoke server (2 ms ticks) the overhead line charged the chunk pump's spans of the
wait between ticks (1,788 a tick) to MSPT. Only the spans the tick records count now,
and a dedicated server records spans anyway for its slow-tick line (HORIZON_SPIKES,
on by default), so they are listed apart and left out of the report's own cost.
The 'other' worker jobs ran at 278k/s for 0.0 ms each: two thread-CPU clock reads
each cost more than the job; they are counted only. A sum of no players' rates no
longer reads -0.0.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`CreativeModeInventoryScreen.selectTab` shows and focuses the search box only
on the search tab; any other tab hides it, lets it lose the focus and clears
its query. Horizon focused the box when the search tab was selected but never
took the focus away, so the box stayed focused behind every other tab.
`Creative::select` now sets the focus from the tab it shows, keeping the blink
reset the search tab always had.
🤖 Agent: the Wardrobe button opens the launcher's Shop tab now, so its label drops "on playhorizon.dev"; the key is renamed in place in all 86 tables (no stale key left), and the i18n test passes.
The registry's daily check (horizon-mods native-compat.yml) marks the
versions horizon-mixins refuses on the newest Nightly with `broken_on`
({horizon, code}) in index.json. The browser's fit line now says
"Doesn't build on the Horizon you play (CODE): a build leaves it out until
its author updates it" when that Horizon is the one the mod would build on.
- horizon-mods index: `Version.broken_on` (empty and absent by default).
- native_fit: `Fit::Broken { code }` for a hook mod, or a mod pinned to the
played commit, when an entry names that commit.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A collector chosen in JAVA_TOOL_OPTIONS or _JAVA_OPTIONS stopped every JVM
horizon-prepare starts, as they pass -XX:+UseSerialGC: "Multiple garbage
collectors selected", so the version was never prepared. java::command, which
they all go through, now removes those variables and JDK_JAVA_OPTIONS.
Fixes#10067
The ItemEntityRenderer row names where its light comes from now, and
EntityRenderer.getBlockLightLevel's note covers dropped items beside mobs and
projectiles.
The three cases quote the old values: the failing probe cell of a falling item
(64 against the eye's 65), the tick's own position against the frame's halfway
one (64.7125 rather than the destination 65.2125), and a burning item's block
light (15 over a dark cell's 0, sky untouched).
EntityType.ITEM declares eyeHeight(0.2125), and EntityRenderer.getPackedLightCoords
reads the block of Entity.getLightProbePosition, which Entity leaves as
getEyePosition: the item's position plus that eye height, at the position the frame
draws (getPosition(partialTick)), not this tick's own.
The dropped-item pass probed the cell of base + 0.1 at the tick's position, so a
falling item at y = 64.85 was lit by the cell at 64 (the floor's light) while its
eye at 65.0625 sits in the cell at 65, and the light jumped a frame early when an
item crossed a cell boundary.
EntityRenderer.getBlockLightLevel is isOnFire ? 15 : the level's block light, so a
burning item is now drawn at full block light with the level's sky light; the
item's fire ticks already clear for a fire-immune stack, which ItemEntity.fireImmune
makes never on fire.
A picked-up item's particle is not an entity and keeps the cell it is drawn in.
Background automatic registry updates and manual mod operations (install,
remove, enable, disable) could mutate the same game home folder concurrently.
Because auto-update loads `mod-browser.json` before downloading and saves upon
completion, a manual install or removal occurring during the download would
be overwritten by the stale snapshot upon completion, losing manually added
mods or reviving removed mods. In addition, temporary save files could collide.
Introduce a per-game-home re-entrant mutex (`browser::lock::lock_home`)
covering mod mutation critical sections across background auto-updates and
manual operations. Use unique atomic temporary file paths during save to
prevent collision.
Co-Authored-By: Antigravity <[email protected]>
A loaded server catching up ran a 20-tick bite's remaining ticks in a burst and finished the apple.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
It opens the launcher's Shop tab now, not the website, so the old label
"Get more cosmetics on playhorizon.dev" was wrong. The key is renamed in all 86
tables, in place.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Pay checks that the checkout's uuid matches the recipient and only opens playhorizon.dev/shop/pay with a validated txn_ id; no card entry in the launcher.
🤖 Agent: the Shop is a plain top-bar tab with no badge; it never opens by itself and isn't the start page; a catalogue failure shows one quiet line and Play is untouched; try-on uses #10153's Viewer (no second renderer).
One mod that horizon-mixins refuses or that doesn't compile used to fail the
whole native build (#horizon-support 2026-11-05: eatinganimation's moved
target, hitbox_plus's support module, worldedit's moved chat.rs). The build
now leaves that mod out, with the mods that need it, and builds again:
- isolate.rs names the mod a horizon-mixins diagnostic is about (mod_id, its
manifest, its support module's name or destination), or the mods the
compiler's errors were traced to, and adds their dependents.
- A refusal costs one horizon-mixins run; a compile failure one compile, at
most 3 times. A server engine export still fails as before.
- native-mods.json records `left_out`, so the same mods turned on read as up
to date, and the Mods page says "X was left out: made for another Horizon
version / clashes with Y / it needs Z, which was left out" (86 languages).
- MIXINS_REV → 5a64af9: horizon-mixins#27 resolves `horizon_client::…`
targets that moved into horizon-client-lib, #28 follows workspace globs for
a module's name and puts the mod id on module errors.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Keep the map-crafting, transmute, mob-effect and frame-map groups and their
isolation sets. The merged game passes the focused 62-record Java replay.
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
The fixture registry takes a pack version so the test can install
cozy 1.0.0, turn coords_hud off as the Mods page does, and install
cozy 1.1.0: the file is updated and the mod is still off. Turned back
on, 1.2.0 keeps it on. With the old unconditional set_enabled(m.on)
the update re-enables it and the test fails at 'the update kept it
off'.
install_version_in() applied java::set_enabled(home, id, m.on) for
every mod of the pack on every install, and installing a registry pack
again is how it updates. horizon-mods.txt is the same list the Mods
page toggles, so a mod the player turned on or off in a pack profile
was reset to the pack author's 'on' by the next pack update.
The install now reads the pack's previous record (pack.json's 'id
version' entries) and carries each listed mod's current on/off choice
through the install, which turns a mod on as an explicit install. A
mod the new pack version adds takes the pack's own 'on', and a fresh
install has no previous record so every mod does, as before. The Java
mod path already keeps '.disabled' through updates the same way.
🤖 Agent: shop price rules (sets partly owned, items inside a set in the basket, Supporter once) match the site's pricing.ts and are tested against hand-worked values; nothing player-visible yet. Requeued after merging main (#10153's lang tails).
🤖 Agent: Requeued on the same head: the parity run died in 1 s on the runner's missing .horizon-prepare.lock (environment, no harness ran), so it's rerunning (42040505129). The interact packet now carries the ray's hit point from the mob's feet. Union on main dd675b574 green.
🤖 Agent: With a scaled resolution, final now draws at the picture's size into its own target, and one linear copy stretches it to the window. Same-size output and packs without a final keep their old paths, so only resolution-scaled pack users take the extra pass. Union on main dd675b574 green (fmt, clippy, 4426 client tests including the render crate).
🤖 Agent: Matches LivingEntity.calculateEntityAnimation: a passenger calls walkAnimation.stop(), which zeroes speed, old speed and position, so the seat moving with the boat no longer swings the legs. Union on main dd675b574 green (fmt, clippy, 4426 client tests).
🤖 Agent: WebGPU allows binding a depth view as unfilterable-float, and textureLoad(...).r is the same texel, so Vulkan, DX12 and Metal read the same depth. The translation test now fails on any GL-run entry point that doesn't translate, except the one known glint case. Union on main dd675b574 green (fmt, clippy, 4426 client tests).
break_speed fires at the end of Player.getDestroySpeed, on the Game that
works out a dig: the window's own prediction, and the host's (single
player's server thread, a LAN or dedicated host) for each player. A mod is
told the block and vanilla's speed, and may change it;
BlockBehaviour.getDestroyProgress then divides it by the hardness.
tick_mining now takes its progress from destroy_progress, like the other
digs, so the speed is worked out in one place. The remote client asks it
for blocks of hardness 0 too, so a mod's speed of 0 stops those as well.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Merged main (9906 landed with the INF-seeded outline_center fold, kept from main) and ran marker_particles on this head: PASS, 2,187 cases and 462,718 lines matching the pinned client. The hive's loop now draws nextInt(1) again on the check that ends it, as BeehiveBlock.animateTick's for-condition does.
`Progress::done` on an advancement parsed from `{"criteria": {}}` (true, no
requirement groups to wait for) against one with a criterion (false), and
`Tree::automatic` over a tree holding both: only the empty one is listed, which
is what the load pass grants the rewards of.
Without the `done` change the first assertion fails at once
(`assertion failed: Progress::default().done(&empty)`), and `Tree::automatic`
does not exist to compile against.
`AdvancementProgress.isDone` loops over the requirement groups, so with no
groups at all it answers true: an advancement whose `criteria` map is empty is
finished from the moment the player's progress loads. `PlayerAdvancements.
checkForAutomaticTriggers` walks every advancement, and for those with no
criteria calls `award(holder, "")` - which finds no `""` criterion to grant, so
it neither toasts nor announces - and then `AdvancementRewards.grant`s it. The
rewards are granted again on the next load, because there is no criterion to
record them under.
Horizon required `!requirements.is_empty()`, so such an advancement was never
done and never paid out. `Progress::done` now keeps vanilla's shape (every group
satisfied, vacuously true with no groups), `Tree::automatic` lists the
advancements nothing can grant, and the progress load grants their rewards
through the new `grant_rewards`, which is the `AdvancementRewards.grant` half of
`award` split out so the toast and announcement stay with a real completion.
Scope, plainly: **no vanilla advancement has an empty criteria map** - all 1224
in the shipped data have at least one criterion - so nothing in the game's own
data changes. What changes is a datapack's: before this, an advancement it
declared with `"criteria": {}` could never be obtained and its rewards never
arrived, where vanilla hands it over on load.
Part of #9997.
ClimateIndex::search, vanilla's Climate.RTree search, is about 5% of
overworld proto generation (callgrind, generate_proto, seed 12345) and 2%
of the nether's. About 21 boxes are measured per search: the root's two
children and the 19 below.
Three changes, none of which changes which leaf a search returns:
- The best leaf's distance goes down into each subtree and back up with
its result. Vanilla measures the best leaf again at the start of every
subtree and after it returns; the values are the same.
- The offset axis is always 0 in the target, so each box's squared
offset distance is stored once instead of measured every time.
- When every bound in the tree and every target value is within 2^29,
no term or sum of a distance can overflow, so a partial sum only grows.
A box's sum then stops once it reaches the best distance so far: the
search would only compare it with that distance and skip the box. The
axes are added depth first, as surface biomes are points in depth.
Otherwise the full wrapping sum is used as before.
The search still walks the same children in the same order from the
same previous leaf, and replaces the best only on a strictly smaller
distance, so ties go the same way. New tests compare it with a copy of
the old search over long target sequences that keep the previous
result, on the vanilla overworld and nether lists and on generated
lists full of ties, inverted spans, extremes and values at the bound.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WZzq8LUjYucp4kaN6UfSg7
Commands typed in the window parse tags against the window's own tags,
which no server packet updates; that gap predates live switching and
needs its own change.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The Settings hint now reads (http://host:port, socks5://host:port). Each language keeps its translation, without the word HTTP and with the socks5 form added; i18n's tests want every string in every table.
Turn on ureq's socks-proxy feature and stop refusing SOCKS addresses, in port-pack's parse and the launcher's settings. socks5h, curl's name for SOCKS5 with remote DNS, is read as socks5, which already works that way in ureq. A test runs a request through a stand-in SOCKS5 proxy.
Fixes#8143
A workspace member with no code whose dependencies are all optional: Cargo
locks them, so a native mod's crate may name them, and no build compiles them
(horizon-client's dependency tree, default and --no-default-features, is
unchanged). All four are pure Rust and use the rustls + ring already locked.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
When entering borderless fullscreen on Windows while the window is
maximized, winit's WM_NCCALCSIZE handler forces the client area to
match the desktop work area (rcWork), which excludes the taskbar.
Because the window is sized to monitor dimensions, this clamps the
bottom edge above the taskbar and shifts the top edge offscreen by the
taskbar height (~48px), leaving the taskbar visible on ultrawide and
standard displays (upstream MC-311683).
Ensure that any transition to fullscreen unmaximizes the window first,
and explicitly pass the active monitor handle to Borderless mode so
the window covers the entire display area without vertical offset.
Fixes#9984
Co-Authored-By: Antigravity 2.0 <[email protected]>
MultiPlayerGameMode.handleContainerInput puts the changed slots into a new fastutil
Int2ObjectOpenHashMap and ServerboundContainerClickPacket writes them as it iterates: the key 0
first, then the table from its last position down. Horizon listed them in slot order. The wire
stream harness's inventory scenario caught it: a shift-click from slot 37 into slot 10 went out
37, 10 from vanilla and 10, 37 from Horizon, a byte-level difference any server can see.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: emotes, part 3 (#10069): others see your emote through the cosmetics service, now based on main. CI verify is green. Polling is bounded: at most once a second, only while Horizon players are in view, and only with Other players' cosmetics on Shown. Sending is signed and only while Show me as Horizon is on. Without horizon-cosmetics#14 deployed, the service answers 404 and the game backs off.
Killing an entity that is no mob posts ENTITY_DIE (frequency 15) at the
entity, as vanilla's kill paths do: BlockAttachedEntity.kill (cushions
punched, burnt, unsupported, struck or blown up; paintings; empty item
frames), ArmorStand.kill (broken, burnt out, blown up, /kill's
generic_kill), VehicleEntity.destroy (boats and minecarts, not a creative
hit, which only discards them) and Entity.kill from /kill for the other
non-living kinds. The attacker is the source when a hit kills it
(attributedTo), otherwise the entity itself. A frame holding an item only
gives the item up and does not die (ItemFrame.hurtServer). The event runs
on the server's path, so single player, LAN guests and dedicated servers
all get it.
New lan_e2e check entity-die: a sneaking host and LAN guest break each
kind 3 blocks from a sculk sensor, which now hears 15.
Part of #10046
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WZzq8LUjYucp4kaN6UfSg7
🤖 Agent: emotes, part 2 (#10069): the wheel and playback, retargeted to main after #10091. CI verify is green. Two clients played every emote in game; captures are in the PR. Visual only, nothing sent to game servers. #10093 follows.
Install ruby 1.0.0, turn it off in the Mods page, run the automatic
update to 1.1.0: the file is replaced and the mod is still off. Turned
back on, the next update keeps it on, and an explicit install of a
disabled mod turns it on again. Without the fix the first update
re-enables it and the test fails at 'the update kept it off'.
install_version_guarded() turned every mod it installed on with
java::set_enabled(home, id, true), and the automatic registry update
(browser/horizon/auto.rs update) installs through the same function.
A content mod the player had turned off in the Mods page therefore came
back on after every automatic update: 'off' is the absence of the
'mod.<id>:true' line in horizon-mods.txt, which the update re-added.
The guarded install no longer touches the flag. The player's own
install (install_version, which the browser and modpack paths use)
turns the mod on after the guarded install, as before, while the
automatic update leaves the mod's current on/off choice alone. A new
install still starts on because it goes through install_version.
`Creative::search_tab` and `select` as `Game::key_pressed` uses them: the
search tab is found by its type, selecting it focuses the search box, and the
query the previous tab left is emptied.
`CreativeModeInventoryScreen.keyPressed` takes the chat key on a tab that is
not the search as "show the search tab": it selects
`CreativeModeTabs.searchTab()` and sets `ignoreTextInput` so the letter the key
carries never reaches the search box. Horizon had neither: T on a creative tab
did nothing, and the letter went nowhere.
`Creative::search_tab` finds the search tab by its type, so the key selects it
through the same path a tab click uses (`select_creative_tab`, which clears the
quick-craft clicks as `selectTab` does). `Game::key_pressed` takes the chat key
there, and the `ignoreTextInput` flag it sets makes `type_chars_into_screen`
(`charTyped`) drop that one letter and no other.
🤖 Agent: emote format, the bottom of the emotes stack (#10069). pr_ready on the whole stack is green except the NixOS-only uname test. pose_body copies the target at full weight: 0.26–1.1 µs per emoting player and frame, measured. #10092 follows once this lands.
The items, orbs, projectiles, minecarts and boats were counted for the level in place
only (the End, on the SMP, when the report ends), so tick_boats' 3 ms had no count
beside it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
--clients horizon,horizon-legacy plays every scenario with two Horizon clients, the
second with the Legacy feel and every combat extra on, the 1.8 rules included, and
compares their serverbound streams as the vanilla comparison does.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The command registries took item and block tags from the resources the
game started with and were built once per world, so /execute and
/give refused a tag a switched mod added (seen in game: Unknown item tag
'c:gems/glint'). They now read the world's tags and are built again
after a live switch's data reload.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
On the official SMP, 12.6 of tick_lan's 14.7 ms a tick and all of tick_sculk_entities'
6.4 ms had no span below them. Name the steps: guest ticks, entity states, the hub's
level pass (world sync, chunks, block changes, containers, entity tracking, player
state), the end of the tick and the host events; sculk's prepare, wardens, step-on and
entity game events. Spans cost nothing while off.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The client sends /tp at a fixed tick, but a loaded server answers 2-6 ticks later from one run to the
next (seen with two identical Horizon clients). The ticks before the teleport compare one to one; from
the client's AcceptTeleportation on they align on it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
#10153 replaced the Wardrobe's still picture with viewer::Viewer. The Shop now
uses its own Viewer (drag to turn, scroll to zoom, Reset view) instead of the
still picture and its Turn button, so there is one renderer.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
23f8bd4fe removed the only consumer of MotionEvent::EnterBlock, and the event was pushed only in the client pass. The server pass pushes it now and the guest server tick triggers it for the acting player.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
The official SMP is mob-bound (5,400 mobs, 73 ms a tick at 60 players) but the report
only said tick_creature_entities took the time, not which mobs. Format 2 adds, without
a restart and at a measured cost:
- mob_costs: one tick in four times every mob's tick and sums it by dimension and type
(ms a tick, mobs ticked, us a mob);
- level_* spans: each dimension's share of the server tick;
- chunk_workers: streaming::job_stats while recording (load/generate/decorate/light...);
- network: wire bytes in/out per player and in all (connection counters, no new
per-packet cost);
- overhead: spans and mob samples a tick at their price measured on the machine;
- horizon-report stop / status.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`ServerLevel.setChunkForced` calls `getChunk` when a chunk becomes forced, so a
command right after `/forceload add` finds it loaded. Horizon only queued it, so
`execute in minecraft:the_nether run function` with `forceload add`, `place
jigsaw` failed with "That position is not loaded" and a skyblock pack's nether
island never appeared.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A data pack's `data/<ns>/structure/*.nbt` were ignored. A jigsaw pool naming one
placed nothing, so `/place jigsaw` failed with "Failed to generate jigsaw" and
`/place template` said the template does not exist; that is how a skyblock pack
places its island. Packs also could not replace vanilla's templates.
The enabled packs' templates now join the worldgen overrides, highest pack first,
over the prepared ones, as `StructureTemplateManager` reads resources. `/place
template` and structure blocks look in the world's `generated` folder, the packs,
then the game's own.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`LevelAccess::load_block_entity` decoded a template's block entity, and that
drops `LootTable`, so a chest placed by `/place template`, `/place jigsaw` or a
structure block came out empty. A skyblock pack's starter chest has its items in
a loot table. Report the table as a level event and hand it to `take_loot_table`,
as `/setblock` and `/data` do (`RandomizableContainer.tryLoadLootTable`).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
26.3 reads a direct block state provider as a state object. A bare string names
a provider in the `worldgen/block_state_provider` registry, so an upgraded
feature with a default-state `simple_state_provider` failed with
`unknown state provider minecraft:air` and was skipped at world start. Vanilla's
own features write `{"id": "minecraft:clay"}`.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Void packs turn off vanilla's iceberg noises with
`{"firstOctave": -1000, "amplitudes": []}`. The upgrader left this as it was and
called it "more than 32 octaves or negative amplitudes"; 26.3 cannot read it, so
the world failed to start with `invalid world generation field: base_octave`.
With no octaves the noise is 0 everywhere, which 26.3 writes as one muted octave:
`{"base_octave": 0, "amplitude_modifiers": [0.0]}`.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`state()` in the generator read only `{"Name", "Properties"}`, or the id alone,
so noise settings with `{"id", "properties"}` failed the world start with
`invalid generator resources: block state`. That is the form 26.3 uses and the
one `horizon-port-pack upgrade-datapack` writes for any non-default state. Decode
these two fields with `BlockStateJsonCodec`, as the rest of worldgen does.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A run's state ids count every change its server made since the login, so a click numbered 53 in one
run and 55 in the other were both up to date. hotbar_swap now also taps a number key with a swing at
nothing, where vanilla sends the slot only on the next tick.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
GameRenderer.pick aims along Entity.calculateViewVector: float radians through Mth.sin and Mth.cos,
the 65536-entry table. Horizon aimed with exact double trigonometry, which moved the hit point a
placement sends (ServerboundUseItemOnPacket's cursor) and an interaction's point by up to ~1e-4 of a
block. The wire stream harness's bridge scenario showed it: same position and look, cursor z
0.7173712 from vanilla, 0.7173119 from Horizon. Anticheats that recompute the cursor from the ray
compare it to vanilla's arithmetic. The other view_vector callers (projectiles, dragon, boats...)
are left for their own checks.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: verified on the desktop at this tree: horizon-cosmetics fmt/clippy/tests (40) and the launcher's fmt/clippy/tests (695) green; pr_ready skipped because the desktop disk was under 45 GB. Point to watch: preview::render now takes a Camera (launcher is its only caller), and lib.rs will conflict by one line with the emote format PR #10091.
Shift-clicking a stonecutter result queued ui.stonecutter.take_result for
every item taken, and the server side played every copy at once: cutting
60 granite played the sound 60 times stacked. Vanilla's lastSoundTime
lets it through once per tick. The menu now queues each take sound once
per click (stonecutter, loom, cartography table), and lan_station_sounds
plays one sound per tick.
Fixes#10148
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`Block.pushEntitiesUp` runs when these blocks gain the collision they add on
top. The farmland/path to-dirt path already emits the `push_entities_up` block
event; the three growth paths did not, so a body standing on them sank into the
new block.
🤖 Agent: Fixed to the 26.3 jar's DTOs: the region preference travels as its id (0/1/2) and the region as its nameId, so a Realm that has a preference parses again (it used to fail the whole RealmServer parse). preferredRegions reads regionDataList/regionName, and the configuration POST puts the slot under options with update_slot's RealmsSlotUpdateDto. New tests pin the POST body and the parsing; CI verify is green on this head.
Preserve the preceding scene color when a translucent shader draw reads
and writes the same buffer, rather than sampling a white fallback.
Reuse the existing snapshot mechanism to avoid attachment feedback.
Validated with Sildur’s Vibrant before-and-after captures and an
original asset-free GPU regression.
Co-Authored-By: Codex <[email protected]>
living_damage_pre and living_damage_post fire at the end of
LivingEntity.actuallyHurt, for players, mobs and the ender dragon. Pre is
told what is about to come off the health, once armour, enchantments,
effects and absorption have taken their part, and a mod may change it.
Post is told what came off, 0 when nothing did. The translated actuallyHurt
in horizon-world gets two hook points whose defaults do nothing.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
From review, against vanilla (getAttackBoundingBox and getHitbox read the
current getVehicle()):
- A mob vehicle's box comes from the mob snapshot, which holds it as it is
after its own tick this tick, not from the last positionRider.
- The seat positionRider records names its vehicle, and is used only while
the mob rides that one: a dismount or a new vehicle no longer reads an old
vehicle's box or attachment.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
1.21.1's client had every recipe and 27 of ATM10's mods listen to
RecipesUpdatedEvent to rebuild what they made of them. 26.1's client gets
only the recipe types a mod asks for, in RecipesReceivedEvent (NeoForge's
documentation, "Client-Side Recipes"), which recipes_received answers.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: wear() still fires the durability trigger after Unbreaking, and the broken stat now comes before the rod. The parity failure at ~17:00 was the playhorizon runner's dead .horizon-prepare.lock link, not this change: rerun on this head.
🤖 Agent: The height probe and the draw share bundle_tooltip, so a hidden grid can't leave an empty box taller than its lines. The parity failure at ~17:00 was the playhorizon runner's dead .horizon-prepare.lock link, not this change: rerun on this head.
🤖 Agent: The tag is copied before name_mob uses it up, like handleInteract's itemStackBefore. Follow-up gap, not this PR: vanilla also fires the trigger with an empty stack on non-item successes. The parity failure at ~17:00 was the playhorizon runner's dead .horizon-prepare.lock link, not this change: rerun on this head.
🤖 Agent: Same two calls finish_consuming makes, before the stack shrinks, as Consumable.onConsume orders them. drink_ominous_bottle awarded neither, so nothing counts twice. The parity failure at ~17:00 was the playhorizon runner's dead .horizon-prepare.lock link, not this change: rerun on this head.
🤖 Agent: The pack-less structure tags are a OnceLock keyed on the first resource root, as StructureSpawns::shared is. location_structure_tags_hold_their_structures passes with prepared resources. The parity failure at ~17:00 was the playhorizon runner's dead .horizon-prepare.lock link, not this change: rerun on this head.
🤖 Agent: On playhorizon (vscreen, lavapipe) every_screen_has_targets, pause_icon, erase_checkbox, controller_settings_pad and settings pass. The empty-label target can add a second target on a rect that already called target_as: drawn_targets dedups only against labelled widgets, not between targets. Nothing on main seems to do that today. Union run on playhorizon (main a1e0c0db0 + this gate's 11 PRs): fmt, clippy, all touched crates' tests pass.
The Wardrobe's still picture becomes a live view of the player's own skin
(both layers, slim or wide arms) wearing their items, drawn by the same CPU
renderer (horizon_cosmetics::preview) only when something changed:
- drag to turn (followed past the view's edges while held), scroll to zoom
up to 3x on the head, Reset view; hovering an owned item's card tries it
on (Nothing takes the slot's off) until the pointer leaves;
- items animate (swing, bob, spin) on the game's 20-tick clock with
Item::animate, as the game does, only while the window is in front and
not under reduced motion (HORIZON_LAUNCHER_MOTION=always keeps them going);
- horizon_cosmetics::viewer::Stage decides each frame: nothing changed, no
picture and no timer; at most 20 pictures a second while animating (one a
tick), 60 while dragging. Pictures are drawn at the view's size in points
(240x330, about 2 ms) and baked up to device pixels;
- preview::Camera holds the turn, tilt and zoom (with headroom above the
head for tall hats and back items), Draw::item_posed keeps items on posed
bones, and viewer::BodyMotion is the hook an emote plays through
(Viewer::play), for the emote format in flight (#10091).
The view is a GPUI view of its own, so a new picture rebuilds it alone;
the pointer is handled by the page's element around it, since GPUI Fast
hit-tests the page's own elements. "Turn" gives way to "Reset view" and a
hint, in every language.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Deferring the attempt to the event phase lets update_from_neighbour_shapes see the real level and ticks, as EndermanLeaveBlockGoal.tick does. leave_block_goal_uses_real_neighbor_shapes_and_keeps_air_placement passes with prepared resources. Still missing: canSurvive and the BLOCK_PLACE game event. Union run on playhorizon (main a1e0c0db0 + this gate's 11 PRs): fmt, clippy, all touched crates' tests pass.
frame, gui_render_post, tooltip_component, experience_orb_pickup and
item_dropped get the NeoForge event ATM10's mods listen to for the same
point: RenderFrameEvent.Pre, RenderGuiEvent.Post,
RenderTooltipEvent.GatherComponents, PlayerXpEvent.PickupXp and
ItemTossEvent. The bracketed notes say where a hook differs (#9921).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Vanilla's Gui.setScreen calls Screen.clearFocus on the screen it leaves, so
a screen that comes back starts with no widget focused. Horizon kept the
focus a mouse click gave a button (the title's focused_button, pause_focus,
death_focus, the options' focus), so going back to the main menu redrew that
button highlighted (AbstractWidget.isHoveredOrFocused) with the pointer
somewhere else.
The front screen is now tracked with key_screen() (which the keyboard's focus
already uses for its own screen changes); when it changes, every menu drops
its click focus. The startup menu does the same for its title behind the
options, before the loading hands that title to the game.
Fixes#9273
Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
`StatsUi::key` as `Game::stats_key` calls it: the cycle wraps both ways, a
number picks its tab and is clamped to the last one, a key without Control is
left alone, an empty Items or Mobs tab is skipped (and not switched to by its
number), and a switch scrolls the new list back to the top.
`StatsScreen.keyPressed` runs `MenuTabBar.keyPressed` first: with Control
held, `TabNavigationBar` cycles the tabs on Tab and Shift+Tab (skipping a tab
that cannot be chosen) and picks one on the number keys, where
`InputConstants.Key.getNumericKeyValue` reads the main row's digits and the
keypad's. Horizon's statistics screen had no keyboard handling at all, so only
the mouse could change tabs.
`StatsUi::key` is that tab-bar key press: it reports whether the bar took the
key, moves to the chosen tab when that tab has rows, and starts that list at
the top as a tab press does. `tab_key` ports `getNextTabIndex` (a digit is
`floorMod(digit - 1, 10)` clamped to the last tab; Tab walks to the next tab
that can be chosen, one full turn at most) and `numeric_key_value` ports
`getNumericKeyValue`. `Game::stats_key` gives it the tabs that have rows
(`stats_tab_active`) and plays the tab's click when the tab changes, and
`Game::key_pressed` hands the statistics screen its keys.
native_health_hud_state described left_height and right_height as the ones
that place the selected item's name and the action bar. They are gone, and
the text no longer reads them.
🤖 Agent: native_fluid_at now allocates the block id String on every call, drains and tank queries included. That's fine for mod helpers, but worth an &'static str if a tick path ever calls it. The fragment stamps (tests.test_stamp_native_hooks passed in the union run). I ran the native_fluids tests on this head on playhorizon, 12 passed, the new dispatch test included.
The two calls in hud_fixture/kinds.rs still passed the 39 the old signatures
took. The probe compares against the vanilla client, which reads neither, so
nothing else changes.
cargo clippy --examples -p horizon-client-lib -p horizon-client -- -D
warnings is clean.
- click-slot without a renderer clicks the menu itself (the vanilla driver's handleContainerInput); horizon-ctl's
clicks where the slot is drawn, which --headless never does.
- The reset's last teleport is the player onto itself: from the console `~` is the world spawn, which put the
player inside bridge's stone.
- BlockPos.asLong decodes with each part signed.
- eat cuts its two bites short: when a full one ends is the server's entity event, late on a busy server.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
MultiPlayerGameMode.ensureHasSentCarriedItem runs in attack, interact, useItem, useItemOn,
releaseUsingItem, dropItem, piercingAttack and continueDestroyBlock only. A swing at nothing,
the start of a dig or an offhand swap sends no slot: a number key pressed in that tick goes out
at the start of the next one (MultiPlayerGameMode.tick). Horizon sent the slot whenever the tick
had a click. The wire stream harness's hotbar_swap scenario caught it: a tap of 2 with a swing at
the air sent SetCarriedItem before the Punch, a tick before vanilla.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ServerPlayer.openMenu, when createMenu returns null for a spectator, sends
container.spectatorCantOpen to the action bar (red). Horizon's refusal in
spectator_open_menu and the chest-boat guest handler was silent.
🤖 Agent: merging. The layout lines only add keys to horizon-extras.txt; files without them load as before (every widget at its default place and style). Verify is green; stacked on #10077.
🤖 Agent: merging. Every widget reads local state only (no packets, no server queries); with every widget off, draw_pvp_hud returns after Hud::any(), so a player who never turns one on pays nothing. Verify is green; pr_ready --quick on playhorizon fails only the render crash:: tests that SIGSEGV under llvmpipe there, and crash.rs is untouched.
🤖 Agent: open_menu already counts the guest as an opener (container_opener_event), so the raise in lan_open_container counted them twice and stopOpen never reached 0, for chests as well as shulker boxes. The author's lan_e2e shulker_menu check pins the lid open, then shut, on both host and guest. Union on main dd675b574 green.
🤖 Agent: The server now sends one minecraft:vibration LevelParticles (count 1, no spread) with the listener's PositionSource and arrival ticks, and only draws locally when the window is the client, so a server-thread world doesn't draw it twice. Tested at a729b3cfe in a union on main dd675b574 (fmt, clippy, 4426 client tests green); this head only adds the author's main merge (test helper conflict) on top.
Mob.getAttackBoundingBox widens the attacker's box on x and z to its
vehicle's before the melee reach is added, so a zombie on a horse, a
chicken jockey or a mob in a boat reaches as far as the side of what it
rides. LivingEntity.getHitbox starts a rider's box at its seat
(vehicle.getPassengerRidingPosition(this).y), so its legs can't be hit; for
a riding player that is 0.6 above its feet (Avatar.DEFAULT_VEHICLE_ATTACHMENT).
positionRider now records each mob rider's own vehicle attachment and its
vehicle's box (mobs, boats, minecarts, primed TNT, displays), which the
melee check and the slime's touch read.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
An Entry on the Servers list can name a host: its folder is then on that
computer, and Entry::command runs the same horizon-server arguments there
as `ssh HOST horizon-server --dir DIR ...`, with the system's ssh and the
player's keys (BatchMode, so no password is asked for or kept). Each
argument goes to the remote shell in single quotes, and a host that ssh
could read as an option is refused. remote::check asks the server there
for its status and says in plain words what is missing: a key, the host,
horizon-server, or a server in that folder.
Lists written before keep reading as they were; nothing on the page uses
remote entries yet.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Jh5A2PzdusBChqE38VZ3zD
Vanilla reads network NBT with NbtAccounter's 2 MiB quota, which
charges each tag an estimate of its heap size: at least 8 bytes (EndTag,
the smallest; a byte tag in a list costs 13). Horizon counted only the
bytes read, plus up to 1,000,000 nodes, so a crafted server could send a
1 MB list of byte tags that vanilla refuses and have Horizon build ~1 M
32-byte Tags from it, about 16 times the memory.
- buf::Reader::nbt caps a network NBT value at NBT_QUOTA / 8 = 262,144
tags (NBT_MAX_TAGS). Every value vanilla accepts has at most that
many, so nothing vanilla accepts is refused, and one value's tags now
take at most ~8 MiB here.
- custom::validate checks component values with the same reader, so it
follows.
- Test: a list of 262,143 byte tags (with its root, 262,144) reads; one
more is refused although it is a few hundred KB.
A full port of NbtAccounter's per-type charges would be closer still;
this bound needs only its smallest charge, so it can't refuse what
vanilla takes.
Found in yahya-nusseibeh's review of Horizon (L2).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
tools/ first on sys.path made unittest discovery import tools/test_world_spawn_parity.py for
tests/test_world_spawn_parity.py.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The dedicated server and a LAN host decrypt each client's
ServerboundKey with the server's private exponent. That used
square-and-multiply, which multiplies only on the exponent's set bits,
with a data-dependent final subtraction in each Montgomery product, and
no blinding: a client controls the ciphertext and can time the reply,
the classic extra-reduction channel. The key is regenerated every start
(as in vanilla) and a recovered one still needs an on-path position,
but vanilla's JCE blinds this operation.
- Mont::mul subtracts n through a mask chosen from the overflow and
the borrow of t − n, never a branch (Big::sub_with_borrow,
Big::select). Every product takes the same steps.
- Mont::pow_secret: a square and a multiply for every bit of the
exponent's limbs, the product kept by a mask. RsaPublicKey::
apply_secret uses it; RsaPrivateKey::decrypt and ::sign call that.
Public-key verification and encryption stay on the faster pow.
- Big::from_be_bytes no longer skips zero bytes: it parses the private
exponent on every call, and the result is the same.
- Test: pow and pow_secret agree with a reference modular power, for
moduli near a limb boundary (the overflow case) and exponents with
leading zero limbs. The 11 existing RSA tests (OpenSSL signature,
known-key decryption, generated keys) pass unchanged.
No wire or parity change. Multiplicative blinding would be the more
complete fix once a modular inverse exists.
Found in yahya-nusseibeh's review of Horizon (L1).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
tab_list::visible with the HUD hidden answers false for a held key with two
listed players and for a local server with a list objective, and the
scoreboard's LAN test asserts the same through the game's call.
Hud.extractRenderState calls extractTabList inside `if (!isHidden)`, so with
the HUD hidden (F1) the player list does not draw at all, whatever the
player-list key or the list objective says.
Horizon's hud_layers only skipped the tab list under F1 when the chat screen
was open: with no screen, holding Tab drew the list over the hidden HUD. The
gate is now in tab_list::visible, next to the key and the local-server rules
it already applied, so every caller gets it.
After a refused login the connection stayed open, so within the 30 s
login window a client could send as many AUTH packets as it liked: with
the 16 connections allowed at once, thousands of guesses a second
against an RCON port on 0.0.0.0, each adding a supervisor log line.
Vanilla does the same, but this module already departs from vanilla for
safety (constant-time comparison, login deadline, client cap), and this
belongs with those.
- A refused login (wrong or empty password) answers as before, waits
REFUSED_PAUSE (1 s) and closes the connection. Its place stays taken
meanwhile, so the client cap also bounds how fast passwords can be
tried: at most 16 guesses a second instead of thousands.
- A command before the login is still refused without closing, and the
wire protocol is unchanged; standard RCON clients connect again after
a refusal.
- Tests: a wrong and an empty password each end their connection after
the pause; a failed login after a successful one does too. The pause
is a Limits field so tests use 200 ms.
Found in yahya-nusseibeh's review of Horizon (L3).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
A focused test drives the public block_entity_accepts_fluid dispatch, as the game does: a water tank that refuses a mod's fluid leaves both insert passes empty and offers native_move_fluids no room, while water and a tank with no hook still fill.
Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
The server runs two async workers and keeps SQLite on tokio's blocking
pool (lib.rs, `blocking`), but /hub/vote, /hub/votes and /hub/report and
the token-protected /v1/export/* still called app.store() on the
workers. While a blocking-pool task holds the store mutex (a large batch,
the daily upkeep), two votes or reports from different addresses park
both workers on it and every handler stops answering, /healthz
included; per-address limits don't help, it takes two requests.
/hub/votes also held votes_cache's lock while waiting for the store.
- hub::vote and hub::votes run their store calls through blocking();
votes reads the cache, releases its lock, counts on the blocking pool
and stores the result after.
- intake::receive checks the day's count and queues the report in one
blocking() call, under one store lock as before; report marks the
issue filed through blocking().
- export::benchmarks and export::hardware read through blocking().
- A failed blocking task answers 503 "try again later" through each
module's own refusal (the hub's keeps its CORS headers).
Behaviour is unchanged otherwise: the crate's 58 tests pass as they are.
Found in yahya-nusseibeh's review of Horizon (M1).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
Part of #9996.
The terrain noise (old_blended_noise) is a lerp between two 16-octave
smeared Perlin stacks. Lerp reads its first branch only where alpha is
not 1 and its second only where alpha is not 0, and in the overworld
about 37% of the coarse points have alpha 1 and 50% alpha 0. The lerp
now passes each branch a bit per Y of each column it reads, and a noise
volume skips the others (DensitySampler::sample_needed,
Noise::add_to_needed; samplers without it still sample everything).
When a column's Y cells change at every Y, as in the stacks'
high-frequency octaves, four Ys in four cells now go through the same
vectorized lane code as four Ys in one cell, each lane with its own
cell's corner terms. Gradient lookups are a loop rather than
array::map, which LLVM had left out of line.
Every value is computed by the same operations in the same order:
snapshots match byte for byte and the tests compare against the
unbatched, unmasked loops.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Fekd3xkgubE8VfXrALh2td
horizon-port-pack read jar entries with read_to_string/read_to_end and
no limit. zip stops an entry only at the size it declares, which can be
4 GiB (more with zip64), and the launcher runs check::check in its own
process on every Java mod a mod-browser search lists, without a click.
So one published mod with a highly compressible mods.toml could abort
the launcher of everyone whose search showed it. A 522 KB jar whose
mods.toml inflates to 512 MiB made `horizon-port-pack check` reach
594 MiB and abort; with this change it peaks at 9 MiB and refuses the
jar.
- crate::entry: read()/read_text() with a per-entry cap, and a Budget
for a running total over one pass of an archive, so many entries
under their cap can't add up either. Limits: 16 MiB per text entry
(JSON, TOML, the manifest), 64 MiB per file kept as it is, 1 GiB per
archive pass. Only bytes actually read count; an entry declaring more
than what is left is refused unread.
- Every zip read in the crate goes through it: jar::read's metadata
(a too-large file refuses the jar), carry's namespace scan and file
copy, worldgen's piece counts, structure-set and biome tags,
processors, upgrade::read_pack and import's JSON index. check.rs kept
its 16 MiB per-class cap and now also refuses a jar whose classes
together unpack to more than 1 GiB (a thousand entries just under the
per-class cap was the same attack).
- Tests: the cap and the running total (declared or not, refused
entries counting), UTF-8, and check() refusing a small jar whose
mods.toml unpacks past 16 MiB.
No parity change: real mod files are far under these limits.
Found in yahya-nusseibeh's review of Horizon (H1).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
It drives a vanilla client on a virtual screen for ~15 minutes: no automatic selection on every client change.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Mine: while an emote plays, the game tells the cosmetics service, signed in as for "Show me
as Horizon" and only while that is on: `POST /v1/emote`, with a session kept an hour
(`account::Kept`). A loop is said again every 30 s.
- Theirs: `horizon_cosmetics::Feed`, a thread, asks `POST /v1/emotes` which Horizon players in
view play one, at most once a second and only while some are in view, then fetches the item
files once. On failure it waits 15 s, doubling up to 10 min. Their models play the emote from
where it is; a player who moves ends theirs at once.
- Follows "Other players' cosmetics": with anything but Shown, nothing is asked and nothing is
drawn.
- The wheel: the account's emotes (`GET /v1/me`'s `emotes`), read when it opens, at most every
10 minutes.
Nothing reaches game servers; vanilla clients see nothing.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
launcher/Cargo.lock pinned rustls 0.23.45 (through ureq 2.12), which
cargo audit flags with RUSTSEC-2026-0346 (client session Debug impls
expose TLS secrets), -0347 (buffered data sent to the outer server after
an ECH rejection), -0348 (Brotli certificate decompression) and -0349
(post-quantum downgrade from an unauthenticated ServerHello), all fixed
in 0.23.46. The launcher carries Microsoft sign-in tokens over this
stack.
- `cargo update -p rustls --precise 0.23.46`: rustls alone moves.
- cargo audit on the new lockfile: no vulnerabilities (8 informational
warnings: unmaintained or yanked crates, unchanged).
Found in yahya-nusseibeh's review of Horizon (L4).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
From review, against vanilla:
- Sensor.isEntityTargetable is range(16) from feet to feet, a sphere; the
nearby scan alone was a cube.
- Panda's PandaPanicGoal and an adult PolarBear's panic use
#panic_environmental_causes as well; their own copies of it lacked freeze
and sulfur_cube_hot. They now share armadillo::environmental.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The mod page opened the project named by HORIZON_LAUNCHER_PROJECT (a
screenshot aid) and then called std::env::remove_var so it wouldn't open
again. By then the download, check and keyring threads are running, and
remove_var races getenv on any of them (glibc's DNS resolution
included): unsound, which is why edition 2024 makes it `unsafe`. The
launcher is on 2021, so it compiled silently.
- A static flag remembers that the page was opened this run; the
environment is no longer written. Same behaviour: the page opens once,
after the first search that finds it.
Found in yahya-nusseibeh's review of Horizon (L5).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
A busy server ran hit_target's reset teleport on the scenario's first tick: the vanilla client's first move
said off the ground and every later move differed. The reset now ends with a teleport, and the scenario waits
until the client has accepted it and ticked 40 more times.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- The wheel: Emote Wheel (`key.emote`, B by default, unbound on a controller; rebindable in
Controls) shows up to 8 emotes around the middle of the screen. To play one: hold the key and
release on an emote, click one, or press its number.
- The emote cancels (and fades out) on movement, an attack, mining, using an item, being hurt,
riding, sleeping, gliding or sneaking. Only a standing player starts one.
- From first person, the camera turns to the front view and eases out from close for the emote,
then comes back.
- Bones: `anim::setup` lays the emote over vanilla's animation, keyed bones only.
- Props: drawn with the cosmetics, growing in and out with the blend.
- `horizon-ctl emote [ID|stop]`.
- HORIZON_COSMETICS_LOCAL: the wheel offers the folder's emotes, and mannequins play its first
one (the art and the bench).
Visual only: nothing reaches the game server.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The server built its world tables from the content root only when the
world had data packs; without one it kept the tables loaded at startup,
so a switched mod's recipes and tags never applied (seen in game:
/recipe give still said Unknown recipe). After a live switch the tables
are built from the new root. An install that waits for a restart says so
on the project page.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
An item of the new `emote` slot carries an `emote` block: per-part tracks of keys (rotation,
offset, a prop's scale) eased smooth (Catmull-Rom), linear, in_out or step, one-shot or looping,
faded in and out over `blend` ticks. `Item::parse` checks its limits (200 ticks, 16 tracks,
32 keys a track and 192 in all, 360° turns and 8 px moves, the root 720° and 16 px, props'
reach through every key). `Emote::pose_body` poses the six bones (keys replace a bone's
rotation, keyed bones only), `pose_props` the props, and `Playing` gives an emote's time and
weight, its cancel included. Visual only, nothing random.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`screen_draw::paint`: an inner scissor draws to the overlap of both, a scissor
with nothing around it is cut to the screen, and one that misses the outer
clip draws nothing while the outer clip takes over again when the inner closes.
`GuiGraphicsExtractor.ScissorStack` pushes each clip intersected with the one
around it (the screen while there is none, `ScreenRectangle.empty()` when they
miss) and pops it back when it closes. `screen_draw::paint` kept a single clip
that each `Draw::Scissor` replaced: a nested scissor stopped the outer clip
applying at all, and the outer one never came back after the inner closed.
`paint` now keeps the stack: `Draw::Scissor` pushes the intersection with the
enclosing clip (or the screen) and `Draw::NoScissor` clips what was drawn since
and pops back to the enclosing clip. `intersect` is `ScreenRectangle.intersection`.
`RegionFileVersion.configure` was the row that named this gap: read from
server.properties and never applied. `Compression::configure` now applies it
from the dedicated server's startup and `compress_records` asks
`Compression::selected()`, so the row says `port` with what each half does.
NautilusAi.findNearestValidAttackTarget picks the closest #nautilus_hostiles
in water from NEAREST_VISIBLE_LIVING_ENTITIES, the sensor's list of what the
nautilus can see; Horizon took any pufferfish within 16 blocks, through
walls. Its cooldown and 1 in 2 are drawn from the level's random
(level.getRandom()), not the nautilus's own.
Armadillo.actuallyHurt panics (rolls out) on #panic_environmental_causes,
which also holds freeze and sulfur_cube_hot; Horizon listed the other six.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Two cases, one per half of the port:
- `compression::tests::the_selected_region_version_follows_the_property` walks
the four names `RegionFileVersion` registers (`gzip`, `deflate`, `none`,
`lz4`) against `Compression::selected()`, checks the default before any is
set is `deflate`, and checks that `zstd` and the wrong case `LZ4` are refused
and leave the selection alone, as vanilla logs the value and keeps the one it
had.
- `save::tests::a_chunk_is_written_with_the_selected_region_version` runs the
function the save path calls, `compress_records`, once per name, and asserts
the record's codec is the selected one and that its bytes decode back to the
same chunk.
With `compress_records` back on `Compression::Zlib` the second test fails with
`left: Zlib, right: None` ("written with none") and would then fail on `lz4`
with `left: Zlib, right: Lz4`.
Also here: the helper-parity test next door now dates its expected records with
`Compression::selected()` rather than `Zlib`, so it keeps checking that the
helpers agree with one thread whatever version the process selected instead of
pinning the default.
Vanilla's `Main.main` hands `server.properties`' `region-file-compression` to
`RegionFileVersion.configure` right after the settings are saved, and every
`RegionFile` written afterwards uses the version it selected (`getSelected`):
`gzip`, `deflate` (the default), `none` or `lz4`. Horizon parsed the key into
`ServerProperties.region_file_compression` and nothing read it, so every chunk
region was written as `Compression::Zlib` whatever a server asked for.
`Compression::configure(name)` and `Compression::selected()` are the two halves
of `RegionFileVersion` over the same kind of process-wide selection, starting at
`VERSION_DEFLATE`, with `REGION_FILE_NAMES` in the registration order vanilla's
error message lists. A name is matched exactly, as `VERSIONS_BY_NAME` does; an
unknown one changes nothing and says so. The dedicated server's startup calls it
where vanilla does, before the EULA check, and logs vanilla's line for a value
the table does not know. `save.rs::compress_records` encodes each record with
the selection, so the chunks the save path writes carry it; the read side needs
no change, as `Compression` already decoded all four and the header's version
byte names each chunk's codec.
focus_on looked a widget up before stepping, so a row scrolled out of view
ended the case: it now steps until the row is drawn. optimize_world's job
finished before the case could look on a fast machine: 60000 linked regions
take long enough and cost almost nothing. A tiling compositor sizes the hidden
window too, which fails every case that scrolls, so the harness warns.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
RegionSelectionPreference travels as its id (0, 1, 2) and the region as its nameId, so a
Realm with a preference no longer fails to parse as a String. PreferredRegionsDto reads
regionDataList / regionName / serviceQuality, RealmsConfigurationDto posts the slot under
options with update_slot's RealmsSlotUpdateDto (worldTemplateId, worldTemplateImage), a Realm
without a preference is automatic by owner (RegionSelectionPreferenceDto.DEFAULT), and the
labels use the realms.configuration.region* keys vanilla's settings tab shows.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
tools/test_wire_stream_parity.py: a Horizon client and a real vanilla 26.3
client join a vanilla server through tools/net_capture.py and play the same
tick-indexed scenarios (walk, sprint-jump, strafe, w-tap, sneak, hits,
sprint combos, block-hit, rod, bow, eat, bridging, inventory, hotbar swaps,
a teleport during a dig). tools/wire_stream.py cuts each serverbound stream
into client ticks (ClientTickEnd ends one) and decodes the packets; every
tick must hold the same packets with the same fields.
Horizon plays a scenario from HORIZON_TICK_SCRIPT (game/tick_script.rs),
before each client tick; vanilla from VanillaTickScript.java, which hooks a
fixed point of Minecraft.tick (Tutorial.onLookAt) and presses the keys
through KeyMapping.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
LocalPlayer.isShiftKeyDown reads input.keyPresses, which LocalPlayer.aiStep
sets from the keys: what the last ServerboundPlayerInputPacket said.
Minecraft.handleKeybinds runs before it, so a sneak pressed in the frame of
a right-click counts from the next tick. Horizon read the live Shift key for
ServerboundInteractPacket's secondary flag and for every secondary-use
check (isSecondaryUseActive): a click in that frame sent a sneaking
interaction a server saw before the sneak itself.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
LocalPlayer.sendChanges sends a passenger's look, then the vehicle it
controls, and only then its sprint (sendIsSprintingIfNeeded); riding a
vehicle it doesn't control, it sends no sprint at all. AbstractBoat.tick
sends the paddles from the boat's own tick, before its passenger's
sendChanges. Horizon sent the sprint first and for any vehicle, and the
paddles last.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
MultiPlayerGameMode.tick, first in Minecraft.tick and run with a screen
open too, sends the slot chosen since the last tick
(ensureHasSentCarriedItem); LocalPlayer.sendChanges sends no slot. Horizon
also sent it at the end of the tick that changed it, before the input and
the move: a number key pressed in a tick went out one tick early, on the
other side of the movement packet a translating proxy (ViaVersion to an
older server) turns into that tick's flying packet. An action in the same
tick still sends the slot first, as ensureHasSentCarriedItem does.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ClientPacketListener.handleMovePlayer accepts the teleport, then runs
MultiPlayerGameMode.stopDestroyBlock: a dig the teleport interrupts (an
anticheat's setback while mining or bridging, a /tp) sends
ABORT_DESTROY_BLOCK and starts the attack strength again. Horizon dropped
the dig without a word, so the server kept counting it.
BlockStatePredictionHandler lives in the ClientLevel: handleLogin (a proxy's
server switch) and handleRespawn into another dimension make a new one, so
the use and dig sequence numbers count from 0 again. Horizon counted on for
the whole connection.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Hud.extractSelectedItemName is hud_overlays::ToolHighlight::draw and
Hud.extractOverlayMessage is hud_overlays::Titles::overlay_message; neither
row existed. map-check: 151 rows, no problems.
text_position_tests asks a ToolHighlight holding a stack and a Titles
holding an action bar for their draw calls at 320x240: the name's text top
is 240 - 59 (and 240 - 45 in a mode that cannot hurt the player), the action
bar translates to 240 - 68 with its text four pixels under that, and a
zeroed timer draws nothing. The font is built from no resources, as
chat_tests does, so the test needs no game assets.
Hud.extractSelectedItemName draws the name at guiHeight - 59, plus 14 in a
mode that cannot hurt the player, and Hud.extractOverlayMessage puts the
action bar at guiHeight - 68: both constants, which the health, food, air and
mount rows above the hotbar never move.
Horizon moved both up by max(leftHeight, rightHeight). That is 59 for a
survival player with armour, so the common case agreed by accident, but it is
69 with a second row of hearts and 81 for a player underwater on a mount, and
the two texts floated ten or twenty pixels above where vanilla draws them.
Both use the constants now, and HealthHud no longer keeps the heights: 26.3's
Hud has no such fields, only the locals of extractItemHotbar that the
contextual-info bar reads.
A mapping bug report needs to know whether gilrs found an SDL row for a pad
and which id such a row would need. pad status now lists mapping_source
(sdl, driver or none) and uuid (32 hex digits, the id an SDL row starts with).
With splitscreen on, PadRouter holds the gilrs reader, so pad status said
backend:false and listed no controllers, unless the reader was lent to P1.
The router now leaves what it sees for the status of each game that has a
control socket; that list is only built when one is open.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Add to basket on the chosen card, the basket's lines and total with the
service's rules (owned items and items inside a set in the basket cost
nothing), a gift to a friend looked up by name, the Supporter badge's
amount within the catalogue's range, and "Ask whoever pays before you
buy. Refunds within 14 days for any reason." by Pay, which starts
Paddle's checkout on the service and opens playhorizon.dev/shop/pay.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The catalogue's items, sets, collections and the Supporter badge as cards
with their prices, the chosen card tried on the account's own skin, and
the founder set's claim (one click where Paddle doesn't sell, else
Paddle's free checkout in the browser). No badge on the tab, it never
opens by itself, and a catalogue that doesn't load says so in one line.
The Wardrobe's one shop button now opens this tab.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
81 of ATM10's mods listen to RenderLevelStageEvent to draw in the
world at a stage of the frame. Main has three hooks for that, none
labelled:
- world_submit (fragment world_canvas): quads and text right after
the particles, before the hand;
- world_primitives (fragment): lines, boxes, beams and text at the end
of the world pass, before the hand;
- world_drawn (API 58): the drawn world's colour, depth and matrices,
once the whole level is drawn.
Each gets "neoforge": "RenderLevelStageEvent (...)", with where it
draws in words, so horizon-port-pack events maps the 81 mods to them.
world_drawn also gets side client and cancellable false; the fragments
had theirs. NeoForge's user documentation doesn't cover this event,
so the labels use only its name.
Fixes#10036
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The Mods page froze every content mod's toggle while a world was open.
Those with only assets and data now reload live, so their toggles stay
active in a world this game serves; the others keep the lock. Each mod
is classified once when the mods are listed, and a native mod's toggle
no longer triggers a resource reload.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
EnchantmentHelper.doPostAttackEffectsWithItemSource runs the victim's
equipment before the attacker's weapon: the player's melee hit and sweep,
arrow, trident and wind charge now meet the mob's own post_attack
effects. Thorns rolls from the level's random, hurts the player from
the wearer by 1-5 and wears the piece by 2 (Unbreaking, breaking with
its entity event); the player's own Thorns doesn't answer a thorns hit,
and a thorns hit that lands whole plays enchant.thorns.hit.
The mace harness gets armored targets with Thorns and Unbreaking; new
lan_e2e check mob-thorns.
Part of #9954
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The note kept 0.1 at least until 0.2 leaves experimental, which would keep
today's HDR path (marks, the 16-bit unorm frame, Smooth Gradients) alive for
0.1 packs alone. The shader API is experimental, which stability.md lets
change in any release without notice. So step 5, which turns 0.2 on by
default, also stops serving 0.1: 0.1 packs are refused with stability.md's
message, the registry lists 0.2 packs, and Golden Hour's 0.2 port becomes the
example. Later experimental lines follow the same rule. The shader API page
tells pack authors.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A --control-socket path over the system's limit was only found when the
game tried to listen, after loading resources: it logged the raw SUN_LEN
error, wrote a crash report and exited 1, leaving startedCleanly:false so
the next start reset fullscreen. It is a bad command line: exit 2 with a
plain message, before the game folder, options.txt or resources are
touched. horizon-ctl launch refuses it as usage too, without starting a
game or creating a log beside the socket. A .json endpoint is TCP and has
no such limit.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The port ran 0.1's grade on linear values, which darkened midtones by about
nine 8-bit steps. The soft shoulder and the saturation are a look written for
display values, so the grade now runs them on the encoded picture, while the
golden tint, the glow and the vignette stay on light. The pack drops its own
[tonemap]: Horizon's curve clips at white in SDR as 0.1's clamp did, and light
above white survives the grade for HDR. The captures now match 0.1 within
about 0.3 steps on average; what's left is the glow around light sources.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
NeoForge documents ServerStartingEvent as fired after
ServerAboutToStartEvent and before ServerStartedEvent. Horizon has
server_about_to_start at the start of loadLevel, and world_lifecycle's
WorldOpenReady once the open has finished, before the tick loop.
Nothing of Horizon's runs between the two NeoForge events, so
WorldOpenReady answers both (30 ATM10 mods listen to Starting).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
1.21.11 sends instrument, provides_trim_material, damage_type,
chicken/variant and zombie_nautilus/variant as EitherHolders (a flag,
then the entry's holder or its ResourceKey), as it does jukebox_playable;
26.1 made them plain holders. Horizon read the flag as the holder, so a
goat horn's slot came one byte long and the entity data around it one
byte short ("could not translate 1.21.11 packet 20/101").
The flag is read for all six now: a holder reads on as the pinned one,
a key (no pinned numbering for it) drops the component.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
From review, against vanilla:
- getActiveItem().get(ATTACK_RANGE) includes the stack's patch: an added
minecraft:attack_range (the codec's defaults: max_reach 3, min_reach 0,
mob_factor 1) sets the reach, and !minecraft:attack_range takes a spear's
away.
- AbstractCubeMob.dealDamage calls isWithinMeleeAttackRange, for the player
it touches and for iron golems; it repeated the default formula.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The catalogue's shop fields (currencies, prices, sets, collections, the
Supporter badge), the player lookup and the web checkout in the wardrobe
client, and the basket's price rules in shop.rs, as the website applies
them. Nothing shows them yet: the Shop tab comes next.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Apply the existing leash writer after the boat's base command tag so live
holders and pending links override stale input data. Keep holder policy
and boat behavior in their existing implementations.
Add an actual Game getter regression and a guarded Java/Game fixture
covering owner clicks, repeated reads, pending UUIDs and detached links.
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
Mob.isWithinMeleeAttackRange takes the active item's attack_range when it
has one: the box is inflated by effectiveMaxRange (max_reach * mob_factor,
2.25 for every spear) instead of the default sqrt(2.04) - 0.6, and a target
within effectiveMinRange (min_reach * mob_factor, 1.0) is out of range.
Horizon always used the default, so a mob given a spear without a
SpearUseGoal (a vindicator, say) hit what stood right beside it and missed
what the spear reached.
New mob AI scenarios: a vindicator with a spear against a player out at the
spear's reach and right beside it, and zombies with a spear (their
SpearUseGoal; the probe now names it as vanilla does).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Seven more hooks whose doc comments already name their NeoForge event:
screen_render_post (ScreenEvent.Render.Post), the five screen input
hooks (ScreenEvent.MouseButtonPressed.Pre, .MouseButtonReleased.Pre,
.MouseScrolled.Pre, .KeyPressed.Pre, .CharacterTyped.Pre), all client,
and item_picked_up (ItemEntityPickupEvent.Pre, server), whose cancel
stands for setCanPickup(TriState.FALSE), with no forced pickup.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Through `ChatScreen::new` with `is_draft`, `key` and `draw` - the calls
`Game::open_chat` and `Game::chat_key_code` make: the draft's drawn text is
grey and italic, an edited draft and a box opened on ordinary text are not,
and the first Backspace on an untouched draft empties the box where an
ordinary box loses one character.
`ChatScreen` keeps the `isDraft` its constructor argument sets: `formatChat`
draws the text grey italic until `onEdited` runs, and `keyPressed` clears the
whole box on the first Backspace while it is still set. Horizon tracked the
draft (`Game::chat_draft`, `chat_draft_shown`) but opened the box as plain
text: `open_chat_method` knew whether the text was the draft and did not pass
it.
`ChatScreen::new` takes `is_draft` (`ChatScreen.init`'s `isDraft`), `edited`
(`onEdited`) clears it, `key` (`keyPressed`) clears the input on a Backspace
while it is set, `format` applies the draft formatter (`formatChat`, which
`ChatScreen.init` registers before `CommandSuggestions.formatText`) and
`resize` keeps it (`ChatScreen.resize` keeps the field).
`Game::open_chat` threads the flag from `open_chat_method`
(`ChatComponent.createScreen`); the in-bed wake-up
(`InBedChatScreen.onPlayerWokeUp`) and `Gui.openChatAndAddText` open plain
text, as vanilla does.
Broadcast unsigned say chat independently of feedback capture and keep the
command source display name. Add a two-client LAN regression for both
TrackOutput states and conditional-chain success.
Co-Authored-By: Codex <[email protected]>
The host's open unpacks the loot table it still holds; the guest's
(lan_open_container) read the slots without it, so a LAN or splitscreen
player met an empty container that kept its table, and
player_generates_container_loot never fired for them. unpack_opened_loot
now runs for the acting guest too.
ChestBlock.getMenuProvider unpacks both halves of a double chest; only
the clicked half was unpacked. After its own roll is consumed, the
partner's is unpacked too (the consumed own roll ends the recursion).
Every documented key: particle, fog and sky colours, potions, spawn eggs,
map, sheep, collar and dye colours (with their old names), text and its
colour codes, the loading screen, clouds, xporb.time, palette.format and
palette.block.* assignments.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A client mod hears the window's player log in to a server, single
player's own included: native_client_player_network_logging_in(), at
the end of the Login arm (ClientPacketListener.handleLogin), after the
new client level. Respawns and dimension changes don't log in again.
Horizon makes the player with the server's first position, so
native_player() is None there; native_send_packet reaches the server.
client_disconnecting, which main fires before a connection is
discarded (Minecraft.disconnect), gets the catalog label
ClientPlayerNetworkEvent.LoggingOut, so horizon-port-pack events finds
it. The docs describe the pair.
Checked in game with horizon-ctl and a local probe mod: one login on
opening a world and one on reopening it, none on a trip to the Nether
or a respawn, and a logout on Save and Quit and on quitting the game.
Fixes#10008
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Edit HUD on the PvP & HUD page (in a world) or the HUD editor key
(unbound at first) opens a screen over the game: each widget in a frame,
dragged anywhere with snapping to the screen's edges and middle and to
the other widgets, the wheel resizing it, a right click sending it back
to its corner; buttons for text colour, shadow, boxes and opacity, Reset
and Done. Widgets with nothing to show yet stand in by their name. The
layout is saved to horizon-extras.txt as it changes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Each HUD widget can leave its corner for a place kept as fractions of the
screen's free room (so it stays on screen at any size) and a size from 50
to 200%, saved as hudPlace lines in horizon-extras.txt only once moved.
The page gains the boxes' opacity, the text colour and the text shadow.
Snapping (screen edges, middle, other boxes) is here for the editor.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Each frame picks the layers of the Overworld's or End's sky whose day,
weather, biome and height hold at the camera, eases them over their
transition, and draws them as 3x2 skyboxes with their blend mode, over the
vanilla sky's disc and sunrise (the End's box) and under the sun, moon and
stars. Packs without optifine/sky draw nothing new.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Six more widgets on the PvP & HUD page, off by default, each from the
client's own state: the frame counter, presses of the last second, the
movement and mouse keys while held, the local time, the distance of the
last hit the crosshair's ray measured, and horizontal speed sampled from
the player's position. Each caches its lines and rebuilds them only when
the shown value changes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Resolved the one conflict, in game/lan_particles.rs: test_hub keeps the
shared helper the vibration test needs, and its guests now carry main's
Joined.expected field and bind through NetConfig::loopback. The
sculk_vibration LAN check survives main's rewrite of tools/lan_e2e.py.
Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
Projectile.mayInteract asks mob_griefing for every owner that isn't a
player, and canBreakBlockInAdventureMode lets no non-player owner break
a block without can_break. An owner UUID that names a loaded armor
stand, boat or other non-mob entity was treated as no owner, so a
burning arrow lit TNT and campfires with mob_griefing off. The arrow now
asks the level for the UUID, as getOwner does.
Fixes#9999
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The shulker_menu check now keeps host and guest screenshots of the lid before and after the guest closes the menu, so a stuck lid is visible as well as asserted.
Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
Parse optifine/sky/world<N>/sky<M>.properties into layers (source, fade
times, blend, rotation, days, weather, biomes, heights, transition), the
sun and moon_phases replacements, and the 3x2 skybox grid's faces.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Keep stale home memories while checking the current HOME POI before
counting a loaded bed claim as a village center. Preserve unavailable
claims and existing job/meeting paths.
Co-Authored-By: Codex <[email protected]>
From review of the previous commit, against vanilla:
- Enderman.canRandomlyTeleportTo refuses ground that holds water (a
waterlogged slab, leaves or roots).
- containsAnyLiquid and findBlocksIn scan the same closed range of cells.
- Both teleport sounds play at the feet (Entity.playSound).
- The game's sculk layer now hears GameEvent.TELEPORT from a mob.
- Enderman.hurtServer teleports inside every hurt, not only the rain's: the
fire, lava and block hurts of baseTick and a fall's, and a NoAI
enderman's, which never teleported. A lingering cloud's indirect_magic
draws the chance too; only a thrown potion skips it.
New mob AI scenarios: enderman_rain_waterlogged (red on the previous commit,
it landed on the slabs at tick 2) and enderman_rain_noai.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ServerPlayer.die posts entity_die at the player, and with a kill credit
awards killed_by for the killer's type. A creeper, sulfur cube or wither
whose own blast kills the player is still credited, as vanilla's player
dies inside the explosion: ExplosionSource carries the mob's type.
New lan_e2e check death-credit.
Fixes#9955
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WZzq8LUjYucp4kaN6UfSg7
When the end portal frame ring is completed by placing the last eye of ender, vanilla destroys blocks in the 3x3 portal opening so that their drops and break effects occur instead of silently deleting them. Also awards minecraft:used for minecraft:ender_eye upon insertion into a frame.
Co-Authored-By: Antigravity <[email protected]>
AgeableMob.isBaby is now a row of its own pointing at Mob::is_baby, which is
`can_be_a_baby && getAge() < 0`; canBeABaby points at
species::can_be_a_baby, and the six classes that override it to false carry a
row each so the false arms are not read as unported.
living.rs::a_negative_age_is_a_baby_only_where_can_be_a_baby_allows_it drives
Mob::new on the loading table's own types and sets Age to -24000, the
BABY_START_AGE a save or a /summon carries, then asks is_baby, dims and
voice_pitch - the three the game draws and hears from.
A parrot, a tadpole, a frog, a zombie nautilus, a camel husk, a slime, a magma
cube, a piglin brute and a creeper keep their type's box (the tadpole 0.4 by
0.3, the camel husk 1.7 by 2.375, the piglin brute 0.6 by 1.95) and voice at
1.0 ± 0.2. The control is a cow at the same age: is_baby true, Cow's
BABY_DIMENSIONS (0.45 by 0.7, eyes at 0.69) and 1.5 ± 0.2, and an adult box and
voice again at age 0. With is_baby back to `age < 0` the test stops at the
parrot.
entity_nbt's save test now runs a frog and a camel husk beside the slime, the
magma cube and the pig: the first four write no Age and keep none of a raw
compound's Age, the pig writes its modeled -37.
AgeableMob.isBaby is `canBeABaby() && getAge() < 0`, and canBeABaby is true
across the ageable tree but for the six classes that override it to false:
Frog, Parrot, ZombieNautilus, CamelHusk, Slime and MagmaCube (an
AbstractCubeMob is an AgeableMob). Everything outside that tree inherits
LivingEntity.isBaby's false, so a Giant, a Creeper, a PiglinBrute (it extends
AbstractPiglin and declares no isBaby of its own) and a Tadpole, whose own Age
runs up from -24000, are never babies either; only the zombies, the piglins and
the zoglin answer a synced DATA_BABY_ID instead of an age.
Mob::is_baby read `extra.age < 0` alone. The list of who can be a baby existed
already, but only as mobs::drops::can_be_baby, which gates the metadata baby
flag and the loot table: the box, the voice, breeding, growth and the goals
read the age and so called a frog, a parrot, a creeper or a tadpole with a
negative Age a baby. The list is now species::can_be_a_baby, on the species
table, and is_baby is `can_be_a_baby && age < 0`.
What that changes: a tadpole (Age -24000, its own DEFAULT_AGE) is drawn at
getAgeScale's 1.0 instead of 0.5 and speaks at 1.0 ± 0.2 instead of 1.5 ± 0.2,
as vanilla does; a parrot, a frog, a zombie nautilus, a camel husk, a slime, a
magma cube, a piglin brute, a creeper or anything else the table cannot make a
baby keeps an adult's box, voice, breeding, growth and drops. The Parrot arm of
voice_pitch was there for the same reason and is dead now: Parrot.getVoicePitch
is `getPitch(random)`, the plain range an adult already gets.
AgeableMob.addAdditionalSaveData writes Age only when canBeABaby, and the
branch that drops the values a raw compound carried named four classes by hand;
it asks the ageable table now, which also covers the frog and the camel husk
that were re-saving an Age vanilla writes for neither.
Options > Mods' switch now reaches the trait mods' dispatch: the game skips
a turned-off mod's Hooks from the next call, on the server thread and in
the window, and turning it on again resumes it with no rebuild while it is
in the build. Registration at start is untouched, so its content stays
registered; RenderHooks keep drawing until a restart; mixin code still
checks native_on.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A content root is built per set of mods (.horizon-cache/mod-content/<stamp>),
so a running game keeps reading the root it was given while a switch
builds another; roots unused for a day are removed. Turning a mod on or
off in Options > Mods, or installing one from Browse Mods, applies at once
when the mod has only assets and data: the client reloads its resources,
then the server reloads its data packs as /reload does and resyncs its
players. A mod that adds blocks, items, mob effects, entity or menu types
or changes world generation keeps loading at start, and the Mods page
says to restart.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Record a marker only for Nightlies installed by automatic updates. Keep
manual and legacy Nightlies out of GUI and CLI cleanup, while retaining
the newest managed Nightlies separately from game releases.
Add regression coverage for manual, legacy, selected and pinned builds.
Co-Authored-By: Codex GPT-6 <[email protected]>
Options > Mods > Browse Mods lists the registry's content mods for this
game (the index the launcher reads), searchable by name, tag and category,
with a page per project: authors, licence, categories, description and
changelog, Install or Update, its switch once installed, and its page on
the website. An install is checked as the launcher's (size, SHA-256,
signature, manifest against the index) and recorded in mod-browser.json,
so the launcher lists, updates and removes it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
What content mods can switch live (assets and data), what needs a world
reload (registries, worldgen), the in-game mod browser, and the native
fast path: every installed native mod built in and switched at run time,
with the dylib and wasm paths left to arismoko's fork.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
In vanilla (\LivingEntity.tick\), a passenger's body yaw matches its vehicle's rotation
(or living body yaw), clamped to keep head yaw within [-85°, 85°], and does not update
limb swing / walk distance from vehicle motion.
Furthermore, \Minecraft.handleKeybinds\ blocks opening the inventory (\keyInventory\)
while in a portal transition (\handlePortalTransitionEffect\), preventing repeatedly
reopening the inventory screen while standing in nether portals.
- Compute ridden vehicle yaw for boats, minecarts, mobs, and cushions in \perspective.rs\.
- Align rider body yaw to vehicle rotation, clamped to [-85°, 85°] of head yaw.
- Disable walk distance accumulation when riding so the player model does not swing limbs.
- Guard \KeyCode::KeyE\ in \game.rs\ and \
emote.rs\ when in portal transition.
Co-Authored-By: Antigravity <[email protected]>
Enderman.teleport is LivingEntity.randomTeleport(x, y, z, true,
#enderman_does_not_teleport_to). Horizon took the first full-collision
block as the ground and landed on top of it, checked water only at the
eyes and lava only at the feet, and never looked at the tag, so endermen
teleported onto magma, into fire, sweet berry bushes and wither roses.
Now, as vanilla: the ground is the first #entities_can_teleport_to block
below the spot, and the enderman lands at the spot's height less the blocks
it went down. The ground and every block in the new box must not be in
#enderman_does_not_teleport_to, and the box must hold no liquid. The
motion is kept (Entity.teleportTo -> snapTo), both teleport sounds play at
the new spot, and GameEvent.TELEPORT goes out from the old one.
The new rain scenarios also showed when the water's hurt comes. It is the
end of LivingEntity.aiStep, after pushEntities, with or without AI, not
baseTick. Enderman.hurtServer draws its 1 in 10 teleport chance after
super.hurtServer (its hurt sound's pitch first), also when the hurt
cooldown refused the damage, and teleports at once. Horizon instead forced
a teleport in rain without the draw, one tick late.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The configuration screen's Settings tab (RealmsSettingsTab): the world's name and
description fields (the click moves the caret between them), the region preference line
and its Region Preference button, and the open/close toggle moved here from the footer
(vanilla keeps it on this tab). Leaving the tab — switching tabs or Back/Escape — saves
the changed name, description and preference through RealmsClient.updateConfiguration
(POST worlds/{id}/configuration with RealmsConfigurationDto's JSON: the active slot, its
settings, the preference and the description).
RealmsPreferredRegionSelectionScreen: the automatic choices and every manual region, a
click selects, Done writes the choice back to the tab and Cancel drops it.
RealmsClient gains getPreferredRegionSelections (GET regions/preferredRegions) and
updateConfiguration; RealmServer reads regionSelectionPreference; RegionSelectionPreferenceDto,
RegionDataDto and RealmSetting serialization join the DTOs.
The tier's render/simulation distance options and the region quality icons are the next
PRs (the icons need sprites the GUI atlas does not carry yet); the region rows say their
name for now.
Co-Authored-By: ZCode <[email protected]>
The Worlds tab's Backup button now opens RealmsBackupScreen: a row per automatic backup
with its date (mco.backup.entry), Restore per row, and Back. A row opens
RealmsBackupInfoScreen: the backup's metadata as translated key/value rows
(mco.backup.entry.<key> when the language has it). Restoring asks nothing (vanilla's
Restore button restores at once) and the list reads again when it is done.
RealmsClient gains backupsFor (GET worlds/{id}/backups) and restoreWorld
(PUT worlds/{id}/backups?backupId={id}, vanilla's 40 s/600 s timeouts), with Backup's DTO
(backupId, lastModified, size, metadata, changeList). The backup id goes into the query
percent-encoded, so it cannot add a parameter.
Download Latest still needs the download-and-unpack flow and is left for its own PR; the
rows show the date and the metadata screen the details (size included).
Co-Authored-By: ZCode <[email protected]>
A client mod hears each client tick of the window, at the top and the
end of vanilla's Minecraft.tick: native_client_tick_pre() before any of
the tick's work, native_client_tick_post() once the world's tick, the
chat queue, music and sounds are done. They fire 20 times a second on
the title screen, while paused and in a world alike, as vanilla's tick
runs, and never on a server (single player's ticks on its own thread).
The fragment labels them ClientTickEvent.Pre / .Post, client, not
cancellable.
Checked in game with horizon-ctl and a local probe mod: about 20 calls
a second of each, always paired, on the title screen, in a creative
world and under the pause menu.
Fixes#9971
Co-Authored-By: Claude Opus 5.5 <[email protected]>
These labels reach tr through a variable (the Launcher settings' slider rows, MODE_NOTES in
the server dialog), so the scan for tr("...") never asked for them and no table had them: they
showed in English in every language. Added to every table, in English, and translated in
fr.json.
The GUI runs the shared pruning path after saving its selection, but its
release-only filter excludes every dated Nightly. Include those builds
so the existing retention, selection, instance-pin and in-use protections
also apply to GUI Nightly updates.
Add regression coverage for following updates, selected and pinned
Nightlies, and retention across Nightly/release/local builds.
Co-Authored-By: Codex GPT-6 <[email protected]>
The landing's raw write is applied before the game finishes it: the game
now asks the hook there, and a cancel puts the previous block back and
skips the neighbour updates and the level events. The crop above trampled
farmland stays, because breaking it is the neighbour update and not a
write of its own.
`FallEvent::Write` carries the block it replaced, so the game has
something to put back. A guest's landing is asked with that guest's UUID.
A player's landing tramples farmland into dirt and breaks turtle eggs with
no hook a server mod can cancel, so a land-claims mod cannot stop a guest
from trampling inside a claim (#9036, #9873 item 3).
`native_entity_change_block_v2` is asked with the player whose landing it
was, the position and both block names, and can cancel the change. It is
additive: `native_entity_change_block` keeps its signature and firing point.
Use the level's backed maps for crafting recipe matching and run map
post-processing on the actual stacks taken by swap, pickup and quick-move.
Keep the original-copy callback used by quick-move and preserve cartography.
Add a real Game/Menu regression and a guarded connected-owner Java/Game
fixture covering previews, consumed inputs, transferred maps and saved backing.
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
The Players tab's second button now opens RealmsInviteCodesScreen: the count in the
title, the subtitle, a row per code with its expiration (or Never), Copy / Edit / Delete
per row, and Create. RealmsEditInviteCodeScreen edits one code: its active flag, the
seven InviteCodeExpiration choices (a click on the slider track picks the nearest — this
screen has no drag event yet), Copy to the clipboard (a new Outcome::Copy the title
turns into Action::CopyText), Delete, Done and Back.
RealmsClient gains inviteCodes (GET api/v1.0/invite/codes?realmId={id}),
createInviteCode (POST .../invite/codes), updateInviteCode (POST .../update) and
deleteInviteCode (DELETE .../delete/{code}), all on the invite-code service with the
token in x-mojangauth, with InviteCode's DTO. The existing redeemInviteCode now goes
through the same helper. An existing expiration reads back as the closest of the seven
choices (InviteCodeExpiration.closestTo).
Co-Authored-By: ZCode <[email protected]>
The messages the Mods page shows after an install (a mod, a native mod, a data pack, a
modpack, a resource pack) and its "isn't set up" errors were built with format!, so they
stayed in English in every language: "Installed JEI for Horizon 0.1.0 into Horizon. Build to
play with it." on a French launcher. They now use trf with named holes. The 11 strings are
added to every table, in English, and translated in fr.json.
PrimedTnt.addAdditionalSaveData stores the igniter as an `owner` UUID
(EntityReference.store) and getOwner resolves it against the level when
asked. tnt_to_nbt now writes the live owner's UUID (a guest, the host
player or a mob), and a loaded TNT looks its saved owner up when it
explodes, so the blast is credited to the player or mob that lit it after
a reload, a portal trip or a chunk unload. An owner that is not online
keeps its UUID through the raw compound.
Fixes#9951
Co-Authored-By: Claude Opus 5.5 <[email protected]>
38 entries still held their English text (the modpack coverage reasons, Performance mode, the
Vulkan fallback notice and a few command-line hints). 15 entries said tu while the other ~110
said vous, sometimes side by side on one page; they now say vous, as the game's own French
table does. Names that read the same in French (Mods, Console, Nightly, Snapshots...) stay.
`horizon-port-pack events <jar or folder>` reads class files for the
NeoForge events a mod listens to (an @SubscribeEvent method's parameter,
a method reference or lambda that takes one event) and the Capabilities
fields it reads, and looks each name up in native-hooks.json's `neoforge`
labels: a hook, another phase's hook, a hook whose label says why it
isn't quite one, or none. Over a folder of jars it ranks the names by
how many mods use them. Names NeoForge changed after 1.21.1 are looked
up under their new ones. It reads names, never code.
On ATM10's 498 jars it takes 8 s: 416 of 497 mods use 326 names, 6,616
uses in all. Main's catalog has a hook for 23% of them, and 43% with
#9924's labels. The top gameplay gaps are ClientTickEvent (95 and 46
mods), RenderLevelStageEvent (81) and ClientPlayerNetworkEvent (53, 35).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
With the switch, the game draws 0.2 packs ([frame] blend = "linear" is
still refused). A renderer made with a 0.2 pack picked draws a float frame
where the device can, else the 8-bit one; a pack picked or dropped later
runs on the frame there is, logged once. The renderer's material, sky,
weather, vertex and shadow entries decode a 0.2 stage's colours and encode
its results; scene_color, opaque_color and the water mirror are decoded
copies, shadow_color is float, and Frame's sky and fog colours are
decoded. The post chain ends in a window-size picture, and the pack's
[tonemap] (or Horizon's clip) draws it onto the frame, encoded at alpha 1.
With HDR on, HDR output lifts nothing of a 0.2 pack's world.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A vanilla list keeps the wheel: AbstractContainerWidget inherits
AbstractScrollArea.mouseScrolled, which scrolls and never reaches the rows, so
CycleButton.mouseScrolled only runs for a button outside a list. Horizon stepped
the row under the cursor, so scrolling a long page changed options.
"Scroll wheel changes settings" keeps that behaviour as an opt-in extra, off by
default. Its saved key is new because the old one was written as true by every
install, and its tooltip has a new key because other languages said "as in
vanilla"; they fall back to English until translated.
Fixes#9633
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A scan of ATM10's 491 mods for their NeoForge event listeners (#9921)
showed nine unlabelled hooks behind events many of them use:
RegisterCommandsEvent (95 mods), ItemTooltipEvent (74), the item and
fluid block capabilities (129, 88), RenderHighlightEvent.Block (41),
ServerStartedEvent / ServerStoppedEvent (48, 41), LivingDropsEvent (36),
LivingKnockBackEvent and ExplosionEvent.Detonate. Each label comes from
the hook's own doc comment and firing point; `cancellable` matches its
mod_api contract, and block_highlight keeps the side and cancellable it
already had.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
VibrationSystem.Ticker.trySelectAndScheduleVibration sends one vibration particle to every player within 32 blocks; the check asserts both the host's window and a LAN guest make it (#8615).
Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
Now that the golem keeps its anger target through updatePersistentAnger:
NeutralMob.playerDied ends anger at the dying player even once the golem
lost sight of them; the save writes the anger target, not the current
target; and anger at a mob is written with that mob's UUID and read back
as anger at a mob, so a reloaded golem that was fighting a monster is not
isAngryAtAllPlayers under universal_anger. A calm mob no longer keeps a
loaded angry_at in its save.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`transmuteCopyIgnoreEmpty`: at 18 + 7 of 25 the carrot on a stick becomes
a fishing rod that kept its custom name and starts whole, where the old
path wrote a fresh plain rod. Wear that Unbreaking took away (0) breaks
nothing, and a stack with no durability never breaks.
`FoodOnAStickItem.use` wears the stick with
`ItemStack.hurtAndConvertOnBreak`, which runs `hurtAndBreak` - and
`hurtAndBreak` asks `processDurabilityChange` first, so Unbreaking has its
say before any damage lands.
`use_steering_stick` added the whole 7 (carrot on a stick) or 1 (warped
fungus on a stick) straight onto `stack.damage`, so an Unbreaking stick
lasted exactly as long as a plain one, and it never reached the mods'
item-damaged hook.
A mod tank can now refuse a fluid through native_block_entity_accepts_fluid (NeoForge's IFluidHandler.isFluidValid). The server asks it before filling a tank in both insert passes and before native_move_fluids counts a tank's room; every tank accepts every fluid unless a mod cancels. A water tank can take no create:chocolate, and a removed mod leaves no fluid id it cannot use behind.
Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
The row pointed at the geometry and carried the missing rule as a gap;
the grid now goes through `item_tooltip::bundle_tooltip`, which asks
`TooltipDisplay::shows("minecraft:bundle_contents")` before the layout.
`BundleItem.getTooltipImage` returns nothing when the stack's
`minecraft:tooltip_display` hides `minecraft:bundle_contents`, so a
bundle given with `hide_tooltip` or with that component hidden drew no
grid. Horizon drew the grid for every bundle, and the tooltip box grew to
hold it.
`item_tooltip::bundle_tooltip` asks the display first and then defers to
`bundle::tooltip_layout`; the tooltip draw uses it for the height probe
and for the draw alike.
A Resolution Scale leaves the pack's picture smaller than the window. Its final pass drew at the window's size, so a shader using gl_FragCoord as texel coordinates (Photon's final) only filled a quarter of the screen. Draw final at the picture's size into an offscreen texture and copy it once to the window with a screen-row fullscreen pass; the no-pack path and same-size output are unchanged.
Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
IronGolem.doHurtTarget throws the target up only when the hit lands, by
0.4f * max(0, 1 - knockback resistance); Horizon threw everything it swung
at by 0.4. The hit and the toss are one event now, like the warden's
sonic boom.
IronGolem.aiStep ends in NeutralMob.updatePersistentAnger(level, true), the
same as a wolf's: every tick it holds a target, a monster too, the anger
timer restarts with a draw of the golem's random. Horizon only angered it at
a player who hit it, so a golem going after a monster drifted from vanilla's
random within a few ticks. The golem now uses the wolf's port, and its
player target goal asks isAngryAt.
LivingEntity.aiStep still drops motion below 0.003 and runs pushEntities
without AI; Horizon's NoAI branch skipped both, which the new mob AI
scenarios (a golem fighting a NoAI zombie, ravager and warden) showed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Run neighbor-shape updates on the authoritative level before the existing eligibility checks, and clear the verified carrier after the handled placement.
Co-Authored-By: Codex <[email protected]>
The configuration screen's Settings tab (RealmsSettingsTab): the world's name and
description fields (the click moves the caret between them), the region preference line
and its Region Preference button, and the open/close toggle moved here from the footer
(vanilla keeps it on this tab). Leaving the tab — switching tabs or Back/Escape — saves
the changed name, description and preference through RealmsClient.updateConfiguration
(POST worlds/{id}/configuration with RealmsConfigurationDto's JSON: the active slot, its
settings, the preference and the description).
RealmsPreferredRegionSelectionScreen: the automatic choices and every manual region, a
click selects, Done writes the choice back to the tab and Cancel drops it.
RealmsClient gains getPreferredRegionSelections (GET regions/preferredRegions) and
updateConfiguration; RealmServer reads regionSelectionPreference; RegionSelectionPreferenceDto,
RegionDataDto and RealmSetting serialization join the DTOs.
The tier's render/simulation distance options and the region quality icons are the next
PRs (the icons need sprites the GUI atlas does not carry yet); the region rows say their
name for now.
Co-Authored-By: ZCode <[email protected]>
A guest's use raised its opener count twice: open_menu counted the block entity's startOpen, then lan_open_container counted it again once the menu was sent. Closing lowered it once, to one, so stopOpen never ran and the lid stayed open with its raised collision (#9500).
The guest open path now relies on the count open_menu already made; the harness keeps the lid open and closed checks for both host and guest.
Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
The tick cases' stand-in world stopped at y 32..96 and x/z -32..31, one block
short of every sample the loop takes and of the neighbours those samples read,
so a block at the edge compared air against the oracle's unending world: that
is where the beehive, the redstone ore and the pointed dripstone differences
came from. Fill the whole box plus a block either way, let a case name the
floor below y = 64, hand the loop the state's own fluid (a waterlogged block
has a water source, which it ticks and drips) and bind the client jar's block
tags in the oracle the way a datapack load does, which a bare bootstrap leaves
empty. Then add the families that read a neighbour, a shape or their own fluid:
dried ghast over soul sand, soul soil, stone and air; hives and a nest embedded
and hanging; a lit redstone ore and a deepslate one embedded and over air; and
stalactite tips over stone and water. 2,097 cases and 456,747 output lines
become 2,187 and 462,718.
BeehiveBlock.animateTick is a `for (i = 0; i < random.nextInt(1) + 1; i++)`
loop, so the bound is drawn again on the check that ends it: a hive always
spends two nextInt(1) draws around its single trySpawnDripParticles attempt.
Horizon hoisted the bound out of the loop and spent one, which left the level
random a draw ahead of vanilla from the first hive the ambient loop touched:
every particle after it moved, and the honey drip of a hive hanging over air
came out at the wrong column.
The corpus had no wither rose case at all. Add a world that is all wither rose,
run the whole ClientLevel.animateTick loop over it and compare every smoke puff
by exact position bits in call order: twenty-seven cases, three seeds and
twenty-four at random ones. The probe now supplies the flower's per-column model
offset the way the client's mesh tables do (a case without it would compare a
shape the game never shows). 2,070 cases and 402,440 output lines become 2,097
and 456,747.
WitherRoseBlock.animateTick reads its centre off getShape(state, level, pos,
CollisionContext.empty()), a flower's own box whose bounds centre is 0.5/0.5
before the block's per-column model offset. AnimateContext::shape_y answers with
the collision shape, which is empty for every flower, so the centre came out as
0 and the whole smoke column sat half a block towards the block's north-west
corner. Take the centre from the pick shape's boxes (BlockFacts::outline) and
keep the model offset on top of it.
The hooks that came before docs/native-hooks-neoforge.md had no
`neoforge`, `side` or `cancellable` keys, though NeoForge mods listen to
their events most: server and level ticks, block and item clicks,
attacks, joins, chunk loads, logins. Each value comes from the hook's
own doc comment or the vanilla method the proposal's table names, and
`cancellable` matches the hook's mod_api contract. Metadata only: no
name, signature or call site changes.
The proposal's status, its next-hooks table (most of it has landed)
and its capabilities and rollout notes now match main.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The example pack written for 0.2: its sky and post passes in linear light,
with the 0.1 pack's gamma constants decoded, and a [tonemap] stage of its
own that eases colours above three quarters of the headroom toward it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
An icon-only button recorded nothing, so the pad could not reach the pause
menu's Report Bugs, Feedback and Player Reporting icons, or Realms' icons. An
active button with an empty label is now a focus target. So are the
Multiplayer warning's "Do not show again" box and Optimize World's Erase
Cached Data box, which A now ticks.
tools/pad_menu_e2e.py gains every_screen_has_targets, which walks the title's
submenus and every options page and fails on a focus screen with no targets.
Replay Studio is allowlisted because it registers nothing yet. New pause_icon
and erase_checkbox cases press the new targets. The pause menu now has
unlabelled widgets, so splitscreen_e2e's setup scenario reads labels with get.
controller_settings_pad's check for a Navigation page now matches exactly: it
tripped on the General page's Navigation Repeat rows.
docs/controllers.md no longer says every rectangle registers itself or that
both modes reach every screen.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The world's depth was bound as a depth texture and read with textureLoad, which naga's GLSL backend cannot emit, so TAA, the depth blit and the shader depth test failed to build on OpenGL. Bind those views as unfilterable float textures instead: wgpu accepts a depth view there, and textureLoad returns the same depth texel on every backend (Vulkan, DX12, Metal, GL).
The shader translation test now fails on any shader the GL backend runs that cannot translate, and pins fs_taa and fs_blit; the HUD's glint, which fails for an unrelated two-samplers-per-image naga limit, is the one allowed exception.
Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
MultiPlayerGameMode.interact sends EntityHitResult.getLocation() minus
entity.position() in every ServerboundInteractPacket. Horizon sent a
fixed [0.0, 1.0, 0.0] for mobs, a point in their middle: armour stands,
saddles, leashes and anything else that reads the click carried
interactAt to the wrong place, unlike vanilla, and a server's reach or
ghost-hand checks saw clicks that vanilla never makes.
mob_hit and the spear's attack_range pick now return the point along
the view ray where the mob's box was met, measured from its feet, and
every caller destructures the third element. A regression test checks
the packet's point against the ray.
ServerGamePacketListenerImpl.handleInteract fires the trigger with the
held stack as it was before the interaction whenever an item interaction
succeeds. NameTagItem.interactLivingEntity and DyeItem.interactLivingEntity
both succeed, but interact_mob returned after name_mob and after the sheep
dye before reaching the shared trigger call.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
A location predicate whose `structures` named a tag
(`#minecraft:shipwreck`, or a data pack's own `#pack:village`) never
matched: the predicates' tag lookup had no `worldgen/structure` case and
the world's pack tags didn't hold that registry. The world's tables now
load the structure tags with the packs' over the game's own, like the
entity type, biome and damage type tags, and a world without packs
reads the game's own structure tags (loaded once).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
Consumable.onConsume awards the item's used statistic and fires the
consume_item trigger for a server player before the consumable's listeners
run and the stack shrinks. A potion and an ominous bottle finish through
finish_drinking, which did neither; food, honey bottles and milk go
through finish_consuming, which already does both.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
Each player-* request writes one log line with what became of it: INFO
and what it did, or WARN and the reason it was refused. The line names
the player and, for the export, the import, the confirm, the release and
a named drop, the hand-over id, so the lines of one hand-over can be
found on both servers. The record and the request's body are never
written.
docs/server.md gives a hand-over in order, with the ids, the commit
point and what undoes itself, and the log lines.
tools/server_transfer_e2e.py runs a whole hand-over between two
dedicated servers over their real control sockets: two engines with
[transfer] on and Velocity forwarding, a small relay per engine that
answers the forwarding request as a proxy does, and horizon-bots
clients. It follows the documented order step by step, checks the
target's player file is on disk when the confirm answers and that the
source writes nothing for a player moved away, then aborts a hand-over
back and checks the player plays on.
Refs #7484
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The request may name where the player arrives, over the record's
compound: `position`, `rotation`, `on_ground` and, now that #8693 keeps
a guest's motion, `velocity`. They belong to the `position` part. The
pitch is taken as Entity.setXRot takes it (modulo a turn, then cut to
+-90) and the yaw as it is; a velocity component over 10 blocks a tick
is taken as 0, as Entity.load takes a saved Motion. A position must be
within the same bounds as the record's own (Entity.load's clamp:
30,000,512 on x and z, 20,000,000 on y), and a rotation or velocity must
be finite numbers.
When the import moves the player to another chunk than the one its view
was built around, the view moves at once: the chunk centre is sent
before the position to confirm, and the chunks out of reach are
forgotten. The reply says so with "recentered".
Refs #7484
Co-Authored-By: Claude Opus 5.5 <[email protected]>
#9829 made the join and leave lines name a player as Player.getDisplayName
does (its team's format and its nickname). place_arrival, which announces
a player placed by player-import or player-confirm, still used the plain
name; it now uses player_display_name too, so an arrival is announced as
a login is.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
martipops's second review of #8730.
The commit point now covers the three files and a power loss: the
player file, advancements and statistics are each written to a new
file, synced, then renamed into place, and the folder is synced on Unix
(save::write_durable, write_dat_durable). Each write returns its error,
and the player stays held and imported when one fails. Ordinary saves
are unchanged.
A retry of `player-import` or `player-confirm` after the commit, whose
reply was lost, names the hand-over of a player already placed from it:
it is answered ok, placed, with `"retried": true`, and nothing is done
again. Another hand-over's id is still refused.
The record's health is refused over 1024 and its absorption over 2048
(max_health and max_absorption), its food exhaustion over 40
(FoodData.addExhaustion) and an effect amplifier outside 0 to 255, as
26.3's bytecode bounds them. A stack's count needs no check:
item_from_nbt already takes one outside ItemStack's 1 to 99 as 1.
docs/server.md says what a proxy does when `moved` is refused after the
commit (the freeze ran out, or an operator's /kill), and that it
confirms only while the export's `freeze_ticks_left` leaves it time.
Refs #7484
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`player-import <uuid> <json>` applies a hand-over's record (from
`player-export <uuid> freeze`) to the held player on the target, whole or
in parts, and tells its client what changed. Unless the request says
`"place": false`, it then places the player as `player-confirm <uuid>
<hand-over>` does.
Placing is the hand-over's commit point: the player file, advancements
and statistics are written to disk synchronously, then the player is
placed, and only then does the reply say ok. A proxy releases the
source after that reply, so a crash of the target no longer leaves the
only current copy in memory. If the file cannot be written the player
stays held and imported.
The record's hand-over id is bound to the held player at the import: a
record without one (an export without freeze) is refused, a retry of the
same record is taken and another hand-over's is not. `player-confirm`
names it, and `player-drop <uuid> [hand-over]` may: a named drop must
name the imported record's. A held player that is leaving (dropped or
kicked, its connection not closed yet) can be neither imported nor
confirmed.
The import reads the whole compound as a file load does: the keys
Horizon does not model, the mods' attachments (given to the game's own
map) and the body's values. The body follows the parts: fall and motion
with `position` (with the saved vehicle), air, fire, freezing and food
exhaustion and timer with `health`. Values no player can have are
refused before anything changes: a position, rotation, motion, health,
absorption, saturation, experience progress or fall that is not a
number, a position past vanilla's clamp, a health of zero or less.
The client is sent the effects the import removed and the recipe book
it brought. Every player gets the arrival's list entry, since every
player is shown its entity; `announce` is the join line only. At
confirm the client is sent its position and inventory again, since what
it did since the import was not acted on.
The control endpoint refuses a request line longer than 16 MiB, which
the game would otherwise read and parse between two ticks.
Refs #7484
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The config reply pinned by api-v1-config.json lists [transfer] with its
defaults; this PR adds max_hold_seconds (60 by default), so the golden
lists it too.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A login that used up a proxy's expectation (player-expect, taken as the
login starts) is held on the target server: it gets the whole join
sequence and the level around it, and its acknowledgements are handled,
but it is not placed. No join message, no list entry for the others, not
among the entities they are shown, not simulated, and nothing else it
sends is acted on. The world is loaded around a held player as around a
placed one. The log says "<name> is held: it arrives from another
server".
A held player nobody takes over is dropped after [transfer]'s
max_hold_seconds (60 by default, 1200 ticks), with one warning: a player
already kicked or dropped, whose connection is slow to close, is not
kicked again on every tick. horizon-server passes the setting with the
others.
A held player keeps a true picture of the others while it waits: a
player who joins meanwhile is added to its list, and one who leaves is
removed from it with its entity, or its client would drop the entity of
a player it has no entry for.
`player-drop <uuid>` forgets the player's expectation and disconnects
the held player without a save or a word to anyone. player-export
refuses a held player. Placing it is the import's work, next.
Refs #7484
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The panel takes the Glass look from docs/design-system.md: the sunset with pixel ridges, frosted
plates, Geist and Bricolage Grotesque Heading, the theme's Hugeicons. What it already did gets a page
each in a sidebar (overview, players, console, backups, crash reports), and confirm()/prompt() become
in-page dialogs. The supervisor serves the theme's fonts and the mark; the CSP gains font-src 'self'.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Jh5A2PzdusBChqE38VZ3zD
The row described it as missing because the prediction only restored the block.
It is game/remote.rs::Game::block_meets_player now, and end_predictions acts on
the answer.
Entity.isColliding on a full block at the player's feet meets a 0.6 x 1.8 box
standing in it, and a block above its head, the one beside it or an empty shape
does not. The snap takes the remembered position while the teleport guard allows
it, and nothing when the block came back clear of the player or the player was
teleported at the same sequence.
ClientLevel.syncBlockState is what BlockStatePredictionHandler.endPredictionsUpTo
calls once the server acknowledges a sequence: when the block takes a state it
did not have, the level restores it, and then, if the prediction remembered a
player position and the player was not teleported since
(lastTeleportSequence < sequence), it asks Entity.isColliding(pos, state) -- the
state's own collision shape at the player's pose against the player's box -- and
calls Player.absSnapTo with the remembered point when they meet.
Horizon's end_predictions restored the block and stopped there, so a break the
server refused under the player, or a placement it pulled back, left the player
standing inside it until its own physics pushed it out.
A prediction now remembers what vanilla's ServerVerifiedState does -- the
sequence, the state the server last gave the block, and the player's position
when the change was first predicted -- and keeps all three through a
retainKnownServerState that only bumps the sequence. end_predictions restores the
block, tests the shape against the player and snaps to the remembered point when
the guard allows it. remote_teleport takes BlockStatePredictionHandler.onTeleport,
so a prediction made before a teleport keeps its block but not its snap.
teleport_to and set_rotation move and turn any entity as /tp and /rotate
do; head_yaw, set_head_yaw, direction, dimensions and push.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
is_shift_key_down, is_crouching, is_sprinting, is_in_water, is_in_lava,
fire_immune, fire_ticks, air_supply, max_air_supply, is_silent,
set_silent, is_invisible and pose, for players and mobs, and the ones
Horizon keeps for armor stands and items.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
level.play_sound and level.send_particles send to every player in range,
as ServerLevel.playSound and sendParticles do, from any server hook (not
only a level hook's LevelWriteHost). The checks the level-hook versions
make move into native_sound_ok and native_particles_ok, shared by both.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
@@ -34,7 +34,10 @@ Feature → files → harness for the most-changed areas: [docs/architecture.md]
- Contributors first: `gh pr list --state open --search <area>` before you start; never duplicate theirs.
- **No hardcoded user-facing strings.** Keep UI translation-ready from the start. In the launcher (`launcher/`), wrap visible text with `tr("...")` or `trf("... {arg} ...", &[("arg", &val)])` from `crate::i18n` instead of raw string literals or `format!`. In the game, use translation keys (`Text::tr`, `lang::get`), never hardcoded English strings.
- **Claim before you start**: comment `/claim` on the issue (no issue yet: open one, then claim it). An assigned
issue is taken: pick another. `/unclaim` when you stop; a claim with no open PR linking it is released after 5 days.
issue is taken, and so is one labelled `in progress` (claimed on Discord): pick another. `/unclaim` when you stop;
a claim with no open PR linking it is released after 5 days. The issues free to claim, without reading threads:
- **Controllers reach what registers.** Draw controls with a registering helper (`ui::button`, `settings_shell::foot_button`); `graphics_style::button` and `screen_draw::button` only paint, and registering one part of a screen ends its crosshair, so register all of it.
## Crates
@@ -48,7 +51,7 @@ Game workspace (root `Cargo.toml`); `launcher/` and `crates/horizon-telemetry-se
| `horizon-command` | Command parsing (Brigadier): the vanilla tree, argument types, selectors, suggestions, the tree's network form. Re-exported as `horizon_client::command` |
| `horizon-client-core` | The client lib's bottom layer: `text.rs`, `lang.rs`, `snbt.rs`, `nbt_path.rs`, `component.rs`, `scoreboard.rs`, `bossbar.rs`, `gamerules.rs`, `block_entity_nbt.rs`, registry/particle/sound tables. Re-exported under the old paths (`horizon_client::text`...) |
| `horizon-client-input` | Key bindings and key names, toggle keys, mouse options, controller settings and glyphs, the clipboard, window options (`keybinds.rs`, `gamepad.rs`, `mouse.rs`, `window.rs`...). Re-exported under the old paths |
| `horizon-client-render` | The client's world data, rendering and screens (chunks, meshing, light, streaming, `gfx`, HUD, menus, settings): ~80 modules that horizon-client re-exports under the same paths. Nothing in it reaches gameplay |
| `horizon-client-render` | The client's world data, rendering and screens (chunks, meshing, light, streaming, `gfx`, HUD, menus, settings), modules that horizon-client re-exports under the same paths. Nothing in it reaches gameplay |
| `horizon-world` | Vanilla world data with no gameplay loop: NBT, regions, chunks, `datafix/`, `worldgen/`, `level/` (server-side block behaviour, ticks, redstone), entity physics, items, effects, food, damage |
@@ -62,6 +65,7 @@ Game workspace (root `Cargo.toml`); `launcher/` and `crates/horizon-telemetry-se
| `horizon-mods`, `cargo-horizon-mod` | Content mods (`.hmod`: manifest, content fence, no code), the registry index and `.hpack`, and `cargo horizon-mod` (`new`, `build`, `run`, `check`, `registry`) ([docs/mods.md](docs/mods.md)). Code mods are native, built by the launcher ([docs/native-mods.md](docs/native-mods.md)) |
| `horizon-optifine` | OptiFine-format pack files below every feature that reads them: `.properties`, the name/range/block matchers, paths and `optifine/` folders, and custom entity models (`cem/`: `.jem`/`.jpm` parsing and baking) |
| `horizon-polytone` | Polytone-format pack files (`assets/*/polytone/`), read clean-room from the public wiki: targets, biome and dimension modifiers and their environment attribute layers (#11486) |
| `horizon-shader-api` | Horizon's own WGSL shader packs ([docs/api/shaders.md](docs/api/shaders.md)): `horizon-shaders.toml`, the `.hshader` package; shared by the game, the launcher and the registry |
| `horizon-shaders` | Shader packs (the `shaders/` folder format), rebuilt clean-room from a written spec of the format; no wgpu |
| `horizon-theme` | Design tokens and fonts for the launcher, the game's own menus and the website |
@@ -89,8 +93,8 @@ reads `README.md`, `launcher/links.json`, `docs/api/`, `roadmap/roadmap.json` an
```sh
python3 tools/pr_ready.py # the self-check: fmt, clippy -D warnings, touched crates' tests, harnesses; paste its block in the PR
python3 tools/pr_ready.py --quick # same without harnesses
cargo test -p horizon-client -p horizon-client-lib --lib --bins --tests # skips the 104 examples (pr_ready does this); `--doc` separately
cargo test -p horizon-client-lib --lib mod_content::tests:: -- --ignored # the 5 slow content-root tests, #[ignore]d; pr_ready runs them when mod content, assets, blocks or items change
cargo test -p horizon-client -p horizon-client-lib --lib --bins --tests # skips the examples (pr_ready does this); `--doc` separately
cargo test -p horizon-client-lib --lib mod_content::tests:: -- --ignored # the slow content-root tests, #[ignore]d; pr_ready runs them when mod content, assets, blocks or items change
cargo build --profile fast -p horizon-client # optimized, quick to link: use it to play or run lan_e2e
python3 tools/parity.py --changed origin/main # the harnesses your branch can affect (CI's parity job)
python3 tools/parity.py --changed-files paths.txt --list # which harnesses cover these files, without running them
@@ -102,8 +106,8 @@ python3 tools/agent_control_e2e.py # horizon-ctl / MCP end to end
- Harnesses and the game need the vanilla data in `.local/` (`minecraft/`, `horizon-client/26.3`, `datagen/`,
`tools/`): see [docs/parity-workflow.md](docs/parity-workflow.md). Without it, pr_ready lists the
harnesses for CI.
- A harness = `tools/test_<name>_parity.py` (324 of them) + a Java oracle (`mods/horizon/src/test/java/dev/horizonlab/parity/Vanilla*Oracle.java`)
+ a Rust probe: `crates/<crate>/examples/<name>_{probe,fixture}.rs` (horizon-world 175, horizon-client 104),
- A harness = `tools/test_<name>_parity.py` + a Java oracle (`mods/horizon/src/test/java/dev/horizonlab/parity/Vanilla*Oracle.java`)
+ a Rust probe: `crates/<crate>/examples/<name>_{probe,fixture}.rs` (mostly horizon-world and horizon-client),
or the game binary with a fixture env var (`game/dispense_fixture.rs`, `game/sculk_fixture.rs`,
`game/command_harness.rs`, `game/net_harness.rs`). Copy a nearby harness for a new one.
- Developing a native mod? Run `horizon-launcher settings native-profile dev` once. `dev` builds the game crate
@@ -117,7 +121,7 @@ python3 tools/agent_control_e2e.py # horizon-ctl / MCP end to end
Source paths remain in `crates/horizon-client/`. Measure actual harness selection on refreshed
records with `tools/parity.py --changed-files paths.txt --list`; historical counts are not a guarantee.
then `horizon-ctl --socket … state|keys|turn|command|screenshot|quit`
@@ -173,7 +177,7 @@ state stays separate from the library tests. This compiles gameplay twice for ex
## How `Game` is organised
- `struct Game` (game.rs, ~290 fields) holds everything. Its methods are spread over ~180 files: game.rs keeps
- `struct Game` (game.rs) holds everything. Its methods are spread over many files: game.rs keeps
the core (tick, render, input, inventory, world open/close) and each `game/<feature>.rs` adds an
`impl Game<'_>` block for one feature, with its own state types. Fields stay private to the gameplay module and its descendants. Gameplay tests run in the gameplay library, separate from reusable-library tests.
Minecraft: Java Edition 26.3, rewritten in Rust. Same worlds, same rules, same terrain, checked against the Java game by more than 300 differential harnesses. Free, open source and unofficial.
**Minecraft: Java Edition 26.3, rewritten in Rust.** Your worlds, the same rules and the same terrain as the Java game, checked against it by hundreds of differential tests, in one native program with no Java. Free, open source (MIT) and unofficial: you play it with your own copy of Java Edition.
> Not an official Minecraft product, nor approved by or associated with Mojang or Microsoft. Horizon contains no Minecraft code, assets or data: you need your own copy of Java Edition 26.3.
## Download
Get **the Horizon launcher** from [playhorizon.dev](https://playhorizon.dev) or [the launcher releases](https://github.com/horizon-rust/horizon/releases?q=launcher&expanded=true) (Linux x86-64, macOS on Apple Silicon, macOS on Intel from the next Nightly, Windows x64). It installs and updates the game, prepares its files from your copy of Minecraft, and updates itself.
**You need** a Microsoft account that owns Java Edition ([why](docs/microsoft-account.md)) and a GPU with Vulkan or OpenGL 3.3 (Linux), Vulkan or DirectX 12 (Windows), or a Mac (Apple Silicon, or Intel from the next Nightly). An existing Minecraft install (official launcher, Prism, MultiMC...) is reused; without one, Play downloads 26.3 from Mojang for your account. Step by step: [Install and first launch](https://playhorizon.dev/docs/install).
<table>
<tr>
<td width="50%"><img src="docs/media/launcher-play.webp" alt="The launcher's Home page: the selected instance ready to play, the version picker, Join a friend and the instance's tabs"></td>
<td width="50%"><img src="docs/media/launcher-settings.webp" alt="The launcher's Settings page: Microsoft account, offline name, updates, appearance and accent colour"></td>
</tr>
</table>
<p align="center">
<img src="docs/media/readme/valley.webp" alt="A river valley with a plains village, savanna hills and a cherry grove, seen from above with a long view distance" width="100%">
</p>
## What works today
- **The game:** vanilla world generation, mobs, villagers, raids, redstone, every command, the HUD and the recipe book, on vanilla saves. Worlds from 1.8 to 26.2 are upgraded. [Status](#project-status).
- **Extras:** new menus, graphics presets, distant terrain up to 1024 chunks, controllers, zoom, minimap and more, off by default where they change the vanilla look. [Settings](https://playhorizon.dev/docs/settings).
- **Packs:** resource packs, and data packs with functions, predicates, item modifiers and world generation.
- **Multiplayer:** invite friends with a code, Open to LAN, join servers (online mode included). [Multiplayer](https://playhorizon.dev/docs/multiplayer).
- **Mods:** content mods (blocks, items, data) and native Rust mods, built by the launcher with nothing to install. Java mod jars do not run. [Mods](https://playhorizon.dev/docs/mods).
Everything below is in the current release.
In the Rust client, with vanilla assets and no mods or shaders:
- **The game.** Survival, creative, hardcore and spectator; vanilla world generation in all three dimensions, mobs, villagers, raids, redstone, every command, the HUD and the recipe book. Vanilla saves open as they are, and worlds from 1.8 to 26.2 are upgraded.
- **Multiplayer.** Join vanilla and Horizon servers (online mode included), open a world to LAN, or invite friends with a code from the launcher's Home page.
- **Graphics.** Distant terrain up to 1,024 chunks, graphics presets, HDR and controllers, all off by default where they would change the vanilla look ([settings](https://playhorizon.dev/docs/settings)).
- **Packs.** Resource packs, data packs and shader packs ([below](#mods-and-shader-packs)).
- **Mods.** Content mods and native Rust mods, installed from the launcher ([below](#mods-and-shader-packs)).
- **Servers.** Host one from the launcher, or run `horizon-server` on a VPS ([below](#host-a-server)).
- **The launcher.** It installs and updates the game, prepares its files from your copy of Minecraft, and updates itself.
<table>
<tr>
<td colspan="2"><img src="docs/media/sunset.webp" alt="Sunset over a savanna river valley and a plains village"></td>
<td width="50%"><img src="docs/media/readme/sunset.webp" alt="Sunset behind a cherry grove on a hill, with a river below"></td>
<td width="50%"><img src="docs/media/readme/nether.webp" alt="A crimson forest in the Nether, with a piglin among the fungi"></td>
</tr>
<tr>
<td width="50%"><img src="docs/media/survival.webp" alt="Survival at dusk: a zombie walks up, with the hotbar, health, hunger and experience"></td>
<td width="50%"><img src="docs/media/settings.webp" alt="Vanilla's Video Settings screen with Horizon's own buttons at the top: Horizon Graphics and Advanced Graphics"></td>
</tr>
<tr>
<td width="50%"><img src="docs/media/village.webp" alt="A plains village among savanna hills, with farms, villagers and an iron golem"></td>
<td width="50%"><img src="docs/media/nether.webp" alt="A crimson forest in the Nether with a zombified piglin"></td>
<td width="50%"><img src="docs/media/readme/survival.webp" alt="Survival in a village square: two villagers, flowers, the hotbar, health and hunger"></td>
<td width="50%"><img src="docs/media/readme/multiplayer.webp" alt="Two players in a LAN world: the other player's name tag and the player list"></td>
</tr>
</table>
<sub>Nightly 2026-11-05 on a Mac mini (Apple M4 Pro, Metal): vanilla's textures, no mods or shader packs, render distance 32 with distant terrain on.</sub>
## Performance
**Horizon draws more frames per second than Java Edition, and loads faster, on the same machine, world and path.**`tools/perf_bench.py` runs each game in turn, from a fresh world (seed 1234), waiting for the terrain and then flying it over a village, a forest, an ocean and a mountain. Measured on 2026-10-29/30 on an idle Windows 11 machine (Ryzen 7 9800X3D, RTX 5080), Horizon v0.2.97 as the launcher installs it, 1280x720 in a window, VSync off, three games per figure. Frames per second, mean of the four scenes, with the slowest 1% of frames in brackets:
"Vanilla + mods" is Sodium, Lithium, FerriteCore and ImmediatelyFast. Horizon reaches the title screen in 2.9–3.2 s against vanilla's 7.5–7.7 s (the modded client's 10.8–11.1 s), loads a scene's chunks 2.9–3.8 times faster, and playing holds a steady working set of 1.1–1.7 GB against 1.9–2.9 GB. Every scene with its confidence interval and the run's load log: [the 2026-10-29 reports](benchmarks/perf/windows-9800x3d-5080-2026-10-29.md). Method, and how to measure your own machine: [docs/benchmark.md](docs/benchmark.md). [playhorizon.dev/benchmarks](https://playhorizon.dev/benchmarks) puts these runs beside a 2016 laptop, a Mac mini and a gaming desktop.
## Download
Get **the Horizon launcher** from [playhorizon.dev](https://playhorizon.dev), or from [the launcher releases](https://github.com/horizon-rust/horizon/releases?q=launcher&expanded=true):
| Platform | File | Needs |
| --- | --- | --- |
| Windows x64 | `horizon-launcher-setup.exe` | Windows 10 or 11, a GPU with Vulkan or DirectX 12 |
| Windows on Arm | `horizon-launcher-setup-arm64.exe` | Windows 11 on Arm |
| macOS, Apple Silicon | `horizon-launcher-macos-arm64.dmg` | macOS 12 or later |
| macOS, Intel | `horizon-launcher-macos-x64.dmg` | macOS 12 or later |
| Linux x86-64 | `horizon-launcher-linux-x86_64.AppImage` or `.tar.gz` | a glibc distribution, X11 or Wayland, a GPU with Vulkan or OpenGL 3.3 |
**You also need a Microsoft account that owns Minecraft: Java Edition.** You sign in with it in the launcher, which plays only with an account that owns the game ([why](docs/microsoft-account.md)). Horizon contains no Minecraft code or assets: an existing install (official launcher, Prism, MultiMC...) is reused, and without one the launcher downloads 26.3 from Mojang for your account. Step by step: [Install and first launch](https://playhorizon.dev/docs/install).
<table>
<tr>
<td width="50%"><img src="docs/media/readme/launcher-home.webp" alt="The launcher's Home page: the Horizon instance, Play, the version picker, the friend invite field, screenshots and the patch log"></td>
<td width="50%"><img src="docs/media/readme/launcher-mods.webp" alt="The launcher's Mods page: Horizon's mod registry with Zoom selected, and tabs for modpacks, resource packs, shader packs and data packs"></td>
</tr>
</table>
## Mods and shader packs
- **Content mods** (`.hmod`): blocks, items, recipes, loot, models and textures, with no code.
- **Native mods** (`.hnmod`): Rust that the launcher compiles into the game for you, with nothing to install.
- **Java mods** (Fabric, NeoForge, Forge) don't run: Horizon has no Java. The launcher carries over the data and assets inside their jars, and imports modpacks (`.mrpack`, CurseForge zips, Prism instances) the same way ([modpacks](https://playhorizon.dev/docs/modpacks)).
- **Shader packs**: Iris/OptiFine-format packs (`shaders/` folders) and Horizon's own WGSL packs, under Video Settings → Shader Packs ([pack format](docs/shaders.md)).
Mods are off by default: a fresh install plays exactly like vanilla 26.3. The launcher's Mods page browses Horizon's registry and Modrinth. [Mods](https://playhorizon.dev/docs/mods) · [make a mod](docs/mods.md).
## Host a server
The launcher's **Servers** page creates, starts, backs up and updates a server, and invites friends over Tailscale. On a VPS, `horizon-server` installs with one command and needs no Java. [Host a server](https://playhorizon.dev/docs/hosting) ([reference](docs/hosting.md)).
## Modpacks
The launcher imports Modrinth and CurseForge packs and plays the mods Horizon can carry over: today, content mods made of data and assets, not Java code. [Modpacks](https://playhorizon.dev/docs/modpacks).
The launcher's **Servers** page creates, starts, backs up and updates a server, and invites friends. On a VPS, `horizon-server` installs with one command, needs no Java, and takes vanilla 26.3 and Horizon players alike. [Host a server](https://playhorizon.dev/docs/hosting) ([reference](docs/hosting.md)).
## Agent control
@@ -80,13 +102,11 @@ AI agents and scripts can play Horizon. `horizon-ctl`, shipped with the game, re
```sh
horizon-ctl launch --world Survival --seed 5 --wait # finds the version the launcher installed
horizon-ctl state # position, health, food, target, screen...
horizon-ctl find-block birch_log # the nearest logs
horizon-ctl goto -0.5 65 -2.5 --near 2.5 # walks there along a path
horizon-ctl break -1 65 -3 # mines it; replies with the drops
horizon-ctl open-inventory && horizon-ctl click-slot 0# menus: slots, buttons by label, text fields
```
Commands block until done and reply with the result; `horizon-ctl help` lists them all. [Agent control](docs/native-client.md#agent-control-horizon-ctl-and-mcp).
`horizon-ctl help` lists every command. [Agent control](docs/native-client.md#agent-control-horizon-ctl-and-mcp).
## Build from source
@@ -105,13 +125,9 @@ cargo run --release -p horizon-client
Prepared files land in the gitignored `.local/`: never commit or share them. Options, controls and architecture: [the native client guide](docs/native-client.md); all developer docs: [docs/README.md](docs/README.md).
## Roadmap
What is in progress, what comes next and where to help: [ROADMAP.md](ROADMAP.md), or [playhorizon.dev/hub](https://playhorizon.dev/hub) with the work board and what shipped.
## Contributing
Issues and pull requests are welcome: read [CONTRIBUTING.md](CONTRIBUTING.md) (vanilla first, small pull requests). Questions: [Discord](https://discord.gg/playhorizon). Before a pull request, run the self-check and paste its output:
Issues and pull requests are welcome: read [CONTRIBUTING.md](CONTRIBUTING.md) (vanilla first, small pull requests), and see [ROADMAP.md](ROADMAP.md) or [playhorizon.dev/hub](https://playhorizon.dev/hub) for what's next. Questions: [Discord](https://discord.gg/playhorizon). Before a pull request, run the self-check and paste its output:
```sh
git config core.hooksPath .githooks
@@ -124,7 +140,7 @@ It runs rustfmt, clippy, the touched crates' tests, the Python tests, the public
**Player-visible gaps: 0 open** of the 833 the [port ledger](tools/port_ledger.py) found, reading every vanilla 26.3 method against Horizon's code: vanilla behaviour a player would notice that Horizon doesn't have yet. By area: Animals [0](https://github.com/horizon-rust/horizon/issues/3512); Monsters and bosses [0](https://github.com/horizon-rust/horizon/issues/3513); Entities, projectiles, vehicles and decorations [0](https://github.com/horizon-rust/horizon/issues/3514); Items, enchantments and inventories [0](https://github.com/horizon-rust/horizon/issues/3515); Blocks, redstone and fluids [0](https://github.com/horizon-rust/horizon/issues/3516); World, world generation and saves [0](https://github.com/horizon-rust/horizon/issues/3517); Screens, HUD and client packets [0](https://github.com/horizon-rust/horizon/issues/3518); Entity and block rendering, models [0](https://github.com/horizon-rust/horizon/issues/3519); Server and players on the server [0](https://github.com/horizon-rust/horizon/issues/3520); Mob AI, villagers and raids [0](https://github.com/horizon-rust/horizon/issues/3521). This number goes down as gaps close (`tools/parity_gaps.py`); the harness results below say that what is tested matches.
The table is the project's own audit: every subsystem matches vanilla in every harness that compares it. 100% means 100% of what the harnesses compare, not that nothing else differs.
The table is the project's own audit: every subsystem matches vanilla in every harness that compares it. 100% means 100% of what the harnesses compare, not that nothing else differs. What players still run into: [known issues](docs/known-issues.md).
### Java parity: 100%
@@ -147,14 +163,8 @@ Scores: 0 (absent), 0.25 (started), 0.5 (playable, major features missing), 0.75
| Audio | 1 | None: the sound engine, sound events, music, situational music, ambience and subtitles match vanilla |
| Block behaviours and ticking | 1 | None: block updates, random ticks, redstone, dispensers (6,000 cases), crafters (4,000), container spills (4,000), copper golems and the sculk family (435 scenarios) match |
### Known issues
Skins served over https show the default skin unless already cached; pack icons show the unknown-pack texture in game; item tooltips lack attribute lines.
## Licence
[MIT](LICENSE), for the project's original work; dependencies keep their own licences ([notices](THIRD_PARTY_NOTICES.md)). Minecraft is a trademark of Mojang Synergies AB; its code and assets belong to Mojang and Microsoft under the [Minecraft EULA](https://www.minecraft.net/en-us/eula).
[MIT](LICENSE), for the project's original work; dependencies keep their own licences ([notices](THIRD_PARTY_NOTICES.md)). Horizon is made by [Ludovic Marie](https://lu4ovic.com) ([@ludovic111](https://github.com/ludovic111)) and [its contributors](https://github.com/horizon-rust/horizon/graphs/contributors); support it on [GitHub Sponsors](https://github.com/sponsors/ludovic111).
## Author
Made by [Ludovic Marie](https://lu4ovic.com) ([@ludovic111](https://github.com/ludovic111)).
NOT AN OFFICIAL MINECRAFT PRODUCT. NOT APPROVED BY OR ASSOCIATED WITH MOJANG OR MICROSOFT. Minecraft is a trademark of Mojang Synergies AB; its code and assets belong to Mojang and Microsoft under the [Minecraft EULA](https://www.minecraft.net/en-us/eula).
@@ -266,7 +266,7 @@ Shader packs were removed and are being rebuilt from scratch. They come back, of
A clean-room rewrite in `horizon-shaderpack`, from the packs' public format documentation only: reading packs, the preprocessor and `.properties` files are merged ([#1584](https://github.com/horizon-rust/horizon/issues/1584), [#1585](https://github.com/horizon-rust/horizon/issues/1585), [#1586](https://github.com/horizon-rust/horizon/issues/1586)); translating programs to WGSL through naga, buffers, uniforms and the terrain, entity, sky, weather and hand stages are in review ([#1698](https://github.com/horizon-rust/horizon/issues/1698) to [#1801](https://github.com/horizon-rust/horizon/issues/1801)).
@@ -298,6 +298,8 @@ Far terrain drawn from the world you really built and explored, caves and builds
Keep a 3D mip store on disk built from real chunks, drawn by the GPU beside the seed preview for areas nobody has visited. Written from scratch and measured before it is kept. Builds on Far Detail and the far-terrain levels of detail already shipped; the open work on them is [#718](https://github.com/horizon-rust/horizon/issues/718).
@@ -308,7 +310,7 @@ Several smaller speed-ups, each measured: idle block entities that sleep, cheape
Block entities and hoppers that skip work when idle (the hopper item scan is already skipped when no item is near); a particle frustum test first (also a parity gap), then particle quads built on the GPU; jigsaw shortcuts that skip blocked connectors and pool elements known not to fit, with layouts unchanged (trial chambers and ancient cities first); batched raw mouse input on Windows for 8000 Hz mice; a better atlas packer for huge resource packs (a 64x pack builds a 704 MiB atlas for 452 MiB of textures); sign-heavy scenes ([#1461](https://github.com/horizon-rust/horizon/issues/1461)). Profile first, keep what pays, show before and after numbers.
@@ -322,6 +324,8 @@ Recognise mods built for versions before 26.1 in the mod browser, so they can be
A mapping table from intermediary and SRG names so pre-26.1 jars can be classified, and a rename for mod ids with dashes (`fabric-api`); the port step then treats them like newer jars ([docs/mod-browser.md](docs/mod-browser.md)).
@@ -406,16 +410,6 @@ The panel's later features (bans, worlds, server.properties import, logs and cra
</details>
#### Ely.by and other sign-in providers
Sign in with other account providers, such as Ely.by or any Yggdrasil-compatible one, beside Microsoft.
<details><summary>Technical</summary>
Issue [#1270](https://github.com/horizon-rust/horizon/issues/1270) asks for Ely.by and Yggdrasil-compatible servers with an account selector in the launcher. It builds on the Microsoft sign-in ([docs/microsoft-account.md](docs/microsoft-account.md)).
</details>
#### Join servers on older versions
Join servers that run older Minecraft versions, such as 1.21 or 1.8 PvP servers, from Horizon.
@@ -536,6 +530,8 @@ Contraptions and physics like the Create and Aeronautics mods, as a mod for Hori
Issue [#805](https://github.com/horizon-rust/horizon/issues/805) (porting QOL mods) closed with this answer: it would come as a native Horizon mod.
@@ -558,6 +554,8 @@ Every past and future Minecraft version prepared for Horizon automatically. When
The port pipeline ([docs/version-ports.md](docs/version-ports.md), `tools/version_port.py`) detects a release, prepares it, diffs its classes against the pinned 26.3, regenerates the data-driven tables, runs every parity harness on it and writes a port report that becomes issues. The goal is as little human work as possible for any version, past or future. Detection and the first slices are merged; the scheduled agent that runs it every day isn't installed yet. Two versions in one build are out of scope.
#### Controllers: tested on real pads, and in the launcher
@@ -568,6 +566,8 @@ Make controller support solid: tested with real PlayStation and Xbox pads and a
The game side is merged ([docs/controllers.md](docs/controllers.md)): pads read with gilrs, menus, rumble, glyphs, remapping and DualSense adaptive triggers, lightbar and haptics, but not yet confirmed on real pads or a Steam Deck (DualSense haptics stay behind a switch until then). Left: a test pass on real hardware, and the same focus navigation in the GPUI launcher.
@@ -668,6 +668,16 @@ A shared resource store keyed by the preparer's digest, no prepare intermediates
</details>
#### A launcher screen readers can read
Screen readers read the launcher's buttons, lists and settings, and every part of it works from the keyboard.
<details><summary>Technical</summary>
Issue [#4537](https://github.com/horizon-rust/horizon/issues/4537): the launcher is GPUI (not Electron), so it exposes nothing to UI Automation, NSAccessibility or AT-SPI yet. Needs an accessibility tree from GPUI (an AccessKit bridge) and a role and label on each widget.
</details>
#### A visible beta label
A clear 'beta' label in the launcher and the game, so nobody is surprised that Horizon is still being built.
On a system Horizon has no download for, such as FreeBSD, the launcher offers to build the game on your computer, as it already does for native mods.
<details><summary>Technical</summary>
Issue [#4354](https://github.com/horizon-rust/horizon/issues/4354): when no release asset matches the platform, the launcher fetches the tagged source and runs the same pinned-toolchain `cargo build --locked` its native-mod builds use, with a warning about the time it takes. The build is unsigned, so the launcher marks the version as built locally.
More: [docs/native-mods.md](docs/native-mods.md)
</details>
#### Horizon on Windows ARM64
A native Horizon for Windows on ARM computers, such as Snapdragon laptops.
@@ -710,6 +732,8 @@ Versions of Horizon for Android, iOS and web browsers. Nothing official yet; con
No official build. Android was first closed as not planned, with touch controls, a mobile interface, packaging and loading the player's own assets all missing; the roadmap now includes it, iOS and a build for the browser as long-term goals. First pieces from contributors: `horizon-dualsense` compiling on iOS ([#1813](https://github.com/horizon-rust/horizon/issues/1813)) and helper-thread limits on Android ([#1751](https://github.com/horizon-rust/horizon/issues/1751)). Preparing without Java ([#1750](https://github.com/horizon-rust/horizon/issues/1750)) matters here too. Horizon ships none of Mojang's files on any platform.
@@ -752,6 +776,8 @@ Record, replay and edit videos without leaving the game: record in a keypress, r
Three stages. Recording: capture gameplay in a keypress with an encoder thread and little frame-time cost. Replay: a replay mode for recorded sessions. Editing: cuts, camera paths, titles and export. None of it exists yet; the agent-control socket ([docs/agent-control.md](docs/agent-control.md)) already lets tools drive the game for scripted shots.
@@ -842,6 +868,8 @@ Every download Horizon makes is checked against the project's signature, includi
Releases carry signed checksums, and the launcher and `horizon-server update` refuse anything unsigned ([#1011](https://github.com/horizon-rust/horizon/issues/1011), [#1014](https://github.com/horizon-rust/horizon/issues/1014)). Left: the first install through `install.sh` and `install.ps1` still trusts only the checksum GitHub publishes, because verifying Ed25519 signatures needs OpenSSL 3, which macOS doesn't ship ([docs/security.md](docs/security.md)).
# `keys.contains(&KeyCode::ShiftLeft)` reads a mapping by its default key: ask by `Action`, so the
# mapping is named and every device that can hold it counts.
disallowed-methods=[
{path="horizon_client_input::held::HeldKeys::contains",reason="use `held(Action::..)`, or `raw` for a key that is no mapping's default",allow-invalid=true},
description="The dedicated server's anticheat: its [anticheat] settings, violation levels and the checks' rules, with no game state (docs/anticheat.md)"
description="The dedicated server's chat filter: its [chat_filter] settings, the slur list's matcher and the mutes, with no game state (docs/chat-filter.md)"
description="The client's lowest layer: chat text and translations, SNBT and NBT paths, the scoreboard, boss bars, game rules and the registry and particle tables commands name"
description="The client's world data, rendering and screens: chunks, meshing, light, streaming, the renderer, the HUD, menus and settings; no gameplay"
"What other Horizon players wear: everything, only their name tag styles, or nothing. Hidden unless you own cosmetics, until you choose. Hidden, the game never asks what they wear."
}
Item::CosmeticsAuras=>"Auras and trails other players wear. Off by default.",
Item::CosmeticsHideFx=>{
"Other players' trails and auras stay hidden; their other cosmetics still show. Off by default."
}
Item::CosmeticsSolo=>{
"Show your cosmetics and your name tag's badge in your single-player worlds too (your name tag with Own name tag). Off: only on servers."
.any(|n|n=="composite: writes buffers of different sizes (size.buffer); skipped"),
"{:?}",
post.notes
);
}
Loaded 100 of 1679 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.